Repository: not-an-aardvark/eslint-plugin-eslint-plugin
Pull request #235 (2021-12-13 15:08:53)

fix: detect function-style rules exported using a variable
Updates our rules to detect old function-style rules that are defined in variables:

```js
const rule = function(context) { return {} };
export default rule;
```

```js
const rule = function(context) { return {} };
module.exports = rule;
```

In particular, this is needed so that our [eslint-plugin/prefer-object-rule](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-object-rule.md) can detect and automatically convert these old function-style rules to the new object-style format.

Follow-up to:
* https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/pull/233
* https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/pull/177
* https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/pull/225


Patch:
diff --git a/lib/utils.js b/lib/utils.js
index 0364602..646b472 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 {};


Test patch:
diff --git a/tests/lib/rules/prefer-object-rule.js b/tests/lib/rules/prefer-object-rule.js
index 5189da8..b3eb633 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 49e54f1..e63a7cd 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 8a45790..5b9063f 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<Options, MessageIds>(123);',
         'export default foo.bar()<Options, MessageIds>(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) => {