1
+ import {
2
+ TSESLint ,
3
+ AST_NODE_TYPES ,
4
+ } from '@typescript-eslint/experimental-utils' ;
1
5
import * as util from '../util' ;
2
6
3
- export default util . createRule ( {
7
+ type MessageIds = 'noNonNull' | 'suggestOptionalChain' ;
8
+
9
+ export default util . createRule < [ ] , MessageIds > ( {
4
10
name : 'no-non-null-assertion' ,
5
11
meta : {
6
12
type : 'problem' ,
@@ -12,16 +18,106 @@ export default util.createRule({
12
18
} ,
13
19
messages : {
14
20
noNonNull : 'Forbidden non-null assertion.' ,
21
+ suggestOptionalChain :
22
+ 'Consider using the optional chain operator `?.` instead. This operator includes runtime checks, so it is safer than the compile-only non-null assertion operator.' ,
15
23
} ,
16
24
schema : [ ] ,
17
25
} ,
18
26
defaultOptions : [ ] ,
19
27
create ( context ) {
28
+ const sourceCode = context . getSourceCode ( ) ;
20
29
return {
21
30
TSNonNullExpression ( node ) : void {
31
+ const suggest : TSESLint . ReportSuggestionArray < MessageIds > = [ ] ;
32
+ function convertTokenToOptional (
33
+ replacement : '?' | '?.' ,
34
+ ) : TSESLint . ReportFixFunction {
35
+ return ( fixer : TSESLint . RuleFixer ) : TSESLint . RuleFix | null => {
36
+ const operator = sourceCode . getTokenAfter (
37
+ node . expression ,
38
+ util . isNonNullAssertionPunctuator ,
39
+ ) ;
40
+ if ( operator ) {
41
+ return fixer . replaceText ( operator , replacement ) ;
42
+ }
43
+
44
+ return null ;
45
+ } ;
46
+ }
47
+ function removeToken ( ) : TSESLint . ReportFixFunction {
48
+ return ( fixer : TSESLint . RuleFixer ) : TSESLint . RuleFix | null => {
49
+ const operator = sourceCode . getTokenAfter (
50
+ node . expression ,
51
+ util . isNonNullAssertionPunctuator ,
52
+ ) ;
53
+ if ( operator ) {
54
+ return fixer . remove ( operator ) ;
55
+ }
56
+
57
+ return null ;
58
+ } ;
59
+ }
60
+
61
+ if ( node . parent ) {
62
+ if (
63
+ ( node . parent . type === AST_NODE_TYPES . MemberExpression ||
64
+ node . parent . type === AST_NODE_TYPES . OptionalMemberExpression ) &&
65
+ node . parent . object === node
66
+ ) {
67
+ if ( ! node . parent . optional ) {
68
+ if ( node . parent . computed ) {
69
+ // it is x![y]?.z
70
+ suggest . push ( {
71
+ messageId : 'suggestOptionalChain' ,
72
+ fix : convertTokenToOptional ( '?.' ) ,
73
+ } ) ;
74
+ } else {
75
+ // it is x!.y?.z
76
+ suggest . push ( {
77
+ messageId : 'suggestOptionalChain' ,
78
+ fix : convertTokenToOptional ( '?' ) ,
79
+ } ) ;
80
+ }
81
+ } else {
82
+ if ( node . parent . computed ) {
83
+ // it is x!?.[y].z
84
+ suggest . push ( {
85
+ messageId : 'suggestOptionalChain' ,
86
+ fix : removeToken ( ) ,
87
+ } ) ;
88
+ } else {
89
+ // it is x!?.y.z
90
+ suggest . push ( {
91
+ messageId : 'suggestOptionalChain' ,
92
+ fix : removeToken ( ) ,
93
+ } ) ;
94
+ }
95
+ }
96
+ } else if (
97
+ ( node . parent . type === AST_NODE_TYPES . CallExpression ||
98
+ node . parent . type === AST_NODE_TYPES . OptionalCallExpression ) &&
99
+ node . parent . callee === node
100
+ ) {
101
+ if ( ! node . parent . optional ) {
102
+ // it is x.y?.z!()
103
+ suggest . push ( {
104
+ messageId : 'suggestOptionalChain' ,
105
+ fix : convertTokenToOptional ( '?.' ) ,
106
+ } ) ;
107
+ } else {
108
+ // it is x.y.z!?.()
109
+ suggest . push ( {
110
+ messageId : 'suggestOptionalChain' ,
111
+ fix : removeToken ( ) ,
112
+ } ) ;
113
+ }
114
+ }
115
+ }
116
+
22
117
context . report ( {
23
118
node,
24
119
messageId : 'noNonNull' ,
120
+ suggest,
25
121
} ) ;
26
122
} ,
27
123
} ;
0 commit comments