Skip to content

Commit a497fde

Browse files
authored
fix(eslint-plugin-template): [eqeqeq] change fix to suggest (#465)
1 parent 3cb9423 commit a497fde

File tree

2 files changed

+178
-54
lines changed

2 files changed

+178
-54
lines changed

packages/eslint-plugin-template/src/rules/eqeqeq.ts

Lines changed: 80 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import type { AST, Binary } from '@angular/compiler';
22
import { ASTWithSource, LiteralPrimitive } from '@angular/compiler';
3+
import type { TSESLint } from '@typescript-eslint/experimental-utils';
34
import {
45
createESLintRule,
56
ensureTemplateParser,
67
} from '../utils/create-eslint-rule';
78
import { getNearestNodeFrom } from '../utils/get-nearest-node-from';
89

910
type Options = [{ readonly allowNullOrUndefined?: boolean }];
10-
export type MessageIds = 'eqeqeq';
11+
export type MessageIds = 'eqeqeq' | 'suggestStrictEquality';
1112
export const RULE_NAME = 'eqeqeq';
1213
const DEFAULT_OPTIONS: Options[0] = { allowNullOrUndefined: false };
1314

@@ -19,6 +20,7 @@ export default createESLintRule<Options, MessageIds>({
1920
description: 'Requires `===` and `!==` in place of `==` and `!=`',
2021
category: 'Best Practices',
2122
recommended: 'error',
23+
suggestion: true,
2224
},
2325
fixable: 'code',
2426
schema: [
@@ -36,6 +38,8 @@ export default createESLintRule<Options, MessageIds>({
3638
messages: {
3739
eqeqeq:
3840
'Expected `{{expectedOperation}}` but received `{{actualOperation}}`',
41+
suggestStrictEquality:
42+
'Replace `{{expectedOperation}}` with `{{actualOperation}}`',
3943
},
4044
},
4145
defaultOptions: [DEFAULT_OPTIONS],
@@ -51,30 +55,37 @@ export default createESLintRule<Options, MessageIds>({
5155
right,
5256
sourceSpan: { start, end },
5357
} = node;
54-
const isNilComparison = [left, right].some(isNilValue);
5558

56-
if (allowNullOrUndefined && isNilComparison) return;
59+
if (allowNullOrUndefined && (isNilValue(left) || isNilValue(right))) {
60+
return;
61+
}
5762

63+
const data = {
64+
actualOperation: operation,
65+
expectedOperation: `${operation}=`,
66+
} as const;
5867
context.report({
5968
loc: {
6069
start: sourceCode.getLocFromIndex(start),
6170
end: sourceCode.getLocFromIndex(end),
6271
},
6372
messageId: 'eqeqeq',
64-
data: {
65-
actualOperation: operation,
66-
expectedOperation: `${operation}=`,
67-
},
68-
fix: (fixer) => {
69-
const { source } = getNearestNodeFrom(node, isASTWithSource) ?? {};
70-
71-
if (!source) return [];
72-
73-
return fixer.insertTextAfterRange(
74-
[start + getSpanLength(left) + 1, end - getSpanLength(right) - 1],
75-
'=',
76-
);
77-
},
73+
data,
74+
...(isStringNonNumericValue(left) || isStringNonNumericValue(right)
75+
? {
76+
fix: (fixer) =>
77+
getFix({ node, left, right, start, end, fixer }),
78+
}
79+
: {
80+
suggest: [
81+
{
82+
messageId: 'suggestStrictEquality',
83+
fix: (fixer) =>
84+
getFix({ node, left, right, start, end, fixer }),
85+
data,
86+
},
87+
],
88+
}),
7889
});
7990
},
8091
};
@@ -85,13 +96,62 @@ function getSpanLength({ span: { start, end } }: AST): number {
8596
return end - start;
8697
}
8798

99+
const getFix = ({
100+
node,
101+
left,
102+
right,
103+
start,
104+
end,
105+
fixer,
106+
}: {
107+
node: Binary;
108+
left: AST;
109+
right: AST;
110+
start: number;
111+
end: number;
112+
fixer: TSESLint.RuleFixer;
113+
}): TSESLint.RuleFix | TSESLint.RuleFix[] => {
114+
const { source } = getNearestNodeFrom(node, isASTWithSource) ?? {};
115+
116+
if (!source) return [];
117+
118+
return fixer.insertTextAfterRange(
119+
[start + getSpanLength(left) + 1, end - getSpanLength(right) - 1],
120+
'=',
121+
);
122+
};
123+
88124
function isASTWithSource(node: unknown): node is ASTWithSource {
89125
return node instanceof ASTWithSource;
90126
}
91127

92-
function isNilValue(ast: AST): ast is LiteralPrimitive {
128+
function isLiteralPrimitive(node: unknown): node is LiteralPrimitive {
129+
return node instanceof LiteralPrimitive;
130+
}
131+
132+
function isNumeric(value: unknown): value is number | string {
133+
return (
134+
!Number.isNaN(Number.parseFloat(String(value))) &&
135+
Number.isFinite(Number(value))
136+
);
137+
}
138+
139+
function isString(value: unknown): value is string {
140+
return typeof value === 'string';
141+
}
142+
143+
function isStringNonNumericValue(
144+
ast: AST,
145+
): ast is LiteralPrimitive & { value: string } {
146+
return (
147+
isLiteralPrimitive(ast) && isString(ast.value) && !isNumeric(ast.value)
148+
);
149+
}
150+
151+
function isNilValue(
152+
ast: AST,
153+
): ast is LiteralPrimitive & { value: null | undefined } {
93154
return (
94-
ast instanceof LiteralPrimitive &&
95-
(ast.value === null || ast.value === undefined)
155+
isLiteralPrimitive(ast) && (ast.value === null || ast.value === undefined)
96156
);
97157
}

packages/eslint-plugin-template/tests/rules/eqeqeq.test.ts

Lines changed: 98 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const ruleTester = new RuleTester({
1313
parser: '@angular-eslint/template-parser',
1414
});
1515
const messageId: MessageIds = 'eqeqeq';
16+
const suggestStrictEquality: MessageIds = 'suggestStrictEquality';
1617

1718
ruleTester.run(RULE_NAME, rule, {
1819
valid: [
@@ -31,7 +32,7 @@ ruleTester.run(RULE_NAME, rule, {
3132
description:
3233
'it should fail if the operation is not strict within interpolation',
3334
annotatedSource: `
34-
{{ test == 'null' }}
35+
{{ 'null' == test }}
3536
~~~~~~~~~~~~~~
3637
`,
3738
messageId,
@@ -40,15 +41,15 @@ ruleTester.run(RULE_NAME, rule, {
4041
expectedOperation: '===',
4142
},
4243
annotatedOutput: `
43-
{{ test === 'null' }}
44+
{{ 'null' === test }}
4445
~~~~~~~~~~~~~~~
4546
`,
4647
}),
4748
convertAnnotatedSourceToFailureCase({
4849
description:
4950
'it should fail if the operation is not strict within attribute directive',
5051
annotatedSource: `
51-
<div [attr.disabled]="test != 'undefined' && null == 3"></div>
52+
<div [attr.disabled]="test != 'undefined' && null == '3'"></div>
5253
~~~~~~~~~~~~~~~~~~~
5354
`,
5455
messageId,
@@ -58,7 +59,7 @@ ruleTester.run(RULE_NAME, rule, {
5859
},
5960
options: [{ allowNullOrUndefined: true }],
6061
annotatedOutput: `
61-
<div [attr.disabled]="test !== 'undefined' && null == 3"></div>
62+
<div [attr.disabled]="test !== 'undefined' && null == '3'"></div>
6263
~~~~~~~~~~~~~~~~~~~~
6364
`,
6465
}),
@@ -74,27 +75,45 @@ ruleTester.run(RULE_NAME, rule, {
7475
actualOperation: '==',
7576
expectedOperation: '===',
7677
},
77-
annotatedOutput: `
78+
suggestions: [
79+
{
80+
messageId: suggestStrictEquality,
81+
output: `
7882
<div *ngIf="test === true || test1 !== undefined"></div>
79-
~~~~~~~~~~~~~
80-
`,
83+
84+
`,
85+
data: {
86+
actualOperation: '==',
87+
expectedOperation: '===',
88+
},
89+
},
90+
],
8191
}),
8292
convertAnnotatedSourceToFailureCase({
8393
description:
8494
'it should fail if the operation is not strict within conditional',
8595
annotatedSource: `
86-
{{ one != two ? c > d : 'hey!' }}
87-
~~~~~~~~~~
96+
{{ one != '02' ? c > d : 'hey!' }}
97+
~~~~~~~~~~~
8898
`,
8999
messageId,
90100
data: {
91101
actualOperation: '!=',
92102
expectedOperation: '!==',
93103
},
94-
annotatedOutput: `
95-
{{ one !== two ? c > d : 'hey!' }}
96-
~~~~~~~~~~~
97-
`,
104+
suggestions: [
105+
{
106+
messageId: suggestStrictEquality,
107+
output: `
108+
{{ one !== '02' ? c > d : 'hey!' }}
109+
110+
`,
111+
data: {
112+
actualOperation: '!=',
113+
expectedOperation: '!==',
114+
},
115+
},
116+
],
98117
}),
99118
convertAnnotatedSourceToFailureCase({
100119
description:
@@ -108,10 +127,19 @@ ruleTester.run(RULE_NAME, rule, {
108127
actualOperation: '==',
109128
expectedOperation: '===',
110129
},
111-
annotatedOutput: `
130+
suggestions: [
131+
{
132+
messageId: suggestStrictEquality,
133+
output: `
112134
{{ a === b && 1 === b ? c > d : 'hey!' }}
113-
~~~~~~~
114-
`,
135+
136+
`,
137+
data: {
138+
actualOperation: '==',
139+
expectedOperation: '===',
140+
},
141+
},
142+
],
115143
}),
116144
convertAnnotatedSourceToFailureCase({
117145
description:
@@ -125,45 +153,72 @@ ruleTester.run(RULE_NAME, rule, {
125153
actualOperation: '!=',
126154
expectedOperation: '!==',
127155
},
128-
annotatedOutput: `
156+
suggestions: [
157+
{
158+
messageId: suggestStrictEquality,
159+
output: `
129160
{{ c > d ? a !== b : 'hey!' }}
130-
~~~~~~~
131-
`,
161+
162+
`,
163+
data: {
164+
actualOperation: '!=',
165+
expectedOperation: '!==',
166+
},
167+
},
168+
],
132169
}),
133170
convertAnnotatedSourceToFailureCase({
134171
description:
135172
'it should fail if the operation is not strict within conditional (falseExp)',
136173
annotatedSource: `
137-
{{ c > d ? 'hey!' : a == b }}
138-
~~~~~~
174+
{{ c > d ? 'hey!' : a == false }}
175+
~~~~~~~~~~
139176
`,
140177
messageId,
141178
data: {
142179
actualOperation: '==',
143180
expectedOperation: '===',
144181
},
145-
annotatedOutput: `
146-
{{ c > d ? 'hey!' : a === b }}
147-
~~~~~~~
148-
`,
182+
suggestions: [
183+
{
184+
messageId: suggestStrictEquality,
185+
output: `
186+
{{ c > d ? 'hey!' : a === false }}
187+
188+
`,
189+
data: {
190+
actualOperation: '==',
191+
expectedOperation: '===',
192+
},
193+
},
194+
],
149195
}),
150196
convertAnnotatedSourceToFailureCase({
151197
description:
152198
'it should fail if the operation is not strict within recursive conditional',
153199
annotatedSource: `
154-
{{ undefined == test1 && a === b ? (c > d ? d != 3 : v === 4) : 'hey!' }}
155-
~~~~~~
200+
{{ undefined == test1 && a === b ? (c > d ? d != '0' : v === 4) : 'hey!' }}
201+
~~~~~~~~
156202
`,
157203
messageId,
158204
options: [{ allowNullOrUndefined: true }],
159205
data: {
160206
actualOperation: '!=',
161207
expectedOperation: '!==',
162208
},
163-
annotatedOutput: `
164-
{{ undefined == test1 && a === b ? (c > d ? d !== 3 : v === 4) : 'hey!' }}
165-
~~~~~~~
166-
`,
209+
suggestions: [
210+
{
211+
messageId: suggestStrictEquality,
212+
output: `
213+
{{ undefined == test1 && a === b ? (c > d ? d !== '0' : v === 4) : 'hey!' }}
214+
215+
`,
216+
data: {
217+
actualOperation: '!=',
218+
expectedOperation: '!==',
219+
},
220+
},
221+
],
167222
}),
168223
convertAnnotatedSourceToFailureCase({
169224
description:
@@ -177,10 +232,19 @@ ruleTester.run(RULE_NAME, rule, {
177232
actualOperation: '!=',
178233
expectedOperation: '!==',
179234
},
180-
annotatedOutput: `
235+
suggestions: [
236+
{
237+
messageId: suggestStrictEquality,
238+
output: `
181239
{{ undefined !== test1 }}
182-
~~~~~~~~~~~~~~~~~~~
183-
`,
240+
241+
`,
242+
data: {
243+
actualOperation: '!=',
244+
expectedOperation: '!==',
245+
},
246+
},
247+
],
184248
}),
185249
],
186250
});

0 commit comments

Comments
 (0)