eslint-interactive 8.1.0

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 (214) hide show
  1. package/LICENSE +25 -0
  2. package/README.md +147 -0
  3. package/bin/eslint-interactive.js +10 -0
  4. package/dist/action/apply-suggestions.d.ts +6 -0
  5. package/dist/action/apply-suggestions.d.ts.map +1 -0
  6. package/dist/action/apply-suggestions.js +28 -0
  7. package/dist/action/apply-suggestions.js.map +1 -0
  8. package/dist/action/disable-per-file.d.ts +6 -0
  9. package/dist/action/disable-per-file.d.ts.map +1 -0
  10. package/dist/action/disable-per-file.js +8 -0
  11. package/dist/action/disable-per-file.js.map +1 -0
  12. package/dist/action/disable-per-line.d.ts +6 -0
  13. package/dist/action/disable-per-line.d.ts.map +1 -0
  14. package/dist/action/disable-per-line.js +8 -0
  15. package/dist/action/disable-per-line.js.map +1 -0
  16. package/dist/action/fix.d.ts +6 -0
  17. package/dist/action/fix.d.ts.map +1 -0
  18. package/dist/action/fix.js +6 -0
  19. package/dist/action/fix.js.map +1 -0
  20. package/dist/action/index.d.ts +7 -0
  21. package/dist/action/index.d.ts.map +1 -0
  22. package/dist/action/index.js +7 -0
  23. package/dist/action/index.js.map +1 -0
  24. package/dist/action/make-fixable-and-fix.d.ts +6 -0
  25. package/dist/action/make-fixable-and-fix.d.ts.map +1 -0
  26. package/dist/action/make-fixable-and-fix.js +28 -0
  27. package/dist/action/make-fixable-and-fix.js.map +1 -0
  28. package/dist/action/print-result-details.d.ts +5 -0
  29. package/dist/action/print-result-details.d.ts.map +1 -0
  30. package/dist/action/print-result-details.js +12 -0
  31. package/dist/action/print-result-details.js.map +1 -0
  32. package/dist/cli/log.d.ts +6 -0
  33. package/dist/cli/log.d.ts.map +1 -0
  34. package/dist/cli/log.js +9 -0
  35. package/dist/cli/log.js.map +1 -0
  36. package/dist/cli/ora.d.ts +4 -0
  37. package/dist/cli/ora.d.ts.map +1 -0
  38. package/dist/cli/ora.js +23 -0
  39. package/dist/cli/ora.js.map +1 -0
  40. package/dist/cli/package.d.ts +2 -0
  41. package/dist/cli/package.d.ts.map +1 -0
  42. package/dist/cli/package.js +6 -0
  43. package/dist/cli/package.js.map +1 -0
  44. package/dist/cli/parse-argv.d.ts +4 -0
  45. package/dist/cli/parse-argv.d.ts.map +1 -0
  46. package/dist/cli/parse-argv.js +50 -0
  47. package/dist/cli/parse-argv.js.map +1 -0
  48. package/dist/cli/prompt.d.ts +53 -0
  49. package/dist/cli/prompt.d.ts.map +1 -0
  50. package/dist/cli/prompt.js +154 -0
  51. package/dist/cli/prompt.js.map +1 -0
  52. package/dist/cli/run.d.ts +8 -0
  53. package/dist/cli/run.d.ts.map +1 -0
  54. package/dist/cli/run.js +46 -0
  55. package/dist/cli/run.js.map +1 -0
  56. package/dist/core-worker.d.ts +21 -0
  57. package/dist/core-worker.d.ts.map +1 -0
  58. package/dist/core-worker.js +52 -0
  59. package/dist/core-worker.js.map +1 -0
  60. package/dist/core.d.ts +84 -0
  61. package/dist/core.d.ts.map +1 -0
  62. package/dist/core.js +196 -0
  63. package/dist/core.js.map +1 -0
  64. package/dist/formatter/colors.d.ts +4 -0
  65. package/dist/formatter/colors.d.ts.map +1 -0
  66. package/dist/formatter/colors.js +5 -0
  67. package/dist/formatter/colors.js.map +1 -0
  68. package/dist/formatter/format-by-files.d.ts +3 -0
  69. package/dist/formatter/format-by-files.d.ts.map +1 -0
  70. package/dist/formatter/format-by-files.js +41 -0
  71. package/dist/formatter/format-by-files.js.map +1 -0
  72. package/dist/formatter/format-by-rules.d.ts +3 -0
  73. package/dist/formatter/format-by-rules.d.ts.map +1 -0
  74. package/dist/formatter/format-by-rules.js +34 -0
  75. package/dist/formatter/format-by-rules.js.map +1 -0
  76. package/dist/formatter/index.d.ts +4 -0
  77. package/dist/formatter/index.d.ts.map +1 -0
  78. package/dist/formatter/index.js +7 -0
  79. package/dist/formatter/index.js.map +1 -0
  80. package/dist/formatter/take-rule-statistics.d.ts +18 -0
  81. package/dist/formatter/take-rule-statistics.d.ts.map +1 -0
  82. package/dist/formatter/take-rule-statistics.js +51 -0
  83. package/dist/formatter/take-rule-statistics.js.map +1 -0
  84. package/dist/index.d.ts +5 -0
  85. package/dist/index.d.ts.map +1 -0
  86. package/dist/index.js +4 -0
  87. package/dist/index.js.map +1 -0
  88. package/dist/plugin/fix/apply-auto-fixes.d.ts +8 -0
  89. package/dist/plugin/fix/apply-auto-fixes.d.ts.map +1 -0
  90. package/dist/plugin/fix/apply-auto-fixes.js +8 -0
  91. package/dist/plugin/fix/apply-auto-fixes.js.map +1 -0
  92. package/dist/plugin/fix/apply-suggestions.d.ts +11 -0
  93. package/dist/plugin/fix/apply-suggestions.d.ts.map +1 -0
  94. package/dist/plugin/fix/apply-suggestions.js +25 -0
  95. package/dist/plugin/fix/apply-suggestions.js.map +1 -0
  96. package/dist/plugin/fix/disable-per-file.d.ts +10 -0
  97. package/dist/plugin/fix/disable-per-file.d.ts.map +1 -0
  98. package/dist/plugin/fix/disable-per-file.js +39 -0
  99. package/dist/plugin/fix/disable-per-file.js.map +1 -0
  100. package/dist/plugin/fix/disable-per-line.d.ts +10 -0
  101. package/dist/plugin/fix/disable-per-line.d.ts.map +1 -0
  102. package/dist/plugin/fix/disable-per-line.js +54 -0
  103. package/dist/plugin/fix/disable-per-line.js.map +1 -0
  104. package/dist/plugin/fix/index.d.ts +6 -0
  105. package/dist/plugin/fix/index.d.ts.map +1 -0
  106. package/dist/plugin/fix/index.js +6 -0
  107. package/dist/plugin/fix/index.js.map +1 -0
  108. package/dist/plugin/fix/make-fixable-and-fix.d.ts +12 -0
  109. package/dist/plugin/fix/make-fixable-and-fix.d.ts.map +1 -0
  110. package/dist/plugin/fix/make-fixable-and-fix.js +61 -0
  111. package/dist/plugin/fix/make-fixable-and-fix.js.map +1 -0
  112. package/dist/plugin/fix-rule.d.ts +10 -0
  113. package/dist/plugin/fix-rule.d.ts.map +1 -0
  114. package/dist/plugin/fix-rule.js +124 -0
  115. package/dist/plugin/fix-rule.js.map +1 -0
  116. package/dist/plugin/index.d.ts +49 -0
  117. package/dist/plugin/index.d.ts.map +1 -0
  118. package/dist/plugin/index.js +11 -0
  119. package/dist/plugin/index.js.map +1 -0
  120. package/dist/plugin/prefer-addition-shorthand-rule.d.ts +7 -0
  121. package/dist/plugin/prefer-addition-shorthand-rule.d.ts.map +1 -0
  122. package/dist/plugin/prefer-addition-shorthand-rule.js +54 -0
  123. package/dist/plugin/prefer-addition-shorthand-rule.js.map +1 -0
  124. package/dist/plugin/rule-fixer.d.ts +80 -0
  125. package/dist/plugin/rule-fixer.d.ts.map +1 -0
  126. package/dist/plugin/rule-fixer.js +118 -0
  127. package/dist/plugin/rule-fixer.js.map +1 -0
  128. package/dist/scene/check-results.d.ts +21 -0
  129. package/dist/scene/check-results.d.ts.map +1 -0
  130. package/dist/scene/check-results.js +22 -0
  131. package/dist/scene/check-results.js.map +1 -0
  132. package/dist/scene/index.d.ts +25 -0
  133. package/dist/scene/index.d.ts.map +1 -0
  134. package/dist/scene/index.js +6 -0
  135. package/dist/scene/index.js.map +1 -0
  136. package/dist/scene/lint.d.ts +8 -0
  137. package/dist/scene/lint.d.ts.map +1 -0
  138. package/dist/scene/lint.js +31 -0
  139. package/dist/scene/lint.js.map +1 -0
  140. package/dist/scene/select-action.d.ts +20 -0
  141. package/dist/scene/select-action.d.ts.map +1 -0
  142. package/dist/scene/select-action.js +46 -0
  143. package/dist/scene/select-action.js.map +1 -0
  144. package/dist/scene/select-rule-ids.d.ts +15 -0
  145. package/dist/scene/select-rule-ids.d.ts.map +1 -0
  146. package/dist/scene/select-rule-ids.js +10 -0
  147. package/dist/scene/select-rule-ids.js.map +1 -0
  148. package/dist/tsconfig.src.tsbuildinfo +1 -0
  149. package/dist/util/array.d.ts +3 -0
  150. package/dist/util/array.d.ts.map +1 -0
  151. package/dist/util/array.js +14 -0
  152. package/dist/util/array.js.map +1 -0
  153. package/dist/util/cache.d.ts +5 -0
  154. package/dist/util/cache.d.ts.map +1 -0
  155. package/dist/util/cache.js +13 -0
  156. package/dist/util/cache.js.map +1 -0
  157. package/dist/util/eslint.d.ts +68 -0
  158. package/dist/util/eslint.d.ts.map +1 -0
  159. package/dist/util/eslint.js +147 -0
  160. package/dist/util/eslint.js.map +1 -0
  161. package/dist/util/filter-script.d.ts +6 -0
  162. package/dist/util/filter-script.d.ts.map +1 -0
  163. package/dist/util/filter-script.js +39 -0
  164. package/dist/util/filter-script.js.map +1 -0
  165. package/dist/util/type-check.d.ts +3 -0
  166. package/dist/util/type-check.d.ts.map +1 -0
  167. package/dist/util/type-check.js +8 -0
  168. package/dist/util/type-check.js.map +1 -0
  169. package/package.json +93 -0
  170. package/src/action/apply-suggestions.ts +40 -0
  171. package/src/action/disable-per-file.ts +16 -0
  172. package/src/action/disable-per-line.ts +16 -0
  173. package/src/action/fix.ts +14 -0
  174. package/src/action/index.ts +6 -0
  175. package/src/action/make-fixable-and-fix.ts +40 -0
  176. package/src/action/print-result-details.ts +18 -0
  177. package/src/cli/log.ts +11 -0
  178. package/src/cli/ora.ts +25 -0
  179. package/src/cli/package.ts +9 -0
  180. package/src/cli/parse-argv.ts +52 -0
  181. package/src/cli/prompt.ts +205 -0
  182. package/src/cli/run.ts +50 -0
  183. package/src/core-worker.ts +66 -0
  184. package/src/core.ts +240 -0
  185. package/src/formatter/colors.ts +5 -0
  186. package/src/formatter/format-by-files.ts +48 -0
  187. package/src/formatter/format-by-rules.ts +37 -0
  188. package/src/formatter/index.ts +9 -0
  189. package/src/formatter/take-rule-statistics.ts +66 -0
  190. package/src/index.ts +4 -0
  191. package/src/plugin/fix/apply-auto-fixes.ts +13 -0
  192. package/src/plugin/fix/apply-suggestions.ts +44 -0
  193. package/src/plugin/fix/disable-per-file.ts +53 -0
  194. package/src/plugin/fix/disable-per-line.ts +65 -0
  195. package/src/plugin/fix/index.ts +13 -0
  196. package/src/plugin/fix/make-fixable-and-fix.ts +77 -0
  197. package/src/plugin/fix-rule.ts +142 -0
  198. package/src/plugin/index.ts +66 -0
  199. package/src/plugin/prefer-addition-shorthand-rule.ts +56 -0
  200. package/src/plugin/rule-fixer.ts +147 -0
  201. package/src/scene/check-results.ts +43 -0
  202. package/src/scene/index.ts +18 -0
  203. package/src/scene/lint.ts +41 -0
  204. package/src/scene/select-action.ts +70 -0
  205. package/src/scene/select-rule-ids.ts +24 -0
  206. package/src/typings/cachedir.d.ts +5 -0
  207. package/src/typings/node-pager.d.ts +4 -0
  208. package/src/util/array.ts +16 -0
  209. package/src/util/cache.ts +11 -0
  210. package/src/util/eslint.ts +162 -0
  211. package/src/util/filter-script.ts +45 -0
  212. package/src/util/type-check.ts +8 -0
  213. package/static/example-filter-script.js +49 -0
  214. package/static/example-fixable-maker-script.js +47 -0
