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, []); + }); + }); });