Skip to content

Commit

Permalink
feat: Add option to set a Nextcloud target version or parse the appinfo
Browse files Browse the repository at this point in the history
You can set a `targetVersion` option like `targetVersion: '25.0.0'` to only report deprecations / removals that happend before that version.
Moreover you can also set `parseAppInfo: true` to parse the `appinfo/info.xml` from a Nextcloud app and only report deprecations / removals before the `max-version` of the Nextcloud dependency.

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
  • Loading branch information
susnux committed Jul 6, 2023
1 parent 3ef6e64 commit 2315a46
Show file tree
Hide file tree
Showing 10 changed files with 901 additions and 3,732 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,30 @@ Configure the rules you want to use under the rules section.
}
```

### Limit the Nextcloud version to report
By default all removed or deprecated API is reported, but if your app targets an older version of Nextcloud then you can limit the reported issues to changes before and with that version.

For example you target Nextcloud 25 and you use `OC.L10n` which was deprecated with Nextcloud 26. To disable reporting that deprecation you can set the target version to *25*:

```json
{
"rules": {
"nextcloud/no-deprecations": ["warn", { "targetVersion": "25.0.0" }],
"nextcloud/no-removed-apis": ["error", { "targetVersion": "25.0.0" }],
}
}
```

It is also possible to detect that your supported Nextcloud version from your `appinfo/info.xml` (`max-version` of your `nextcloud` dependency):
```json
{
"rules": {
"nextcloud/no-deprecations": ["warn", { "parseAppInfo": true }],
"nextcloud/no-removed-apis": ["error", { "parseAppInfo": true }],
}
}
```

## Supported Shared Configurations

* `nextcloud/recommended`: Recommended configuration that loads the Nextcloud ESlint plugin, adds the Nextcloud environment and configures all recommended Nextcloud rules.
Expand Down
62 changes: 44 additions & 18 deletions lib/rules/no-deprecations.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"use strict";

const { createVersionValidator } = require('../utils/version-parser.js')

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
Expand Down Expand Up @@ -88,29 +90,48 @@ module.exports = {
},
fixable: null, // or "code" or "whitespace"
schema: [
// fill in your schema
{
// We accept one option which is an object
type: "object",
properties: {
// if we should try to find an appinfo and only handle APIs removed before the max-version
parseAppInfo: {
type: "boolean"
},
// Set a Nextcloud target version, only APIs removed before that versions are checked
targetVersion: {
type: "string"
}
},
"additionalProperties": false
}
],
messages: {
deprecatedGlobal: "The global property or function {{name}} was deprecated in Nextcloud {{version}}"
}
},

create: function (context) {
const checkTargetVersion = createVersionValidator(context)

return {
MemberExpression: function (node) {
// OC.x
if (node.object.name === 'OC'
&& oc.hasOwnProperty(node.property.name)) {
&& oc.hasOwnProperty(node.property.name)
&& checkTargetVersion(oc[node.property.name])) {
context.report(node, "The property or function OC." + node.property.name + " was deprecated in Nextcloud " + oc[node.property.name]);
}
// OCA.x
if (node.object.name === 'OCA'
&& oca.hasOwnProperty(node.property.name)) {
&& oca.hasOwnProperty(node.property.name)
&& checkTargetVersion(oca[node.property.name])) {
context.report(node, "The property or function OCA." + node.property.name + " was deprecated in Nextcloud " + oca[node.property.name]);
}
// OCP.x
if (node.object.name === 'OCP'
&& ocp.hasOwnProperty(node.property.name)) {
&& ocp.hasOwnProperty(node.property.name)
&& checkTargetVersion(ocp[node.property.name])) {
context.report(node, "The property or function OCP." + node.property.name + " was deprecated in Nextcloud " + ocp[node.property.name]);
}

Expand All @@ -119,28 +140,33 @@ module.exports = {
&& node.object.object.name === 'OC'
&& oc_sub.hasOwnProperty(node.object.property.name)
&& oc_sub[node.object.property.name].hasOwnProperty(node.property.name)) {
const prop = [
"OC",
node.object.property.name,
node.property.name,
].join('.');
const version = oc_sub[node.object.property.name][node.property.name]
context.report(node, "The property or function " + prop + " was deprecated in Nextcloud " + version);
if (checkTargetVersion(version)) {
const prop = [

Check warning on line 145 in lib/rules/no-deprecations.js

View check run for this annotation

Codecov / codecov/patch

lib/rules/no-deprecations.js#L144-L145

Added lines #L144 - L145 were not covered by tests
"OC",
node.object.property.name,
node.property.name,
].join('.');
const version = oc_sub[node.object.property.name][node.property.name]
context.report(node, "The property or function " + prop + " was deprecated in Nextcloud " + version);

Check warning on line 151 in lib/rules/no-deprecations.js

View check run for this annotation

Codecov / codecov/patch

lib/rules/no-deprecations.js#L150-L151

Added lines #L150 - L151 were not covered by tests
}
}
},
Program() {
// Logic adapted from https://github.com/eslint/eslint/blob/master/lib/rules/no-restricted-globals.js
const scope = context.getScope();
const report = ref => {
const node = ref.identifier;
context.report({
node,
messageId: 'deprecatedGlobal',
data: {
name: node.name,
version: global[node.name]
},
});
if (checkTargetVersion(global[node.name])) {
context.report({
node,
messageId: 'deprecatedGlobal',
data: {
name: node.name,
version: global[node.name]
},
});
}
}

// Report variables declared elsewhere (ex: variables defined as "global" by eslint)
Expand Down
75 changes: 49 additions & 26 deletions lib/rules/no-removed-apis.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"use strict";
"use strict"

const { createVersionValidator } = require('../utils/version-parser.js')

//------------------------------------------------------------------------------
// Rule Definition
Expand Down Expand Up @@ -52,14 +54,30 @@ module.exports = {
},
fixable: null, // or "code" or "whitespace"
schema: [
// fill in your schema
{
// We accept one option which is an object
type: "object",
properties: {
// if we should try to find an appinfo and only handle APIs removed before the max-version
parseAppInfo: {
type: "boolean"
},
// Set a Nextcloud target version, only APIs removed before that versions are checked
targetVersion: {
type: "string"
}
},
"additionalProperties": false
}
],
messages: {
removedGlobal: "The global property or function {{name}} was removed in Nextcloud {{version}}"
}
},

create: function (context) {
const checkTargetVersion = createVersionValidator(context)

return {
MemberExpression: function (node) {
// OCA.x
Expand All @@ -70,53 +88,58 @@ module.exports = {

// OC.x
if (node.object.name === 'OC'
&& oc.hasOwnProperty(node.property.name)) {
context.report(node, "The property or function OC." + node.property.name + " was removed in Nextcloud " + oc[node.property.name]);
&& oc.hasOwnProperty(node.property.name)
&& checkTargetVersion(oc[node.property.name])) {
context.report(node, "The property or function OC." + node.property.name + " was removed in Nextcloud " + oc[node.property.name])
}

// OC.x.y
if (node.object.type === 'MemberExpression'
&& node.object.object.name === 'OC'
&& oc_sub.hasOwnProperty(node.object.property.name)
&& oc_sub[node.object.property.name].hasOwnProperty(node.property.name)) {
const prop = [
"OC",
node.object.property.name,
node.property.name,
].join('.');
const version = oc_sub[node.object.property.name][node.property.name]
context.report(node, "The property or function " + prop + " was removed in Nextcloud " + version);
if (checkTargetVersion(version)) {
const prop = [
"OC",
node.object.property.name,
node.property.name,
].join('.')
context.report(node, "The property or function " + prop + " was removed in Nextcloud " + version)
}
}
},
Program() {
// Logic adapted from https://github.com/eslint/eslint/blob/master/lib/rules/no-restricted-globals.js
const scope = context.getScope();
const scope = context.getScope()
const report = ref => {
const node = ref.identifier;
context.report({
node,
messageId: 'removedGlobal',
data: {
name: node.name,
version: global[node.name]
},
});
const node = ref.identifier
if (checkTargetVersion(global[node.name])) {
context.report({
node,
messageId: 'removedGlobal',
data: {
name: node.name,
version: global[node.name]
},
})
}
}

// Report variables declared elsewhere (ex: variables defined as "global" by eslint)
scope.variables.forEach(variable => {
if (!variable.defs.length && global.hasOwnProperty(variable.name)) {
variable.references.forEach(report);
variable.references.forEach(report)

Check warning on line 132 in lib/rules/no-removed-apis.js

View check run for this annotation

Codecov / codecov/patch

lib/rules/no-removed-apis.js#L132

Added line #L132 was not covered by tests
}
});
})

// Report variables not declared at all
scope.through.forEach(reference => {
if (global.hasOwnProperty(reference.identifier.name)) {
report(reference);
report(reference)
}
});
})
}
};
}
}
};
}
108 changes: 108 additions & 0 deletions lib/utils/version-parser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
const { XMLParser } = require('fast-xml-parser')
const fs = require('node:fs')
const path = require('node:path')
const semver = require('semver')

/**
* Check if a given path exists and is a directory
*
* @param {string} filePath The path
* @return {boolean}
*/
const isDirectory = (filePath) => {
const stats = fs.lstatSync(filePath, { throwIfNoEntry: false })
return stats !== undefined && stats.isDirectory()
}

/**
* Check if a given path exists and is a directory
*
* @param {string} filePath The path
* @return {boolean}
*/
const isFile = (filePath) => {
const stats = fs.lstatSync(filePath, { throwIfNoEntry: false })
return stats !== undefined && stats.isFile()
}

/**
* Find the path of nearest `appinfo/info.xml` relative to given path
*
* @param {string} currentPath
* @return {string|undefined} Either the full path including the `info.xml` part or `undefined` if no found
*/
const findAppinfo = (currentPath) => {
while (currentPath && currentPath !== path.sep) {
const appinfoPath = `${currentPath}${path.sep}appinfo`
if (isDirectory(appinfoPath) && isFile(`${appinfoPath}${path.sep}info.xml`)) {
return `${appinfoPath}${path.sep}info.xml`
}
currentPath = path.resolve(currentPath, '..')
}
return undefined
}

/**
* Make sure that versions like '25' can be handled by semver
*
* @param {string} version The pure version string
* @return {string} Sanitized version string
*/
const sanitizeTargetVersion = (version) => {
let sanitizedVersion = version
const sections = sanitizedVersion.split('.').length
if (sections < 3) {
sanitizedVersion = sanitizedVersion + '.0'.repeat(3 - sections)
}
// now version should look like '25.0.0'
if (!semver.valid(sanitizedVersion)) {
throw Error(`[@nextcloud/eslint-plugin] Invalid target version ${version} found`)
}
return sanitizedVersion
}

function createVersionValidator({ cwd, physicalFilename, options }) {
const settings = options[0]

if (settings?.targetVersion) {
// check if the rule version is lower than the current target version
const maxVersion = sanitizeTargetVersion(settings.targetVersion)
return (version) => semver.lte(version, maxVersion)
}

// Try to find appinfo and parse the supported version
if (settings?.parseAppInfo) {
// Current working directory, either the filename (can be empty) or the cwd property
const currentDirectory = path.isAbsolute(physicalFilename)
? path.resolve(path.dirname(physicalFilename))
: path.dirname(path.resolve(cwd, physicalFilename))

// The nearest appinfo
const appinfoPath = findAppinfo(currentDirectory)
if (appinfoPath) {
const parser = new XMLParser({
attributeNamePrefix: '@',
ignoreAttributes: false,
})
const xml = parser.parse(fs.readFileSync(appinfoPath))
let maxVersion = xml?.info?.dependencies?.nextcloud?.['@max-version']
if (typeof maxVersion !== 'string') {
throw Error(`[@nextcloud/eslint-plugin] AppInfo does not contain a max-version (location: ${appinfoPath})`)
}
maxVersion = sanitizeTargetVersion(maxVersion)
return (version) => semver.lte(version, maxVersion)
}
throw Error('[@nextcloud/eslint-plugin] AppInfo parsing was enabled, but no `appinfo/info.xml` was found.')
}

// If not configured or parsing is disabled, every rule should be handled
return () => true
}

module.exports = {
createVersionValidator,
findAppinfo,
isDirectory,
isFile,
sanitizeTargetVersion,
}
Loading

0 comments on commit 2315a46

Please sign in to comment.