package/src/core.ts ADDED
@@ -0,0 +1,240 @@
1
+ import { join } from 'path';
2
+ import { fileURLToPath } from 'url';
3
+ import { ESLint } from 'eslint';
4
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
5
+ import isInstalledGlobally = require('is-installed-globally');
6
+ import { format } from './formatter/index.js';
7
+ import {
8
+ eslintInteractivePlugin,
9
+ FixRuleOption,
10
+ FixableMaker,
11
+ SuggestionFilter,
12
+ Fix,
13
+ OVERLAPPED_PROBLEM_MESSAGE,
14
+ } from './plugin/index.js';
15
+ import { unique } from './util/array.js';
16
+ import { getCacheDir } from './util/cache.js';
17
+ import { filterResultsByRuleId, scanUsedPluginsFromResults } from './util/eslint.js';
18
+
19
+ const MAX_AUTOFIX_PASSES = 10;
20
+
21
+ /**
22
+ * Generate results to undo.
23
+ * @param resultsOfLint The results of lint.
24
+ * @returns The results to undo.
25
+ */
26
+ function generateResultsToUndo(resultsOfLint: ESLint.LintResult[]): ESLint.LintResult[] {
27
+ return resultsOfLint.map((resultOfLint) => {
28
+ // NOTE: THIS IS HACK.
29
+ return { ...resultOfLint, output: resultOfLint.source };
30
+ });
31
+ }
32
+
33
+ function hasOverlappedProblems(results: ESLint.LintResult[]): boolean {
34
+ return results.flatMap((result) => result.messages).some((message) => message.message === OVERLAPPED_PROBLEM_MESSAGE);
35
+ }
36
+
37
+ /**
38
+ * Get all the rules loaded from eslintrc.
39
+ * @param targetFilePaths The target file paths.
40
+ * @param options The eslint option.
41
+ * @returns The rule ids loaded from eslintrc.
42
+ */
43
+ async function getUsedRuleIds(targetFilePaths: string[], options: ESLint.Options): Promise<string[]> {
44
+ const eslintToGetRules = new ESLint(options);
45
+ const configs = await Promise.all(
46
+ targetFilePaths.map(async (filePath) => eslintToGetRules.calculateConfigForFile(filePath)),
47
+ );
48
+ return unique(configs.map((config) => config.rules).flatMap((rules) => Object.keys(rules)));
49
+ }
50
+
51
+ export type Undo = () => Promise<void>;
52
+
53
+ /** The config of eslint-interactive */
54
+ export type Config = {
55
+ patterns: string[];
56
+ rulePaths?: string[] | undefined;
57
+ extensions?: string[] | undefined;
58
+ formatterName?: string;
59
+ cache?: boolean;
60
+ cacheLocation?: string;
61
+ cwd?: string;
62
+ };
63
+
64
+ /** Default config of `Core` */
65
+ export const DEFAULT_BASE_CONFIG = {
66
+ cache: true,
67
+ cacheLocation: join(getCacheDir(), '.eslintcache'),
68
+ formatterName: 'codeframe',
69
+ };
70
+
71
+ /**
72
+ * The core of eslint-interactive.
73
+ * It uses ESLint's Node.js API to output a summary of problems, fix problems, apply suggestions, etc.
74
+ */
75
+ export class Core {
76
+ readonly config: Config;
77
+ /** The base options of ESLint */
78
+ readonly baseOptions: ESLint.Options;
79
+
80
+ constructor(config: Config) {
81
+ this.config = config;
82
+ this.baseOptions = {
83
+ cache: this.config.cache ?? DEFAULT_BASE_CONFIG.cache,
84
+ cacheLocation: this.config.cacheLocation ?? DEFAULT_BASE_CONFIG.cacheLocation,
85
+ rulePaths: this.config.rulePaths,
86
+ extensions: this.config.extensions,
87
+ cwd: this.config.cwd,
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Lint project.
93
+ * @returns The results of linting
94
+ */
95
+ async lint(): Promise<ESLint.LintResult[]> {
96
+ const eslint = new ESLint(this.baseOptions);
97
+ const results = await eslint.lintFiles(this.config.patterns);
98
+ return results;
99
+ }
100
+
101
+ /**
102
+ * Returns summary of lint results.
103
+ * @param results The lint results of the project to print summary
104
+ */
105
+ formatResultSummary(results: ESLint.LintResult[]): string {
106
+ // get used plugins from `results`
107
+ const plugins = scanUsedPluginsFromResults(results);
108
+
109
+ // get `rulesMeta` from `results`
110
+ const eslint = new ESLint({
111
+ ...this.baseOptions,
112
+ overrideConfig: { plugins },
113
+ });
114
+ // NOTE: `getRulesMetaForResults` is a feature added in ESLint 7.29.0.
115
+ // Therefore, the function may not exist in versions lower than 7.29.0.
116
+ const rulesMeta: ESLint.LintResultData['rulesMeta'] = eslint.getRulesMetaForResults?.(results) ?? {};
117
+
118
+ return format(results, { rulesMeta: rulesMeta, cwd: this.config.cwd ?? process.cwd() });
119
+ }
120
+
121
+ /**
122
+ * Returns details of lint results.
123
+ * @param results The lint results of the project to print summary
124
+ * @param ruleIds The rule ids to print details
125
+ */
126
+ async formatResultDetails(results: ESLint.LintResult[], ruleIds: (string | null)[]): Promise<string> {
127
+ const eslint = new ESLint(this.baseOptions);
128
+ const formatterName = this.config.formatterName ?? DEFAULT_BASE_CONFIG.formatterName;
129
+
130
+ // When eslint-interactive is installed globally, eslint-formatter-codeframe will also be installed globally.
131
+ // On the other hand, `eslint.loadFormatter` cannot load the globally installed formatter by name. So here it loads them by path.
132
+ const resolvedFormatterNameOrPath =
133
+ isInstalledGlobally && formatterName === 'codeframe'
134
+ ? fileURLToPath(
135
+ // @ts-expect-error
136
+ await import.meta.resolve(
137
+ 'eslint-formatter-codeframe',
138
+ // @ts-expect-error
139
+ await import.meta.resolve('eslint-interactive'),
140
+ ),
141
+ )
142
+ : formatterName;
143
+
144
+ const formatter = await eslint.loadFormatter(resolvedFormatterNameOrPath);
145
+ return formatter.format(filterResultsByRuleId(results, ruleIds));
146
+ }
147
+
148
+ /**
149
+ * Run `eslint --fix`.
150
+ * @param ruleIds The rule ids to fix
151
+ */
152
+ async applyAutoFixes(results: ESLint.LintResult[], ruleIds: string[]): Promise<Undo> {
153
+ return await this.fix(results, ruleIds, { name: 'applyAutoFixes', args: {} });
154
+ }
155
+
156
+ /**
157
+ * Add disable comments per line.
158
+ * @param results The lint results of the project to add disable comments
159
+ * @param ruleIds The rule ids to add disable comments
160
+ * @param description The description of the disable comments
161
+ */
162
+ async disablePerLine(results: ESLint.LintResult[], ruleIds: string[], description?: string): Promise<Undo> {
163
+ return await this.fix(results, ruleIds, { name: 'disablePerLine', args: { description } });
164
+ }
165
+
166
+ /**
167
+ * Add disable comments per file.
168
+ * @param results The lint results of the project to add disable comments
169
+ * @param ruleIds The rule ids to add disable comments
170
+ * @param description The description of the disable comments
171
+ */
172
+ async disablePerFile(results: ESLint.LintResult[], ruleIds: string[], description?: string): Promise<Undo> {
173
+ return await this.fix(results, ruleIds, { name: 'disablePerFile', args: { description } });
174
+ }
175
+
176
+ /**
177
+ * Apply suggestions.
178
+ * @param results The lint results of the project to apply suggestions
179
+ * @param ruleIds The rule ids to apply suggestions
180
+ * @param filter The script to filter suggestions
181
+ * */
182
+ async applySuggestions(results: ESLint.LintResult[], ruleIds: string[], filter: SuggestionFilter): Promise<Undo> {
183
+ return await this.fix(results, ruleIds, { name: 'applySuggestions', args: { filter } });
184
+ }
185
+
186
+ /**
187
+ * Make forcibly fixable and run `eslint --fix`.
188
+ * @param results The lint results of the project to apply suggestions
189
+ * @param ruleIds The rule ids to apply suggestions
190
+ * @param fixableMaker The function to make `Linter.LintMessage` forcibly fixable.
191
+ * */
192
+ async makeFixableAndFix(results: ESLint.LintResult[], ruleIds: string[], fixableMaker: FixableMaker): Promise<Undo> {
193
+ return await this.fix(results, ruleIds, { name: 'makeFixableAndFix', args: { fixableMaker } });
194
+ }
195
+
196
+ /**
197
+ * Fix source codes.
198
+ * @param fix The fix information to do.
199
+ */
200
+ private async fix(resultsOfLint: ESLint.LintResult[], ruleIds: string[], fix: Fix): Promise<Undo> {
201
+ // NOTE: Extract only necessary results and files for performance
202
+ const filteredResultsOfLint = filterResultsByRuleId(resultsOfLint, ruleIds);
203
+ const targetFilePaths = filteredResultsOfLint.map((result) => result.filePath);
204
+ const usedRuleIds = await getUsedRuleIds(targetFilePaths, this.baseOptions);
205
+
206
+ // TODO: refactor
207
+ let results = filteredResultsOfLint;
208
+ for (let i = 0; i < MAX_AUTOFIX_PASSES; i++) {
209
+ const eslint = new ESLint({
210
+ ...this.baseOptions,
211
+ // This is super hack to load ESM plugin/rule.
212
+ // ref: https://github.com/eslint/eslint/issues/15453#issuecomment-1001200953
213
+ plugins: {
214
+ 'eslint-interactive': eslintInteractivePlugin,
215
+ },
216
+ overrideConfig: {
217
+ plugins: ['eslint-interactive'],
218
+ rules: {
219
+ 'eslint-interactive/fix': [2, { results, ruleIds, fix } as FixRuleOption],
220
+ // Turn off all rules except `eslint-interactive/fix` when fixing for performance.
221
+ ...Object.fromEntries(usedRuleIds.map((ruleId) => [ruleId, 'off'])),
222
+ },
223
+ },
224
+ // NOTE: Only fix the `fix` rule problems.
225
+ fix: (message) => message.ruleId === 'eslint-interactive/fix',
226
+ // Don't interpret lintFiles arguments as glob patterns for performance.
227
+ globInputPaths: false,
228
+ });
229
+ const resultsToFix = await eslint.lintFiles(targetFilePaths);
230
+ await ESLint.outputFixes(resultsToFix);
231
+ if (!hasOverlappedProblems(resultsToFix)) break;
232
+ results = await this.lint();
233
+ }
234
+
235
+ return async () => {
236
+ const resultsToUndo = generateResultsToUndo(filteredResultsOfLint);
237
+ await ESLint.outputFixes(resultsToUndo);
238
+ };
239
+ }
240
+ }
@@ -0,0 +1,5 @@
1
+ /* istanbul ignore file */
2
+
3
+ export const FAILED_COLOR = 'redBright' as const;
4
+ export const ERROR_COLOR = 'red' as const;
5
+ export const WARNING_COLOR = 'yellow' as const;
@@ -0,0 +1,48 @@
1
+ import chalk from 'chalk';
2
+ import { ESLint } from 'eslint';
3
+ import { ERROR_COLOR, FAILED_COLOR, WARNING_COLOR } from './colors.js';
4
+
5
+ function pluralize(word: string, count: number): string {
6
+ return count > 1 ? `${word}s` : word;
7
+ }
8
+
9
+ export function formatByFiles(results: ESLint.LintResult[]): string {
10
+ let errorCount = 0;
11
+ let failureCount = 0;
12
+ let passCount = 0;
13
+ let warningCount = 0;
14
+
15
+ results.forEach(function (result) {
16
+ const messages = result.messages;
17
+
18
+ if (messages.length === 0) {
19
+ passCount++;
20
+ } else {
21
+ failureCount++;
22
+ warningCount += result.warningCount;
23
+ errorCount += result.errorCount;
24
+ }
25
+ });
26
+
27
+ const fileCount = passCount + failureCount;
28
+ const problemCount = errorCount + warningCount;
29
+
30
+ let summary = '';
31
+ summary += `- ${fileCount} ${pluralize('file', fileCount)}`;
32
+ summary += ' (';
33
+ summary += `${passCount} ${pluralize('file', passCount)} passed`;
34
+ summary += ', ';
35
+ summary += chalk[FAILED_COLOR](`${failureCount} ${pluralize('file', failureCount)} failed`);
36
+ summary += ') checked.\n';
37
+
38
+ if (problemCount > 0) {
39
+ summary += `- ${problemCount} ${pluralize('problem', problemCount)}`;
40
+ summary += ' (';
41
+ summary += chalk[ERROR_COLOR](`${errorCount} ${pluralize('error', errorCount)}`);
42
+ summary += ', ';
43
+ summary += chalk[WARNING_COLOR](`${warningCount} ${pluralize('warning', warningCount)}`);
44
+ summary += ') found.';
45
+ }
46
+
47
+ return chalk.bold(summary);
48
+ }
@@ -0,0 +1,37 @@
1
+ import chalk from 'chalk';
2
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
3
+ import Table = require('cli-table');
4
+ import { ESLint } from 'eslint';
5
+ // import terminalLink from 'terminal-link';
6
+ import { ERROR_COLOR } from './colors.js';
7
+ import { takeRuleStatistics } from './take-rule-statistics.js';
8
+
9
+ function numCell(num: number): string {
10
+ return num > 0 ? chalk[ERROR_COLOR].bold(num) : num.toString();
11
+ }
12
+
13
+ export function formatByRules(results: ESLint.LintResult[], _data?: ESLint.LintResultData): string {
14
+ const ruleStatistics = takeRuleStatistics(results);
15
+ const table = new Table({
16
+ head: ['Rule', 'Error', 'Warning', 'is fixable', 'has suggestions'],
17
+ });
18
+
19
+ ruleStatistics.forEach((ruleStatistic) => {
20
+ const { ruleId, errorCount, warningCount, isFixableCount, hasSuggestionsCount } = ruleStatistic;
21
+
22
+ // NOTE: Disable documentation links temporarily due to problems with cli-table.
23
+ // ref: https://github.com/mizdra/eslint-interactive/issues/81
24
+ // const ruleMetaData = data?.rulesMeta[ruleId];
25
+ // const ruleCell = ruleMetaData?.docs?.url ? terminalLink(ruleId, ruleMetaData?.docs.url) : ruleId;
26
+ const ruleCell = ruleId;
27
+ table.push([
28
+ ruleCell,
29
+ numCell(errorCount),
30
+ numCell(warningCount),
31
+ numCell(isFixableCount),
32
+ numCell(hasSuggestionsCount),
33
+ ]);
34
+ });
35
+
36
+ return table.toString();
37
+ }
@@ -0,0 +1,9 @@
1
+ import { ESLint } from 'eslint';
2
+ import { formatByFiles } from './format-by-files.js';
3
+ import { formatByRules } from './format-by-rules.js';
4
+
5
+ export { takeRuleStatistics, type RuleStatistic } from './take-rule-statistics.js';
6
+
7
+ export function format(results: ESLint.LintResult[], data?: ESLint.LintResultData): string {
8
+ return formatByFiles(results) + '\n' + formatByRules(results, data);
9
+ }
@@ -0,0 +1,66 @@
1
+ import { ESLint, Linter } from 'eslint';
2
+ import { groupBy } from '../util/array.js';
3
+
4
+ /**
5
+ * The type representing the lint results of a rule unit.
6
+ */
7
+ export type RuleStatistic = {
8
+ ruleId: string;
9
+ errorCount: number;
10
+ warningCount: number;
11
+ isFixableCount: number;
12
+ isFixableErrorCount: number;
13
+ isFixableWarningCount: number;
14
+ hasSuggestionsCount: number;
15
+ hasSuggestionsErrorCount: number;
16
+ hasSuggestionsWarningCount: number;
17
+ };
18
+
19
+ /** 指定されたルールのエラー/警告の件数などの統計を取る */
20
+ function takeRuleStatistic(ruleId: string, messages: Linter.LintMessage[]): RuleStatistic {
21
+ let errorCount = 0;
22
+ let warningCount = 0;
23
+ let isFixableErrorCount = 0;
24
+ let isFixableWarningCount = 0;
25
+ let hasSuggestionsErrorCount = 0;
26
+ let hasSuggestionsWarningCount = 0;
27
+
28
+ for (const message of messages) {
29
+ if (message.severity === 2) {
30
+ errorCount++;
31
+ if (message.fix) isFixableErrorCount++;
32
+ if (message.suggestions && message.suggestions.length > 0) hasSuggestionsErrorCount++;
33
+ } else if (message.severity === 1) {
34
+ warningCount++;
35
+ if (message.fix) isFixableWarningCount++;
36
+ if (message.suggestions && message.suggestions.length > 0) hasSuggestionsWarningCount++;
37
+ }
38
+ }
39
+
40
+ return {
41
+ ruleId,
42
+ errorCount,
43
+ warningCount,
44
+ isFixableCount: isFixableErrorCount + isFixableWarningCount,
45
+ isFixableErrorCount: isFixableErrorCount,
46
+ isFixableWarningCount: isFixableWarningCount,
47
+ hasSuggestionsCount: hasSuggestionsErrorCount + hasSuggestionsWarningCount,
48
+ hasSuggestionsErrorCount,
49
+ hasSuggestionsWarningCount,
50
+ };
51
+ }
52
+
53
+ /** ルールごとのエラー/警告の件数などの統計を取る */
54
+ export function takeRuleStatistics(results: ESLint.LintResult[]): RuleStatistic[] {
55
+ const messages = results.flatMap((result) => result.messages).filter((message) => message.ruleId !== null);
56
+
57
+ const ruleIdToMessages = groupBy(messages, (message) => message.ruleId);
58
+
59
+ const ruleStatistics: RuleStatistic[] = [];
60
+ for (const [ruleId, messages] of ruleIdToMessages) {
61
+ // NOTE: Exclude problems with a null `ruleId`.
62
+ // ref: ref: https://github.com/eslint/eslint/blob/f1b7499a5162d3be918328ce496eb80692353a5a/docs/developer-guide/nodejs-api.md?plain=1#L372
63
+ if (ruleId !== null) ruleStatistics.push(takeRuleStatistic(ruleId, messages));
64
+ }
65
+ return ruleStatistics;
66
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { run, type Options } from './cli/run.js';
2
+ export { Core, type Config } from './core.js';
3
+ export { takeRuleStatistics, type RuleStatistic } from './formatter/index.js';
4
+ export { type FixableMaker, type SuggestionFilter, type FixContext } from './plugin/index.js';
@@ -0,0 +1,13 @@
1
+ import { Rule } from 'eslint';
2
+ import { notEmpty } from '../../util/type-check.js';
3
+ import { FixContext } from '../index.js';
4
+
5
+ // eslint-disable-next-line @typescript-eslint/ban-types
6
+ export type FixToApplyAutoFixesArgs = {};
7
+
8
+ /**
9
+ * Create fix to apply auto-fixes.
10
+ */
11
+ export function createFixToApplyAutoFixes(context: FixContext, _args: FixToApplyAutoFixesArgs): Rule.Fix[] {
12
+ return context.messages.map((message) => message.fix).filter(notEmpty);
13
+ }
@@ -0,0 +1,44 @@
1
+ import { Linter, Rule } from 'eslint';
2
+ import { FixContext } from '../index.js';
3
+
4
+ export type SuggestionFilter = (
5
+ suggestions: Linter.LintSuggestion[],
6
+ message: Linter.LintMessage,
7
+ context: FixContext,
8
+ ) => Linter.LintSuggestion | null | undefined;
9
+
10
+ export type FixToApplySuggestionsArgs = {
11
+ filter: SuggestionFilter;
12
+ };
13
+
14
+ function getApplicableSuggestion(
15
+ message: Linter.LintMessage,
16
+ filter: SuggestionFilter,
17
+ context: FixContext,
18
+ ): Linter.LintSuggestion | null {
19
+ if (!message.suggestions || message.suggestions.length === 0) return null;
20
+ const suggestion = filter(message.suggestions, message, context);
21
+ return suggestion ?? null;
22
+ }
23
+
24
+ function generateFixPerMessage(
25
+ context: FixContext,
26
+ filter: SuggestionFilter,
27
+ message: Linter.LintMessage,
28
+ ): Rule.Fix | null {
29
+ const suggestion = getApplicableSuggestion(message, filter, context);
30
+ if (!suggestion) return null;
31
+ return suggestion.fix;
32
+ }
33
+
34
+ /**
35
+ * Create fix to apply suggestions.
36
+ */
37
+ export function createFixToApplySuggestions(context: FixContext, args: FixToApplySuggestionsArgs): Rule.Fix[] {
38
+ const fixes = [];
39
+ for (const message of context.messages) {
40
+ const fix = generateFixPerMessage(context, args.filter, message);
41
+ if (fix) fixes.push(fix);
42
+ }
43
+ return fixes;
44
+ }
@@ -0,0 +1,53 @@
1
+ import { Rule } from 'eslint';
2
+ import type { Comment } from 'estree';
3
+ import { unique } from '../../util/array.js';
4
+ import {
5
+ DisableComment,
6
+ findShebang,
7
+ mergeRuleIdsAndDescription,
8
+ parseDisableComment,
9
+ toCommentText,
10
+ } from '../../util/eslint.js';
11
+ import { notEmpty } from '../../util/type-check.js';
12
+ import { FixContext } from '../index.js';
13
+
14
+ export type FixToDisablePerFileArgs = {
15
+ description?: string;
16
+ };
17
+
18
+ function findDisableCommentPerFile(commentsInFile: Comment[]): DisableComment | undefined {
19
+ return commentsInFile.map(parseDisableComment).find((comment) => comment?.scope === 'file');
20
+ }
21
+
22
+ function generateFix(context: FixContext, description?: string): Rule.Fix | null {
23
+ const ruleIdsToDisable = unique(context.messages.map((message) => message.ruleId).filter(notEmpty));
24
+ if (ruleIdsToDisable.length === 0) return null;
25
+
26
+ const commentsInFile = context.sourceCode.getAllComments();
27
+ const disableCommentPerFile = findDisableCommentPerFile(commentsInFile);
28
+ if (disableCommentPerFile) {
29
+ const text = toCommentText({
30
+ type: 'Block',
31
+ scope: 'file',
32
+ ...mergeRuleIdsAndDescription(disableCommentPerFile, {
33
+ ruleIds: ruleIdsToDisable,
34
+ description,
35
+ }),
36
+ });
37
+ return context.fixer.replaceTextRange(disableCommentPerFile.range, text);
38
+ } else {
39
+ const text = toCommentText({ type: 'Block', scope: 'file', ruleIds: ruleIdsToDisable, description }) + '\n';
40
+
41
+ const shebang = findShebang(context.sourceCode.text);
42
+ // if shebang exists, insert comment after shebang
43
+ return context.fixer.insertTextAfterRange(shebang?.range ?? [0, 0], text);
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Create fix to add disable comment per file.
49
+ */
50
+ export function createFixToDisablePerFile(context: FixContext, args: FixToDisablePerFileArgs): Rule.Fix[] {
51
+ const fix = generateFix(context, args.description);
52
+ return fix ? [fix] : [];
53
+ }
@@ -0,0 +1,65 @@
1
+ import { Linter, Rule } from 'eslint';
2
+ import type { Comment } from 'estree';
3
+ import { groupBy, unique } from '../../util/array.js';
4
+ import { DisableComment, mergeRuleIdsAndDescription, parseDisableComment, toCommentText } from '../../util/eslint.js';
5
+ import { notEmpty } from '../../util/type-check.js';
6
+ import { FixContext } from '../index.js';
7
+
8
+ export type FixToDisablePerLineArgs = {
9
+ description?: string;
10
+ };
11
+
12
+ function findDisableCommentPerLine(commentsInFile: Comment[], line: number): DisableComment | undefined {
13
+ const commentsInPreviousLine = commentsInFile.filter((comment) => comment.loc?.start.line === line - 1);
14
+ return commentsInPreviousLine.map(parseDisableComment).find((comment) => comment?.scope === 'next-line');
15
+ }
16
+
17
+ function generateFixPerLine(
18
+ context: FixContext,
19
+ description: string | undefined,
20
+ line: number,
21
+ messagesInLine: Linter.LintMessage[],
22
+ ): Rule.Fix | null {
23
+ const ruleIdsToDisable = unique(messagesInLine.map((message) => message.ruleId).filter(notEmpty));
24
+ if (ruleIdsToDisable.length === 0) return null;
25
+
26
+ const commentsInFile = context.sourceCode.getAllComments();
27
+ const disableCommentPerLine = findDisableCommentPerLine(commentsInFile, line);
28
+ if (disableCommentPerLine) {
29
+ const text = toCommentText({
30
+ type: 'Block',
31
+ scope: 'next-line',
32
+ ...mergeRuleIdsAndDescription(disableCommentPerLine, {
33
+ ruleIds: ruleIdsToDisable,
34
+ description,
35
+ }),
36
+ });
37
+ return context.fixer.replaceTextRange(disableCommentPerLine.range, text);
38
+ } else {
39
+ const headNodeIndex = context.sourceCode.getIndexFromLoc({ line: line, column: 0 });
40
+ const headNode = context.sourceCode.getNodeByRangeIndex(headNodeIndex);
41
+ if (headNode === null) return null; // For some reason, it seems to be null sometimes.
42
+
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
+ if ((headNode.type as any) === 'JSXText') {
45
+ const commentText = toCommentText({ type: 'Block', scope: 'next-line', ruleIds: ruleIdsToDisable, description });
46
+ return context.fixer.insertTextBeforeRange([headNodeIndex, headNodeIndex], '{' + commentText + '}\n');
47
+ } else {
48
+ const commentText = toCommentText({ type: 'Line', scope: 'next-line', ruleIds: ruleIdsToDisable, description });
49
+ return context.fixer.insertTextBeforeRange([headNodeIndex, headNodeIndex], commentText + '\n');
50
+ }
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Create fix to add disable comment per line.
56
+ */
57
+ export function createFixToDisablePerLine(context: FixContext, args: FixToDisablePerLineArgs): Rule.Fix[] {
58
+ const lineToMessages = groupBy(context.messages, (message) => message.line);
59
+ const fixes = [];
60
+ for (const [line, messagesInLine] of lineToMessages) {
61
+ const fix = generateFixPerLine(context, args.description, line, messagesInLine);
62
+ if (fix) fixes.push(fix);
63
+ }
64
+ return fixes;
65
+ }
@@ -0,0 +1,13 @@
1
+ export {
2
+ type SuggestionFilter,
3
+ type FixToApplySuggestionsArgs,
4
+ createFixToApplySuggestions,
5
+ } from './apply-suggestions.js';
6
+ export { type FixToApplyAutoFixesArgs, createFixToApplyAutoFixes } from './apply-auto-fixes.js';
7
+ export { type FixToDisablePerFileArgs, createFixToDisablePerFile } from './disable-per-file.js';
8
+ export { type FixToDisablePerLineArgs, createFixToDisablePerLine } from './disable-per-line.js';
9
+ export {
10
+ type FixableMaker,
11
+ type FixToMakeFixableAndFixArgs,
12
+ createFixToMakeFixableAndFix,
13
+ } from './make-fixable-and-fix.js';
@@ -0,0 +1,77 @@
1
+ import { Linter, Rule, SourceCode } from 'eslint';
2
+ import { traverse } from 'estraverse';
3
+ import type { Node } from 'estree';
4
+ import { unreachable } from '../../util/type-check.js';
5
+ import { FixContext } from '../index.js';
6
+
7
+ export type FixableMaker = (
8
+ message: Linter.LintMessage,
9
+ node: Node | null,
10
+ context: FixContext,
11
+ ) => Rule.Fix | null | undefined;
12
+
13
+ export type FixToMakeFixableAndFixArgs = {
14
+ fixableMaker: FixableMaker;
15
+ };
16
+
17
+ /**
18
+ * Check the node is the source of the message.
19
+ */
20
+ function isMessageSourceNode(sourceCode: SourceCode, node: Node, message: Linter.LintMessage): boolean {
21
+ if (message.nodeType === undefined) return false;
22
+
23
+ // In some cases there may be no `endLine` or `endColumn`.
24
+ if (message.endLine === undefined || message.endColumn === undefined) return false;
25
+ // If `nodeType` is exists, `range` must be exists.
26
+ if (node.range === undefined) return unreachable();
27
+
28
+ const index = sourceCode.getIndexFromLoc({
29
+ line: message.line,
30
+ // NOTE: `column` of `ESLint.LintMessage` is 1-based, but `column` of `ESTree.Position` is 0-based.
31
+ column: message.column - 1,
32
+ });
33
+ const endIndex = sourceCode.getIndexFromLoc({
34
+ line: message.endLine,
35
+ // NOTE: `column` of `ESLint.LintMessage` is 1-based, but `column` of `ESTree.Position` is 0-based.
36
+ column: message.endColumn - 1,
37
+ });
38
+ const nodeType = message.nodeType;
39
+
40
+ return node.range[0] === index && node.range[1] === endIndex && node.type === nodeType;
41
+ }
42
+
43
+ function getMessageToSourceNode(sourceCode: SourceCode, messages: Linter.LintMessage[]): Map<Linter.LintMessage, Node> {
44
+ const result = new Map<Linter.LintMessage, Node>();
45
+
46
+ traverse(sourceCode.ast, {
47
+ // Required to traverse extension nodes such as `JSXElement`.
48
+ fallback: 'iteration',
49
+ enter(node: Node) {
50
+ for (const message of messages) {
51
+ if (isMessageSourceNode(sourceCode, node, message)) {
52
+ result.set(message, node);
53
+ }
54
+ }
55
+ },
56
+ });
57
+ return result;
58
+ }
59
+
60
+ function generateFixes(context: FixContext, args: FixToMakeFixableAndFixArgs): Rule.Fix[] {
61
+ const messageToNode = getMessageToSourceNode(context.sourceCode, context.messages);
62
+
63
+ const fixes: Rule.Fix[] = [];
64
+ for (const message of context.messages) {
65
+ const node = messageToNode.get(message) ?? null;
66
+ const fix = args.fixableMaker(message, node, context);
67
+ if (fix) fixes.push(fix);
68
+ }
69
+ return fixes;
70
+ }
71
+
72
+ /**
73
+ * Create fix to make fixable and fix.
74
+ */
75
+ export function createFixToMakeFixableAndFix(context: FixContext, args: FixToMakeFixableAndFixArgs): Rule.Fix[] {
76
+ return generateFixes(context, args);
77
+ }