eslint-interactive 12.0.0 → 13.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +34 -58
  2. package/bin/eslint-interactive.js +8 -11
  3. package/dist/action/index.d.ts +0 -2
  4. package/dist/action/index.d.ts.map +1 -1
  5. package/dist/action/index.js +0 -2
  6. package/dist/action/index.js.map +1 -1
  7. package/dist/action/print-result-details.d.ts.map +1 -1
  8. package/dist/action/print-result-details.js +6 -1
  9. package/dist/action/print-result-details.js.map +1 -1
  10. package/dist/cli/log.d.ts +3 -3
  11. package/dist/cli/log.d.ts.map +1 -1
  12. package/dist/cli/log.js +5 -4
  13. package/dist/cli/log.js.map +1 -1
  14. package/dist/cli/parse-argv.d.ts +2 -22
  15. package/dist/cli/parse-argv.d.ts.map +1 -1
  16. package/dist/cli/parse-argv.js +37 -43
  17. package/dist/cli/parse-argv.js.map +1 -1
  18. package/dist/cli/prompt.d.ts +1 -11
  19. package/dist/cli/prompt.d.ts.map +1 -1
  20. package/dist/cli/prompt.js +3 -44
  21. package/dist/cli/prompt.js.map +1 -1
  22. package/dist/cli/run.d.ts.map +1 -1
  23. package/dist/cli/run.js +6 -20
  24. package/dist/cli/run.js.map +1 -1
  25. package/dist/core-worker.d.ts +2 -4
  26. package/dist/core-worker.d.ts.map +1 -1
  27. package/dist/core-worker.js +3 -10
  28. package/dist/core-worker.js.map +1 -1
  29. package/dist/core.d.ts +8 -5
  30. package/dist/core.d.ts.map +1 -1
  31. package/dist/core.js +61 -47
  32. package/dist/core.js.map +1 -1
  33. package/dist/eslint/linter.d.ts +2 -3
  34. package/dist/eslint/linter.d.ts.map +1 -1
  35. package/dist/eslint/linter.js.map +1 -1
  36. package/dist/eslint/source-code-fixer.d.ts +1 -5
  37. package/dist/eslint/source-code-fixer.d.ts.map +1 -1
  38. package/dist/fix/make-fixable-and-fix.d.ts +2 -3
  39. package/dist/fix/make-fixable-and-fix.d.ts.map +1 -1
  40. package/dist/fix/make-fixable-and-fix.js +14 -41
  41. package/dist/fix/make-fixable-and-fix.js.map +1 -1
  42. package/dist/formatter/format-by-files.d.ts.map +1 -1
  43. package/dist/formatter/format-by-files.js +1 -0
  44. package/dist/formatter/format-by-files.js.map +1 -1
  45. package/dist/formatter/format-by-rules.d.ts +6 -1
  46. package/dist/formatter/format-by-rules.d.ts.map +1 -1
  47. package/dist/formatter/format-by-rules.js +7 -2
  48. package/dist/formatter/format-by-rules.js.map +1 -1
  49. package/dist/formatter/index.d.ts +3 -1
  50. package/dist/formatter/index.d.ts.map +1 -1
  51. package/dist/formatter/index.js +3 -2
  52. package/dist/formatter/index.js.map +1 -1
  53. package/dist/formatter/sort-rule-statistics.d.ts +5 -0
  54. package/dist/formatter/sort-rule-statistics.d.ts.map +1 -0
  55. package/dist/formatter/sort-rule-statistics.js +34 -0
  56. package/dist/formatter/sort-rule-statistics.js.map +1 -0
  57. package/dist/index.d.ts +2 -2
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +2 -2
  60. package/dist/index.js.map +1 -1
  61. package/dist/plugin.d.ts.map +1 -1
  62. package/dist/plugin.js +14 -0
  63. package/dist/plugin.js.map +1 -1
  64. package/dist/scene/lint.d.ts.map +1 -1
  65. package/dist/scene/lint.js +13 -14
  66. package/dist/scene/lint.js.map +1 -1
  67. package/dist/scene/select-action.d.ts.map +1 -1
  68. package/dist/scene/select-action.js +3 -9
  69. package/dist/scene/select-action.js.map +1 -1
  70. package/dist/type.d.ts +12 -0
  71. package/dist/type.d.ts.map +1 -0
  72. package/dist/type.js +2 -0
  73. package/dist/type.js.map +1 -0
  74. package/dist/util/eslint.d.ts.map +1 -1
  75. package/dist/util/eslint.js +1 -1
  76. package/dist/util/eslint.js.map +1 -1
  77. package/dist/util/type-check.d.ts +0 -3
  78. package/dist/util/type-check.d.ts.map +1 -1
  79. package/package.json +20 -20
  80. package/src/action/index.ts +0 -2
  81. package/src/action/print-result-details.ts +7 -1
  82. package/src/cli/log.ts +5 -4
  83. package/src/cli/parse-argv.ts +45 -61
  84. package/src/cli/prompt.ts +4 -48
  85. package/src/cli/run.ts +6 -22
  86. package/src/core-worker.ts +6 -21
  87. package/src/core.ts +52 -50
  88. package/src/eslint/linter.ts +2 -3
  89. package/src/fix/make-fixable-and-fix.ts +15 -47
  90. package/src/formatter/format-by-files.ts +1 -0
  91. package/src/formatter/format-by-rules.ts +18 -3
  92. package/src/formatter/index.ts +8 -3
  93. package/src/formatter/sort-rule-statistics.ts +35 -0
  94. package/src/index.ts +2 -2
  95. package/src/plugin.ts +14 -0
  96. package/src/scene/lint.ts +16 -18
  97. package/src/scene/select-action.ts +1 -8
  98. package/src/type.ts +13 -0
  99. package/src/util/eslint.ts +2 -2
  100. package/src/util/type-check.ts +0 -4
  101. package/bin/_eslint-interactive.js +0 -9
  102. package/dist/action/apply-suggestions.d.ts +0 -6
  103. package/dist/action/apply-suggestions.d.ts.map +0 -1
  104. package/dist/action/apply-suggestions.js +0 -27
  105. package/dist/action/apply-suggestions.js.map +0 -1
  106. package/dist/action/make-fixable-and-fix.d.ts +0 -6
  107. package/dist/action/make-fixable-and-fix.d.ts.map +0 -1
  108. package/dist/action/make-fixable-and-fix.js +0 -27
  109. package/dist/action/make-fixable-and-fix.js.map +0 -1
  110. package/dist/config.d.ts +0 -47
  111. package/dist/config.d.ts.map +0 -1
  112. package/dist/config.js +0 -98
  113. package/dist/config.js.map +0 -1
  114. package/dist/eslint/use-at-your-own-risk.d.ts +0 -5
  115. package/dist/eslint/use-at-your-own-risk.d.ts.map +0 -1
  116. package/dist/eslint/use-at-your-own-risk.js +0 -6
  117. package/dist/eslint/use-at-your-own-risk.js.map +0 -1
  118. package/dist/util/file-system.d.ts +0 -5
  119. package/dist/util/file-system.d.ts.map +0 -1
  120. package/dist/util/file-system.js +0 -10
  121. package/dist/util/file-system.js.map +0 -1
  122. package/dist/util/filter-script.d.ts +0 -6
  123. package/dist/util/filter-script.d.ts.map +0 -1
  124. package/dist/util/filter-script.js +0 -38
  125. package/dist/util/filter-script.js.map +0 -1
  126. package/src/action/apply-suggestions.ts +0 -39
  127. package/src/action/make-fixable-and-fix.ts +0 -39
  128. package/src/config.ts +0 -142
  129. package/src/eslint/use-at-your-own-risk.js +0 -6
  130. package/src/util/file-system.ts +0 -10
  131. package/src/util/filter-script.ts +0 -45
  132. package/static/example-filter-script.js +0 -40
  133. package/static/example-fixable-maker-script.js +0 -38
