eslint-interactive 11.0.2 → 11.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 (73) hide show
  1. package/bin/eslint-interactive.js +1 -11
  2. package/dist/action/apply-suggestions.js.map +1 -1
  3. package/dist/action/disable-per-file.js.map +1 -1
  4. package/dist/action/disable-per-line.js.map +1 -1
  5. package/dist/action/make-fixable-and-fix.js.map +1 -1
  6. package/dist/action/print-result-details.d.ts.map +1 -1
  7. package/dist/action/print-result-details.js +2 -2
  8. package/dist/action/print-result-details.js.map +1 -1
  9. package/dist/cli/pager.js.map +1 -1
  10. package/dist/cli/parse-argv.js +1 -1
  11. package/dist/cli/parse-argv.js.map +1 -1
  12. package/dist/cli/run.d.ts.map +1 -1
  13. package/dist/cli/run.js +2 -3
  14. package/dist/cli/run.js.map +1 -1
  15. package/dist/config.d.ts +3 -2
  16. package/dist/config.d.ts.map +1 -1
  17. package/dist/config.js +1 -0
  18. package/dist/config.js.map +1 -1
  19. package/dist/core.d.ts +8 -9
  20. package/dist/core.d.ts.map +1 -1
  21. package/dist/core.js +36 -21
  22. package/dist/core.js.map +1 -1
  23. package/dist/eslint/linter.d.ts +2 -2
  24. package/dist/eslint/linter.d.ts.map +1 -1
  25. package/dist/eslint/linter.js +9 -4
  26. package/dist/eslint/linter.js.map +1 -1
  27. package/dist/eslint/report-translator.js.map +1 -1
  28. package/dist/eslint/rule-fixer.d.ts.map +1 -1
  29. package/dist/eslint/source-code-fixer.d.ts.map +1 -1
  30. package/dist/eslint/source-code-fixer.js.map +1 -1
  31. package/dist/eslint/use-at-your-own-risk.d.ts +4 -0
  32. package/dist/eslint/use-at-your-own-risk.d.ts.map +1 -0
  33. package/dist/eslint/use-at-your-own-risk.js +6 -0
  34. package/dist/eslint/use-at-your-own-risk.js.map +1 -0
  35. package/dist/fix/apply-suggestions.js.map +1 -1
  36. package/dist/fix/disable-per-file.d.ts.map +1 -1
  37. package/dist/fix/disable-per-file.js +6 -6
  38. package/dist/fix/disable-per-file.js.map +1 -1
  39. package/dist/fix/disable-per-line.d.ts.map +1 -1
  40. package/dist/fix/disable-per-line.js +18 -7
  41. package/dist/fix/disable-per-line.js.map +1 -1
  42. package/dist/fix/make-fixable-and-fix.js.map +1 -1
  43. package/dist/formatter/format-by-files.js.map +1 -1
  44. package/dist/formatter/format-by-rules.d.ts.map +1 -1
  45. package/dist/formatter/format-by-rules.js +1 -3
  46. package/dist/formatter/format-by-rules.js.map +1 -1
  47. package/dist/formatter/take-rule-statistics.js.map +1 -1
  48. package/dist/plugin.d.ts +4 -0
  49. package/dist/plugin.d.ts.map +1 -0
  50. package/dist/plugin.js +73 -0
  51. package/dist/plugin.js.map +1 -0
  52. package/dist/scene/check-results.js.map +1 -1
  53. package/dist/scene/lint.js.map +1 -1
  54. package/dist/scene/select-action.js.map +1 -1
  55. package/dist/util/array.js.map +1 -1
  56. package/dist/util/eslint.d.ts +4 -0
  57. package/dist/util/eslint.d.ts.map +1 -1
  58. package/dist/util/eslint.js +23 -4
  59. package/dist/util/eslint.js.map +1 -1
  60. package/package.json +19 -25
  61. package/src/action/print-result-details.ts +2 -2
  62. package/src/cli/parse-argv.ts +1 -1
  63. package/src/cli/run.ts +2 -4
  64. package/src/config.ts +5 -2
  65. package/src/core.ts +49 -45
  66. package/src/eslint/linter.ts +11 -8
  67. package/src/eslint/use-at-your-own-risk.d.ts +62 -0
  68. package/src/eslint/use-at-your-own-risk.js +6 -0
  69. package/src/fix/disable-per-file.ts +8 -6
  70. package/src/fix/disable-per-line.ts +19 -5
  71. package/src/formatter/format-by-rules.ts +2 -3
  72. package/src/plugin.ts +71 -0
  73. package/src/util/eslint.ts +27 -4
