Skip to content

Commit f2eaf15

Browse files
authored
feat(rules): add auto-fix support to multiple rules (#30)
1 parent f22f51b commit f2eaf15

8 files changed

Lines changed: 573 additions & 14 deletions

src/rules/block-scope-case.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export default createRule({
1919
},
2020
defaultOptions: [],
2121
create: (context) => {
22+
const { sourceCode } = context;
23+
2224
return {
2325
SwitchCase: (node) => {
2426
if (node.consequent.length > 0 && node.consequent[0].type !== 'BlockStatement') {
@@ -29,6 +31,18 @@ export default createRule({
2931
line: node.loc.start.line,
3032
},
3133
messageId: 'default',
34+
fix: (fixer) => {
35+
const colonToken = sourceCode.getTokenBefore(node.consequent[0]);
36+
37+
if (!colonToken) {
38+
return null;
39+
}
40+
41+
const lastStatement = node.consequent[node.consequent.length - 1];
42+
const indent = ' '.repeat(node.loc.start.column);
43+
44+
return [fixer.insertTextAfter(colonToken, ' {'), fixer.insertTextAfter(lastStatement, `\n${indent}}`)];
45+
},
3246
});
3347
}
3448
},

src/rules/catch-error-name.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,22 @@ export default createRule<Options, MessageIds>({
6464
fixedName: expectedName,
6565
},
6666
fix: (fixer) => {
67-
if (node.param) {
68-
return fixer.replaceText(node.param, 'error');
67+
if (!node.param) {
68+
return null;
6969
}
7070

71-
return null;
71+
// renaming would conflict with an existing usage of the expected name
72+
if (new RegExp(`\\b${expectedName}\\b`).test(context.sourceCode.getText(node.body))) {
73+
return null;
74+
}
75+
76+
const variable = context.sourceCode
77+
.getDeclaredVariables(node)
78+
.find(({ name }) => name === originalName);
79+
80+
const references = variable?.references.map(({ identifier }) => identifier) ?? [];
81+
82+
return [node.param, ...references].map((identifier) => fixer.replaceText(identifier, expectedName));
7283
},
7384
});
7485
}

src/rules/comment-syntax.ts

Lines changed: 129 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { TSESLint } from '@typescript-eslint/utils';
12
import { createRule } from '../util';
23
import { oneLine } from '../util/messages';
34

@@ -134,13 +135,20 @@ export default createRule<Options, MessageIds>({
134135
const comments = sourceCode.getAllComments();
135136

136137
for (const comment of comments) {
138+
// start of the comment value, right after the `//` or `/*`
139+
const valueStart = comment.range[0] + 2;
140+
137141
if (comment.type === 'Line') {
138142
const firstChar = comment.value?.charCodeAt(0);
139143
const secondChar = comment.value[1];
140144
const lastChar = comment.value[comment.value.length - 1];
141145

142146
if (firstChar !== SPACE_CHARCODE && firstChar !== SLASH_CHARCODE && !isRegion(comment.value)) {
143-
context.report({ node: comment, messageId: 'shouldStartWithSpace' });
147+
context.report({
148+
node: comment,
149+
messageId: 'shouldStartWithSpace',
150+
fix: (fixer) => fixer.insertTextBeforeRange([valueStart, valueStart], ' '),
151+
});
144152

145153
// if this one fails, the others are interpreted incorrectly
146154
continue;
@@ -151,11 +159,19 @@ export default createRule<Options, MessageIds>({
151159
isCapital(secondChar) &&
152160
!isCapitalizedOrAllowed(comment.value.slice(1), ignoredWords)
153161
) {
154-
context.report({ node: comment, messageId: 'lineCommentCapital' });
162+
context.report({
163+
node: comment,
164+
messageId: 'lineCommentCapital',
165+
fix: (fixer) => fixer.replaceTextRange([valueStart + 1, valueStart + 2], secondChar.toLowerCase()),
166+
});
155167
}
156168

157169
if (lastChar === '.' && !comment.value.endsWith('etc.') && !comment.value.endsWith('...')) {
158-
context.report({ node: comment, messageId: 'lineCommentEnding' });
170+
context.report({
171+
node: comment,
172+
messageId: 'lineCommentEnding',
173+
fix: (fixer) => fixer.removeRange([comment.range[1] - 1, comment.range[1]]),
174+
});
159175
}
160176

161177
continue;
@@ -169,9 +185,37 @@ export default createRule<Options, MessageIds>({
169185
continue;
170186
}
171187

188+
const valueLines = lines;
189+
const indent = ' '.repeat(comment.loc.start.column + 1);
190+
191+
const lineStartOffsets: number[] = [];
192+
193+
let currentOffset = 0;
194+
195+
for (const valueLine of valueLines) {
196+
lineStartOffsets.push(currentOffset);
197+
currentOffset += valueLine.length + 1;
198+
}
199+
200+
// absolute source range of the line at `index` in the sliced `lines` array
201+
const lineRange = (index: number): [number, number] => {
202+
const start = valueStart + lineStartOffsets[index + 1];
203+
204+
return [start, start + valueLines[index + 1].length];
205+
};
206+
172207
// verify the first char is a '*'
173208
if (lines[0] !== STAR) {
174-
context.report({ node: comment, messageId: 'shouldStartWithBlock' });
209+
context.report({
210+
node: comment,
211+
messageId: 'shouldStartWithBlock',
212+
fix: (fixer) => {
213+
const content = valueLines[0].replace(/^\*+\s*/, '').trim();
214+
const range: [number, number] = [valueStart, valueStart + valueLines[0].length];
215+
216+
return fixer.replaceTextRange(range, content ? `*\n${indent}* ${content}` : '*');
217+
},
218+
});
175219
continue;
176220
}
177221

@@ -199,12 +243,29 @@ export default createRule<Options, MessageIds>({
199243

200244
if (isLastLine) {
201245
if (!BLOCK_COMMENT_END.test(line)) {
202-
context.report({ node: comment, messageId: 'shouldEndWithBlock' });
246+
context.report({
247+
node: comment,
248+
messageId: 'shouldEndWithBlock',
249+
fix: (fixer) => {
250+
const [start] = lineRange(i);
251+
const contentEnd = start + line.replace(/\s+$/, '').length;
252+
253+
return fixer.replaceTextRange([contentEnd, comment.range[1] - 2], `\n${indent}`);
254+
},
255+
});
203256
break;
204257
}
205258

206259
if (EMPTY_BLOCK_COMMENT_LINE.test(prevLine)) {
207-
context.report({ node: comment, messageId: 'noSpaceBeforeEnd' });
260+
context.report({
261+
node: comment,
262+
messageId: 'noSpaceBeforeEnd',
263+
fix: (fixer) => {
264+
const [prevStart, prevEnd] = lineRange(i - 1);
265+
266+
return fixer.removeRange([prevStart - 1, prevEnd]);
267+
},
268+
});
208269
break;
209270
}
210271

@@ -226,7 +287,16 @@ export default createRule<Options, MessageIds>({
226287

227288
if (isCurrentLineJSDoc && !insideCodeBlock) {
228289
if (prevLine && !EMPTY_BLOCK_COMMENT_LINE.test(prevLine) && !JS_DOC_REGEX.test(prevLine)) {
229-
context.report({ node: comment, messageId: 'spaceBeforeJSDoc', data: { ...lineData } });
290+
context.report({
291+
node: comment,
292+
messageId: 'spaceBeforeJSDoc',
293+
data: { ...lineData },
294+
fix: (fixer) => {
295+
const [start] = lineRange(i);
296+
297+
return fixer.insertTextBeforeRange([start, start], `${indent}*\n`);
298+
},
299+
});
230300
break;
231301
}
232302

@@ -238,7 +308,23 @@ export default createRule<Options, MessageIds>({
238308
}
239309

240310
if (!isCurrentLineEmpty && !BLOCK_COMMENT_LINE_START.test(line)) {
241-
context.report({ node: comment, messageId: 'invalidBlockCommentLine', data: { ...lineData } });
311+
context.report({
312+
node: comment,
313+
messageId: 'invalidBlockCommentLine',
314+
data: { ...lineData },
315+
fix: (fixer) => {
316+
const [start] = lineRange(i);
317+
const starMatch = line.match(/^\s*\*/);
318+
319+
if (starMatch) {
320+
return fixer.insertTextAfterRange([start, start + starMatch[0].length], ' ');
321+
}
322+
323+
const leadingWhitespace = line.match(/^\s*/)?.[0] ?? '';
324+
325+
return fixer.insertTextAfterRange([start, start + leadingWhitespace.length], '* ');
326+
},
327+
});
242328
break;
243329
}
244330

@@ -249,7 +335,14 @@ export default createRule<Options, MessageIds>({
249335
prevListItemIndentation = line.match(LIST_ITEM_INDENTATION)?.[1].length;
250336

251337
if (spaces !== 1) {
252-
context.report({ node: comment, messageId: 'invalidListItem', data: { ...lineData } });
338+
context.report({
339+
node: comment,
340+
messageId: 'invalidListItem',
341+
data: { ...lineData },
342+
fix: (fixer) => {
343+
return fixer.replaceTextRange(lineRange(i), line.replace(/^(\s*\*\s*(?:-|\d\.))\s*/, '$1 '));
344+
},
345+
});
253346
break;
254347
}
255348

@@ -276,7 +369,20 @@ export default createRule<Options, MessageIds>({
276369
const firstChar = text[0];
277370

278371
if (isLetter(firstChar) && !(isCapital(firstChar) || isCapitalizedOrAllowed(text, ignoredWords))) {
279-
context.report({ node: comment, messageId: 'paragraphCapitalized', data: { ...lineData } });
372+
context.report({
373+
node: comment,
374+
messageId: 'paragraphCapitalized',
375+
data: { ...lineData },
376+
fix: (fixer) => {
377+
const [start] = lineRange(i);
378+
const prefixLength = line.length - text.length;
379+
380+
return fixer.replaceTextRange(
381+
[start + prefixLength, start + prefixLength + 1],
382+
firstChar.toUpperCase(),
383+
);
384+
},
385+
});
280386
break;
281387
}
282388
}
@@ -287,8 +393,19 @@ export default createRule<Options, MessageIds>({
287393
if (isNextLineEmpty || isLastContentLine || isNextLineCodeBlock) {
288394
const lastChar = line[line.length - 1];
289395

396+
const appendDot = (fixer: TSESLint.RuleFixer) => {
397+
const [start] = lineRange(i);
398+
399+
return fixer.insertTextAfterRange([start, start + line.replace(/\s+$/, '').length], '.');
400+
};
401+
290402
if (isLastContentLine && !allowedParagraphEndings.some((ending) => ending === lastChar)) {
291-
context.report({ node: comment, messageId: 'shouldEndWithDot', data: { ...lineData } });
403+
context.report({
404+
node: comment,
405+
messageId: 'shouldEndWithDot',
406+
data: { ...lineData },
407+
fix: appendDot,
408+
});
292409
break;
293410
}
294411

@@ -297,6 +414,7 @@ export default createRule<Options, MessageIds>({
297414
node: comment,
298415
messageId: 'invalidParagraphEnding',
299416
data: { ...lineData, allowedParagraphEndings: `[${allowedParagraphEndings.join(' ')}]` },
417+
fix: appendDot,
300418
});
301419

302420
break;

src/rules/lines-around-comment.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,10 @@ export default createRule<Options, MessageIds>({
344344
node: token,
345345
messageId,
346346
fix(fixer) {
347+
if (messageId === 'after') {
348+
return fixer.insertTextAfterRange(token.range, '\n');
349+
}
350+
347351
return fixer.insertTextBeforeRange(range, '\n');
348352
},
349353
});

test/rules/block-scope-case.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,42 @@ ruleTester({
4343
break;
4444
}
4545
`,
46+
output: stripIndent`
47+
const foo = 1;
48+
49+
switch (foo) {
50+
case 1: {
51+
break;
52+
}
53+
}
54+
`,
55+
errors: [
56+
{
57+
messageId,
58+
},
59+
],
60+
},
61+
{
62+
// default case with multiple statements
63+
code: stripIndent`
64+
const foo = 1;
65+
66+
switch (foo) {
67+
default:
68+
console.log(foo);
69+
break;
70+
}
71+
`,
72+
output: stripIndent`
73+
const foo = 1;
74+
75+
switch (foo) {
76+
default: {
77+
console.log(foo);
78+
break;
79+
}
80+
}
81+
`,
4682
errors: [
4783
{
4884
messageId,

test/rules/catch-error-name.spec.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,58 @@ ruleTester({
5252
} catch (error) {}
5353
`,
5454
},
55+
{
56+
// references to the parameter are renamed as well
57+
code: stripIndent`
58+
try {} catch (err) {
59+
console.log(err.message, err);
60+
}
61+
`,
62+
errors: [
63+
{
64+
messageId: 'default',
65+
},
66+
],
67+
output: stripIndent`
68+
try {} catch (error) {
69+
console.log(error.message, error);
70+
}
71+
`,
72+
},
73+
{
74+
// the configured name is used, not a hardcoded one
75+
code: stripIndent`
76+
try {} catch (error) {}
77+
`,
78+
options: [
79+
{
80+
name: 'exception',
81+
ignore: [],
82+
},
83+
],
84+
errors: [
85+
{
86+
messageId: 'default',
87+
},
88+
],
89+
output: stripIndent`
90+
try {} catch (exception) {}
91+
`,
92+
},
93+
{
94+
// no fix when the expected name is already used inside the catch block
95+
code: stripIndent`
96+
try {} catch (err) {
97+
const error = 1;
98+
console.log(err, error);
99+
}
100+
`,
101+
errors: [
102+
{
103+
messageId: 'default',
104+
},
105+
],
106+
output: null,
107+
},
55108
],
56109
});

0 commit comments

Comments
 (0)