Repository: eslint-community/eslint-plugin-eslint-plugin (js)

Issue:
Rules do not work with named exports
Hello, and thanks for the plugin.

I tried to use these rules on my plugin today, but none of them seem to work, meaning that none of the rules were triggered.
I think this is because this plugin only looks for code matching `export default foo` or `module.exports = foo`.

My plugin does not use these and instead uses named exports. My plugin is a modern ESLint plugin written in TypeScript, and [using default exports in TypeScript is considered an anti-pattern](https://basarat.gitbook.io/typescript/main-1/defaultisbad).

For example, here is what [one of my rules](https://github.com/IsaacScript/isaacscript/blob/main/packages/eslint-plugin-isaacscript/src/rules/no-explicit-array-loops.ts) looks like:

```ts
import { ESLintUtils } from "@typescript-eslint/utils";

const createRule = ESLintUtils.RuleCreator(
  (ruleName) =>
    `https://github.com/IsaacScript/isaacscript/blob/main/packages/eslint-plugin-isaacscript/docs/rules/${ruleName}.md`,
);

export type Options = [];
export type MessageIds = "noExplicitArray";

export const noExplicitArrayLoops = createRule<Options, MessageIds>({
  name: "no-explicit-array-loops",
  meta: {
    type: "problem",
    docs: {
      description: "Disallows explicit iteration for arrays",
      recommended: "recommended",
      requiresTypeChecking: true,
    },
    schema: [],
    messages: {
      noExplicitArray:
        'Explicit iteration over arrays is disallowed. (Get rid of the "Object.values()".)',
    },
    fixable: "code",
  },
  defaultOptions: [],
  create(context) {
    // Rule logic here.
  },
});
```

Thus, I propose that the "detect rule" logic of this plugin is updated in some way. If you can advise on what the correct solution is, I can submit a pull request, as I would love to use the rules that this project offers. Thank you.

Pull request description:
feat: support named exports in ESM/TS
This adds support for named exports in ESM (whether it be TS or not).

For example:

```ts
export const rule: RuleModule = {
  create: () => { ... };
};
```

Also, exported symbols:

```ts
const rule = { ... };

export {rule};
```

While ESLint plugins are still usually CJS at time of writing this, many are written as ESM sources, and an increasing number are using named exports.

### Reviewer notes

You'll want to hide whitespace changes when reviewing this, as prettier reformatted a bunch of it thanks to changing the level of nesting/chaining.

Fixes #375

Patch:
diff --git a/lib/utils.js b/lib/utils.js
index 4ad42c2..da5faa7 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -118,55 +118,82 @@ function isTypeScriptRuleHelper(node) {
  * Helper for `getRuleInfo`. Handles ESM and TypeScript rules.
  */
 function getRuleExportsESM(ast, scopeManager) {
-  return ast.body
-    .filter((statement) =>
-      [
-        'ExportDefaultDeclaration', // export default rule;
-        'TSExportAssignment', // export = rule;
-      ].includes(statement.type)
-    )
-    .map((statement) => statement.declaration || statement.expression)
-
-    .reduce((currentExports, node) => {
-      if (node.type === 'ObjectExpression') {
-        // Check `export default { create() {}, meta: {} }`
-        return collectInterestingProperties(
-          node.properties,
-          INTERESTING_RULE_KEYS
-        );
-      } else if (isFunctionRule(node)) {
-        // Check `export default function(context) { return { ... }; }`
-        return { create: node, meta: null, isNewStyle: false };
-      } else if (isTypeScriptRuleHelper(node)) {
-        // Check `export default someTypeScriptHelper({ create() {}, meta: {} });
-        return collectInterestingProperties(
-          node.arguments[0].properties,
-          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') {
-            // Check `const possibleRule = { ... }; export default possibleRule;
-            return collectInterestingProperties(
-              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(
-              possibleRule.arguments[0].properties,
-              INTERESTING_RULE_KEYS
-            );
+  const possibleNodes = [];
+
+  for (const statement of ast.body) {
+    switch (statement.type) {
+      // export default rule;
+      case 'ExportDefaultDeclaration': {
+        possibleNodes.push(statement.declaration);
+        break;
+      }
+      // export = rule;
+      case 'TSExportAssignment': {
+        possibleNodes.push(statement.expression);
+        break;
+      }
+      // export const rule = { ... };
+      // or export {rule};
+      case 'ExportNamedDeclaration': {
+        for (const specifier of statement.specifiers) {
+          possibleNodes.push(specifier.local);
+        }
+        if (statement.declaration) {
+          if (statement.declaration.type === 'VariableDeclaration') {
+            for (const declarator of statement.declaration.declarations) {
+              if (declarator.init) {
+                possibleNodes.push(declarator.init);
+              }
+            }
+          } else {
+            possibleNodes.push(statement.declaration);
           }
         }
+        break;
       }
-      return currentExports;
-    }, {});
+    }
+  }
+
+  return possibleNodes.reduce((currentExports, node) => {
+    if (node.type === 'ObjectExpression') {
+      // Check `export default { create() {}, meta: {} }`
+      return collectInterestingProperties(
+        node.properties,
+        INTERESTING_RULE_KEYS
+      );
+    } else if (isFunctionRule(node)) {
+      // Check `export default function(context) { return { ... }; }`
+      return { create: node, meta: null, isNewStyle: false };
+    } else if (isTypeScriptRuleHelper(node)) {
+      // Check `export default someTypeScriptHelper({ create() {}, meta: {} });
+      return collectInterestingProperties(
+        node.arguments[0].properties,
+        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') {
+          // Check `const possibleRule = { ... }; export default possibleRule;
+          return collectInterestingProperties(
+            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(
+            possibleRule.arguments[0].properties,
+            INTERESTING_RULE_KEYS
+          );
+        }
+      }
+    }
+    return currentExports;
+  }, {});
 }
 
 /**


Test patch:
diff --git a/tests/lib/utils.js b/tests/lib/utils.js
index fe77182..e90ecd6 100644
--- a/tests/lib/utils.js
+++ b/tests/lib/utils.js
@@ -50,6 +50,12 @@ describe('utils', () => {
         'module.exports = createESLintRule({ create() {}, meta: {} });',
         'module.exports = util.createRule({ create() {}, meta: {} });',
         'module.exports = ESLintUtils.RuleCreator(docsUrl)({ create() {}, meta: {} });',
+
+        // Named export of a rule, only supported in ESM within this plugin
+        'module.exports.rule = { create: function() {} };',
+        'exports.rule = { create: function() {} };',
+        'const rule = { create: function() {} }; module.exports.rule = rule;',
+        'const rule = { create: function() {} }; exports.rule = rule;',
       ].forEach((noRuleCase) => {
         it(`returns null for ${noRuleCase}`, () => {
           const ast = espree.parse(noRuleCase, { ecmaVersion: 8, range: true });
@@ -65,15 +71,11 @@ describe('utils', () => {
     describe('the file does not have a valid rule (ESM)', () => {
       [
         '',
-        'export const foo = { create() {} }',
         '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 {}; }',
-
         // Exports function but no object return inside function.
         'export default function (context) { }',
         'export default function (context) { return; }',
@@ -209,6 +211,12 @@ describe('utils', () => {
           meta: { type: 'ObjectExpression' },
           isNewStyle: true,
         },
+        // No helper, exported variable.
+        'export const rule = { create() {}, meta: {} };': {
+          create: { type: 'FunctionExpression' },
+          meta: { type: 'ObjectExpression' },
+          isNewStyle: true,
+        },
         // no helper, variable with type.
         'const rule: Rule.RuleModule = { create() {}, meta: {} }; export default rule;':
           {
@@ -216,6 +224,33 @@ describe('utils', () => {
             meta: { type: 'ObjectExpression' },
             isNewStyle: true,
           },
+        // no helper, exported variable with type.
+        'export const rule: Rule.RuleModule = { create() {}, meta: {} };': {
+          create: { type: 'FunctionExpression' },
+          meta: { type: 'ObjectExpression' },
+          isNewStyle: true,
+        },
+        // no helper, exported reference with type.
+        'const rule: Rule.RuleModule = { create() {}, meta: {} }; export {rule};':
+          {
+            create: { type: 'FunctionExpression' },
+            meta: { type: 'ObjectExpression' },
+            isNewStyle: true,
+          },
+        // no helper, exported aliased reference with type.
+        'const foo: Rule.RuleModule = { create() {}, meta: {} }; export {foo as rule};':
+          {
+            create: { type: 'FunctionExpression' },
+            meta: { type: 'ObjectExpression' },
+            isNewStyle: true,
+          },
+        // no helper, exported variable with type in multiple declarations
+        'export const foo = 5, rule: Rule.RuleModule = { create() {}, meta: {} };':
+          {
+            create: { type: 'FunctionExpression' },
+            meta: { type: 'ObjectExpression' },
+            isNewStyle: true,
+          },
         // No helper, variable, `export =` syntax.
         'const rule = { create() {}, meta: {} }; export = rule;': {
           create: { type: 'FunctionExpression' },
@@ -474,6 +509,16 @@ describe('utils', () => {
             meta: { type: 'ObjectExpression' },
             isNewStyle: true,
           },
+        'export const rule = { create() {}, meta: {} };': {
+          create: { type: 'FunctionExpression' },
+          meta: { type: 'ObjectExpression' },
+          isNewStyle: true,
+        },
+        'const rule = { create() {}, meta: {} }; export {rule};': {
+          create: { type: 'FunctionExpression' },
+          meta: { type: 'ObjectExpression' },
+          isNewStyle: true,
+        },
 
         // ESM (function style)
         'export default function (context) { return {}; }': {