Repository: sarbbottam/eslint-find-rules
Pull request #354 (2024-10-04 00:13:21)

feature: Support Flat configs in v8
## Description
- Handle flat config in `rule-finder.js`
- Add a new `--flatConfig` flag
- Load ESLint class using FlatESLint class
- Add unit tests for basic flat config


Patch:
diff --git a/README.md b/README.md
index 8e62a31..ecb022a 100644
--- a/README.md
+++ b/README.md
@@ -46,7 +46,7 @@ Then run it with: `$ npm run --silent eslint-find-option-rules` (the `--silent`
 
 ```
 available options are -a|--all-available, -c|--current, -d|--deprecated, -p|--plugin, -u|--unused
-available flags are -n|--no-error, --no-core, -i/--include deprecated, and --ext .js
+available flags are -n|--no-error, --no-core, -i/--include deprecated, --ext .js, and --flatConfig
 ```
 
 By default it will error out only for `-d|--deprecated` and `-u|--unused`,
@@ -58,6 +58,8 @@ By default, deprecated rules will be omitted from the output of `-a|--all-availa
 
 By default, rules will be searched for files having `.js` extension.  If you want to find rules using another extension (`.json` for example), use the `--ext .json` flag (or `--ext .js --ext .json` if you need multiple extensions).
 
+By default, ESLint will handle configs in Legacy mode. If you want to handle Flat config files, you need to add the `--flatConfig` flag.
+
 **NOTE:** Deprecated rules are found by looking at the metadata of the rule definition.  All core rules and many plugin rules use this flag to indicate deprecated rules.  But if you find a plugin that does not mark their rules as deprecated in the rule metadata, please file a pull request with that project.
 
 ### Specify a file
diff --git a/package.json b/package.json
index 497861b..a05d46d 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,7 @@
     "pretest": "npm run lint",
     "tests-only": "mocha --recursive",
     "test": "npm run tests-only",
-    "posttest": "aud --production",
+    "posttest": "npx npm@'>=10.2' audit --production",
     "update-contributors": "all-contributors generate",
     "commit": "git-cz",
     "validate": "npm-run-all --parallel lint cover --sequential check-coverage",
@@ -35,16 +35,14 @@
   ],
   "license": "MIT",
   "dependencies": {
-    "cliui": "^7.0.2",
+    "cliui": "^7.0.4",
     "eslint-rule-documentation": "^1.0.23",
     "glob": "^7.2.3",
-    "which": "^1.3.1",
     "window-size": "^0.3.0",
     "yargs": "^16.2.0"
   },
   "devDependencies": {
     "all-contributors-cli": "^4.11.2",
-    "aud": "^2.0.4",
     "babel-cli": "^6.26.0",
     "babel-core": "^6.26.3",
     "babel-preset-env": "^1.7.0",
diff --git a/src/bin/find.js b/src/bin/find.js
index c3d8b0e..c9adc90 100755
--- a/src/bin/find.js
+++ b/src/bin/find.js
@@ -10,7 +10,8 @@ const options = {
   n: [],
   error: ['error'],
   core: ['core'],
-  verbose: ['verbose', 'v']
+  verbose: ['verbose', 'v'],
+  flatConfig: ['flatConfig']
 };
 const optionsThatError = ['getUnusedRules', 'getDeprecatedRules'];
 
@@ -28,6 +29,7 @@ const argv = require('yargs')
   })
   .default('error', true)
   .default('core', true)
+  .default('flatConfig', false)
   .argv;
 const getRuleURI = require('eslint-rule-documentation');
 const getRuleFinder = require('../lib/rule-finder');
@@ -39,7 +41,8 @@ const specifiedFile = argv._[0];
 const finderOptions = {
   omitCore: !argv.core,
   includeDeprecated: argv.include === 'deprecated',
-  ext: argv.ext
+  ext: argv.ext,
+  useFlatConfig: argv.flatConfig
 };
 const ruleFinder = await getRuleFinder(specifiedFile, finderOptions);
 const errorOut = argv.error && !argv.n;
diff --git a/src/lib/rule-finder.js b/src/lib/rule-finder.js
index 3f8d19a..c5057cd 100644
--- a/src/lib/rule-finder.js
+++ b/src/lib/rule-finder.js
@@ -6,6 +6,26 @@ const difference = require('./array-diff');
 const getSortedRules = require('./sort-rules');
 const normalizePluginName = require('./normalize-plugin-name');
 
+let builtinRules, FlatESLint;
+try {
+  const eslintInternal = require('eslint/use-at-your-own-risk');
+  builtinRules = eslintInternal.builtinRules;
+  FlatESLint = eslintInternal.FlatESLint;
+} catch (e) {}
+
+function _loadEslint(options, useFlatConfig) {
+  if (!useFlatConfig) {
+    // Ignore any config applicable depending on the location on the filesystem
+    options.useEslintrc = false;
+  } else if (!FlatESLint) {
+    throw 'This version of ESLint does not support flat config.';
+  }
+
+  return useFlatConfig
+    ? new FlatESLint(options)
+    : new ESLint(options);
+}
+
 function _getConfigFile(specifiedFile) {
   if (specifiedFile) {
     if (path.isAbsolute(specifiedFile)) {
@@ -17,13 +37,11 @@ function _getConfigFile(specifiedFile) {
   return require(path.join(process.cwd(), 'package.json')).main;
 }
 
-async function _getConfigs(overrideConfigFile, files) {
-  const esLint = new ESLint({
-    // Ignore any config applicable depending on the location on the filesystem
-    useEslintrc: false,
+async function _getConfigs(overrideConfigFile, files, useFlatConfig) {
+  const esLint = _loadEslint({
     // Point to the particular config
     overrideConfigFile
-  });
+  }, useFlatConfig);
 
   const configs = files.map(async filePath => (
     await esLint.isPathIgnored(filePath) ? false : esLint.calculateConfigForFile(filePath)
@@ -31,11 +49,15 @@ async function _getConfigs(overrideConfigFile, files) {
   return new Set((await Promise.all(configs)).filter(Boolean));
 }
 
-async function _getConfig(configFile, files) {
-  return Array.from(await _getConfigs(configFile, files)).reduce((prev, item) => {
+async function _getConfig(configFile, files, useFlatConfig) {
+  return Array.from(await _getConfigs(configFile, files, useFlatConfig)).reduce((prev, item) => {
+    const plugins = useFlatConfig 
+      ? Object.assign({}, prev.plugins, item.plugins)
+      : [...new Set([].concat(prev.plugins || [], item.plugins || []))]
+    
     return Object.assign(prev, item, {
       rules: Object.assign({}, prev.rules, item.rules),
-      plugins: [...new Set([].concat(prev.plugins || [], item.plugins || []))]
+      plugins
     });
   }, {});
 }
@@ -52,26 +74,36 @@ function _notDeprecated(rule) {
   return !_isDeprecated(rule);
 }
 
-function _getPluginRules(config) {
+function _getPluginRules(config, useFlatConfig) {
   const pluginRules = new Map();
   const plugins = config.plugins;
   /* istanbul ignore else */
   if (plugins) {
-    plugins.forEach(plugin => {
-      const normalized = normalizePluginName(plugin);
-      const pluginConfig = require(normalized.module);
-      const rules = pluginConfig.rules === undefined ? {} : pluginConfig.rules;
-
-      Object.keys(rules).forEach(ruleName =>
-        pluginRules.set(`${normalized.prefix}/${ruleName}`, rules[ruleName])
-      );
-    });
+    if (useFlatConfig) {
+      Object.entries(config.plugins)
+        .filter(([, { rules }]) => rules)
+        .forEach(([pluginName, { rules }]) => {
+          Object.keys(rules).forEach(ruleName =>
+            pluginRules.set(`${pluginName}/${ruleName}`, rules[ruleName])
+          );
+        });
+    } else {
+      plugins.forEach(plugin => {
+        const normalized = normalizePluginName(plugin);
+        const pluginConfig = require(normalized.module);
+        if (pluginConfig.rules) {
+          Object.keys(pluginConfig.rules).forEach(ruleName =>
+            pluginRules.set(`${normalized.prefix}/${ruleName}`, pluginConfig.rules[ruleName])
+          );
+        }
+      });
+    }
   }
   return pluginRules;
 }
 
 function _getCoreRules() {
-  return new Linter().getRules();
+  return builtinRules || new Linter().getRules();
 }
 
 function _filterRuleNames(ruleNames, rules, predicate) {
@@ -94,13 +126,13 @@ function _createExtensionRegExp(extensions) {
   return new RegExp(`.\\.(?:${normalizedExts.join("|")})$`);
 }
 
-function RuleFinder(config, {omitCore, includeDeprecated}) {
+function RuleFinder(config, {omitCore, includeDeprecated, useFlatConfig}) {
   let currentRuleNames = _getCurrentNamesRules(config);
   if (omitCore) {
     currentRuleNames = currentRuleNames.filter(_isNotCore);
   }
 
-  const pluginRules = _getPluginRules(config); // eslint-disable-line vars-on-top
+  const pluginRules = _getPluginRules(config, useFlatConfig); // eslint-disable-line vars-on-top
   const coreRules = _getCoreRules();
   const allRules = omitCore ? pluginRules : new Map([...coreRules, ...pluginRules]);
 
@@ -140,7 +172,7 @@ async function createRuleFinder(specifiedFile, options) {
   const files = glob.sync(`**/*`, {dot: true, matchBase: true})
     .filter(file => extensionRegExp.test(file));
 
-  const config = await _getConfig(configFile, files);
+  const config = await _getConfig(configFile, files, options.useFlatConfig);
 
   return new RuleFinder(config, options);
 }


Test patch:
diff --git a/test/fixtures/post-v8/eslint-flat-config.js b/test/fixtures/post-v8/eslint-flat-config.js
new file mode 100644
index 0000000..a113911
--- /dev/null
+++ b/test/fixtures/post-v8/eslint-flat-config.js
@@ -0,0 +1,29 @@
+module.exports = [
+  {
+    files: ["**/*.js"],
+    plugins: {
+      plugin: {
+        rules: {
+          "foo-rule": {},
+          "old-plugin-rule": { meta: { deprecated: true } },
+          "bar-rule": {},
+        },
+      },
+    },
+    rules: {
+      "foo-rule": [2],
+      "plugin/foo-rule": [2],
+    },
+  },
+  {
+    files: ["**/*.json"],
+    plugins: {
+      jsonPlugin: {
+        rules: { "foo-rule": {} },
+      },
+    },
+    rules: {
+      "jsonPlugin/foo-rule": [2],
+    },
+  },
+];
diff --git a/test/lib/rule-finder.js b/test/lib/rule-finder.js
index 03c2ce8..9ad5aa3 100644
--- a/test/lib/rule-finder.js
+++ b/test/lib/rule-finder.js
@@ -29,17 +29,28 @@ const mockCreateRequire = (getExport, plugins, relative) => {
   });
 };
 