package/src/config.ts CHANGED
@@ -1,8 +1,9 @@
1
- import type { LegacyESLint, FlatESLint } from 'eslint/use-at-your-own-risk';
1
+ import { ESLint } from 'eslint';
2
2
  import { cliOptionsDefaults, ParsedCLIOptions } from './cli/parse-argv.js';
3
+ import { FlatESLint } from './eslint/use-at-your-own-risk.js';
3
4
  import { DeepPartial } from './util/type-check.js';
4
5
  type LegacyESLintOptions = { type: 'eslintrc' } & Pick<
5
- LegacyESLint.Options,
6
+ ESLint.Options,
6
7
  | 'useEslintrc'
7
8
  | 'overrideConfigFile'
8
9
  | 'extensions'
@@ -13,6 +14,7 @@ type LegacyESLintOptions = { type: 'eslintrc' } & Pick<
13
14
  | 'overrideConfig'
14
15
  | 'cwd'
15
16
  | 'resolvePluginsRelativeTo'
17
+ | 'plugins'
16
18
  >;
17
19
  type FlatESLintOptions = { type: 'flat' } & Pick<
18
20
  FlatESLint.Options,
@@ -110,6 +112,7 @@ export function normalizeConfig(config: Config): NormalizedConfig {
110
112
  cwd,
111
113
  resolvePluginsRelativeTo:
112
114
  config.eslintOptions.resolvePluginsRelativeTo ?? configDefaults.eslintOptions.resolvePluginsRelativeTo,
115
+ plugins: config.eslintOptions.plugins,
113
116
  };