@@ -1,46 +1,23 @@
1
1
  import { parseArgs } from 'node:util';
2
- import type { DeepPartial } from '../util/type-check.js';
2
+ import type { Config, SortField, SortOrder } from '../type.js';
3
3
  import { VERSION } from './package.js';
4
4
 
5
- export type ParsedCLIOptions = {
6
- patterns: string[];
7
- formatterName: string | undefined;
8
- quiet: boolean | undefined;
9
- useEslintrc: boolean | undefined;
10
- overrideConfigFile: string | undefined;
11
- extensions: string[] | undefined;
12
- rulePaths: string[] | undefined;
13
- ignorePath: string | undefined;
14
- cache: boolean | undefined;
15
- cacheLocation: string | undefined;
16
- resolvePluginsRelativeTo: string | undefined;
17
- flags: string[] | undefined;
18
- };
19
-
20
- /** Default CLI Options */
21
- export const cliOptionsDefaults = {
22
- formatterName: 'stylish',
23
- quiet: false,
24
- useEslintrc: true,
25
- cache: false,
26
- } satisfies DeepPartial<ParsedCLIOptions>;
5
+ const VALID_SORT_FIELDS: readonly SortField[] = ['rule', 'error', 'warning', 'fixable', 'suggestions'];
6
+ const VALID_SORT_ORDERS: readonly SortOrder[] = ['asc', 'desc'];
27
7
 
28
8
  /** Parse CLI options */