+const mockedBuiltinRules = new Map()
+  .set('foo-rule', {})
+  .set('old-rule', { meta: { deprecated: true } })
+  .set('bar-rule', {})
+  .set('baz-rule', {})
+
 const getRuleFinder = proxyquire('../../src/lib/rule-finder', {
   eslint: {
     Linter: class {
       getRules() {
-        return new Map()
-          .set('foo-rule', {})
-          .set('old-rule', {meta: {deprecated: true}})
-          .set('bar-rule', {})
-          .set('baz-rule', {});
+        return mockedBuiltinRules
       }
-    }
+    },
+  },
+  "eslint/use-at-your-own-risk": {
+    builtinRules: mockedBuiltinRules
+  },
+  "../rules": {
+    get(id) {
+      return mockedBuiltinRules.get(id)
+    },
+    '@global': true
   },
   module: {
     createRequire: (relative) => mockCreateRequire(
@@ -109,17 +120,28 @@ const getRuleFinder = proxyquire('../../src/lib/rule-finder', {
   }
 });
 
+const mockedDedupedBuiltinRules = new Map()
+  .set('foo-rule', {})
+  .set('bar-rule', {})
+  .set('plugin/duplicate-foo-rule', {})
+  .set('plugin/duplicate-bar-rule', {})
+
 const getRuleFinderForDedupeTests = proxyquire('../../src/lib/rule-finder', {
   eslint: {
     Linter: class {
       getRules() {
-        return new Map()
-          .set('foo-rule', {})
-          .set('bar-rule', {})
-          .set('plugin/duplicate-foo-rule', {})
-          .set('plugin/duplicate-bar-rule', {});
+        return mockedDedupedBuiltinRules
       }
-    }
+    },
+  },
+  "eslint/use-at-your-own-risk": {
+    builtinRules: mockedDedupedBuiltinRules
+  },
+  "../rules": {
+    get(id) {
+      return mockedDedupedBuiltinRules.get(id)
+    },
+    '@global': true
   },
   module: {
     createRequire: (relative) => mockCreateRequire(
@@ -141,6 +163,19 @@ const getRuleFinderForDedupeTests = proxyquire('../../src/lib/rule-finder', {
   }
 });
 
+const getRuleFinderNoFlatSupport = proxyquire('../../src/lib/rule-finder', {
+  eslint: {
+    Linter: class {
+      getRules() {
+        return mockedBuiltinRules
+      }
+    },
+  },
+  'eslint/use-at-your-own-risk': {
+    FlatESLint: undefined
+  }
+});
+
 const noSpecifiedFile = path.resolve(process.cwd(), `./test/fixtures/${eslintVersion}/no-path`);
 const specifiedFileRelative = `./test/fixtures/${eslintVersion}/eslint.json`;
 const specifiedFileAbsolute = path.join(process.cwd(), specifiedFileRelative);
@@ -148,6 +183,7 @@ const noRulesFile = path.join(process.cwd(), `./test/fixtures/${eslintVersion}/e
 const noDuplicateRulesFiles = `./test/fixtures/${eslintVersion}/eslint-dedupe-plugin-rules.json`;
 const usingDeprecatedRulesFile = path.join(process.cwd(), `./test/fixtures/${eslintVersion}/eslint-with-deprecated-rules.json`);
 const usingWithOverridesFile = path.join(process.cwd(), `./test/fixtures/${eslintVersion}/eslint-with-overrides.json`);
+const specifiedFlatConfigFileRelative = `./test/fixtures/post-v8/eslint-flat-config.js`;
 
 describe('rule-finder', function() {
   // increase timeout because proxyquire adds a significant delay
@@ -629,4 +665,119 @@ describe('rule-finder', function() {
     ]);
   });
 
+  it('flat config - should throw an exception if FlatESLint is not defined', async () => {
+    try {
+      await getRuleFinderNoFlatSupport(specifiedFlatConfigFileRelative, {useFlatConfig: true})
+      assert.fail('Expected an error to be thrown');
+    } catch (error) {
+      assert.strictEqual(error, 'This version of ESLint does not support flat config.')
+    }
+  });
+
+  (semver.satisfies(eslintPkg.version, '>= 8') ? describe : describe.skip)('flat config - supported', () => {
+    it('specifiedFile (relative path) - unused rules', async () => {
+      const ruleFinder = await getRuleFinder(specifiedFlatConfigFileRelative, {useFlatConfig: true});
+      assert.deepEqual(ruleFinder.getUnusedRules(), [
+        'bar-rule',
+        'baz-rule',
+        'plugin/bar-rule'
+      ]);
+    });
+
+    it('specifiedFile (relative path) - unused rules including deprecated', async () => {
+      const ruleFinder = await getRuleFinder(specifiedFlatConfigFileRelative, {includeDeprecated: true, useFlatConfig: true});
+      assert.deepEqual(ruleFinder.getUnusedRules(), [
+        'bar-rule',
+        'baz-rule',
+        'old-rule',
+        'plugin/bar-rule',
+        'plugin/old-plugin-rule'
+      ]);
+    });
+
+    it('specifiedFile (relative path) - current rules', async () => {
+      const ruleFinder = await getRuleFinder(specifiedFlatConfigFileRelative, {useFlatConfig: true});
+      assert.deepEqual(ruleFinder.getCurrentRules(), [
+        'foo-rule',
+        'plugin/foo-rule'
+      ]);
+    });
+
+    it('specifiedFile (relative path) - current rules with ext', async () => {
+      const ruleFinder = await getRuleFinder(specifiedFlatConfigFileRelative, { ext: ['.json'], useFlatConfig: true});
+      assert.deepEqual(ruleFinder.getCurrentRules(), [
+        'jsonPlugin/foo-rule'
+      ]);
+    });
+
+    it('specifiedFile (relative path) - current rules with ext without dot', async () => {
+      const ruleFinder = await getRuleFinder(specifiedFlatConfigFileRelative, { ext: ['json'], useFlatConfig: true});
+      assert.deepEqual(ruleFinder.getCurrentRules(), [
+        'jsonPlugin/foo-rule'
+      ]);
+    });
+
+    it('specifiedFile (relative path) - current rules with ext not found', async () => {
+      const ruleFinder = await getRuleFinder(specifiedFlatConfigFileRelative, { ext: ['.ts'], useFlatConfig: true });
+      assert.deepEqual(ruleFinder.getCurrentRules(), []);
+    });
+
+    it('specifiedFile (relative path) - plugin rules', async () => {
+      const ruleFinder = await getRuleFinder(specifiedFlatConfigFileRelative, { useFlatConfig: true});
+      assert.deepEqual(ruleFinder.getPluginRules(), [
+        'plugin/bar-rule',
+        'plugin/foo-rule'
+      ]);
+    });
+
+    it('specifiedFile (relative path) - plugin rules including deprecated', async () => {
+      const ruleFinder = await getRuleFinder(specifiedFlatConfigFileRelative, {includeDeprecated: true, useFlatConfig: true});
+      assert.deepEqual(ruleFinder.getPluginRules(), [
+        'plugin/bar-rule',
+        'plugin/foo-rule',
+        'plugin/old-plugin-rule'
+      ]);
+    });
+
+    it('specifiedFile (relative path) - all available rules', async () => {
+      const ruleFinder = await getRuleFinder(specifiedFlatConfigFileRelative, { useFlatConfig: true });
+      assert.deepEqual(
+        ruleFinder.getAllAvailableRules(),
+        [
+          'bar-rule',
+          'baz-rule',
+          'foo-rule',
+          'plugin/bar-rule',
+          'plugin/foo-rule'
+        ]
+      );
+    });
+
+    it('specifiedFile (relative path) - all available rules without core', async () => {
+      const ruleFinder = await getRuleFinder(specifiedFlatConfigFileRelative, {omitCore: true, useFlatConfig: true});
+      assert.deepEqual(
+        ruleFinder.getAllAvailableRules(),
+        [
+          'plugin/bar-rule',
+          'plugin/foo-rule'
+        ]
+      );
+    });
+
+    it('specifiedFile (relative path) - all available rules including deprecated', async () => {
+      const ruleFinder = await getRuleFinder(specifiedFlatConfigFileRelative, {includeDeprecated: true, useFlatConfig: true});
+      assert.deepEqual(
+        ruleFinder.getAllAvailableRules(),
+        [
+          'bar-rule',
+          'baz-rule',
+          'foo-rule',
+          'old-rule',
+          'plugin/bar-rule',
+          'plugin/foo-rule',
+          'plugin/old-plugin-rule'
+        ]
+      );
+    });
+  });
 });