Skip to content

Commit 6b41299

Browse files
authored
Support TypeScript type assertions in many rules (#2909)
1 parent cd53d9e commit 6b41299

34 files changed

Lines changed: 1601 additions & 38 deletions

rules/prefer-math-min-max.js

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,27 @@ const isNumberTypeAnnotation = typeAnnotation => {
2323
return false;
2424
};
2525

26-
const getExpressionText = (node, sourceCode) => {
27-
const expressionNode = node.type === 'TSAsExpression' ? node.expression : node;
26+
function unwrapNode(node) {
27+
if (
28+
node.type === 'TSAsExpression'
29+
|| node.type === 'TSTypeAssertion'
30+
|| node.type === 'TSNonNullExpression'
31+
) {
32+
return unwrapNode(node.expression);
33+
}
34+
35+
return node;
36+
}
2837

29-
if (node.type === 'TSAsExpression') {
30-
return getExpressionText(expressionNode, sourceCode);
38+
function getTypeAnnotation(node) {
39+
if (node.type === 'TSNonNullExpression') {
40+
return getTypeAnnotation(node.expression);
3141
}
3242

33-
return sourceCode.getText(expressionNode);
34-
};
43+
if (node.type === 'TSAsExpression' || node.type === 'TSTypeAssertion') {
44+
return node.typeAnnotation;
45+
}
46+
}
3547

3648
/** @param {import('eslint').Rule.RuleContext} context */
3749
const create = context => {
@@ -64,7 +76,9 @@ const create = context => {
6476
return;
6577
}
6678

67-
const [leftText, rightText, alternateText, consequentText] = [left, right, alternate, consequent].map(node => getExpressionText(node, context.sourceCode));
79+
const [leftText, rightText, alternateText, consequentText] = [left, right, alternate, consequent].map(
80+
node => context.sourceCode.getText(unwrapNode(node)),
81+
);
6882

6983
const isGreaterOrEqual = operator === '>' || operator === '>=';
7084
const isLessOrEqual = operator === '<' || operator === '<=';
@@ -93,15 +107,14 @@ const create = context => {
93107
}
94108

95109
for (const node of [left, right]) {
96-
let expressionNode = node;
97-
98-
if (expressionNode.typeAnnotation && expressionNode.type === 'TSAsExpression') {
99-
// Ignore if the test is not a number comparison operator
100-
if (!isNumberTypeAnnotation(expressionNode.typeAnnotation)) {
101-
return;
102-
}
103-
104-
expressionNode = expressionNode.expression;
110+
const expressionNode = unwrapNode(node);
111+
const typeAnnotation = getTypeAnnotation(node);
112+
if (
113+
node !== expressionNode
114+
&& typeAnnotation
115+
&& !isNumberTypeAnnotation(typeAnnotation)
116+
) {
117+
return;
105118
}
106119

107120
// Find variable declaration

rules/prefer-ternary.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ const create = context => {
157157
&& isSameReference(left, alternate.left)
158158
) {
159159
return merge({
160-
before: `${before}${sourceCode.getText(left)} ${operator} `,
160+
before: `${before}${getParenthesizedText(left, context)} ${operator} `,
161161
after,
162162
consequent: right,
163163
alternate: alternate.right,

rules/utils/is-same-reference.js

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,24 @@ function equalLiteralValue(left, right) {
9797
return left.value === right.value;
9898
}
9999

100+
/**
101+
Unwrap ChainExpression (`?.`) and TypeScript type assertion (`as`, `<Type>`, or `!`) nodes.
102+
@param {ASTNode} node The node to unwrap.
103+
@returns {ASTNode} The unwrapped node.
104+
*/
105+
function unwrapNode(node) {
106+
if (
107+
node.type === 'ChainExpression'
108+
|| node.type === 'TSAsExpression'
109+
|| node.type === 'TSTypeAssertion'
110+
|| node.type === 'TSNonNullExpression'
111+
) {
112+
return unwrapNode(node.expression);
113+
}
114+
115+
return node;
116+
}
117+
100118
/**
101119
Check if two expressions reference the same value. For example:
102120
a = a
@@ -108,16 +126,10 @@ Check if two expressions reference the same value. For example:
108126
@returns {boolean} `true` if both sides match and reference the same value.
109127
*/
110128
export default function isSameReference(left, right) {
111-
if (left.type !== right.type) {
112-
// Handle `a.b` and `a?.b` are samely.
113-
if (left.type === 'ChainExpression') {
114-
return isSameReference(left.expression, right);
115-
}
116-
117-
if (right.type === 'ChainExpression') {
118-
return isSameReference(left, right.expression);
119-
}
129+
left = unwrapNode(left);
130+
right = unwrapNode(right);
120131

132+
if (left.type !== right.type) {
121133
return false;
122134
}
123135

@@ -136,10 +148,6 @@ export default function isSameReference(left, right) {
136148
return equalLiteralValue(left, right);
137149
}
138150

139-
case 'ChainExpression': {
140-
return isSameReference(left.expression, right.expression);
141-
}
142-
143151
case 'MemberExpression': {
144152
const nameA = getStaticPropertyName(left);
145153

rules/utils/should-add-parentheses-to-conditional-expression-child.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,7 @@ export default function shouldAddParenthesesToConditionalExpressionChild(node) {
99
// Lower precedence, see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#Table
1010
|| node.type === 'AssignmentExpression'
1111
|| node.type === 'YieldExpression'
12-
|| node.type === 'SequenceExpression';
12+
|| node.type === 'SequenceExpression'
13+
|| node.type === 'TSAsExpression'
14+
|| node.type === 'TSTypeAssertion';
1315
}

test/no-useless-length-check.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import outdent from 'outdent';
2-
import {getTester} from './utils/test.js';
2+
import {getTester, parsers} from './utils/test.js';
33

44
const {test} = getTester(import.meta);
55

@@ -163,5 +163,21 @@ test.snapshot({
163163
);
164164
}
165165
`,
166+
{
167+
code: '(array as any[]).length === 0 || (array as any[]).every(Boolean)',
168+
languageOptions: {parser: parsers.typescript},
169+
},
170+
{
171+
code: '(array as any[]).length > 0 && (array as any[]).some(Boolean)',
172+
languageOptions: {parser: parsers.typescript},
173+
},
174+
{
175+
code: 'array!.length === 0 || array!.every(Boolean)',
176+
languageOptions: {parser: parsers.typescript},
177+
},
178+
{
179+
code: 'array!.length > 0 && array!.some(Boolean)',
180+
languageOptions: {parser: parsers.typescript},
181+
},
166182
],
167183
});

test/prefer-at.js

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import outdent from 'outdent';
2-
import {getTester} from './utils/test.js';
2+
import {getTester, parsers} from './utils/test.js';
33

44
const {test} = getTester(import.meta);
55

@@ -43,6 +43,30 @@ test.snapshot({
4343
'function foo() {return arguments[arguments.length - 1]}',
4444
'class Foo {bar; baz() {return this.bar[this.bar.length - 1]}}',
4545
'class Foo {#bar; baz() {return this.#bar[this.#bar.length - 1]}}',
46+
{
47+
code: '(array as Foo[])[array.length - 1]',
48+
languageOptions: {parser: parsers.typescript},
49+
},
50+
{
51+
code: '(array as Foo[])[(array as Foo[]).length - 1]',
52+
languageOptions: {parser: parsers.typescript},
53+
},
54+
{
55+
code: '<Foo[]>array[array.length - 1]',
56+
languageOptions: {parser: parsers.typescript},
57+
},
58+
{
59+
code: '<Foo[]>array[(<Foo[]>array).length - 1]',
60+
languageOptions: {parser: parsers.typescript},
61+
},
62+
{
63+
code: 'array![array.length - 1]',
64+
languageOptions: {parser: parsers.typescript},
65+
},
66+
{
67+
code: 'array![array!.length - 1]',
68+
languageOptions: {parser: parsers.typescript},
69+
},
4670
],
4771
});
4872

@@ -67,6 +91,18 @@ test.snapshot({
6791
'(( string )).charAt(string.length - 1);',
6892
'(( string.charAt ))(string.length - 1);',
6993
'(( string.charAt(string.length - 1) ));',
94+
{
95+
code: '(string as string).charAt((string as string).length - 1);',
96+
languageOptions: {parser: parsers.typescript},
97+
},
98+
{
99+
code: '<string>string.charAt((<string>string).length - 1);',
100+
languageOptions: {parser: parsers.typescript},
101+
},
102+
{
103+
code: 'string!.charAt(string!.length - 1);',
104+
languageOptions: {parser: parsers.typescript},
105+
},
70106
],
71107
});
72108

@@ -112,6 +148,30 @@ test.snapshot({
112148
'(( array.slice(-1).pop ))();',
113149
'(( array.slice(-1).pop() ));',
114150
'array.slice(-1)[0].pop().shift().slice(-1)',
151+
{
152+
code: '(array as Foo[]).slice(-1)[0]',
153+
languageOptions: {parser: parsers.typescript},
154+
},
155+
{
156+
code: '(array as Foo[]).slice(-1).pop()',
157+
languageOptions: {parser: parsers.typescript},
158+
},
159+
{
160+
code: '<Foo[]>array.slice(-1)[0]',
161+
languageOptions: {parser: parsers.typescript},
162+
},
163+
{
164+
code: '<Foo[]>array.slice(-1).shift()',
165+
languageOptions: {parser: parsers.typescript},
166+
},
167+
{
168+
code: 'array!.slice(-1)[0]',
169+
languageOptions: {parser: parsers.typescript},
170+
},
171+
{
172+
code: 'array!.slice(-1).pop()',
173+
languageOptions: {parser: parsers.typescript},
174+
},
115175
],
116176
});
117177

test/prefer-classlist-toggle.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import outdent from 'outdent';
2-
import {getTester} from './utils/test.js';
2+
import {getTester, parsers} from './utils/test.js';
33

44
const {test} = getTester(import.meta);
55

@@ -290,6 +290,26 @@ test.snapshot({
290290
element.classList.remove('className');
291291
}
292292
`,
293+
{
294+
code: outdent`
295+
if (condition) {
296+
(element as Element).classList.add('className');
297+
} else {
298+
(element as Element).classList.remove('className');
299+
}
300+
`,
301+
languageOptions: {parser: parsers.typescript},
302+
},
303+
{
304+
code: outdent`
305+
if (condition) {
306+
element!.classList.add('className');
307+
} else {
308+
element!.classList.remove('className');
309+
}
310+
`,
311+
languageOptions: {parser: parsers.typescript},
312+
},
293313
],
294314
});
295315

test/prefer-logical-operator-over-ternary.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import outdent from 'outdent';
2-
import {getTester} from './utils/test.js';
2+
import {getTester, parsers} from './utils/test.js';
33

44
const {test} = getTester(import.meta);
55

@@ -41,5 +41,25 @@ test.snapshot({
4141
const foo = []
4242
a && b ? a && b : 1
4343
`,
44+
{
45+
code: '(foo as string) ? (foo as string) : bar;',
46+
languageOptions: {parser: parsers.typescript},
47+
},
48+
{
49+
code: '(foo.bar as string) ? (foo.bar as string) : foo.baz',
50+
languageOptions: {parser: parsers.typescript},
51+
},
52+
{
53+
code: '(a && b as boolean) ? (a && b as boolean) : bar',
54+
languageOptions: {parser: parsers.typescript},
55+
},
56+
{
57+
code: 'foo! ? foo! : bar;',
58+
languageOptions: {parser: parsers.typescript},
59+
},
60+
{
61+
code: 'foo.bar! ? foo.bar! : foo.baz',
62+
languageOptions: {parser: parsers.typescript},
63+
},
4464
],
4565
});

test/prefer-math-min-max.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,16 @@ test.snapshot({
116116
return (a as string) > b ? a : b;
117117
}
118118
`,
119+
outdent`
120+
function foo(a, b) {
121+
return (<bigint>a) > b ? a : b;
122+
}
123+
`,
124+
outdent`
125+
function foo(a, b) {
126+
return (<string>a) > b ? a : b;
127+
}
128+
`,
119129
outdent`
120130
function foo(a: string, b) {
121131
return a > b ? a : b;
@@ -153,6 +163,16 @@ test.snapshot({
153163
154164
var value = foo > bar ? bar : foo;
155165
`,
166+
outdent`
167+
function foo(a, b) {
168+
return (a as string)! > b ? (a as string)! : b;
169+
}
170+
`,
171+
outdent`
172+
function foo(a, b) {
173+
return (<string>a)! > b ? (<string>a)! : b;
174+
}
175+
`,
156176
],
157177
invalid: [
158178
outdent`
@@ -188,5 +208,20 @@ test.snapshot({
188208
189209
var value = foo > bar ? bar : foo;
190210
`,
211+
outdent`
212+
function foo(a, b) {
213+
return (a as number) > b ? (a as number) : b;
214+
}
215+
`,
216+
outdent`
217+
function foo(a, b) {
218+
return (<number>a) > b ? (<number>a) : b;
219+
}
220+
`,
221+
outdent`
222+
function foo(a, b) {
223+
return a! > b ? a! : b;
224+
}
225+
`,
191226
],
192227
});

0 commit comments

Comments
 (0)