forked from typescript-eslint/typescript-eslint
-
Notifications
You must be signed in to change notification settings - Fork 0
/
no-non-null-asserted-optional-chain.ts
148 lines (136 loc) · 4.59 KB
/
no-non-null-asserted-optional-chain.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import { TSESTree, TSESLint, AST_NODE_TYPES } from '@typescript-eslint/utils';
import * as ts from 'typescript';
import * as semver from 'semver';
import * as util from '../util';
const is3dot9 = semver.satisfies(
ts.version,
`>= 3.9.0 || >= 3.9.1-rc || >= 3.9.0-beta`,
{
includePrerelease: true,
},
);
export default util.createRule({
name: 'no-non-null-asserted-optional-chain',
meta: {
type: 'problem',
docs: {
description:
'Disallow non-null assertions after an optional chain expression',
recommended: 'error',
},
hasSuggestions: true,
messages: {
noNonNullOptionalChain:
'Optional chain expressions can return undefined by design - using a non-null assertion is unsafe and wrong.',
suggestRemovingNonNull: 'You should remove the non-null assertion.',
},
schema: [],
},
defaultOptions: [],
create(context) {
// TS3.9 made a breaking change to how non-null works with optional chains.
// Pre-3.9, `x?.y!.z` means `(x?.y).z` - i.e. it essentially scrubbed the optionality from the chain
// Post-3.9, `x?.y!.z` means `x?.y!.z` - i.e. it just asserts that the property `y` is non-null, not the result of `x?.y`.
// This means that for > 3.9, x?.y!.z is valid!
//
// NOTE: these cases are still invalid for 3.9:
// - x?.y.z!
// - (x?.y)!.z
const baseSelectors = {
// non-nulling a wrapped chain will scrub all nulls introduced by the chain
// (x?.y)!
// (x?.())!
'TSNonNullExpression > ChainExpression'(
node: TSESTree.ChainExpression,
): void {
// selector guarantees this assertion
const parent = node.parent as TSESTree.TSNonNullExpression;
context.report({
node,
messageId: 'noNonNullOptionalChain',
// use a suggestion instead of a fixer, because this can obviously break type checks
suggest: [
{
messageId: 'suggestRemovingNonNull',
fix(fixer): TSESLint.RuleFix {
return fixer.removeRange([
parent.range[1] - 1,
parent.range[1],
]);
},
},
],
});
},
// non-nulling at the end of a chain will scrub all nulls introduced by the chain
// x?.y!
// x?.()!
'ChainExpression > TSNonNullExpression'(
node: TSESTree.TSNonNullExpression,
): void {
context.report({
node,
messageId: 'noNonNullOptionalChain',
// use a suggestion instead of a fixer, because this can obviously break type checks
suggest: [
{
messageId: 'suggestRemovingNonNull',
fix(fixer): TSESLint.RuleFix {
return fixer.removeRange([node.range[1] - 1, node.range[1]]);
},
},
],
});
},
};
if (is3dot9) {
return baseSelectors;
}
return {
...baseSelectors,
[[
// > :not(ChainExpression) because that case is handled by a previous selector
'MemberExpression > TSNonNullExpression.object > :not(ChainExpression)',
'CallExpression > TSNonNullExpression.callee > :not(ChainExpression)',
].join(', ')](child: TSESTree.Node): void {
// selector guarantees this assertion
const node = child.parent as TSESTree.TSNonNullExpression;
let current = child;
while (current) {
switch (current.type) {
case AST_NODE_TYPES.MemberExpression:
if (current.optional) {
// found an optional chain! stop traversing
break;
}
current = current.object;
continue;
case AST_NODE_TYPES.CallExpression:
if (current.optional) {
// found an optional chain! stop traversing
break;
}
current = current.callee;
continue;
default:
// something that's not a ChainElement, which means this is not an optional chain we want to check
return;
}
}
context.report({
node,
messageId: 'noNonNullOptionalChain',
// use a suggestion instead of a fixer, because this can obviously break type checks
suggest: [
{
messageId: 'suggestRemovingNonNull',
fix(fixer): TSESLint.RuleFix {
return fixer.removeRange([node.range[1] - 1, node.range[1]]);
},
},
],
});
},
};
},
});