Skip to content

Commit 498aa24

Browse files
authoredJan 18, 2020
feat(eslint-plugin): add no-non-null-asserted-optional-chain (#1469)
1 parent 9c5b857 commit 498aa24

File tree

7 files changed

+364
-79
lines changed

7 files changed

+364
-79
lines changed
 

‎packages/eslint-plugin/README.md

+79-78
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Disallows using a non-null assertion after an optional chain expression (`no-non-null-asserted-optional-chain`)
2+
3+
## Rule Details
4+
5+
Optional chain expressions are designed to return `undefined` if the optional property is nullish.
6+
Using non-null assertions after an optional chain expression is wrong, and introduces a serious type safety hole into your code.
7+
8+
Examples of **incorrect** code for this rule:
9+
10+
```ts
11+
/* eslint @typescript-eslint/no-non-null-asserted-optional-chain: "error" */
12+
13+
foo?.bar!;
14+
foo?.bar!.baz;
15+
foo?.bar()!;
16+
foo?.bar!();
17+
foo?.bar!().baz;
18+
```
19+
20+
Examples of **correct** code for this rule:
21+
22+
```ts
23+
/* eslint @typescript-eslint/no-non-null-asserted-optional-chain: "error" */
24+
25+
foo?.bar;
26+
(foo?.bar).baz;
27+
foo?.bar();
28+
foo?.bar();
29+
foo?.bar().baz;
30+
```
31+
32+
## When Not To Use It
33+
34+
If you are not using TypeScript 3.7 (or greater), then you will not need to use this rule, as the operator is not supported.
35+
36+
## Further Reading
37+
38+
- [TypeScript 3.7 Release Notes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html)
39+
- [Optional Chaining Proposal](https://github.com/tc39/proposal-optional-chaining/)

‎packages/eslint-plugin/src/configs/all.json

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"@typescript-eslint/no-misused-new": "error",
4545
"@typescript-eslint/no-misused-promises": "error",
4646
"@typescript-eslint/no-namespace": "error",
47+
"@typescript-eslint/no-non-null-asserted-optional-chain": "error",
4748
"@typescript-eslint/no-non-null-assertion": "error",
4849
"@typescript-eslint/no-parameter-properties": "error",
4950
"@typescript-eslint/no-require-imports": "error",

‎packages/eslint-plugin/src/rules/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import noMisusedNew from './no-misused-new';
3838
import noMisusedPromises from './no-misused-promises';
3939
import noNamespace from './no-namespace';
4040
import noNonNullAssertion from './no-non-null-assertion';
41+
import noNonNullAssertedOptionalChain from './no-non-null-asserted-optional-chain';
4142
import noParameterProperties from './no-parameter-properties';
4243
import noRequireImports from './no-require-imports';
4344
import noThisAlias from './no-this-alias';
@@ -120,6 +121,7 @@ export default {
120121
'no-misused-promises': noMisusedPromises,
121122
'no-namespace': noNamespace,
122123
'no-non-null-assertion': noNonNullAssertion,
124+
'no-non-null-asserted-optional-chain': noNonNullAssertedOptionalChain,
123125
'no-parameter-properties': noParameterProperties,
124126
'no-require-imports': noRequireImports,
125127
'no-this-alias': noThisAlias,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { TSESTree, TSESLint } from '@typescript-eslint/experimental-utils';
2+
import * as util from '../util';
3+
4+
type MessageIds = 'noNonNullOptionalChain' | 'suggestRemovingNonNull';
5+
6+
export default util.createRule<[], MessageIds>({
7+
name: 'no-non-null-asserted-optional-chain',
8+
meta: {
9+
type: 'problem',
10+
docs: {
11+
description:
12+
'Disallows using a non-null assertion after an optional chain expression',
13+
category: 'Possible Errors',
14+
recommended: false,
15+
},
16+
messages: {
17+
noNonNullOptionalChain:
18+
'Optional chain expressions can return undefined by design - using a non-null assertion is unsafe and wrong.',
19+
suggestRemovingNonNull: 'You should remove the non-null assertion.',
20+
},
21+
schema: [],
22+
},
23+
defaultOptions: [],
24+
create(context) {
25+
return {
26+
'TSNonNullExpression > :matches(OptionalMemberExpression, OptionalCallExpression)'(
27+
node:
28+
| TSESTree.OptionalCallExpression
29+
| TSESTree.OptionalMemberExpression,
30+
): void {
31+
// selector guarantees this assertion
32+
const parent = node.parent as TSESTree.TSNonNullExpression;
33+
context.report({
34+
node,
35+
messageId: 'noNonNullOptionalChain',
36+
// use a suggestion instead of a fixer, because this can obviously break type checks
37+
suggest: [
38+
{
39+
messageId: 'suggestRemovingNonNull',
40+
fix(fixer): TSESLint.RuleFix {
41+
return fixer.removeRange([
42+
parent.range[1] - 1,
43+
parent.range[1],
44+
]);
45+
},
46+
},
47+
],
48+
});
49+
},
50+
};
51+
},
52+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import rule from '../../src/rules/no-non-null-asserted-optional-chain';
2+
import { RuleTester } from '../RuleTester';
3+
4+
const ruleTester = new RuleTester({
5+
parser: '@typescript-eslint/parser',
6+
});
7+
8+
ruleTester.run('no-non-null-asserted-optional-chain', rule, {
9+
valid: [
10+
'foo.bar!',
11+
'foo.bar()!',
12+
'foo?.bar',
13+
'foo?.bar()',
14+
'(foo?.bar).baz!',
15+
'(foo?.bar()).baz!',
16+
],
17+
invalid: [
18+
{
19+
code: 'foo?.bar!',
20+
errors: [
21+
{
22+
messageId: 'noNonNullOptionalChain',
23+
suggestions: [
24+
{
25+
messageId: 'suggestRemovingNonNull',
26+
output: 'foo?.bar',
27+
},
28+
],
29+
},
30+
],
31+
},
32+
{
33+
code: 'foo?.["bar"]!',
34+
errors: [
35+
{
36+
messageId: 'noNonNullOptionalChain',
37+
suggestions: [
38+
{
39+
messageId: 'suggestRemovingNonNull',
40+
output: 'foo?.["bar"]',
41+
},
42+
],
43+
},
44+
],
45+
},
46+
{
47+
code: 'foo?.bar()!',
48+
errors: [
49+
{
50+
messageId: 'noNonNullOptionalChain',
51+
suggestions: [
52+
{
53+
messageId: 'suggestRemovingNonNull',
54+
output: 'foo?.bar()',
55+
},
56+
],
57+
},
58+
],
59+
},
60+
{
61+
code: 'foo.bar?.()!',
62+
errors: [
63+
{
64+
messageId: 'noNonNullOptionalChain',
65+
suggestions: [
66+
{
67+
messageId: 'suggestRemovingNonNull',
68+
output: 'foo.bar?.()',
69+
},
70+
],
71+
},
72+
],
73+
},
74+
{
75+
code: 'foo?.bar!()',
76+
errors: [
77+
{
78+
messageId: 'noNonNullOptionalChain',
79+
suggestions: [
80+
{
81+
messageId: 'suggestRemovingNonNull',
82+
output: 'foo?.bar()',
83+
},
84+
],
85+
},
86+
],
87+
},
88+
{
89+
code: '(foo?.bar)!.baz',
90+
errors: [
91+
{
92+
messageId: 'noNonNullOptionalChain',
93+
suggestions: [
94+
{
95+
messageId: 'suggestRemovingNonNull',
96+
output: '(foo?.bar).baz',
97+
},
98+
],
99+
},
100+
],
101+
},
102+
{
103+
code: 'foo?.["bar"]!.baz',
104+
errors: [
105+
{
106+
messageId: 'noNonNullOptionalChain',
107+
suggestions: [
108+
{
109+
messageId: 'suggestRemovingNonNull',
110+
output: 'foo?.["bar"].baz',
111+
},
112+
],
113+
},
114+
],
115+
},
116+
{
117+
code: '(foo?.bar)!().baz',
118+
errors: [
119+
{
120+
messageId: 'noNonNullOptionalChain',
121+
suggestions: [
122+
{
123+
messageId: 'suggestRemovingNonNull',
124+
output: '(foo?.bar)().baz',
125+
},
126+
],
127+
},
128+
],
129+
},
130+
{
131+
code: '(foo?.bar)!',
132+
errors: [
133+
{
134+
messageId: 'noNonNullOptionalChain',
135+
suggestions: [
136+
{
137+
messageId: 'suggestRemovingNonNull',
138+
output: '(foo?.bar)',
139+
},
140+
],
141+
},
142+
],
143+
},
144+
{
145+
code: '(foo?.bar)!()',
146+
errors: [
147+
{
148+
messageId: 'noNonNullOptionalChain',
149+
suggestions: [
150+
{
151+
messageId: 'suggestRemovingNonNull',
152+
output: '(foo?.bar)()',
153+
},
154+
],
155+
},
156+
],
157+
},
158+
{
159+
code: '(foo?.bar!)',
160+
errors: [
161+
{
162+
messageId: 'noNonNullOptionalChain',
163+
suggestions: [
164+
{
165+
messageId: 'suggestRemovingNonNull',
166+
output: '(foo?.bar)',
167+
},
168+
],
169+
},
170+
],
171+
},
172+
{
173+
code: '(foo?.bar!)()',
174+
errors: [
175+
{
176+
messageId: 'noNonNullOptionalChain',
177+
suggestions: [
178+
{
179+
messageId: 'suggestRemovingNonNull',
180+
output: '(foo?.bar)()',
181+
},
182+
],
183+
},
184+
],
185+
},
186+
],
187+
});

‎packages/eslint-plugin/tools/generate-configs.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ interface LinterConfig extends TSESLint.Linter.Config {
1919
}
2020

2121
const RULE_NAME_PREFIX = '@typescript-eslint/';
22-
const MAX_RULE_NAME_LENGTH = 32;
22+
const MAX_RULE_NAME_LENGTH = Object.keys(rules).reduce(
23+
(acc, name) => Math.max(acc, name.length),
24+
0,
25+
);
2326
const DEFAULT_RULE_SETTING = 'warn';
2427
const BASE_RULES_TO_BE_OVERRIDDEN = new Map(
2528
Object.entries(rules)

0 commit comments

Comments
 (0)
Please sign in to comment.