From de071498f826ad0ac178f6d64df1cbcb76bc72c9 Mon Sep 17 00:00:00 2001 From: lotmek Date: Thu, 3 Oct 2024 18:05:04 -0400 Subject: [PATCH] feat: support flat config in eslint v8 --- README.md | 4 +- src/bin/find.js | 7 +- src/lib/rule-finder.js | 76 ++++++--- test/fixtures/post-v8/eslint-flat-config.js | 29 ++++ test/lib/rule-finder.js | 175 ++++++++++++++++++-- 5 files changed, 254 insertions(+), 37 deletions(-) create mode 100644 test/fixtures/post-v8/eslint-flat-config.js 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/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); } 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' + ] + ); + }); + }); });