From f55dc1b55ea84bfb4752ffdff5fbf1da6575f131 Mon Sep 17 00:00:00 2001 From: Lars Kappert Date: Tue, 25 Feb 2025 07:06:51 +0100 Subject: [PATCH] Add support for eslint v9 config files + settings --- .../fixtures/plugins/eslint2/eslint.config.ts | 19 ++++++++ .../knip/fixtures/plugins/eslint2/knip.json | 3 ++ .../eslint2/node_modules/@eslint/js/index.js | 6 +++ .../node_modules/@eslint/js/package.json | 3 ++ .../index.js | 0 .../package.json | 3 ++ .../eslint2/node_modules/eslint/index.js | 3 ++ .../eslint2/node_modules/eslint/package.json | 4 ++ .../node_modules/typescript-eslint/index.js | 43 +++++++++++++++++++ .../typescript-eslint/package.json | 9 ++++ .../fixtures/plugins/eslint2/package.json | 12 ++++++ packages/knip/src/plugins/eslint/helpers.ts | 35 +++++++++++---- packages/knip/src/plugins/eslint/index.ts | 28 +++++++++--- packages/knip/src/plugins/eslint/types.ts | 8 ++-- packages/knip/src/plugins/xo/index.ts | 4 +- packages/knip/src/plugins/xo/types.ts | 4 +- packages/knip/test/plugins/eslint2.test.ts | 21 +++++++++ 17 files changed, 184 insertions(+), 21 deletions(-) create mode 100644 packages/knip/fixtures/plugins/eslint2/eslint.config.ts create mode 100644 packages/knip/fixtures/plugins/eslint2/knip.json create mode 100644 packages/knip/fixtures/plugins/eslint2/node_modules/@eslint/js/index.js create mode 100644 packages/knip/fixtures/plugins/eslint2/node_modules/@eslint/js/package.json create mode 100644 packages/knip/fixtures/plugins/eslint2/node_modules/eslint-import-resolver-typescript/index.js create mode 100644 packages/knip/fixtures/plugins/eslint2/node_modules/eslint-import-resolver-typescript/package.json create mode 100644 packages/knip/fixtures/plugins/eslint2/node_modules/eslint/index.js create mode 100644 packages/knip/fixtures/plugins/eslint2/node_modules/eslint/package.json create mode 100644 packages/knip/fixtures/plugins/eslint2/node_modules/typescript-eslint/index.js create mode 100644 packages/knip/fixtures/plugins/eslint2/node_modules/typescript-eslint/package.json create mode 100644 packages/knip/fixtures/plugins/eslint2/package.json create mode 100644 packages/knip/test/plugins/eslint2.test.ts diff --git a/packages/knip/fixtures/plugins/eslint2/eslint.config.ts b/packages/knip/fixtures/plugins/eslint2/eslint.config.ts new file mode 100644 index 000000000..f86448531 --- /dev/null +++ b/packages/knip/fixtures/plugins/eslint2/eslint.config.ts @@ -0,0 +1,19 @@ +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default [ + ...tseslint.config(eslint.configs.recommended, tseslint.configs.recommended), + { + files: ['**/*.{ts,tsx}'], + settings: { + 'import/parsers': { + '@typescript-eslint/parser': ['.ts', '.tsx'], + }, + 'import/resolver': { + typescript: true, + node: true, + }, + }, + rules: {}, + }, +]; diff --git a/packages/knip/fixtures/plugins/eslint2/knip.json b/packages/knip/fixtures/plugins/eslint2/knip.json new file mode 100644 index 000000000..af29f9e74 --- /dev/null +++ b/packages/knip/fixtures/plugins/eslint2/knip.json @@ -0,0 +1,3 @@ +{ + "eslint": ["eslint.config.ts"] +} diff --git a/packages/knip/fixtures/plugins/eslint2/node_modules/@eslint/js/index.js b/packages/knip/fixtures/plugins/eslint2/node_modules/@eslint/js/index.js new file mode 100644 index 000000000..8f5f8520e --- /dev/null +++ b/packages/knip/fixtures/plugins/eslint2/node_modules/@eslint/js/index.js @@ -0,0 +1,6 @@ +module.exports = { + configs: { + all: [], + recommended: [], + }, +}; diff --git a/packages/knip/fixtures/plugins/eslint2/node_modules/@eslint/js/package.json b/packages/knip/fixtures/plugins/eslint2/node_modules/@eslint/js/package.json new file mode 100644 index 000000000..05e8955ed --- /dev/null +++ b/packages/knip/fixtures/plugins/eslint2/node_modules/@eslint/js/package.json @@ -0,0 +1,3 @@ +{ + "name": "@eslint/js" +} diff --git a/packages/knip/fixtures/plugins/eslint2/node_modules/eslint-import-resolver-typescript/index.js b/packages/knip/fixtures/plugins/eslint2/node_modules/eslint-import-resolver-typescript/index.js new file mode 100644 index 000000000..e69de29bb diff --git a/packages/knip/fixtures/plugins/eslint2/node_modules/eslint-import-resolver-typescript/package.json b/packages/knip/fixtures/plugins/eslint2/node_modules/eslint-import-resolver-typescript/package.json new file mode 100644 index 000000000..0b5d6530c --- /dev/null +++ b/packages/knip/fixtures/plugins/eslint2/node_modules/eslint-import-resolver-typescript/package.json @@ -0,0 +1,3 @@ +{ + "name": "eslint-import-resolver-typescript" +} diff --git a/packages/knip/fixtures/plugins/eslint2/node_modules/eslint/index.js b/packages/knip/fixtures/plugins/eslint2/node_modules/eslint/index.js new file mode 100644 index 000000000..b2dc471f8 --- /dev/null +++ b/packages/knip/fixtures/plugins/eslint2/node_modules/eslint/index.js @@ -0,0 +1,3 @@ +module.exports = { + Linter, +}; \ No newline at end of file diff --git a/packages/knip/fixtures/plugins/eslint2/node_modules/eslint/package.json b/packages/knip/fixtures/plugins/eslint2/node_modules/eslint/package.json new file mode 100644 index 000000000..2877896f0 --- /dev/null +++ b/packages/knip/fixtures/plugins/eslint2/node_modules/eslint/package.json @@ -0,0 +1,4 @@ +{ + "name": "eslint", + "bin": "index.js" +} diff --git a/packages/knip/fixtures/plugins/eslint2/node_modules/typescript-eslint/index.js b/packages/knip/fixtures/plugins/eslint2/node_modules/typescript-eslint/index.js new file mode 100644 index 000000000..759743b2a --- /dev/null +++ b/packages/knip/fixtures/plugins/eslint2/node_modules/typescript-eslint/index.js @@ -0,0 +1,43 @@ +Object.defineProperty(exports, '__esModule', { value: true }); +exports.config = exports.configs = exports.plugin = exports.parser = void 0; + +const parserBase = { parseForESLint: () => {} }; +const config_helper_1 = { + config: (...args) => { + return [ + { + rules: {}, + }, + { + name: 'typescript-eslint/base', + languageOptions: { parser: [], sourceType: 'module' }, + plugins: { '@typescript-eslint': [] }, + }, + { + files: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'], + rules: {}, + name: 'typescript-eslint/eslint-recommended', + }, + { + name: 'typescript-eslint/recommended', + rules: {}, + }, + ]; + }, +}; + +exports.parser = { + meta: parserBase.meta, + parseForESLint: parserBase.parseForESLint, +}; + +exports.configs = { + recommended: [], +}; + +exports.default = { + config: config_helper_1.config, + configs: exports.configs, + parser: exports.parser, + plugin: exports.plugin, +}; diff --git a/packages/knip/fixtures/plugins/eslint2/node_modules/typescript-eslint/package.json b/packages/knip/fixtures/plugins/eslint2/node_modules/typescript-eslint/package.json new file mode 100644 index 000000000..09abf1942 --- /dev/null +++ b/packages/knip/fixtures/plugins/eslint2/node_modules/typescript-eslint/package.json @@ -0,0 +1,9 @@ +{ + "name": "typescript-eslint", + "type": "commonjs", + "exports": { + ".": { + "default": "./index.js" + } + } +} diff --git a/packages/knip/fixtures/plugins/eslint2/package.json b/packages/knip/fixtures/plugins/eslint2/package.json new file mode 100644 index 000000000..eda154346 --- /dev/null +++ b/packages/knip/fixtures/plugins/eslint2/package.json @@ -0,0 +1,12 @@ +{ + "name": "@fixtures/eslint2", + "scripts": { + "lint": "eslint" + }, + "devDependencies": { + "@eslint/js": "*", + "eslint": "*", + "eslint-import-resolver-typescript": "*", + "typescript-eslint": "*" + } +} diff --git a/packages/knip/src/plugins/eslint/helpers.ts b/packages/knip/src/plugins/eslint/helpers.ts index 5ab98ae05..743d63b3e 100644 --- a/packages/knip/src/plugins/eslint/helpers.ts +++ b/packages/knip/src/plugins/eslint/helpers.ts @@ -2,12 +2,29 @@ import type { PluginOptions } from '../../types/config.js'; import { compact } from '../../util/array.js'; import { type ConfigInput, type Input, toConfig, toDeferResolve } from '../../util/input.js'; import { getPackageNameFromFilePath, getPackageNameFromModuleSpecifier } from '../../util/modules.js'; -import { isAbsolute, isInternal } from '../../util/path.js'; +import { extname, isAbsolute, isInternal } from '../../util/path.js'; import { getDependenciesFromConfig } from '../babel/index.js'; -import type { ESLintConfig, OverrideConfig } from './types.js'; +import type { ESLintConfig, ESLintConfigDeprecated, OverrideConfigDeprecated } from './types.js'; -export const getDependencies = ( - config: ESLintConfig | OverrideConfig, +export const getInputs = ( + config: ESLintConfigDeprecated | OverrideConfigDeprecated | ESLintConfig, + options: PluginOptions +): (Input | ConfigInput)[] => { + const { configFileName } = options; + + if (extname(configFileName) === '.json' || !/eslint\.config/.test(configFileName)) { + return getInputsDeprecated(config as ESLintConfigDeprecated | OverrideConfigDeprecated, options); + } + + const dependencies = (config as ESLintConfig).flatMap(config => + config.settings ? getDependenciesFromSettings(config.settings).filter(id => id !== '@typescript-eslint/parser') : [] + ); + + return compact(dependencies).map(id => toDeferResolve(id)); +}; + +const getInputsDeprecated = ( + config: ESLintConfigDeprecated | OverrideConfigDeprecated, options: PluginOptions ): (Input | ConfigInput)[] => { const extendsSpecifiers = config.extends ? compact([config.extends].flat().map(resolveExtendSpecifier)) : []; @@ -25,9 +42,9 @@ export const getDependencies = ( const settings = config.settings ? getDependenciesFromSettings(config.settings) : []; // const rules = getDependenciesFromRules(config.rules); // TODO enable in next major? Unexpected/breaking in certain cases w/ eslint v8 const rules = getDependenciesFromRules({}); - const overrides = config.overrides ? [config.overrides].flat().flatMap(d => getDependencies(d, options)) : []; - const x = compact([...extendsSpecifiers, ...plugins, parser, ...settings, ...rules]).map(toDeferResolve); - return [...extendConfigs, ...x, ...babelDependencies, ...overrides]; + const overrides = config.overrides ? [config.overrides].flat().flatMap(d => getInputsDeprecated(d, options)) : []; + const deferred = compact([...extendsSpecifiers, ...plugins, parser, ...settings, ...rules]).map(toDeferResolve); + return [...extendConfigs, ...deferred, ...babelDependencies, ...overrides]; }; const isQualifiedSpecifier = (specifier: string) => @@ -59,12 +76,12 @@ const resolveExtendSpecifier = (specifier: string) => { return resolveSpecifier(namespace, specifier); }; -const getDependenciesFromRules = (rules: ESLintConfig['rules'] = {}) => +const getDependenciesFromRules = (rules: ESLintConfigDeprecated['rules'] = {}) => Object.keys(rules).flatMap(ruleKey => ruleKey.includes('/') ? [resolveSpecifier('eslint-plugin', ruleKey.split('/').slice(0, -1).join('/'))] : [] ); -const getDependenciesFromSettings = (settings: ESLintConfig['settings'] = {}) => { +const getDependenciesFromSettings = (settings: ESLintConfigDeprecated['settings'] = {}) => { return Object.entries(settings).flatMap(([settingKey, settings]) => { if (settingKey === 'import/resolver') { return (typeof settings === 'string' ? [settings] : Object.keys(settings)) diff --git a/packages/knip/src/plugins/eslint/index.ts b/packages/knip/src/plugins/eslint/index.ts index 994f2957c..5db6e15ba 100644 --- a/packages/knip/src/plugins/eslint/index.ts +++ b/packages/knip/src/plugins/eslint/index.ts @@ -1,7 +1,7 @@ import type { IsPluginEnabled, Plugin, ResolveConfig } from '../../types/config.js'; import { hasDependency } from '../../util/plugin.js'; -import { getDependencies } from './helpers.js'; -import type { ESLintConfig } from './types.js'; +import { getInputs } from './helpers.js'; +import type { ESLintConfigDeprecated } from './types.js'; // New: https://eslint.org/docs/latest/use/configure/configuration-files // Old: https://eslint.org/docs/latest/use/configure/configuration-files-deprecated @@ -24,10 +24,28 @@ const entry = ['eslint.config.{js,cjs,mjs,ts,cts,mts}']; const config = ['.eslintrc', '.eslintrc.{js,json,cjs}', '.eslintrc.{yml,yaml}', 'package.json']; -const resolveConfig: ResolveConfig = (localConfig, options) => getDependencies(localConfig, options); +const resolveConfig: ResolveConfig = (localConfig, options) => getInputs(localConfig, options); -const note = `For ESLint v8 users: if relying on [configuration cascading](https://eslint.org/docs/v8.x/use/configure/configuration-files#cascading-and-hierarchy), -consider using something like this: +const note = `### ESLint v9 + +Only regular \`import\` statements are considered by default. +The configuration object is not resolved to find dependencies for \`settings\` such as \`"eslint-import-resolver-typescript"\`. +To enable this, lift the \`entry\` to a \`config\` file like so: + +\`\`\`json +{ + "eslint": ["eslint.config.ts"] +} +\`\`\` + +This is not enabled by default, since this exception may be thrown by a \`@rushstack/eslint-*\` package: + +> \`Error: Failed to patch ESLint because the calling module was not recognized.\` + +### ESLint v8 + +If relying on [configuration cascading](https://eslint.org/docs/v8.x/use/configure/configuration-files#cascading-and-hierarchy), +consider using an extended glob pattern like this: \`\`\`json { diff --git a/packages/knip/src/plugins/eslint/types.ts b/packages/knip/src/plugins/eslint/types.ts index 21fb0627a..8481d4f3e 100644 --- a/packages/knip/src/plugins/eslint/types.ts +++ b/packages/knip/src/plugins/eslint/types.ts @@ -21,9 +21,11 @@ type BaseConfig = { rules?: Rules; }; -export type OverrideConfig = BaseConfig & { files: string[]; overrides: OverrideConfig }; +export type ESLintConfig = BaseConfig[]; -export type ESLintConfig = BaseConfig & { +export type OverrideConfigDeprecated = BaseConfig & { files: string[]; overrides: OverrideConfigDeprecated }; + +export type ESLintConfigDeprecated = BaseConfig & { env?: Record; - overrides?: OverrideConfig[]; + overrides?: OverrideConfigDeprecated[]; }; diff --git a/packages/knip/src/plugins/xo/index.ts b/packages/knip/src/plugins/xo/index.ts index 9a7c7be12..76fa2b207 100644 --- a/packages/knip/src/plugins/xo/index.ts +++ b/packages/knip/src/plugins/xo/index.ts @@ -1,6 +1,6 @@ import type { IsPluginEnabled, Plugin, ResolveConfig } from '../../types/config.js'; import { hasDependency } from '../../util/plugin.js'; -import { getDependencies } from '../eslint/helpers.js'; +import { getInputs } from '../eslint/helpers.js'; import type { XOConfig } from './types.js'; // https://github.com/xojs/xo#config @@ -18,7 +18,7 @@ const config = ['package.json', '.xo-config', '.xo-config.{js,cjs,json}', 'xo.co const entry = ['.xo-config.{js,cjs}', 'xo.config.{js,cjs}']; const resolveConfig: ResolveConfig = async (config, options) => { - const inputs = getDependencies(config, options); + const inputs = getInputs(config, options); return [...inputs]; }; diff --git a/packages/knip/src/plugins/xo/types.ts b/packages/knip/src/plugins/xo/types.ts index 8b89ac814..2753c9b74 100644 --- a/packages/knip/src/plugins/xo/types.ts +++ b/packages/knip/src/plugins/xo/types.ts @@ -1,6 +1,6 @@ -import type { ESLintConfig } from '../eslint/types.js'; +import type { ESLintConfigDeprecated } from '../eslint/types.js'; -export type XOConfig = ESLintConfig & { +export type XOConfig = ESLintConfigDeprecated & { envs?: string[] | undefined; globals?: string[] | undefined; ignores?: string[] | undefined; diff --git a/packages/knip/test/plugins/eslint2.test.ts b/packages/knip/test/plugins/eslint2.test.ts new file mode 100644 index 000000000..557fc02ae --- /dev/null +++ b/packages/knip/test/plugins/eslint2.test.ts @@ -0,0 +1,21 @@ +import { test } from 'bun:test'; +import assert from 'node:assert/strict'; +import { main } from '../../src/index.js'; +import { resolve } from '../../src/util/path.js'; +import baseArguments from '../helpers/baseArguments.js'; +import baseCounters from '../helpers/baseCounters.js'; + +const cwd = resolve('fixtures/plugins/eslint2'); + +test('Find dependencies with the ESLint plugin (2)', async () => { + const { counters } = await main({ + ...baseArguments, + cwd, + }); + + assert.deepEqual(counters, { + ...baseCounters, + processed: 1, + total: 1, + }); +});