114
117
  } else {
115
118
  eslintOptions = {
package/src/core.ts CHANGED
@@ -1,10 +1,8 @@
1
1
  import { writeFile } from 'node:fs/promises';
2
- import { fileURLToPath } from 'node:url';
3
- import { ESLint, Linter, Rule } from 'eslint';
4
- import eslintPkg, { LegacyESLint as LegacyESLintNS } from 'eslint/use-at-your-own-risk';
5
- import isInstalledGlobally from 'is-installed-globally';
2
+ import { ESLint, Rule } from 'eslint';
6
3
  import { DescriptionPosition } from './cli/prompt.js';
7
4
  import { Config, NormalizedConfig, normalizeConfig } from './config.js';
5
+ import { LegacyESLint, FlatESLint } from './eslint/use-at-your-own-risk.js';
8
6
  import {
9
7
  createFixToApplyAutoFixes,
10
8
  createFixToApplySuggestions,
@@ -18,16 +16,15 @@ import {
18
16
  verifyAndFix,
19
17
  } from './fix/index.js';
20
18
  import { format } from './formatter/index.js';
19
+ import { plugin } from './plugin.js';
21
20
  import { filterResultsByRuleId } from './util/eslint.js';
22
21
 
23
- const { LegacyESLint, FlatESLint } = eslintPkg;
24
-
25
22
  /**
26
23
  * Generate results to undo.
27
24
  * @param resultsOfLint The results of lint.
28
25
  * @returns The results to undo.
29
26
  */
30
- function generateResultsToUndo(resultsOfLint: LegacyESLintNS.LintResult[]): LegacyESLintNS.LintResult[] {
27
+ function generateResultsToUndo(resultsOfLint: ESLint.LintResult[]): ESLint.LintResult[] {
31
28
  return resultsOfLint.map((resultOfLint) => {
32
29
  // NOTE: THIS IS HACK.
33
30
  return { ...resultOfLint, output: resultOfLint.source };
@@ -42,17 +39,47 @@ export type Undo = () => Promise<void>;
42
39
  */
43
40
  export class Core {
44
41
  readonly config: NormalizedConfig;
45
- readonly eslint: LegacyESLintNS;
42
+ readonly eslint: ESLint;
46
43
 
47
44
  constructor(config: Config) {
48
45
  this.config = normalizeConfig(config);
49
46
  const eslintOptions = this.config.eslintOptions;
50
47
  if (eslintOptions.type === 'eslintrc') {
51
48
  const { type, ...rest } = eslintOptions;
52
- this.eslint = new LegacyESLint(rest);
49
+ this.eslint = new LegacyESLint({
50
+ ...rest,
51
+ plugins: {
52
+ ...rest.plugins,
53
+ 'eslint-interactive': plugin,
54
+ },
55
+ overrideConfig: {
56
+ ...rest.overrideConfig,
57
+ plugins: [...(rest.overrideConfig?.plugins ?? []), 'eslint-interactive'],
58
+ rules: {
59
+ ...rest.overrideConfig?.rules,
60
+ 'eslint-interactive/source-code-snatcher': 'error',
61
+ },
62
+ },
63
+ });
53
64
  } else {
54
65
  const { type, ...rest } = eslintOptions;
55
- this.eslint = new FlatESLint(rest);
66
+ const overrideConfigs =
67
+ Array.isArray(rest.overrideConfig) ? rest.overrideConfig
68
+ : rest.overrideConfig ? [rest.overrideConfig]
69
+ : [];
70
+ this.eslint = new FlatESLint({
71
+ ...rest,
72
+ overrideConfig: [
73
+ ...overrideConfigs,
74
+ {
75
+ ...rest.overrideConfig,
76
+ plugins: { 'eslint-interactive': plugin },
77
+ rules: {
78
+ 'eslint-interactive/source-code-snatcher': 'error',
79
+ },
80
+ },
81
+ ],
82
+ });
56
83
  }
57
84
  }
58
85
 
@@ -80,17 +107,9 @@ export class Core {
80
107
  * @param results The lint results of the project to print summary
81
108
  * @param ruleIds The rule ids to print details
82
109
  */
83
- async formatResultDetails(results: LegacyESLintNS.LintResult[], ruleIds: (string | null)[]): Promise<string> {
110
+ async formatResultDetails(results: ESLint.LintResult[], ruleIds: (string | null)[]): Promise<string> {
84
111
  const formatterName = this.config.formatterName;
85
-
86
- // When eslint-interactive is installed globally, eslint-formatter-codeframe will also be installed globally.
87
- // On the other hand, `eslint.loadFormatter` cannot load the globally installed formatter by name. So here it loads them by path.
88
- const resolvedFormatterNameOrPath =
89
- isInstalledGlobally && formatterName === 'codeframe'
90
- ? fileURLToPath(import.meta.resolve('eslint-formatter-codeframe', import.meta.resolve('eslint-interactive')))
91
- : formatterName;
92
-
93
- const formatter = await this.eslint.loadFormatter(resolvedFormatterNameOrPath);
112
+ const formatter = await this.eslint.loadFormatter(formatterName);
94
113
  return formatter.format(filterResultsByRuleId(results, ruleIds));
95
114
  }
96
115
 
@@ -98,7 +117,7 @@ export class Core {
98
117
  * Run `eslint --fix`.
99
118
  * @param ruleIds The rule ids to fix
100
119
  */
101
- async applyAutoFixes(results: LegacyESLintNS.LintResult[], ruleIds: string[]): Promise<Undo> {
120
+ async applyAutoFixes(results: ESLint.LintResult[], ruleIds: string[]): Promise<Undo> {
102
121
  return this.fix(results, ruleIds, (context) => createFixToApplyAutoFixes(context, {}));
103
122
  }
104
123
 
@@ -110,7 +129,7 @@ export class Core {
110
129
  * @param descriptionPosition The position of the description
111
130
  */
112
131
  async disablePerLine(
113
- results: LegacyESLintNS.LintResult[],
132
+ results: ESLint.LintResult[],
114
133
  ruleIds: string[],
115
134
  description?: string,
116
135
  descriptionPosition?: DescriptionPosition,
@@ -128,7 +147,7 @@ export class Core {
128
147
  * @param descriptionPosition The position of the description
129
148
  */
130
149
  async disablePerFile(
131
- results: LegacyESLintNS.LintResult[],
150
+ results: ESLint.LintResult[],
132
151
  ruleIds: string[],
133
152
  description?: string,
134
153
  descriptionPosition?: DescriptionPosition,
@@ -145,7 +164,7 @@ export class Core {
145
164
  * @param description The comment explaining the reason for converting
146
165
  */
147
166
  async convertErrorToWarningPerFile(
148
- results: LegacyESLintNS.LintResult[],
167
+ results: ESLint.LintResult[],
149
168
  ruleIds: string[],
150
169
  description?: string,
151
170
  ): Promise<Undo> {
@@ -158,11 +177,7 @@ export class Core {
158
177
  * @param ruleIds The rule ids to apply suggestions
159
178
  * @param filter The script to filter suggestions
160
179
  */
161
- async applySuggestions(
162
- results: LegacyESLintNS.LintResult[],
163
- ruleIds: string[],
164
- filter: SuggestionFilter,
165
- ): Promise<Undo> {
180
+ async applySuggestions(results: ESLint.LintResult[], ruleIds: string[], filter: SuggestionFilter): Promise<Undo> {
166
181
  return this.fix(results, ruleIds, (context) => createFixToApplySuggestions(context, { filter }));
167
182
  }
168
183
 
@@ -172,11 +187,7 @@ export class Core {
172
187
  * @param ruleIds The rule ids to apply suggestions
173
188
  * @param fixableMaker The function to make `Linter.LintMessage` forcibly fixable.
174
189
  */
175
- async makeFixableAndFix(
176
- results: LegacyESLintNS.LintResult[],
177
- ruleIds: string[],
178
- fixableMaker: FixableMaker,
179
- ): Promise<Undo> {
190
+ async makeFixableAndFix(results: ESLint.LintResult[], ruleIds: string[], fixableMaker: FixableMaker): Promise<Undo> {
180
191
  return this.fix(results, ruleIds, (context) => createFixToMakeFixableAndFix(context, { fixableMaker }));
181
192
  }
182
193
 
@@ -185,26 +196,19 @@ export class Core {
185
196
  * @param fix The fix information to do.
186
197
  */
187
198
  private async fix(
188
- resultsOfLint: LegacyESLintNS.LintResult[],
199
+ resultsOfLint: ESLint.LintResult[],
189
200
  ruleIds: string[],
190
201
  fixCreator: (context: FixContext) => Rule.Fix[],
191
202
  ): Promise<Undo> {
192
203
  // NOTE: Extract only necessary results and files for performance
193
204
  const filteredResultsOfLint = filterResultsByRuleId(resultsOfLint, ruleIds);
194
- const linter = new Linter({ configType: this.config.eslintOptions.type });
195
205
 
196
206
  // eslint-disable-next-line prefer-const
197
207
  for (let { filePath, source } of filteredResultsOfLint) {
198
208
  if (!source) throw new Error('Source code is required to apply fixes.');
199
- const config: Linter.Config | Linter.FlatConfig[] =
200
- this.config.eslintOptions.type === 'eslintrc'
201
- ? // eslint-disable-next-line no-await-in-loop
202
- await this.eslint.calculateConfigForFile(filePath)
203
- : // NOTE: For some reason, if files is not specified, it will not match .jsx
204
- // eslint-disable-next-line no-await-in-loop
205
- [{ ...(await this.eslint.calculateConfigForFile(filePath)), files: ['**/*.*', '**/*'] }];
206
-
207
- const fixedResult = verifyAndFix(linter, source, config, filePath, ruleIds, fixCreator);
209
+
210
+ // eslint-disable-next-line no-await-in-loop
211
+ const fixedResult = await verifyAndFix(this.eslint, source, filePath, ruleIds, fixCreator);
208
212
 
209
213
  // Write the fixed source code to the file
210
214
  if (fixedResult.fixed) {
@@ -7,8 +7,9 @@
7
7
  * @author aladdin-add
8
8
  */
9
9
 
10
- import { Linter, Rule } from 'eslint';
10
+ import { ESLint, Rule } from 'eslint';
11
11
  import { FixContext } from '../fix/index.js';
12
+ import { getLastSourceCode } from '../plugin.js';
12
13
  import { ruleFixer } from './rule-fixer.js';
13
14
  import { SourceCodeFixer } from './source-code-fixer.js';
14
15
 
@@ -24,14 +25,13 @@ type FixedResult = {
24
25
  * @param linter
25
26
  */
26
27
  // eslint-disable-next-line max-params
27
- export function verifyAndFix(
28
- linter: Linter,
28
+ export async function verifyAndFix(
29
+ eslint: ESLint,
29
30
  text: string,
30
- config: Linter.Config | Linter.FlatConfig[],
31
31
  filePath: string,
32
32
  ruleIds: string[],
33
33
  fixCreator: (context: FixContext) => Rule.Fix[],
34
- ): FixedResult {
34
+ ): Promise<FixedResult> {
35
35
  let fixedResult: FixedResult;
36
36
  let fixed = false;
37
37
  let passNumber = 0;
@@ -49,10 +49,13 @@ export function verifyAndFix(
49
49
  do {
50
50
  passNumber++;
51
51
 
52
- const messages = linter
53
- .verify(currentText, config, filePath)
52
+ // eslint-disable-next-line no-await-in-loop
53
+ const results = await eslint.lintText(currentText, { filePath });
54
+ const messages = results
55
+ .flatMap((result) => result.messages)
54
56
  .filter((message) => message.ruleId && ruleIds.includes(message.ruleId));
55
- const sourceCode = linter.getSourceCode();
57
+ const sourceCode = getLastSourceCode();
58
+ if (!sourceCode) throw new Error('Failed to get the last source code.');
56
59
 
57
60
  // Create `Rule.Fix[]`
58
61
  const fixContext: FixContext = {
@@ -0,0 +1,62 @@
1
+ import { ESLint, Linter, Rule } from 'eslint';
2
+
3
+ /** @deprecated */
4
+ export const LegacyESLint: typeof ESLint;
5
+
6
+ /** @deprecated */
7
+ // eslint-disable-next-line @typescript-eslint/no-namespace
8
+ export namespace FlatESLint {
9
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
10
+ interface Options {
11
+ // File enumeration
12
+ cwd?: string | undefined;
13
+ errorOnUnmatchedPattern?: boolean | undefined;
14
+ globInputPaths?: boolean | undefined;
15
+ ignore?: boolean | undefined;
16
+
17
+ // Linting
18
+ allowInlineConfig?: boolean | undefined;
19
+ baseConfig?: Linter.FlatConfig | Linter.FlatConfig[] | undefined;
20
+ overrideConfig?: Linter.FlatConfig | Linter.FlatConfig[] | undefined;
21
+ overrideConfigFile?: boolean | string | undefined;
22
+ reportUnusedDisableDirectives?: Linter.StringSeverity | undefined;
23
+
24
+ // Autofix
25
+ fix?: boolean | ((message: Linter.LintMessage) => boolean) | undefined;
26
+ fixTypes?: Rule.RuleMetaData['type'][] | undefined;
27
+
28
+ // Cache-related
29
+ cache?: boolean | undefined;
30
+ cacheLocation?: string | undefined;
31
+ cacheStrategy?: 'content' | 'metadata' | undefined;
32
+ }
33
+ }
34
+ /** @deprecated */
35
+ export declare class FlatESLint {
36
+ constructor(options?: FlatESLint.Options);
37
+
38
+ static get version(): string;
39
+
40
+ static outputFixes(results: ESLint.LintResult[]): Promise<void>;
41
+
42
+ static getErrorResults(results: ESLint.LintResult[]): ESLint.LintResult[];
43
+
44
+ getRulesMetaForResults(results: ESLint.LintResult[]): Record<string, Rule.RuleMetaData>;
45
+
46
+ lintFiles(patterns: string | string[]): Promise<ESLint.LintResult[]>;
47
+
48
+ lintText(
49
+ code: string,
50
+ options?: { filePath?: string | undefined; warnIgnored?: boolean | undefined },
51
+ ): Promise<ESLint.LintResult[]>;
52
+
53
+ loadFormatter(name?: string): Promise<ESLint.Formatter>;
54
+
55
+ calculateConfigForFile(filePath: string): Promise<Linter.FlatConfig | undefined>;
56
+
57
+ findConfigFile(): Promise<string | undefined>;
58
+
59
+ isPathIgnored(filePath: string): Promise<boolean>;
60
+ }
61
+ /** @deprecated */
62
+ export function shouldUseFlatConfig(): Promise<boolean>;
@@ -0,0 +1,6 @@
1
+ // @ts-nocheck
2
+ import eslintPkg from 'eslint/use-at-your-own-risk';
3
+
4
+ export const LegacyESLint = eslintPkg.LegacyESLint;
5
+ export const FlatESLint = eslintPkg.FlatESLint;
6
+ export const shouldUseFlatConfig = eslintPkg.shouldUseFlatConfig;
@@ -39,10 +39,9 @@ function generateFix(
39
39
 
40
40
  // if shebang exists, insert comment after shebang
41
41
  const shebang = findShebang(context.sourceCode.text);
42
- const lineToInsert = disableCommentPerFile
43
- ? disableCommentPerFile.loc.start.line
44
- : shebang
45
- ? sourceCode.getLocFromIndex(shebang.range[0]).line + 1
42
+ const lineToInsert =
43
+ disableCommentPerFile ? disableCommentPerFile.loc.start.line
44
+ : shebang ? sourceCode.getLocFromIndex(shebang.range[0]).line + 1
46
45
  : 1;
47
46
 
48
47
  const fixes: Rule.Fix[] = [];
@@ -54,6 +53,7 @@ function generateFix(
54
53
  fixer,
55
54
  sourceCode,
56
55
  line: lineToInsert,
56
+ column: 0,
57
57
  description,
58
58
  }),
59
59
  );
@@ -65,8 +65,9 @@ function generateFix(
65
65
  fixer,
66
66
  disableComment: disableCommentPerFile,
67
67
  newRules: mergeRuleIds(disableCommentPerFile.ruleIds, ruleIdsToDisable),
68
- newDescription: isPreviousLine
69
- ? disableCommentPerFile.description
68
+ newDescription:
69
+ isPreviousLine ?
70
+ disableCommentPerFile.description
70
71
  : mergeDescription(disableCommentPerFile.description, description),
71
72
  }),
72
73
  );
@@ -76,6 +77,7 @@ function generateFix(
76
77
  fixer,
77
78
  sourceCode,
78
79
  line: lineToInsert,
80
+ column: 0,
79
81
  scope: 'file',
80
82
  ruleIds: ruleIdsToDisable,
81
83
  description: isPreviousLine ? undefined : description,
@@ -4,8 +4,10 @@ import { mergeFixes } from '../eslint/report-translator.js';
4
4
  import { groupBy, unique } from '../util/array.js';
5
5
  import {
6
6
  DisableComment,
7
+ getStartColumnOfTemplateExpression,
7
8
  insertDescriptionCommentStatementBeforeLine,
8
9
  insertDisableCommentStatementBeforeLine,
10
+ isLineInTemplateLiteral,
9
11
  mergeDescription,
10
12
  mergeRuleIds,
11
13
  parseDisableComment,
@@ -30,6 +32,7 @@ function generateFixesPerLine(
30
32
  description: string | undefined,
31
33
  descriptionPosition: DescriptionPosition | undefined,
32
34
  line: number,
35
+ column: number,
33
36
  messagesInLine: Linter.LintMessage[],
34
37
  ): Rule.Fix | null {
35
38
  const { fixer, sourceCode } = context;
@@ -48,6 +51,7 @@ function generateFixesPerLine(
48
51
  fixer,
49
52
  sourceCode,
50
53
  line: disableCommentPerLine ? disableCommentPerLine.loc.start.line : line,
54
+ column,
51
55
  description,
52
56
  }),
53
57
  );
@@ -58,8 +62,9 @@ function generateFixesPerLine(
58
62
  fixer,
59
63
  disableComment: disableCommentPerLine,
60
64
  newRules: mergeRuleIds(disableCommentPerLine.ruleIds, ruleIdsToDisable),
61
- newDescription: isPreviousLine
62
- ? disableCommentPerLine.description
65
+ newDescription:
66
+ isPreviousLine ?
67
+ disableCommentPerLine.description
63
68
  : mergeDescription(disableCommentPerLine.description, description),
64
69
  }),
65
70
  );
@@ -69,6 +74,7 @@ function generateFixesPerLine(
69
74
  fixer,
70
75
  sourceCode,
71
76
  line,
77
+ column,
72
78
  scope: 'next-line',
73
79
  ruleIds: ruleIdsToDisable,
74
80
  description: isPreviousLine ? undefined : description,
@@ -82,10 +88,18 @@ function generateFixesPerLine(
82
88
  * Create fix to add disable comment per line.
83
89
  */
84
90
  export function createFixToDisablePerLine(context: FixContext, args: FixToDisablePerLineArgs): Rule.Fix[] {
85
- const lineToMessages = groupBy(context.messages, (message) => message.line);
91
+ const groupedMessages = groupBy(context.messages, (message) => {
92
+ if (isLineInTemplateLiteral(context.sourceCode, message.line)) {
93
+ const column = getStartColumnOfTemplateExpression(context.sourceCode, message);
94
+ return `${message.line}:${column}`;
95
+ } else {
96
+ return `${message.line}:0`;
97
+ }
98
+ });
86
99
  const fixes: Rule.Fix[] = [];
87
- for (const [line, messagesInLine] of lineToMessages) {
88
- const fix = generateFixesPerLine(context, args.description, args.descriptionPosition, line, messagesInLine);
100
+ for (const [lineAndColumn, messagesInLine] of groupedMessages) {
101
+ const [line, column] = lineAndColumn.split(':').map(Number) as [number, number];
102
+ const fix = generateFixesPerLine(context, args.description, args.descriptionPosition, line, column, messagesInLine);
89
103
  if (fix) fixes.push(fix);
90
104
  }
91
105
  return fixes;
@@ -44,9 +44,8 @@ export function formatByRules(results: ESLint.LintResult[], data?: ESLint.LintRe
44
44
  ruleStatistics.forEach((ruleStatistic) => {
45
45
  const { ruleId } = ruleStatistic;
46
46
  const ruleMetaData = data?.rulesMeta[ruleId];
47
- const ruleCell = ruleMetaData?.docs?.url
48
- ? terminalLink(ruleId, ruleMetaData?.docs.url, { fallback: false })
49
- : ruleId;
47
+ const ruleCell =
48
+ ruleMetaData?.docs?.url ? terminalLink(ruleId, ruleMetaData?.docs.url, { fallback: false }) : ruleId;
50
49
  result = result.replace(` ${ruleId} `, ` ${ruleCell} `);
51
50
  });
52
51
 
package/src/plugin.ts ADDED
@@ -0,0 +1,71 @@
1
+ import { ESLint, Rule, SourceCode } from 'eslint';
2
+
3
+ let lastSourceCode: SourceCode | null = null;
4
+
5
+ export function getLastSourceCode() {
6
+ return lastSourceCode;
7
+ }
8
+
9
+ export const plugin: ESLint.Plugin = {
10
+ rules: {
11
+ /**
12
+ * This is a rule for getting a `SourceCode` instance.
13
+ * `ESLint` class does not provide a method for getting a `SourceCode` instance. As an alternative, we have prepared this custom rule.
14
+ */
15
+ 'source-code-snatcher': {
16
+ create(context) {
17
+ lastSourceCode = context.sourceCode;
18
+ return {};
19
+ },
20
+ },
21
+ /** This is a rule for testing purposes. */
22
+ 'prefer-addition-shorthand': {
23
+ meta: {
24
+ type: 'suggestion',
25
+ // @ts-ignore
26
+ hasSuggestions: true,
27
+ },
28
+ create(context: Rule.RuleContext) {
29
+ return {
30
+ // eslint-disable-next-line @typescript-eslint/naming-convention
31
+ AssignmentExpression: (node) => {
32
+ if (node.left.type !== 'Identifier') return;
33
+ const leftIdentifier = node.left;
34
+ if (node.right.type !== 'BinaryExpression') return;
35
+ const rightBinaryExpression = node.right;
36
+ if (rightBinaryExpression.operator !== '+') return;
37
+ if (rightBinaryExpression.left.type !== 'Identifier') return;
38
+ const rightIdentifier = rightBinaryExpression.left;
39
+ if (leftIdentifier.name !== rightIdentifier.name) return;
40
+ if (rightBinaryExpression.right.type !== 'Literal' || rightBinaryExpression.right.value !== 1) return;
41
+
42
+ context.report({
43
+ node,
44
+ message: 'The addition method is redundant.',
45
+ suggest: [
46
+ {
47
+ desc: 'Use `val += 1` instead.',
48
+ fix(fixer) {
49
+ return fixer.replaceText(node, `${leftIdentifier.name} += 1`);
50
+ },
51
+ },
52
+ {
53
+ desc: 'Use `val++` instead.',
54
+ fix(fixer) {
55
+ return fixer.replaceText(node, `${leftIdentifier.name}++`);
56
+ },
57
+ },
58
+ {
59
+ desc: 'Use `++val` instead.',
60
+ fix(fixer) {
61
+ return fixer.replaceText(node, `++${leftIdentifier.name}`);
62
+ },
63
+ },
64
+ ],
65
+ });
66
+ },
67
+ };
68
+ },
69
+ },
70
+ },
71
+ };
@@ -109,6 +109,27 @@ function isLineInJSXText(sourceCode: SourceCode, line: number): boolean {
109
109
  return headNode?.type === 'JSXText';
110
110
  }
111
111
 
112
+ export function isLineInTemplateLiteral(sourceCode: SourceCode, line: number): boolean {
113
+ const headNodeIndex = sourceCode.getIndexFromLoc({ line, column: 0 });
114
+ const headNode = sourceCode.getNodeByRangeIndex(headNodeIndex);
115
+ return headNode?.type === 'TemplateElement';
116
+ }
117
+
118
+ export function getStartColumnOfTemplateExpression(sourceCode: SourceCode, message: Linter.LintMessage): number {
119
+ for (let i = message.column; i >= 1; i--) {
120
+ const index = sourceCode.getIndexFromLoc({
121
+ line: message.line,
122
+ // Convert 1-indexed to 0-indexed
123
+ column: i - 1,
124
+ });
125
+ const node = sourceCode.getNodeByRangeIndex(index);
126
+ if (node?.type === 'TemplateElement') {
127
+ return i;
128
+ }
129
+ }
130
+ throw new Error(`unreachable: The line ${message.line} does not have a template element.`);
131
+ }
132
+
112
133
  /**
113
134
  * Merge the ruleIds of the disable comments.
114
135
  * @param a The ruleIds of first disable comment
@@ -136,11 +157,12 @@ export function insertDescriptionCommentStatementBeforeLine(args: {
136
157
  fixer: Rule.RuleFixer;
137
158
  sourceCode: SourceCode;
138
159
  line: number;
160
+ column: number;
139
161
  description: string;
140
162
  }): Rule.Fix {
141
- const { fixer, sourceCode, line, description } = args;
163
+ const { fixer, sourceCode, line, column, description } = args;
142
164
  const indent = getIndentFromLine(sourceCode, line);
143
- const headNodeIndex = sourceCode.getIndexFromLoc({ line, column: 0 });
165
+ const headNodeIndex = sourceCode.getIndexFromLoc({ line, column });
144
166
 
145
167
  if (isLineInJSXText(sourceCode, line)) {
146
168
  const commentText = toCommentText({ type: 'Block', text: description });
@@ -175,13 +197,14 @@ export function insertDisableCommentStatementBeforeLine(args: {
175
197
  fixer: Rule.RuleFixer;
176
198
  sourceCode: SourceCode;
177
199
  line: number;
200
+ column: number;
178
201
  scope: 'file' | 'next-line';
179
202
  ruleIds: string[];
180
203
  description: string | undefined;
181
204
  }) {
182
- const { fixer, sourceCode, line, scope, ruleIds, description } = args;
205
+ const { fixer, sourceCode, line, column, scope, ruleIds, description } = args;
183
206
  const indent = getIndentFromLine(sourceCode, line);
184
- const headNodeIndex = sourceCode.getIndexFromLoc({ line, column: 0 });
207
+ const headNodeIndex = sourceCode.getIndexFromLoc({ line, column });
185
208
  const isInJSXText = isLineInJSXText(sourceCode, line);
186
209
  const type = isInJSXText || scope === 'file' ? 'Block' : 'Line';
187
210
  const disableCommentText = toDisableCommentText({