From 2fa44c65c04b4da33e7caecbf2faffeb90c172e9 Mon Sep 17 00:00:00 2001 From: Bryan Mishkin <698306+bmish@users.noreply.github.com> Date: Mon, 13 Dec 2021 10:05:25 -0500 Subject: [PATCH] fix: detect function-style rules exported using a variable --- lib/utils.js | 22 ++++++++++++----- tests/lib/rules/prefer-object-rule.js | 13 ++++++++++ tests/lib/rules/require-meta-docs-url.js | 30 ++++++++++++++++++++++++ tests/lib/utils.js | 13 ++++++++++ 4 files changed, 72 insertions(+), 6 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index 03646026..646b4720 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -141,6 +141,7 @@ function getRuleExportsESM(ast, scopeManager) { INTERESTING_RULE_KEYS ); } else if (node.type === 'Identifier') { + // Rule could be stored in a variable before being exported. const possibleRule = findVariableValue(node, scopeManager); if (possibleRule) { if (possibleRule.type === 'ObjectExpression') { @@ -149,6 +150,9 @@ function getRuleExportsESM(ast, scopeManager) { possibleRule.properties, INTERESTING_RULE_KEYS ); + } else if (isFunctionRule(possibleRule)) { + // Check `const possibleRule = function(context) { return { ... } }; export default possibleRule;` + return { create: possibleRule, meta: null, isNewStyle: false }; } else if (isTypeScriptRuleHelper(possibleRule)) { // Check `const possibleRule = someTypeScriptHelper({ ... }); export default possibleRule; return collectInterestingProperties( @@ -197,13 +201,19 @@ function getRuleExportsCJS(ast, scopeManager) { INTERESTING_RULE_KEYS ); } else if (node.right.type === 'Identifier') { + // Rule could be stored in a variable before being exported. const possibleRule = findVariableValue(node.right, scopeManager); - if (possibleRule && possibleRule.type === 'ObjectExpression') { - // Check `const possibleRule = { ... }; module.exports = possibleRule; - return collectInterestingProperties( - possibleRule.properties, - INTERESTING_RULE_KEYS - ); + if (possibleRule) { + if (possibleRule.type === 'ObjectExpression') { + // Check `const possibleRule = { ... }; module.exports = possibleRule; + return collectInterestingProperties( + possibleRule.properties, + INTERESTING_RULE_KEYS + ); + } else if (isFunctionRule(possibleRule)) { + // Check `const possibleRule = function(context) { return { ... } }; module.exports = possibleRule;` + return { create: possibleRule, meta: null, isNewStyle: false }; + } } } return {}; diff --git a/tests/lib/rules/prefer-object-rule.js b/tests/lib/rules/prefer-object-rule.js index 5189da81..b3eb6336 100644 --- a/tests/lib/rules/prefer-object-rule.js +++ b/tests/lib/rules/prefer-object-rule.js @@ -121,6 +121,12 @@ ruleTester.run('prefer-object-rule', rule, { `, errors: [{ messageId: 'preferObject', line: 2, column: 26 }], }, + { + code: 'const rule = (context) => { return {}; }; module.exports = rule;', + output: + 'const rule = {create: (context) => { return {}; }}; module.exports = rule;', + errors: [{ messageId: 'preferObject', line: 1, column: 14 }], + }, // ESM { @@ -149,5 +155,12 @@ ruleTester.run('prefer-object-rule', rule, { parserOptions: { sourceType: 'module' }, errors: [{ messageId: 'preferObject', line: 1, column: 16 }], }, + { + code: 'const rule = (context) => { return {}; }; export default rule;', + output: + 'const rule = {create: (context) => { return {}; }}; export default rule;', + parserOptions: { sourceType: 'module' }, + errors: [{ messageId: 'preferObject', line: 1, column: 14 }], + }, ], }); diff --git a/tests/lib/rules/require-meta-docs-url.js b/tests/lib/rules/require-meta-docs-url.js index 49e54f1f..e63a7cdf 100644 --- a/tests/lib/rules/require-meta-docs-url.js +++ b/tests/lib/rules/require-meta-docs-url.js @@ -1012,5 +1012,35 @@ url: "plugin-name/test.md" ], errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, + { + // Function rule in variable. + filename: 'test.js', + code: `const rule = function(context) { return {}; }; module.exports = rule;`, + output: null, + options: [{ pattern: 'plugin-name/{{ name }}.md' }], + errors: [ + { + message: '`meta.docs.url` property is missing.', + type: 'FunctionExpression', + }, + ], + }, + { + // Object rule in variable. + filename: 'test.js', + code: `const rule = { create: function(context) { return {}; }, meta: {} }; module.exports = rule;`, + output: `const rule = { create: function(context) { return {}; }, meta: { +docs: { +url: "plugin-name/test.md" +} +} }; module.exports = rule;`, + options: [{ pattern: 'plugin-name/{{ name }}.md' }], + errors: [ + { + message: '`meta.docs.url` property is missing.', + type: 'ObjectExpression', + }, + ], + }, ], }); diff --git a/tests/lib/utils.js b/tests/lib/utils.js index 8a45790b..5b9063f9 100644 --- a/tests/lib/utils.js +++ b/tests/lib/utils.js @@ -69,6 +69,7 @@ describe('utils', () => { 'export default { foo: {} }', 'const foo = {}; export default foo', 'const foo = 123; export default foo', + 'const foo = function(){}; export default foo', // Exports function but not default export. 'export function foo (context) { return {}; }', @@ -116,6 +117,8 @@ describe('utils', () => { 'export default foo.bar(123);', 'export default foo.bar()(123);', 'const notRule = foo(); export default notRule;', + 'const notRule = function(){}; export default notRule;', + 'const notRule = {}; export default notRule;', ].forEach((noRuleCase) => { it(`returns null for ${noRuleCase}`, () => { const ast = typescriptEslintParser.parse(noRuleCase, { @@ -347,6 +350,11 @@ describe('utils', () => { meta: { type: 'ObjectExpression' }, isNewStyle: true, }, + 'const rule = function(context) {return{};}; module.exports = rule;': { + create: { type: 'FunctionExpression' }, + meta: null, + isNewStyle: false, + }, }; Object.keys(CASES).forEach((ruleSource) => { @@ -421,6 +429,11 @@ describe('utils', () => { meta: null, isNewStyle: false, }, + 'const rule = function(context) {return {};}; export default rule;': { + create: { type: 'FunctionExpression' }, + meta: null, + isNewStyle: false, + }, }; Object.keys(CASES).forEach((ruleSource) => {