Skip to content

Commit 5bd4c0a

Browse files
fix(eslint-plugin): [consistent-type-definitions] don't leave trailing parens when fixing type to interface (typescript-eslint#10235)
* [consistent-type-definitions] remove closing parens around a type * cov * better fixer logic * test fixer edge cases * unused imports
1 parent b347c04 commit 5bd4c0a

File tree

2 files changed

+148
-24
lines changed

2 files changed

+148
-24
lines changed

packages/eslint-plugin/src/rules/consistent-type-definitions.ts

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
22

3-
import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils';
3+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
44

5-
import { createRule } from '../util';
5+
import { createRule, nullThrows, NullThrowsReasons } from '../util';
66

77
export default createRule({
88
name: 'consistent-type-definitions',
@@ -54,32 +54,45 @@ export default createRule({
5454
node: node.id,
5555
messageId: 'interfaceOverType',
5656
fix(fixer) {
57-
const typeNode = node.typeParameters ?? node.id;
58-
const fixes: TSESLint.RuleFix[] = [];
57+
const typeToken = nullThrows(
58+
context.sourceCode.getTokenBefore(
59+
node.id,
60+
token => token.value === 'type',
61+
),
62+
NullThrowsReasons.MissingToken('type keyword', 'type alias'),
63+
);
5964

60-
const firstToken = context.sourceCode.getTokenBefore(node.id);
61-
if (firstToken) {
62-
fixes.push(fixer.replaceText(firstToken, 'interface'));
63-
fixes.push(
64-
fixer.replaceTextRange(
65-
[typeNode.range[1], node.typeAnnotation.range[0]],
66-
' ',
67-
),
68-
);
69-
}
65+
const equalsToken = nullThrows(
66+
context.sourceCode.getTokenBefore(
67+
node.typeAnnotation,
68+
token => token.value === '=',
69+
),
70+
NullThrowsReasons.MissingToken('=', 'type alias'),
71+
);
7072

71-
const afterToken = context.sourceCode.getTokenAfter(
72-
node.typeAnnotation,
73+
const beforeEqualsToken = nullThrows(
74+
context.sourceCode.getTokenBefore(equalsToken, {
75+
includeComments: true,
76+
}),
77+
NullThrowsReasons.MissingToken('before =', 'type alias'),
7378
);
74-
if (
75-
afterToken &&
76-
afterToken.type === AST_TOKEN_TYPES.Punctuator &&
77-
afterToken.value === ';'
78-
) {
79-
fixes.push(fixer.remove(afterToken));
80-
}
8179

82-
return fixes;
80+
return [
81+
// replace 'type' with 'interface'.
82+
fixer.replaceText(typeToken, 'interface'),
83+
84+
// delete from the = to the { of the type, and put a space to be pretty.
85+
fixer.replaceTextRange(
86+
[beforeEqualsToken.range[1], node.typeAnnotation.range[0]],
87+
' ',
88+
),
89+
90+
// remove from the closing } through the end of the statement.
91+
fixer.removeRange([
92+
node.typeAnnotation.range[1],
93+
node.range[1],
94+
]),
95+
];
8396
},
8497
});
8598
},

packages/eslint-plugin/tests/rules/consistent-type-definitions.test.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,18 @@ export type W<T> = {
9696
options: ['interface'],
9797
output: `interface T { x: number; }`,
9898
},
99+
{
100+
code: noFormat`type T /* comment */={ x: number; };`,
101+
errors: [
102+
{
103+
column: 6,
104+
line: 1,
105+
messageId: 'interfaceOverType',
106+
},
107+
],
108+
options: ['interface'],
109+
output: `interface T /* comment */ { x: number; }`,
110+
},
99111
{
100112
code: `
101113
export type W<T> = {
@@ -350,5 +362,104 @@ export declare type Test = {
350362
}
351363
`,
352364
},
365+
{
366+
code: noFormat`
367+
type Foo = ({
368+
a: string;
369+
});
370+
`,
371+
errors: [
372+
{
373+
line: 2,
374+
messageId: 'interfaceOverType',
375+
},
376+
],
377+
output: `
378+
interface Foo {
379+
a: string;
380+
}
381+
`,
382+
},
383+
{
384+
code: noFormat`
385+
type Foo = ((((((((({
386+
a: string;
387+
})))))))));
388+
`,
389+
errors: [
390+
{
391+
line: 2,
392+
messageId: 'interfaceOverType',
393+
},
394+
],
395+
output: `
396+
interface Foo {
397+
a: string;
398+
}
399+
`,
400+
},
401+
{
402+
// no closing semicolon
403+
code: noFormat`
404+
type Foo = {
405+
a: string;
406+
}
407+
`,
408+
errors: [
409+
{
410+
line: 2,
411+
messageId: 'interfaceOverType',
412+
},
413+
],
414+
output: `
415+
interface Foo {
416+
a: string;
417+
}
418+
`,
419+
},
420+
{
421+
// no closing semicolon; ensure we don't erase subsequent code.
422+
code: noFormat`
423+
type Foo = {
424+
a: string;
425+
}
426+
type Bar = string;
427+
`,
428+
errors: [
429+
{
430+
line: 2,
431+
messageId: 'interfaceOverType',
432+
},
433+
],
434+
output: `
435+
interface Foo {
436+
a: string;
437+
}
438+
type Bar = string;
439+
`,
440+
},
441+
{
442+
// no closing semicolon; ensure we don't erase subsequent code.
443+
code: noFormat`
444+
type Foo = ((({
445+
a: string;
446+
})))
447+
448+
const bar = 1;
449+
`,
450+
errors: [
451+
{
452+
line: 2,
453+
messageId: 'interfaceOverType',
454+
},
455+
],
456+
output: `
457+
interface Foo {
458+
a: string;
459+
}
460+
461+
const bar = 1;
462+
`,
463+
},
353464
],
354465
});

0 commit comments

Comments
 (0)