Skip to content

Commit 8293546

Browse files
fix(eslint-plugin): [no-confusing-non-null-assertion] check !in and !instanceof (#9994)
* ban !in and !instanceof * lintfix * snapshots * cov
1 parent b75d42b commit 8293546

File tree

4 files changed

+219
-40
lines changed

4 files changed

+219
-40
lines changed

packages/eslint-plugin/docs/rules/no-confusing-non-null-assertion.mdx

+16-3
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,27 @@ import TabItem from '@theme/TabItem';
99
>
1010
> See **https://typescript-eslint.io/rules/no-confusing-non-null-assertion** for documentation.
1111
12-
Using a non-null assertion (`!`) next to an assign or equals check (`=` or `==` or `===`) creates code that is confusing as it looks similar to a not equals check (`!=` `!==`).
12+
Using a non-null assertion (`!`) next to an assignment or equality check (`=` or `==` or `===`) creates code that is confusing as it looks similar to an inequality check (`!=` `!==`).
1313

1414
```typescript
15-
a! == b; // a non-null assertions(`!`) and an equals test(`==`)
15+
a! == b; // a non-null assertion(`!`) and an equals test(`==`)
1616
a !== b; // not equals test(`!==`)
17-
a! === b; // a non-null assertions(`!`) and an triple equals test(`===`)
17+
a! === b; // a non-null assertion(`!`) and a triple equals test(`===`)
1818
```
1919

20+
Using a non-null assertion (`!`) next to an in test (`in`) or an instanceof test (`instanceof`) creates code that is confusing since it may look like the operator is negated, but it is actually not.
21+
22+
{/* prettier-ignore */}
23+
```typescript
24+
a! in b; // a non-null assertion(`!`) and an in test(`in`)
25+
a !in b; // also a non-null assertion(`!`) and an in test(`in`)
26+
!(a in b); // a negated in test
27+
28+
a! instanceof b; // a non-null assertion(`!`) and an instanceof test(`instanceof`)
29+
a !instanceof b; // also a non-null assertion(`!`) and an instanceof test(`instanceof`)
30+
!(a instanceof b); // a negated instanceof test
31+
````
32+
2033
This rule flags confusing `!` assertions and suggests either removing them or wrapping the asserted expression in `()` parenthesis.
2134

2235
## Examples
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,36 @@
11
import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
22
import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils';
3+
import type {
4+
ReportDescriptor,
5+
RuleFix,
6+
} from '@typescript-eslint/utils/ts-eslint';
37

48
import { createRule } from '../util';
59

6-
export default createRule({
10+
type MessageId =
11+
| 'confusingEqual'
12+
| 'confusingAssign'
13+
| 'confusingOperator'
14+
| 'notNeedInEqualTest'
15+
| 'notNeedInAssign'
16+
| 'notNeedInOperator'
17+
| 'wrapUpLeft';
18+
19+
const confusingOperators = new Set([
20+
'=',
21+
'==',
22+
'===',
23+
'in',
24+
'instanceof',
25+
] as const);
26+
type ConfusingOperator =
27+
typeof confusingOperators extends Set<infer T> ? T : never;
28+
29+
function isConfusingOperator(operator: string): operator is ConfusingOperator {
30+
return confusingOperators.has(operator as ConfusingOperator);
31+
}
32+
33+
export default createRule<[], MessageId>({
734
name: 'no-confusing-non-null-assertion',
835
meta: {
936
type: 'problem',
@@ -15,68 +42,127 @@ export default createRule({
1542
hasSuggestions: true,
1643
messages: {
1744
confusingEqual:
18-
'Confusing combinations of non-null assertion and equal test like "a! == b", which looks very similar to not equal "a !== b".',
45+
'Confusing combination of non-null assertion and equality test like `a! == b`, which looks very similar to `a !== b`.',
1946
confusingAssign:
20-
'Confusing combinations of non-null assertion and equal test like "a! = b", which looks very similar to not equal "a != b".',
21-
notNeedInEqualTest: 'Unnecessary non-null assertion (!) in equal test.',
47+
'Confusing combination of non-null assertion and assignment like `a! = b`, which looks very similar to `a != b`.',
48+
confusingOperator:
49+
'Confusing combination of non-null assertion and `{{operator}}` operator like `a! {{operator}} b`, which might be misinterpreted as `!(a {{operator}} b)`.',
50+
51+
notNeedInEqualTest:
52+
'Remove unnecessary non-null assertion (!) in equality test.',
2253
notNeedInAssign:
23-
'Unnecessary non-null assertion (!) in assignment left hand.',
54+
'Remove unnecessary non-null assertion (!) in assignment left-hand side.',
55+
56+
notNeedInOperator:
57+
'Remove possibly unnecessary non-null assertion (!) in the left operand of the `{{operator}}` operator.',
58+
2459
wrapUpLeft:
25-
'Wrap up left hand to avoid putting non-null assertion "!" and "=" together.',
60+
'Wrap the left-hand side in parentheses to avoid confusion with "{{operator}}" operator.',
2661
},
2762
schema: [],
2863
},
2964
defaultOptions: [],
3065
create(context) {
66+
function confusingOperatorToMessageData(
67+
operator: ConfusingOperator,
68+
): Pick<ReportDescriptor<MessageId>, 'messageId' | 'data'> {
69+
switch (operator) {
70+
case '=':
71+
return {
72+
messageId: 'confusingAssign',
73+
};
74+
case '==':
75+
case '===':
76+
return {
77+
messageId: 'confusingEqual',
78+
};
79+
case 'in':
80+
case 'instanceof':
81+
return {
82+
messageId: 'confusingOperator',
83+
data: { operator },
84+
};
85+
// istanbul ignore next
86+
default:
87+
operator satisfies never;
88+
throw new Error(`Unexpected operator ${operator as string}`);
89+
}
90+
}
91+
3192
return {
3293
'BinaryExpression, AssignmentExpression'(
3394
node: TSESTree.AssignmentExpression | TSESTree.BinaryExpression,
3495
): void {
35-
function isLeftHandPrimaryExpression(
36-
node: TSESTree.Expression | TSESTree.PrivateIdentifier,
37-
): boolean {
38-
return node.type === AST_NODE_TYPES.TSNonNullExpression;
39-
}
96+
const operator = node.operator;
4097

41-
if (
42-
node.operator === '==' ||
43-
node.operator === '===' ||
44-
node.operator === '='
45-
) {
46-
const isAssign = node.operator === '=';
98+
if (isConfusingOperator(operator)) {
99+
// Look for a non-null assertion as the last token on the left hand side.
100+
// That way, we catch things like `1 + two! === 3`, even though the left
101+
// hand side isn't a non-null assertion AST node.
47102
const leftHandFinalToken = context.sourceCode.getLastToken(node.left);
48103
const tokenAfterLeft = context.sourceCode.getTokenAfter(node.left);
49104
if (
50105
leftHandFinalToken?.type === AST_TOKEN_TYPES.Punctuator &&
51106
leftHandFinalToken.value === '!' &&
52107
tokenAfterLeft?.value !== ')'
53108
) {
54-
if (isLeftHandPrimaryExpression(node.left)) {
109+
if (node.left.type === AST_NODE_TYPES.TSNonNullExpression) {
110+
let suggestions: TSESLint.SuggestionReportDescriptor<MessageId>[];
111+
switch (operator) {
112+
case '=':
113+
suggestions = [
114+
{
115+
messageId: 'notNeedInAssign',
116+
fix: (fixer): RuleFix => fixer.remove(leftHandFinalToken),
117+
},
118+
];
119+
break;
120+
121+
case '==':
122+
case '===':
123+
suggestions = [
124+
{
125+
messageId: 'notNeedInEqualTest',
126+
fix: (fixer): RuleFix => fixer.remove(leftHandFinalToken),
127+
},
128+
];
129+
break;
130+
131+
case 'in':
132+
case 'instanceof':
133+
suggestions = [
134+
{
135+
messageId: 'notNeedInOperator',
136+
data: { operator },
137+
fix: (fixer): RuleFix => fixer.remove(leftHandFinalToken),
138+
},
139+
{
140+
messageId: 'wrapUpLeft',
141+
data: { operator },
142+
fix: wrapUpLeftFixer(node),
143+
},
144+
];
145+
break;
146+
147+
// istanbul ignore next
148+
default:
149+
operator satisfies never;
150+
return;
151+
}
55152
context.report({
56153
node,
57-
messageId: isAssign ? 'confusingAssign' : 'confusingEqual',
58-
suggest: [
59-
{
60-
messageId: isAssign
61-
? 'notNeedInAssign'
62-
: 'notNeedInEqualTest',
63-
fix: (fixer): TSESLint.RuleFix[] => [
64-
fixer.remove(leftHandFinalToken),
65-
],
66-
},
67-
],
154+
...confusingOperatorToMessageData(operator),
155+
suggest: suggestions,
68156
});
69157
} else {
70158
context.report({
71159
node,
72-
messageId: isAssign ? 'confusingAssign' : 'confusingEqual',
160+
...confusingOperatorToMessageData(operator),
73161
suggest: [
74162
{
75163
messageId: 'wrapUpLeft',
76-
fix: (fixer): TSESLint.RuleFix[] => [
77-
fixer.insertTextBefore(node.left, '('),
78-
fixer.insertTextAfter(node.left, ')'),
79-
],
164+
data: { operator },
165+
fix: wrapUpLeftFixer(node),
80166
},
81167
],
82168
});
@@ -87,3 +173,12 @@ export default createRule({
87173
};
88174
},
89175
});
176+
177+
function wrapUpLeftFixer(
178+
node: TSESTree.AssignmentExpression | TSESTree.BinaryExpression,
179+
): TSESLint.ReportFixFunction {
180+
return (fixer): TSESLint.RuleFix[] => [
181+
fixer.insertTextBefore(node.left, '('),
182+
fixer.insertTextAfter(node.left, ')'),
183+
];
184+
}

packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-confusing-non-null-assertion.shot

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/eslint-plugin/tests/rules/no-confusing-non-null-assertion.test.ts

+72-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
/* eslint "@typescript-eslint/internal/plugin-test-formatting": ["error", { formatWithPrettier: false }] */
44
/* eslint-enable eslint-comments/no-use */
55

6-
import { RuleTester } from '@typescript-eslint/rule-tester';
6+
import { noFormat, RuleTester } from '@typescript-eslint/rule-tester';
77

88
import rule from '../../src/rules/no-confusing-non-null-assertion';
99

@@ -18,6 +18,8 @@ ruleTester.run('no-confusing-non-null-assertion', rule, {
1818
'a != b;',
1919
'(a + b!) == c;',
2020
'(a + b!) = c;',
21+
'(a + b!) in c;',
22+
'(a || b!) instanceof c;',
2123
],
2224
invalid: [
2325
{
@@ -148,5 +150,74 @@ ruleTester.run('no-confusing-non-null-assertion', rule, {
148150
},
149151
],
150152
},
153+
{
154+
code: 'a! in b;',
155+
errors: [
156+
{
157+
messageId: 'confusingOperator',
158+
data: { operator: 'in' },
159+
line: 1,
160+
column: 1,
161+
suggestions: [
162+
{
163+
messageId: 'notNeedInOperator',
164+
output: 'a in b;',
165+
},
166+
{
167+
messageId: 'wrapUpLeft',
168+
output: '(a!) in b;',
169+
},
170+
],
171+
},
172+
],
173+
},
174+
{
175+
code: noFormat`
176+
a !in b;
177+
`,
178+
errors: [
179+
{
180+
messageId: 'confusingOperator',
181+
data: { operator: 'in' },
182+
line: 2,
183+
column: 1,
184+
suggestions: [
185+
{
186+
messageId: 'notNeedInOperator',
187+
output: `
188+
a in b;
189+
`,
190+
},
191+
{
192+
messageId: 'wrapUpLeft',
193+
output: `
194+
(a !)in b;
195+
`,
196+
},
197+
],
198+
},
199+
],
200+
},
201+
{
202+
code: 'a! instanceof b;',
203+
errors: [
204+
{
205+
messageId: 'confusingOperator',
206+
data: { operator: 'instanceof' },
207+
line: 1,
208+
column: 1,
209+
suggestions: [
210+
{
211+
messageId: 'notNeedInOperator',
212+
output: 'a instanceof b;',
213+
},
214+
{
215+
messageId: 'wrapUpLeft',
216+
output: '(a!) instanceof b;',
217+
},
218+
],
219+
},
220+
],
221+
},
151222
],
152223
});

0 commit comments

Comments
 (0)