From abefd1efc9d9adc0142b2c7527af8d5f3ec60377 Mon Sep 17 00:00:00 2001
From: Bryan Mishkin <698306+bmish@users.noreply.github.com>
Date: Sun, 19 Jun 2022 22:42:45 -0400
Subject: [PATCH 1/3] feat: handle properties behind spread syntax in
`require-meta-*` rules (#251)
* fix: handle meta properties in spread syntax
* add unit tests
---
lib/rules/require-meta-docs-description.js | 17 ++--
lib/rules/require-meta-docs-url.js | 25 +++---
lib/rules/require-meta-fixable.js | 14 ++--
lib/rules/require-meta-has-suggestions.js | 15 ++--
lib/rules/require-meta-schema.js | 7 +-
lib/rules/require-meta-type.js | 10 +--
lib/utils.js | 24 ++++++
.../rules/require-meta-docs-description.js | 11 ++-
tests/lib/rules/require-meta-docs-url.js | 58 ++++++++++++++
tests/lib/rules/require-meta-fixable.js | 12 ++-
.../lib/rules/require-meta-has-suggestions.js | 14 +++-
tests/lib/rules/require-meta-schema.js | 10 ++-
tests/lib/rules/require-meta-type.js | 10 ++-
tests/lib/utils.js | 77 +++++++++++++++++++
14 files changed, 246 insertions(+), 58 deletions(-)
diff --git a/lib/rules/require-meta-docs-description.js b/lib/rules/require-meta-docs-description.js
index d3346eb5..226efa33 100644
--- a/lib/rules/require-meta-docs-description.js
+++ b/lib/rules/require-meta-docs-description.js
@@ -45,6 +45,7 @@ module.exports = {
return {
Program() {
const sourceCode = context.getSourceCode();
+ const { scopeManager } = sourceCode;
const info = utils.getRuleInfo(sourceCode);
if (info === null) {
@@ -57,17 +58,13 @@ module.exports = {
: DEFAULT_PATTERN;
const metaNode = info.meta;
- const docsNode =
- metaNode &&
- metaNode.properties &&
- metaNode.properties.find(
- (p) => p.type === 'Property' && utils.getKeyName(p) === 'docs'
- );
+ const docsNode = utils
+ .evaluateObjectProperties(metaNode, scopeManager)
+ .find((p) => p.type === 'Property' && utils.getKeyName(p) === 'docs');
- const descriptionNode =
- docsNode &&
- docsNode.value.properties &&
- docsNode.value.properties.find(
+ const descriptionNode = utils
+ .evaluateObjectProperties(docsNode && docsNode.value, scopeManager)
+ .find(
(p) =>
p.type === 'Property' && utils.getKeyName(p) === 'description'
);
diff --git a/lib/rules/require-meta-docs-url.js b/lib/rules/require-meta-docs-url.js
index 6c0dee2b..49324675 100644
--- a/lib/rules/require-meta-docs-url.js
+++ b/lib/rules/require-meta-docs-url.js
@@ -50,7 +50,6 @@ module.exports = {
*/
create(context) {
const options = context.options[0] || {};
- const sourceCode = context.getSourceCode();
const filename = context.getFilename();
const ruleName =
filename === ''
@@ -75,24 +74,24 @@ module.exports = {
return {
Program() {
+ const sourceCode = context.getSourceCode();
+ const { scopeManager } = sourceCode;
+
const info = util.getRuleInfo(sourceCode);
if (info === null) {
return;
}
const metaNode = info.meta;
- const docsPropNode =
- metaNode &&
- metaNode.properties &&
- metaNode.properties.find(
- (p) => p.type === 'Property' && util.getKeyName(p) === 'docs'
- );
- const urlPropNode =
- docsPropNode &&
- docsPropNode.value.properties &&
- docsPropNode.value.properties.find(
- (p) => p.type === 'Property' && util.getKeyName(p) === 'url'
- );
+ const docsPropNode = util
+ .evaluateObjectProperties(metaNode, scopeManager)
+ .find((p) => p.type === 'Property' && util.getKeyName(p) === 'docs');
+ const urlPropNode = util
+ .evaluateObjectProperties(
+ docsPropNode && docsPropNode.value,
+ scopeManager
+ )
+ .find((p) => p.type === 'Property' && util.getKeyName(p) === 'url');
const staticValue = urlPropNode
? getStaticValue(urlPropNode.value, context.getScope())
diff --git a/lib/rules/require-meta-fixable.js b/lib/rules/require-meta-fixable.js
index 506e7619..e03d3ae3 100644
--- a/lib/rules/require-meta-fixable.js
+++ b/lib/rules/require-meta-fixable.js
@@ -48,6 +48,7 @@ module.exports = {
context.options[0] && context.options[0].catchNoFixerButFixableProperty;
const sourceCode = context.getSourceCode();
+ const { scopeManager } = sourceCode;
const ruleInfo = utils.getRuleInfo(sourceCode);
let contextIdentifiers;
let usesFixFunctions;
@@ -62,10 +63,7 @@ module.exports = {
return {
Program(ast) {
- contextIdentifiers = utils.getContextIdentifiers(
- sourceCode.scopeManager,
- ast
- );
+ contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast);
},
CallExpression(node) {
if (
@@ -86,11 +84,9 @@ module.exports = {
'Program:exit'() {
const metaFixableProp =
ruleInfo &&
- ruleInfo.meta &&
- ruleInfo.meta.type === 'ObjectExpression' &&
- ruleInfo.meta.properties.find(
- (prop) => utils.getKeyName(prop) === 'fixable'
- );
+ utils
+ .evaluateObjectProperties(ruleInfo.meta, scopeManager)
+ .find((prop) => utils.getKeyName(prop) === 'fixable');
if (metaFixableProp) {
const staticValue = getStaticValue(
diff --git a/lib/rules/require-meta-has-suggestions.js b/lib/rules/require-meta-has-suggestions.js
index ccaf93e0..939b5175 100644
--- a/lib/rules/require-meta-has-suggestions.js
+++ b/lib/rules/require-meta-has-suggestions.js
@@ -30,16 +30,14 @@ module.exports = {
create(context) {
const sourceCode = context.getSourceCode();
+ const { scopeManager } = sourceCode;
const ruleInfo = utils.getRuleInfo(sourceCode);
let contextIdentifiers;
let ruleReportsSuggestions;
return {
Program(ast) {
- contextIdentifiers = utils.getContextIdentifiers(
- sourceCode.scopeManager,
- ast
- );
+ contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast);
},
CallExpression(node) {
if (
@@ -78,12 +76,9 @@ module.exports = {
},
'Program:exit'() {
const metaNode = ruleInfo && ruleInfo.meta;
- const hasSuggestionsProperty =
- metaNode && metaNode.type === 'ObjectExpression'
- ? metaNode.properties.find(
- (prop) => utils.getKeyName(prop) === 'hasSuggestions'
- )
- : undefined;
+ const hasSuggestionsProperty = utils
+ .evaluateObjectProperties(metaNode, scopeManager)
+ .find((prop) => utils.getKeyName(prop) === 'hasSuggestions');
const hasSuggestionsStaticValue =
hasSuggestionsProperty &&
getStaticValue(hasSuggestionsProperty.value, context.getScope());
diff --git a/lib/rules/require-meta-schema.js b/lib/rules/require-meta-schema.js
index 4f218f3c..0789bc6a 100644
--- a/lib/rules/require-meta-schema.js
+++ b/lib/rules/require-meta-schema.js
@@ -64,10 +64,9 @@ module.exports = {
Program(ast) {
contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast);
- schemaNode =
- metaNode &&
- metaNode.properties &&
- metaNode.properties.find(
+ schemaNode = utils
+ .evaluateObjectProperties(metaNode, scopeManager)
+ .find(
(p) => p.type === 'Property' && utils.getKeyName(p) === 'schema'
);
diff --git a/lib/rules/require-meta-type.js b/lib/rules/require-meta-type.js
index 12082b3c..d06e6d62 100644
--- a/lib/rules/require-meta-type.js
+++ b/lib/rules/require-meta-type.js
@@ -41,6 +41,7 @@ module.exports = {
return {
Program() {
const sourceCode = context.getSourceCode();
+ const { scopeManager } = sourceCode;
const info = utils.getRuleInfo(sourceCode);
if (info === null) {
@@ -48,12 +49,9 @@ module.exports = {
}
const metaNode = info.meta;
- const typeNode =
- metaNode &&
- metaNode.properties &&
- metaNode.properties.find(
- (p) => p.type === 'Property' && utils.getKeyName(p) === 'type'
- );
+ const typeNode = utils
+ .evaluateObjectProperties(metaNode, scopeManager)
+ .find((p) => p.type === 'Property' && utils.getKeyName(p) === 'type');
if (!typeNode) {
context.report({
diff --git a/lib/utils.js b/lib/utils.js
index 6eef18ad..26138755 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -738,4 +738,28 @@ module.exports = {
).suggest === parent.parent.parent
);
},
+
+ /**
+ * List all properties contained in an object.
+ * Evaluates and includes any properties that may be behind spreads.
+ * @param {Node} objectNode
+ * @param {ScopeManager} scopeManager
+ * @returns {Node[]} the list of all properties that could be found
+ */
+ evaluateObjectProperties(objectNode, scopeManager) {
+ if (!objectNode || objectNode.type !== 'ObjectExpression') {
+ return [];
+ }
+
+ return objectNode.properties.flatMap((property) => {
+ if (property.type === 'SpreadElement') {
+ const value = findVariableValue(property.argument, scopeManager);
+ if (value && value.type === 'ObjectExpression') {
+ return value.properties;
+ }
+ return [];
+ }
+ return [property];
+ });
+ },
};
diff --git a/tests/lib/rules/require-meta-docs-description.js b/tests/lib/rules/require-meta-docs-description.js
index dc9849c4..1b40d7e2 100644
--- a/tests/lib/rules/require-meta-docs-description.js
+++ b/tests/lib/rules/require-meta-docs-description.js
@@ -11,7 +11,7 @@ const RuleTester = require('eslint').RuleTester;
// Tests
// ------------------------------------------------------------------------------
-const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
+const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 9 } });
ruleTester.run('require-meta-docs-description', rule, {
valid: [
'foo()',
@@ -107,6 +107,15 @@ ruleTester.run('require-meta-docs-description', rule, {
create(context) {}
};
`,
+ // Spread.
+ `
+ const extraDocs = { description: 'enforce foo' };
+ const extraMeta = { docs: { ...extraDocs } };
+ module.exports = {
+ meta: { ...extraMeta },
+ create(context) {}
+ };
+ `,
],
invalid: [
diff --git a/tests/lib/rules/require-meta-docs-url.js b/tests/lib/rules/require-meta-docs-url.js
index e63a7cdf..7aae0093 100644
--- a/tests/lib/rules/require-meta-docs-url.js
+++ b/tests/lib/rules/require-meta-docs-url.js
@@ -155,6 +155,19 @@ tester.run('require-meta-docs-url', rule, {
},
],
},
+ {
+ // Spread.
+ filename: 'test-rule',
+ code: `
+ const extraDocs = { url: "path/to/test-rule.md" };
+ const extraMeta = { docs: { ...extraDocs } };
+ module.exports = {
+ meta: { ...extraMeta },
+ create() {}
+ }
+ `,
+ options: [{ pattern: 'path/to/{{name}}.md' }],
+ },
],
invalid: [
@@ -624,6 +637,51 @@ url: "plugin-name/test.md"
],
errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
},
+ {
+ // URL missing, spreads present.
+ filename: 'test.js',
+ code: `
+ const extraDocs = { };
+ const extraMeta = { docs: { ...extraDocs } };
+ module.exports = {
+ meta: { ...extraMeta },
+ create() {}
+ }
+ `,
+ output: `
+ const extraDocs = { };
+ const extraMeta = { docs: { ...extraDocs,
+url: "plugin-name/test.md" } };
+ module.exports = {
+ meta: { ...extraMeta },
+ create() {}
+ }
+ `,
+ options: [{ pattern: 'plugin-name/{{ name }}.md' }],
+ errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
+ },
+ {
+ // URL wrong inside spreads.
+ filename: 'test.js',
+ code: `
+ const extraDocs = { url: 'wrong' };
+ const extraMeta = { docs: { ...extraDocs } };
+ module.exports = {
+ meta: { ...extraMeta },
+ create() {}
+ }
+ `,
+ output: `
+ const extraDocs = { url: "plugin-name/test.md" };
+ const extraMeta = { docs: { ...extraDocs } };
+ module.exports = {
+ meta: { ...extraMeta },
+ create() {}
+ }
+ `,
+ options: [{ pattern: 'plugin-name/{{ name }}.md' }],
+ errors: [{ messageId: 'mismatch', type: 'Literal' }],
+ },
{
// CJS file extension
filename: 'test.cjs',
diff --git a/tests/lib/rules/require-meta-fixable.js b/tests/lib/rules/require-meta-fixable.js
index 3fe38572..75de8443 100644
--- a/tests/lib/rules/require-meta-fixable.js
+++ b/tests/lib/rules/require-meta-fixable.js
@@ -16,7 +16,7 @@ const RuleTester = require('eslint').RuleTester;
// Tests
// ------------------------------------------------------------------------------
-const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
+const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 9 } });
ruleTester.run('require-meta-fixable', rule, {
valid: [
// No `meta`.
@@ -189,6 +189,16 @@ ruleTester.run('require-meta-fixable', rule, {
`,
options: [{ catchNoFixerButFixableProperty: true }],
},
+ // Spread.
+ `
+ const extra = { 'fixable': 'code' };
+ module.exports = {
+ meta: { ...extra },
+ create(context) {
+ context.report({node, message, fix: foo});
+ }
+ };
+ `,
],
invalid: [
diff --git a/tests/lib/rules/require-meta-has-suggestions.js b/tests/lib/rules/require-meta-has-suggestions.js
index 8c357f1e..fa3adc60 100644
--- a/tests/lib/rules/require-meta-has-suggestions.js
+++ b/tests/lib/rules/require-meta-has-suggestions.js
@@ -11,7 +11,7 @@ const RuleTester = require('eslint').RuleTester;
// Tests
// ------------------------------------------------------------------------------
-const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
+const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 9 } });
ruleTester.run('require-meta-has-suggestions', rule, {
valid: [
'module.exports = context => { return {}; };',
@@ -171,7 +171,7 @@ ruleTester.run('require-meta-has-suggestions', rule, {
}
};
`,
- // Spread syntax.
+ // Unrelated spread syntax.
{
code: `
const extra = {};
@@ -185,6 +185,16 @@ ruleTester.run('require-meta-has-suggestions', rule, {
ecmaVersion: 9,
},
},
+ // Related spread.
+ `
+ const extra = { hasSuggestions: true };
+ module.exports = {
+ meta: { ...extra },
+ create(context) {
+ context.report({node, message, suggest: [{}]});
+ }
+ };
+ `,
],
invalid: [
diff --git a/tests/lib/rules/require-meta-schema.js b/tests/lib/rules/require-meta-schema.js
index 789aff94..a0c2a4d0 100644
--- a/tests/lib/rules/require-meta-schema.js
+++ b/tests/lib/rules/require-meta-schema.js
@@ -11,7 +11,7 @@ const RuleTester = require('eslint').RuleTester;
// Tests
// ------------------------------------------------------------------------------
-const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
+const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 9 } });
ruleTester.run('require-meta-schema', rule, {
valid: [
`
@@ -109,6 +109,14 @@ ruleTester.run('require-meta-schema', rule, {
code: 'module.exports = { create(context) {} };',
options: [{ requireSchemaPropertyWhenOptionless: false }],
},
+ // Spread.
+ `
+ const extra = { schema: [] };
+ module.exports = {
+ meta: { ...extra },
+ create(context) {}
+ };
+ `,
],
invalid: [
diff --git a/tests/lib/rules/require-meta-type.js b/tests/lib/rules/require-meta-type.js
index cb05f6d1..e4bfbd71 100644
--- a/tests/lib/rules/require-meta-type.js
+++ b/tests/lib/rules/require-meta-type.js
@@ -16,7 +16,7 @@ const RuleTester = require('eslint').RuleTester;
// Tests
// ------------------------------------------------------------------------------
-const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
+const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 9 } });
ruleTester.run('require-meta-type', rule, {
valid: [
`
@@ -76,6 +76,14 @@ ruleTester.run('require-meta-type', rule, {
`,
errors: [{ messageId: 'missing' }],
},
+ // Spread.
+ `
+ const extra = { type: 'problem' };
+ module.exports = {
+ meta: { ...extra },
+ create(context) {}
+ };
+ `,
],
invalid: [
diff --git a/tests/lib/utils.js b/tests/lib/utils.js
index 83a57d50..9b797706 100644
--- a/tests/lib/utils.js
+++ b/tests/lib/utils.js
@@ -1274,4 +1274,81 @@ describe('utils', () => {
});
});
});
+
+ describe('evaluateObjectProperties', function () {
+ it('behaves correctly with simple object expression', function () {
+ const ast = espree.parse('const obj = { a: 123, b: foo() };', {
+ ecmaVersion: 9,
+ range: true,
+ });
+ const scopeManager = eslintScope.analyze(ast);
+ const result = utils.evaluateObjectProperties(
+ ast.body[0].declarations[0].init,
+ scopeManager
+ );
+ assert.deepEqual(result, ast.body[0].declarations[0].init.properties);
+ });
+
+ it('behaves correctly with spreads of objects', function () {
+ const ast = espree.parse(
+ `
+ const extra1 = { a: 123 };
+ const extra2 = { b: 456 };
+ const obj = { ...extra1, c: 789, ...extra2 };
+ `,
+ {
+ ecmaVersion: 9,
+ range: true,
+ }
+ );
+ const scopeManager = eslintScope.analyze(ast);
+ const result = utils.evaluateObjectProperties(
+ ast.body[2].declarations[0].init,
+ scopeManager
+ );
+ assert.deepEqual(result, [
+ ...ast.body[0].declarations[0].init.properties, // First spread properties
+ ...ast.body[2].declarations[0].init.properties.filter(
+ (property) => property.type !== 'SpreadElement'
+ ), // Non-spread properties
+ ...ast.body[1].declarations[0].init.properties, // Second spread properties
+ ]);
+ });
+
+ it('behaves correctly with non-variable spreads', function () {
+ const ast = espree.parse(`function foo() {} const obj = { ...foo() };`, {
+ ecmaVersion: 9,
+ range: true,
+ });
+ const scopeManager = eslintScope.analyze(ast);
+ const result = utils.evaluateObjectProperties(
+ ast.body[1].declarations[0].init,
+ scopeManager
+ );
+ assert.deepEqual(result, []);
+ });
+
+ it('behaves correctly with spread with variable that cannot be found', function () {
+ const ast = espree.parse(`const obj = { ...foo };`, {
+ ecmaVersion: 9,
+ range: true,
+ });
+ const scopeManager = eslintScope.analyze(ast);
+ const result = utils.evaluateObjectProperties(
+ ast.body[0].declarations[0].init,
+ scopeManager
+ );
+ assert.deepEqual(result, []);
+ });
+
+ it('behaves correctly when passed wrong node type', function () {
+ const ast = espree.parse(`foo();`, {
+ ecmaVersion: 9,
+ range: true,
+ });
+ const scopeManager = eslintScope.analyze(ast);
+ const result = utils.evaluateObjectProperties(ast.body[0], scopeManager);
+ assert.deepEqual(result, []);
+ });
+ });
});
From 09b243b05e2664f79235e0b7fd91969852e4fba7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=94=AF=E7=84=B6?=
Date: Mon, 20 Jun 2022 10:54:21 +0800
Subject: [PATCH 2/3] chore: skip release-it checking npm
---
package.json | 3 +++
1 file changed, 3 insertions(+)
diff --git a/package.json b/package.json
index 8d93eeec..a9b9cdc0 100644
--- a/package.json
+++ b/package.json
@@ -84,6 +84,9 @@
},
"github": {
"release": true
+ },
+ "npm": {
+ "skipChecks": true
}
}
}
From 34bcb742d12b11b6d71f03dca41a4ef3c666ff62 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=94=AF=E7=84=B6?=
Date: Mon, 20 Jun 2022 10:55:46 +0800
Subject: [PATCH 3/3] chore: release v4.3.0
---
CHANGELOG.md | 7 +++++++
package.json | 2 +-
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 742ddaeb..1303fc58 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
+## [4.3.0](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/compare/v4.2.0...v4.3.0) (2022-06-20)
+
+
+### Features
+
+* handle properties behind spread syntax in `require-meta-*` rules ([#251](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/251)) ([abefd1e](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/abefd1efc9d9adc0142b2c7527af8d5f3ec60377))
+
## [4.2.0](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/compare/v4.1.0...v4.2.0) (2022-05-16)
diff --git a/package.json b/package.json
index a9b9cdc0..cb1844e4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-eslint-plugin",
- "version": "4.2.0",
+ "version": "4.3.0",
"description": "An ESLint plugin for linting ESLint plugins",
"author": "Teddy Katz",
"main": "lib/index.js",