29
- export function parseArgv(argv: string[]): ParsedCLIOptions {
9
+ export function parseArgv(argv: string[]): Config {
30
10
  const options = {
31
- 'eslintrc': { type: 'boolean', default: cliOptionsDefaults.useEslintrc },
32
11
  'config': { type: 'string', short: 'c' },
33
- 'ext': { type: 'string', multiple: true },
34
- 'resolve-plugins-relative-to': { type: 'string' },
35
- 'rulesdir': { type: 'string', multiple: true },
36
- 'ignore-path': { type: 'string' },
37
- 'format': { type: 'string', default: cliOptionsDefaults.formatterName },
38
- 'quiet': { type: 'boolean', default: cliOptionsDefaults.quiet },
39
- 'cache': { type: 'boolean', default: cliOptionsDefaults.cache },
12
+ 'format': { type: 'string' },
13
+ 'quiet': { type: 'boolean' },
14
+ 'cache': { type: 'boolean' },
40
15
  'cache-location': { type: 'string' },
41
16
  'version': { type: 'boolean' },
42
17
  'help': { type: 'boolean' },
43
18
  'flag': { type: 'string', multiple: true },
19
+ 'sort': { type: 'string' },
20
+ 'sort-order': { type: 'string' },
44
21
  } as const;
45
22
 
46
23
  const { values, positionals } = parseArgs({
@@ -51,6 +28,20 @@ export function parseArgv(argv: string[]): ParsedCLIOptions {
51
28
  options,
52
29
  });
53
30
 
31
+ // Validate `--sort` and `--sort-order`
32
+ if (values.sort !== undefined && !VALID_SORT_FIELDS.includes(values.sort as SortField)) {
33
+ console.error(`Invalid --sort value: "${values.sort}". Must be one of: ${VALID_SORT_FIELDS.join(', ')}`);
34
+ // eslint-disable-next-line n/no-process-exit
35
+ process.exit(1);
36
+ }
37
+ if (values['sort-order'] !== undefined && !VALID_SORT_ORDERS.includes(values['sort-order'] as SortOrder)) {
38
+ console.error(
39
+ `Invalid --sort-order value: "${values['sort-order']}". Must be one of: ${VALID_SORT_ORDERS.join(', ')}`,
40
+ );
41
+ // eslint-disable-next-line n/no-process-exit
42
+ process.exit(1);
43
+ }
44
+
54
45
  if (values.version) {
55
46
  console.log(VERSION);
56
47
  // eslint-disable-next-line n/no-process-exit
@@ -58,53 +49,46 @@ export function parseArgv(argv: string[]): ParsedCLIOptions {
58
49
  }
59
50
 
60
51
  if (values.help) {
61
- console.log(`
62
- eslint-interactive [file.js] [dir]
52
+ console.log(
53
+ `
54
+ eslint-interactive [...patterns]
63
55
 
64
56
  Options:
65
- --help Show help [boolean]
66
- --version Show version number [boolean]
67
- --eslintrc Enable use of configuration from .eslintrc.* [boolean] [default: true]
68
- -c, --config Use this configuration, overriding .eslintrc.* config options if present [string]
69
- --resolve-plugins-relative-to A folder where plugins should be resolved from, CWD by default [string]
70
- --ext Specify JavaScript file extensions [array]
71
- --rulesdir Use additional rules from this directory [array]
72
- --ignore-path Specify path of ignore file [string]
73
- --format Specify the format to be used for the \`Display problem messages\` action [string] [default: "stylish"]
74
- --quiet Report errors only [boolean] [default: false]
75
- --cache Only check changed files [boolean] [default: false]
76
- --cache-location Path to the cache file or directory [string]
77
- --flag Enable a feature flag (requires ESLint v9.6.0+) [array]
57
+ --help Show help
58
+ --version Show version number
59
+ -c, --config <path> Use this configuration, overriding config options if present
60
+ --format <nameOrPath> Specify the format to be used for the "Display problem messages" action
61
+ --quiet Report errors only
62
+ --cache Only check changed files
63
+ --cache-location <path> Path to the cache file or directory
64
+ --flag <name> Enable a feature flag (requires ESLint v9.6.0+)
65
+ --sort <field> Sort rules by: rule, error, warning, fixable, suggestions
66
+ --sort-order <direction> Sort direction: asc, desc (default: desc for counts, asc for rule)
78
67
 
79
68
  Examples:
80
- eslint-interactive ./src Lint ./src/ directory
81
- eslint-interactive ./src ./test Lint multiple directories
82
- eslint-interactive './src/**/*.{ts,tsx,vue}' Lint with glob pattern
83
- eslint-interactive ./src --ext .ts,.tsx,.vue Lint with custom extensions
84
- eslint-interactive ./src --rulesdir ./rules Lint with custom rules
85
- eslint-interactive ./src --no-eslintrc --config ./.eslintrc.ci.js Lint with custom config
86
- `);
69
+ eslint-interactive Lint all files in the project
70
+ eslint-interactive src test Lint specified directories
71
+ eslint-interactive 'src/**/*.{ts,tsx,vue}' Lint with glob pattern
72
+ eslint-interactive --sort error Sort rules by error count (descending)
73
+ eslint-interactive --sort rule Sort rules by rule name (ascending)
74
+ `.trim(),
75
+ );
87
76
  // eslint-disable-next-line n/no-process-exit
88
77
  process.exit(0);
89
78
  }
90
79
 
91
80
  const patterns = positionals.map((pattern) => pattern.toString());
92
- const rulePaths = values.rulesdir?.map((rulePath) => rulePath.toString());
93
- const extensions = values.ext?.map((extension) => extension.toString()).flatMap((extension) => extension.split(','));
94
81
  const formatterName = values.format;
95
82
 
96
83
  return {
97
84
  patterns,
98
85
  formatterName,
99
86
  quiet: values.quiet,
100
- useEslintrc: values.eslintrc,
101
87
  overrideConfigFile: values.config,
102
- extensions,
103
- rulePaths,
104
- ignorePath: values['ignore-path'],
105
88
  cache: values.cache,
106
89
  cacheLocation: values['cache-location'],
107
- resolvePluginsRelativeTo: values['resolve-plugins-relative-to'],
108
90
  flags: values.flag,
91
+ sort: values.sort as SortField | undefined,
92
+ sortOrder: values['sort-order'] as SortOrder | undefined,
109
93
  };
110
94
  }
package/src/cli/prompt.ts CHANGED
@@ -20,8 +20,7 @@ export type Action =
20
20
  | 'disablePerLine'
21
21
  | 'disablePerFile'
22
22
  | 'convertErrorToWarningPerFile'
23
- | 'applySuggestions'
24
- | 'makeFixableAndFix'
23
+ | 'relintAndReselectRules'
25
24
  | 'reselectRules';
26
25
 
27
26
  /**
@@ -78,9 +77,8 @@ export async function promptToInputAction(
78
77
  const foldedStatistics = ruleStatistics.reduce(
79
78
  (a, b) => ({
80
79
  isFixableCount: a.isFixableCount + b.isFixableCount,
81
- hasSuggestionsCount: a.hasSuggestionsCount + b.hasSuggestionsCount,
82
80
  }),
83
- { isFixableCount: 0, hasSuggestionsCount: 0 },
81
+ { isFixableCount: 0 },
84
82
  );
85
83
 
86
84
  const choices = [
@@ -89,16 +87,8 @@ export async function promptToInputAction(
89
87
  { name: 'disablePerLine', message: '🔧 Disable per line' },
90
88
  { name: 'disablePerFile', message: '🔧 Disable per file' },
91
89
  { name: 'convertErrorToWarningPerFile', message: '🔧 Convert error to warning per file' },
92
- {
93
- name: 'applySuggestions',
94
- message: '🔧 Apply suggestions (experimental, for experts)',
95
- disabled: foldedStatistics.hasSuggestionsCount === 0,
96
- },
97
- {
98
- name: 'makeFixableAndFix',
99
- message: '🔧 Make forcibly fixable and run `eslint --fix` (experimental, for experts)',
100
- },
101
- { name: 'reselectRules', message: '↩️ Reselect rules' },
90
+ { name: 'relintAndReselectRules', message: '↩️ Go back (with re-lint)' },
91
+ { name: 'reselectRules', message: '↩️ Go back' },
102
92
  ];
103
93
 
104
94
  const { action } = await prompt<{
@@ -199,37 +189,3 @@ export async function promptToInputWhatToDoNext(): Promise<NextStep> {
199
189
  ]);
200
190
  return nextStep;
201
191
  }
202
-
203
- /**
204
- * Ask the user if they want to reuse the filter script.
205
- * @returns If it reuses, `true`, if not, `false`.
206
- */
207
- export async function promptToInputReuseFilterScript(): Promise<boolean> {
208
- const { reuseFilterScript } = await prompt<{ reuseFilterScript: boolean }>([
209
- {
210
- name: 'reuseFilterScript',
211
- type: 'confirm',
212
- message: 'Do you want to reuse a previously edited filter script?',
213
- initial: true,
214
- onCancel,
215
- },
216
- ]);
217
- return reuseFilterScript;
218
- }
219
-
220
- /**
221
- * Ask the user if they want to reuse the script.
222
- * @returns If it reuses, `true`, if not, `false`.
223
- */
224
- export async function promptToInputReuseScript(): Promise<boolean> {
225
- const { reuseScript } = await prompt<{ reuseScript: boolean }>([
226
- {
227
- name: 'reuseScript',
228
- type: 'confirm',
229
- message: 'Do you want to reuse a previously edited script?',
230
- initial: true,
231
- onCancel,
232
- },
233
- ]);
234
- return reuseScript;
235
- }
package/src/cli/run.ts CHANGED
@@ -3,13 +3,9 @@ import { fileURLToPath } from 'node:url';
3
3
  import { Worker } from 'node:worker_threads';
4
4
  import { wrap } from 'comlink';
5
5
  import nodeEndpoint from 'comlink/dist/esm/node-adapter.mjs';
6
- import isInstalledGlobally from 'is-installed-globally';
7
6
  import terminalLink from 'terminal-link';
8
- import { warn } from '../cli/log.js';
9
7
  import { parseArgv } from '../cli/parse-argv.js';
10
- import { translateCLIOptions } from '../config.js';
11
8
  import type { SerializableCore } from '../core-worker.js';
12
- import { shouldUseFlatConfig } from '../eslint/use-at-your-own-risk.js';
13
9
  import type { NextScene } from '../scene/index.js';
14
10
  import { checkResults, lint, selectAction, selectRuleIds } from '../scene/index.js';
15
11
 
@@ -21,30 +17,18 @@ export type Options = {
21
17
  * Run eslint-interactive.
22
18
  */
23
19
  export async function run(options: Options) {
24
- if (isInstalledGlobally) {
25
- warn(
26
- 'eslint-interactive is installed globally. ' +
27
- 'The globally installed eslint-interactive is not officially supported because some features do not work. ' +
28
- 'It is recommended to install eslint-interactive locally.',
29
- );
30
- }
31
- const parsedCLIOptions = parseArgv(options.argv);
32
- // eslint-disable-next-line @typescript-eslint/no-deprecated
33
- const usingFlatConfig = await shouldUseFlatConfig();
34
- const config = translateCLIOptions(parsedCLIOptions, usingFlatConfig ? 'flat' : 'eslintrc');
20
+ const config = parseArgv(options.argv);
35
21
 
36
22
  // Directly executing the Core API will hog the main thread and halt the spinner.
37
23
  // So we wrap it with comlink and run it on the Worker.
38
24
  const worker = new Worker(join(dirname(fileURLToPath(import.meta.url)), '..', 'core-worker.js'), {
39
25
  env: {
40
- ...process.env,
41
- // NOTE:
42
- // - `terminal-link` uses `supports-hyperlinks` and `supports-color` to determine if a terminal that supports hyperlinks is in use.
43
- // - If the terminal does not support hyperlinks, it will fallback to not print the link.
44
- // - However, due to the specifications of Node.js, the decision does not work well on worker_threads.
45
- // - So here we use a special environment variable to force the printing mode to be switched.
46
- // ref: https://github.com/chalk/supports-color/issues/97, https://github.com/nodejs/node/issues/26946
26
+ // In worker threads, stdin is recognized as noTTY. Therefore, `util.styleText` and `terminalLink` disable colors and links.
27
+ // To work around this, we use environment variables to force colors and links to be enabled.
28
+ // ref: https://github.com/nodejs/node/issues/26946
29
+ FORCE_COLOR: process.stdin.isTTY ? '1' : '0',
47
30
  FORCE_HYPERLINK: terminalLink.isSupported ? '1' : '0',
31
+ ...process.env,
48
32
  },
49
33
  // NOTE: Pass CLI options (--unhandled-rejections=strict, etc.) to the worker
50
34
  execArgv: process.execArgv,
@@ -1,10 +1,8 @@
1
1
  import { parentPort } from 'node:worker_threads';
2
2
  import { expose, proxy } from 'comlink';
3
3
  import nodeEndpoint from 'comlink/dist/esm/node-adapter.mjs';
4
- import type { ESLint } from 'eslint';
5
- import type { Config } from './config.js';
6
4
  import { Core } from './core.js';
7
- import type { FixableMaker, SuggestionFilter } from './fix/index.js';
5
+ import type { Config } from './type.js';
8
6
 
9
7
  /**
10
8
  * @file This is a wrapper module for using the Core API with comlink.
@@ -29,6 +27,11 @@ export class SerializableCore {
29
27
  formatResultSummary(...args: Parameters<Core['formatResultSummary']>): ReturnType<Core['formatResultSummary']> {
30
28
  return this.core.formatResultSummary(...args);
31
29
  }
30
+ getSortedRuleIdsInResults(
31
+ ...args: Parameters<Core['getSortedRuleIdsInResults']>
32
+ ): ReturnType<Core['getSortedRuleIdsInResults']> {
33
+ return this.core.getSortedRuleIdsInResults(...args);
34
+ }
32
35
  async formatResultDetails(...args: Parameters<Core['formatResultDetails']>): ReturnType<Core['formatResultDetails']> {
33
36
  return this.core.formatResultDetails(...args);
34
37
  }
@@ -46,24 +49,6 @@ export class SerializableCore {
46
49
  ): ReturnType<Core['convertErrorToWarningPerFile']> {
47
50
  return proxy(await this.core.convertErrorToWarningPerFile(...args));
48
51
  }
49
- async applySuggestions(
50
- results: ESLint.LintResult[],
51
- ruleIds: string[],
52
- filterScript: string,
53
- ): ReturnType<Core['applySuggestions']> {
54
- // eslint-disable-next-line no-eval -- TODO: replace with a better solution
55
- const filter = eval(filterScript) as SuggestionFilter;
56
- return proxy(await this.core.applySuggestions(results, ruleIds, filter));
57
- }
58
- async makeFixableAndFix(
59
- results: ESLint.LintResult[],
60
- ruleIds: string[],
61
- fixableMakerScript: string,
62
- ): ReturnType<Core['makeFixableAndFix']> {
63
- // eslint-disable-next-line no-eval -- TODO: replace with a better solution
64
- const fixableMaker = eval(fixableMakerScript) as FixableMaker;
65
- return proxy(await this.core.makeFixableAndFix(results, ruleIds, fixableMaker));
66
- }
67
52
  }
68
53
 
69
54
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
package/src/core.ts CHANGED
@@ -1,9 +1,7 @@
1
1
  import { writeFile } from 'node:fs/promises';
2
- import type { ESLint, Rule } from 'eslint';
2
+ import type { Rule } from 'eslint';
3
+ import { ESLint } from 'eslint';
3
4
  import type { DescriptionPosition } from './cli/prompt.js';
4
- import type { Config, NormalizedConfig } from './config.js';
5
- import { normalizeConfig } from './config.js';
6
- import { FlatESLint, LegacyESLint } from './eslint/use-at-your-own-risk.js';
7
5
  import type { FixableMaker, FixContext, SuggestionFilter } from './fix/index.js';
8
6
  import {
9
7
  createFixToApplyAutoFixes,
@@ -14,8 +12,9 @@ import {
14
12
  createFixToMakeFixableAndFix,
15
13
  verifyAndFix,
16
14
  } from './fix/index.js';
17
- import { format } from './formatter/index.js';
15
+ import { format, sortRuleStatistics, takeRuleStatistics } from './formatter/index.js';
18
16
  import { plugin } from './plugin.js';
17
+ import type { Config, SortField, SortOrder } from './type.js';
19
18
  import { filterResultsByRuleId } from './util/eslint.js';
20
19
 
21
20
  /**
@@ -37,49 +36,41 @@ export type Undo = () => Promise<void>;
37
36
  * It uses ESLint's Node.js API to output a summary of problems, fix problems, apply suggestions, etc.
38
37
  */
39
38
  export class Core {
40
- readonly config: NormalizedConfig;
41
- readonly eslint: InstanceType<typeof FlatESLint> | InstanceType<typeof LegacyESLint>;
39
+ readonly #cwd: string;
40
+ readonly #patterns: string[];
41
+ readonly #quiet: boolean;
42
+ readonly #formatterName: string | undefined;
43
+ readonly #sort: SortField | undefined;
44
+ readonly #sortOrder: SortOrder | undefined;
45
+ readonly #eslint: ESLint;
42
46
 
43
47
  constructor(config: Config) {
44
- this.config = normalizeConfig(config);
45
- const eslintOptions = this.config.eslintOptions;
46
- if (eslintOptions.type === 'eslintrc') {
47
- const { type, ...rest } = eslintOptions;
48
- this.eslint = new LegacyESLint({
49
- ...rest,
50
- plugins: {
51
- ...rest.plugins,
52
- 'eslint-interactive': plugin,
53
- },
54
- overrideConfig: {
55
- ...rest.overrideConfig,
56
- plugins: [...(rest.overrideConfig?.plugins ?? []), 'eslint-interactive'],
48
+ this.#cwd = config.cwd ?? process.cwd();
49
+ this.#patterns = config.patterns;
50
+ this.#quiet = config.quiet ?? false;
51
+ this.#formatterName = config.formatterName;
52
+ this.#sort = config.sort;
53
+ this.#sortOrder = config.sortOrder;
54
+
55
+ // NOTE: Passing an option that does not exist to `new ESLint(...)` will throw an error.
56
+ // Therefore, only options supported by ESLint are extracted into the `eslintOptions` variable.
57
+ const { formatterName, patterns, quiet, sort, sortOrder, ...eslintOptions } = config;
58
+ const overrideConfigs =
59
+ Array.isArray(eslintOptions.overrideConfig) ? eslintOptions.overrideConfig
60
+ : eslintOptions.overrideConfig ? [eslintOptions.overrideConfig]
61
+ : [];
62
+ this.#eslint = new ESLint({
63
+ ...eslintOptions,
64
+ overrideConfig: [
65
+ ...overrideConfigs,
66
+ {
67
+ plugins: { 'eslint-interactive': plugin },
57
68
  rules: {
58
- ...rest.overrideConfig?.rules,
59
69
  'eslint-interactive/source-code-snatcher': 'error',
60
70
  },
61
71
  },
62
- });
63
- } else {
64
- const { type, ...rest } = eslintOptions;
65
- const overrideConfigs =
66
- Array.isArray(rest.overrideConfig) ? rest.overrideConfig
67
- : rest.overrideConfig ? [rest.overrideConfig]
68
- : [];
69
- this.eslint = new FlatESLint({
70
- ...rest,
71
- overrideConfig: [
72
- ...overrideConfigs,
73
- {
74
- ...rest.overrideConfig,
75
- plugins: { 'eslint-interactive': plugin },
76
- rules: {
77
- 'eslint-interactive/source-code-snatcher': 'error',
78
- },
79
- },
80
- ],
81
- });
82
- }
72
+ ],
73
+ });
83
74
  }
84
75
 
85
76
  /**
@@ -87,8 +78,8 @@ export class Core {
87
78
  * @returns The results of linting
88
79
  */
89
80
  async lint(): Promise<ESLint.LintResult[]> {
90
- let results = await this.eslint.lintFiles(this.config.patterns);
91
- if (this.config.quiet) results = LegacyESLint.getErrorResults(results);
81
+ let results = await this.#eslint.lintFiles(this.#patterns);
82
+ if (this.#quiet) results = ESLint.getErrorResults(results);
92
83
  return results;
93
84
  }
94
85
 
@@ -97,8 +88,20 @@ export class Core {
97
88
  * @param results The lint results of the project to print summary
98
89
  */
99
90
  formatResultSummary(results: ESLint.LintResult[]): string {
100
- const rulesMeta = this.eslint.getRulesMetaForResults(results);
101
- return format(results, { rulesMeta, cwd: this.config.cwd });
91
+ const rulesMeta = this.#eslint.getRulesMetaForResults(results);
92
+ return format(results, { rulesMeta, cwd: this.#cwd }, { sort: this.#sort, sortOrder: this.#sortOrder });
93
+ }
94
+
95
+ /**
96
+ * Returns ruleIds from lint results, sorted according to the configured sort options.
97
+ * @param results The lint results of the project
98
+ */
99
+ getSortedRuleIdsInResults(results: ESLint.LintResult[]): string[] {
100
+ let ruleStatistics = takeRuleStatistics(results);
101
+ if (this.#sort) {
102
+ ruleStatistics = sortRuleStatistics(ruleStatistics, this.#sort, this.#sortOrder);
103
+ }
104
+ return ruleStatistics.map((s) => s.ruleId);
102
105
  }
103
106
 
104
107
  /**
@@ -107,8 +110,7 @@ export class Core {
107
110
  * @param ruleIds The rule ids to print details
108
111
  */
109
112
  async formatResultDetails(results: ESLint.LintResult[], ruleIds: (string | null)[]): Promise<string> {
110
- const formatterName = this.config.formatterName;
111
- const formatter = await this.eslint.loadFormatter(formatterName);
113
+ const formatter = await this.#eslint.loadFormatter(this.#formatterName);
112
114
  return formatter.format(filterResultsByRuleId(results, ruleIds));
113
115
  }
114
116
 
@@ -207,7 +209,7 @@ export class Core {
207
209
  if (!source) throw new Error('Source code is required to apply fixes.');
208
210
 
209
211
  // eslint-disable-next-line no-await-in-loop
210
- const fixedResult = await verifyAndFix(this.eslint, source, filePath, ruleIds, fixCreator);
212
+ const fixedResult = await verifyAndFix(this.#eslint, source, filePath, ruleIds, fixCreator);
211
213
 
212
214
  // Write the fixed source code to the file
213
215
  if (fixedResult.fixed) {
@@ -218,7 +220,7 @@ export class Core {
218
220
 
219
221
  return async () => {
220
222
  const resultsToUndo = generateResultsToUndo(filteredResultsOfLint);
221
- await LegacyESLint.outputFixes(resultsToUndo);
223
+ await ESLint.outputFixes(resultsToUndo);
222
224
  };
223
225
  }
224
226
  }
@@ -7,12 +7,11 @@
7
7
  * @author aladdin-add
8
8
  */
9
9
 
10
- import type { Rule } from 'eslint';
10
+ import type { ESLint, Rule } from 'eslint';
11
11
  import type { FixContext } from '../fix/index.js';
12
12
  import { getLastSourceCode } from '../plugin.js';
13
13
  import { ruleFixer } from './rule-fixer.js';
14
14
  import { SourceCodeFixer } from './source-code-fixer.js';
15
- import type { FlatESLint, LegacyESLint } from './use-at-your-own-risk.js';
16
15
 
17
16
  const MAX_AUTOFIX_PASSES = 10;
18
17
 
@@ -27,7 +26,7 @@ type FixedResult = {
27
26
  */
28
27
 
29
28
  export async function verifyAndFix(
30
- eslint: InstanceType<typeof FlatESLint> | InstanceType<typeof LegacyESLint>,
29
+ eslint: ESLint,
31
30
  text: string,
32
31
  filePath: string,
33
32
  ruleIds: string[],
@@ -1,12 +1,9 @@
1
- import type { Linter, Rule, SourceCode } from 'eslint';
2
- import { traverse } from 'estraverse';
3
- import type { Node } from 'estree';
4
- import { unreachable } from '../util/type-check.js';
1
+ import type { AST, Linter, Rule, SourceCode } from 'eslint';
5
2
  import type { FixContext } from './index.js';
6
3
 
7
4
  export type FixableMaker = (
8
5
  message: Linter.LintMessage,
9
- node: Node | null,
6
+ range: AST.Range,
10
7
  context: FixContext,
11
8
  ) => Rule.Fix | null | undefined;
12
9
 
@@ -14,58 +11,29 @@ export type FixToMakeFixableAndFixArgs = {
14
11
  fixableMaker: FixableMaker;
15
12
  };
16
13
 
17
- /**
18
- * Check the node is the source of the message.
19
- */
20
- function isMessageSourceNode(sourceCode: SourceCode, node: Node, message: Linter.LintMessage): boolean {
21
- // eslint-disable-next-line @typescript-eslint/no-deprecated -- TODO: Do not use `nodeType` in the future.
22
- if (message.nodeType === undefined) return false;
23
-
24
- // In some cases there may be no `endLine` or `endColumn`.
25
- if (message.endLine === undefined || message.endColumn === undefined) return false;
26
- // If `nodeType` is exists, `range` must be exists.
27
- if (node.range === undefined) return unreachable();
28
-
14
+ function getMessageRange(sourceCode: SourceCode, message: Linter.LintMessage): AST.Range {
29
15
  const index = sourceCode.getIndexFromLoc({
30
16
  line: message.line,
31
17
  // NOTE: `column` of `ESLint.LintMessage` is 1-based, but `column` of `ESTree.Position` is 0-based.
32
18
  column: message.column - 1,
33
19
  });
34
- const endIndex = sourceCode.getIndexFromLoc({
35
- line: message.endLine,
36
- // NOTE: `column` of `ESLint.LintMessage` is 1-based, but `column` of `ESTree.Position` is 0-based.
37
- column: message.endColumn - 1,
38
- });
39
- // eslint-disable-next-line @typescript-eslint/no-deprecated -- TODO: Do not use `nodeType` in the future.
40
- const nodeType = message.nodeType;
41
-
42
- return node.range[0] === index && node.range[1] === endIndex && node.type === nodeType;
43
- }
44
-
45
- function getMessageToSourceNode(sourceCode: SourceCode, messages: Linter.LintMessage[]): Map<Linter.LintMessage, Node> {
46
- const result = new Map<Linter.LintMessage, Node>();
47
-
48
- traverse(sourceCode.ast, {
49
- // Required to traverse extension nodes such as `JSXElement`.
50
- fallback: 'iteration',
51
- enter(node: Node) {
52
- for (const message of messages) {
53
- if (isMessageSourceNode(sourceCode, node, message)) {
54
- result.set(message, node);
55
- }
56
- }
57
- },
58
- });
59
- return result;
20
+ if (message.endLine && message.endColumn) {
21
+ const endIndex = sourceCode.getIndexFromLoc({
22
+ line: message.endLine,
23
+ // NOTE: `column` of `ESLint.LintMessage` is 1-based, but `column` of `ESTree.Position` is 0-based.
24
+ column: message.endColumn - 1,
25
+ });
26
+ return [index, endIndex];
27
+ } else {
28
+ return [index, index];
29
+ }
60
30
  }
61
31
 
62
32
  function generateFixes(context: FixContext, args: FixToMakeFixableAndFixArgs): Rule.Fix[] {
63
- const messageToNode = getMessageToSourceNode(context.sourceCode, context.messages);
64
-
65
33
  const fixes: Rule.Fix[] = [];
66
34
  for (const message of context.messages) {
67
- const node = messageToNode.get(message) ?? null;
68
- const fix = args.fixableMaker(message, node, context);
35
+ const range = getMessageRange(context.sourceCode, message);
36
+ const fix = args.fixableMaker(message, range, context);
69
37
  if (fix) fixes.push(fix);
70
38
  }
71
39
  return fixes;
@@ -1,3 +1,4 @@
1
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
1
2
  import { styleText } from 'node:util';
2
3
  import type { ESLint } from 'eslint';
3
4
  import { ERROR_COLOR, FAILED_COLOR, WARNING_COLOR } from './colors.js';
@@ -1,8 +1,11 @@
1
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
1
2
  import { styleText } from 'node:util';
2
3
  import type { ESLint } from 'eslint';
3
4
  import terminalLink from 'terminal-link';
5
+ import type { SortField, SortOrder } from '../type.js';
4
6
  import { ERROR_COLOR } from './colors.js';
5
7
  import { formatTable } from './format-table.js';
8
+ import { sortRuleStatistics } from './sort-rule-statistics.js';
6
9
  import { takeRuleStatistics } from './take-rule-statistics.js';
7
10
 
8
11
  const headerRow = ['Rule', 'Error', 'Warning', 'is fixable', 'has suggestions'];
@@ -15,14 +18,26 @@ type Row = [
15
18
  hasSuggestionsCount: string,
16
19
  ];
17
20
 
21
+ export type FormatByRulesSortOptions = {
22
+ sort?: SortField | undefined;
23
+ sortOrder?: SortOrder | undefined;
24
+ };
25
+
18
26
  function numCell(num: number): string {
19
27
  return num > 0 ? styleText([ERROR_COLOR, 'bold'], num.toString()) : num.toString();
20
28
  }
21
29
 
22
- export function formatByRules(results: ESLint.LintResult[], data?: ESLint.LintResultData): string {
23
- const ruleStatistics = takeRuleStatistics(results);
24
- const rows: Row[] = [];
30
+ export function formatByRules(
31
+ results: ESLint.LintResult[],
32
+ data?: ESLint.LintResultData,
33
+ sortOptions?: FormatByRulesSortOptions,
34
+ ): string {
35
+ let ruleStatistics = takeRuleStatistics(results);
36
+ if (sortOptions?.sort) {
37
+ ruleStatistics = sortRuleStatistics(ruleStatistics, sortOptions.sort, sortOptions.sortOrder);
38
+ }
25
39
 
40
+ const rows: Row[] = [];
26
41
  ruleStatistics.forEach((ruleStatistic) => {
27
42
  const { ruleId, errorCount, warningCount, isFixableCount, hasSuggestionsCount } = ruleStatistic;
28
43
  const ruleMetaData = data?.rulesMeta[ruleId];