Skip to content

Commit a2a8a0a

Browse files
jridgewellbradzacher
andcommittedDec 16, 2019
feat(eslint-plugin): [no-unnec-cond] check optional chaining (#1315)
Co-authored-by: Brad Zacher <brad.zacher@gmail.com>
1 parent f9a9fbf commit a2a8a0a

File tree

4 files changed

+281
-3
lines changed

4 files changed

+281
-3
lines changed
 

‎packages/eslint-plugin/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e
179179
| [`@typescript-eslint/no-require-imports`](./docs/rules/no-require-imports.md) | Disallows invocation of `require()` | | | |
180180
| [`@typescript-eslint/no-this-alias`](./docs/rules/no-this-alias.md) | Disallow aliasing `this` | :heavy_check_mark: | | |
181181
| [`@typescript-eslint/no-type-alias`](./docs/rules/no-type-alias.md) | Disallow the use of type aliases | | | |
182-
| [`@typescript-eslint/no-unnecessary-condition`](./docs/rules/no-unnecessary-condition.md) | Prevents conditionals where the type is always truthy or always falsy | | | :thought_balloon: |
182+
| [`@typescript-eslint/no-unnecessary-condition`](./docs/rules/no-unnecessary-condition.md) | Prevents conditionals where the type is always truthy or always falsy | | :wrench: | :thought_balloon: |
183183
| [`@typescript-eslint/no-unnecessary-qualifier`](./docs/rules/no-unnecessary-qualifier.md) | Warns when a namespace qualifier is unnecessary | | :wrench: | :thought_balloon: |
184184
| [`@typescript-eslint/no-unnecessary-type-arguments`](./docs/rules/no-unnecessary-type-arguments.md) | Warns if an explicitly specified type argument is the default for that type parameter | | :wrench: | :thought_balloon: |
185185
| [`@typescript-eslint/no-unnecessary-type-assertion`](./docs/rules/no-unnecessary-type-assertion.md) | Warns if a type assertion does not change the type of an expression | :heavy_check_mark: | :wrench: | :thought_balloon: |

‎packages/eslint-plugin/docs/rules/no-unnecessary-condition.md

+12-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ Any expression being used as a condition must be able to evaluate as truthy or f
55
The following expressions are checked:
66

77
- Arguments to the `&&`, `||` and `?:` (ternary) operators
8-
- Conditions for `if`, `for`, `while`, and `do-while` statements.
8+
- Conditions for `if`, `for`, `while`, and `do-while` statements
9+
- Base values of optional chain expressions
910

1011
Examples of **incorrect** code for this rule:
1112

@@ -22,6 +23,11 @@ function foo(arg: 'bar' | 'baz') {
2223
if (arg) {
2324
}
2425
}
26+
27+
function bar<T>(arg: string) {
28+
// arg can never be nullish, so ?. is unnecessary
29+
return arg?.length;
30+
}
2531
```
2632

2733
Examples of **correct** code for this rule:
@@ -39,6 +45,11 @@ function foo(arg: string) {
3945
if (arg) {
4046
}
4147
}
48+
49+
function bar(arg?: string | null) {
50+
// Necessary, since arg might be nullish
51+
return arg?.length;
52+
}
4253
```
4354

4455
## Options

‎packages/eslint-plugin/src/rules/no-unnecessary-condition.ts

+62-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
TSESTree,
33
AST_NODE_TYPES,
4+
AST_TOKEN_TYPES,
45
} from '@typescript-eslint/experimental-utils';
56
import ts, { TypeFlags } from 'typescript';
67
import {
@@ -14,6 +15,9 @@ import {
1415
createRule,
1516
getParserServices,
1617
getConstrainedTypeAtLocation,
18+
isNullableType,
19+
nullThrows,
20+
NullThrowsReasons,
1721
} from '../util';
1822

1923
// Truthiness utilities
@@ -65,7 +69,8 @@ export type MessageId =
6569
| 'neverNullish'
6670
| 'alwaysNullish'
6771
| 'literalBooleanExpression'
68-
| 'never';
72+
| 'never'
73+
| 'neverOptionalChain';
6974
export default createRule<Options, MessageId>({
7075
name: 'no-unnecessary-conditionals',
7176
meta: {
@@ -91,6 +96,7 @@ export default createRule<Options, MessageId>({
9196
additionalProperties: false,
9297
},
9398
],
99+
fixable: 'code',
94100
messages: {
95101
alwaysTruthy: 'Unnecessary conditional, value is always truthy.',
96102
alwaysFalsy: 'Unnecessary conditional, value is always falsy.',
@@ -101,6 +107,7 @@ export default createRule<Options, MessageId>({
101107
literalBooleanExpression:
102108
'Unnecessary conditional, both sides of the expression are literal values',
103109
never: 'Unnecessary conditional, value is `never`',
110+
neverOptionalChain: 'Unnecessary optional chain on a non-nullish value',
104111
},
105112
},
106113
defaultOptions: [
@@ -112,6 +119,7 @@ export default createRule<Options, MessageId>({
112119
create(context, [{ allowConstantLoopConditions, ignoreRhs }]) {
113120
const service = getParserServices(context);
114121
const checker = service.program.getTypeChecker();
122+
const sourceCode = context.getSourceCode();
115123

116124
function getNodeType(node: TSESTree.Node): ts.Type {
117125
const tsNode = service.esTreeNodeToTSNodeMap.get(node);
@@ -260,6 +268,57 @@ export default createRule<Options, MessageId>({
260268
checkNode(node.test);
261269
}
262270

271+
function checkOptionalChain(
272+
node: TSESTree.OptionalMemberExpression | TSESTree.OptionalCallExpression,
273+
beforeOperator: TSESTree.Node,
274+
fix: '' | '.',
275+
): void {
276+
// We only care if this step in the chain is optional. If just descend
277+
// from an optional chain, then that's fine.
278+
if (!node.optional) {
279+
return;
280+
}
281+
282+
const type = getNodeType(node);
283+
if (
284+
isTypeFlagSet(type, ts.TypeFlags.Any) ||
285+
isTypeFlagSet(type, ts.TypeFlags.Unknown) ||
286+
isNullableType(type, { allowUndefined: true })
287+
) {
288+
return;
289+
}
290+
291+
const questionDotOperator = nullThrows(
292+
sourceCode.getTokenAfter(
293+
beforeOperator,
294+
token =>
295+
token.type === AST_TOKEN_TYPES.Punctuator && token.value === '?.',
296+
),
297+
NullThrowsReasons.MissingToken('operator', node.type),
298+
);
299+
300+
context.report({
301+
node,
302+
loc: questionDotOperator.loc,
303+
messageId: 'neverOptionalChain',
304+
fix(fixer) {
305+
return fixer.replaceText(questionDotOperator, fix);
306+
},
307+
});
308+
}
309+
310+
function checkOptionalMemberExpression(
311+
node: TSESTree.OptionalMemberExpression,
312+
): void {
313+
checkOptionalChain(node, node.object, '.');
314+
}
315+
316+
function checkOptionalCallExpression(
317+
node: TSESTree.OptionalCallExpression,
318+
): void {
319+
checkOptionalChain(node, node.callee, '');
320+
}
321+
263322
return {
264323
BinaryExpression: checkIfBinaryExpressionIsNecessaryConditional,
265324
ConditionalExpression: checkIfTestExpressionIsNecessaryConditional,
@@ -268,6 +327,8 @@ export default createRule<Options, MessageId>({
268327
IfStatement: checkIfTestExpressionIsNecessaryConditional,
269328
LogicalExpression: checkLogicalExpressionForUnnecessaryConditionals,
270329
WhileStatement: checkIfLoopIsNecessaryConditional,
330+
OptionalMemberExpression: checkOptionalMemberExpression,
331+
OptionalCallExpression: checkOptionalCallExpression,
271332
};
272333
},
273334
});

‎packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts

+206
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,62 @@ do {} while(true)
121121
`,
122122
options: [{ allowConstantLoopConditions: true }],
123123
},
124+
`
125+
let foo: undefined | { bar: true };
126+
foo?.bar;
127+
`,
128+
`
129+
let foo: null | { bar: true };
130+
foo?.bar;
131+
`,
132+
`
133+
let foo: undefined;
134+
foo?.bar;
135+
`,
136+
`
137+
let foo: undefined;
138+
foo?.bar.baz;
139+
`,
140+
`
141+
let foo: null;
142+
foo?.bar;
143+
`,
144+
`
145+
let anyValue: any;
146+
anyValue?.foo;
147+
`,
148+
`
149+
let unknownValue: unknown;
150+
unknownValue?.foo;
151+
`,
152+
`
153+
let foo: undefined | (() => {});
154+
foo?.();
155+
`,
156+
`
157+
let foo: null | (() => {});
158+
foo?.();
159+
`,
160+
`
161+
let foo: undefined;
162+
foo?.();
163+
`,
164+
`
165+
let foo: undefined;
166+
foo?.().bar;
167+
`,
168+
`
169+
let foo: null;
170+
foo?.();
171+
`,
172+
`
173+
let anyValue: any;
174+
anyValue?.();
175+
`,
176+
`
177+
let unknownValue: unknown;
178+
unknownValue?.();
179+
`,
124180
],
125181
invalid: [
126182
// Ensure that it's checking in all the right places
@@ -267,5 +323,155 @@ do {} while(true)
267323
ruleError(4, 13, 'alwaysTruthy'),
268324
],
269325
},
326+
{
327+
code: `
328+
let foo = { bar: true };
329+
foo?.bar;
330+
foo ?. bar;
331+
foo ?.
332+
bar;
333+
foo
334+
?. bar;
335+
`,
336+
output: `
337+
let foo = { bar: true };
338+
foo.bar;
339+
foo . bar;
340+
foo .
341+
bar;
342+
foo
343+
. bar;
344+
`,
345+
errors: [
346+
{
347+
messageId: 'neverOptionalChain',
348+
line: 3,
349+
column: 4,
350+
endLine: 3,
351+
endColumn: 6,
352+
},
353+
{
354+
messageId: 'neverOptionalChain',
355+
line: 4,
356+
column: 5,
357+
endLine: 4,
358+
endColumn: 7,
359+
},
360+
{
361+
messageId: 'neverOptionalChain',
362+
line: 5,
363+
column: 5,
364+
endLine: 5,
365+
endColumn: 7,
366+
},
367+
{
368+
messageId: 'neverOptionalChain',
369+
line: 8,
370+
column: 3,
371+
endLine: 8,
372+
endColumn: 5,
373+
},
374+
],
375+
},
376+
{
377+
code: `
378+
let foo = () => {};
379+
foo?.();
380+
foo ?. ();
381+
foo ?.
382+
();
383+
foo
384+
?. ();
385+
`,
386+
output: `
387+
let foo = () => {};
388+
foo();
389+
foo ();
390+
foo${' '}
391+
();
392+
foo
393+
();
394+
`,
395+
errors: [
396+
{
397+
messageId: 'neverOptionalChain',
398+
line: 3,
399+
column: 4,
400+
endLine: 3,
401+
endColumn: 6,
402+
},
403+
{
404+
messageId: 'neverOptionalChain',
405+
line: 4,
406+
column: 5,
407+
endLine: 4,
408+
endColumn: 7,
409+
},
410+
{
411+
messageId: 'neverOptionalChain',
412+
line: 5,
413+
column: 5,
414+
endLine: 5,
415+
endColumn: 7,
416+
},
417+
{
418+
messageId: 'neverOptionalChain',
419+
line: 8,
420+
column: 3,
421+
endLine: 8,
422+
endColumn: 5,
423+
},
424+
],
425+
},
426+
{
427+
code: `
428+
let foo = () => {};
429+
foo?.(bar);
430+
foo ?. (bar);
431+
foo ?.
432+
(bar);
433+
foo
434+
?. (bar);
435+
`,
436+
output: `
437+
let foo = () => {};
438+
foo(bar);
439+
foo (bar);
440+
foo${' '}
441+
(bar);
442+
foo
443+
(bar);
444+
`,
445+
errors: [
446+
{
447+
messageId: 'neverOptionalChain',
448+
line: 3,
449+
column: 4,
450+
endLine: 3,
451+
endColumn: 6,
452+
},
453+
{
454+
messageId: 'neverOptionalChain',
455+
line: 4,
456+
column: 5,
457+
endLine: 4,
458+
endColumn: 7,
459+
},
460+
{
461+
messageId: 'neverOptionalChain',
462+
line: 5,
463+
column: 5,
464+
endLine: 5,
465+
endColumn: 7,
466+
},
467+
{
468+
messageId: 'neverOptionalChain',
469+
line: 8,
470+
column: 3,
471+
endLine: 8,
472+
endColumn: 5,
473+
},
474+
],
475+
},
270476
],
271477
});

0 commit comments

Comments
 (0)
Please sign in to comment.