1
1
import type * as ESTree from "estree"
2
+ import type { TSESTree } from "@typescript-eslint/types"
2
3
import { ReferenceTracker } from "eslint-utils"
3
4
import type { Variable } from "eslint-scope"
4
5
import type { RuleContext } from "../../types"
@@ -35,27 +36,37 @@ export function* extractStoreReferences(
35
36
}
36
37
}
37
38
39
+ export type StoreChecker = (
40
+ node : ESTree . Expression | TSESTree . Expression ,
41
+ options ?: { consistent ?: boolean } ,
42
+ ) => boolean
43
+ type StoreCheckerWithOptions = (
44
+ node : ESTree . Expression ,
45
+ options : { consistent : boolean } ,
46
+ ) => boolean
47
+
38
48
/**
39
49
* Creates a function that checks whether the given expression node is a store instance or not.
40
50
*/
41
- export function createStoreChecker (
42
- context : RuleContext ,
43
- ) : ( node : ESTree . Expression ) => boolean {
51
+ export function createStoreChecker ( context : RuleContext ) : StoreChecker {
44
52
const tools = getTypeScriptTools ( context )
45
- if ( tools ) {
46
- return createStoreCheckerForTS ( tools )
47
- }
53
+ const checker = tools
54
+ ? createStoreCheckerForTS ( tools )
55
+ : createStoreCheckerForES ( context )
48
56
49
- return createStoreCheckerForES ( context )
57
+ return ( node , options ) =>
58
+ checker ( node as ESTree . Expression , {
59
+ consistent : options ?. consistent ?? false ,
60
+ } )
50
61
}
51
62
52
63
/**
53
64
* Creates a function that checks whether the given expression node is a store instance or not, for EcmaScript.
54
65
*/
55
66
function createStoreCheckerForES (
56
67
context : RuleContext ,
57
- ) : ( node : ESTree . Expression ) => boolean {
58
- const variables = new Set < Variable > ( )
68
+ ) : StoreCheckerWithOptions {
69
+ const storeVariables = new Map < Variable , { const : boolean } > ( )
59
70
for ( const { node } of extractStoreReferences ( context ) ) {
60
71
const parent = getParent ( node )
61
72
if (
@@ -65,36 +76,42 @@ function createStoreCheckerForES(
65
76
) {
66
77
continue
67
78
}
79
+ const decl = getParent ( parent )
80
+ if ( ! decl || decl . type !== "VariableDeclaration" ) {
81
+ continue
82
+ }
68
83
69
84
const variable = findVariable ( context , parent . id )
70
85
if ( variable ) {
71
- variables . add ( variable )
86
+ storeVariables . set ( variable , { const : decl . kind === "const" } )
72
87
}
73
88
}
74
89
75
- return ( node ) => {
90
+ return ( node , options ) => {
76
91
if ( node . type !== "Identifier" || node . name . startsWith ( "$" ) ) {
77
92
return false
78
93
}
79
94
const variable = findVariable ( context , node )
80
95
if ( ! variable ) {
81
96
return false
82
97
}
83
- return variables . has ( variable )
98
+ const info = storeVariables . get ( variable )
99
+ if ( ! info ) {
100
+ return false
101
+ }
102
+ return options . consistent ? info . const : true
84
103
}
85
104
}
86
105
87
106
/**
88
107
* Creates a function that checks whether the given expression node is a store instance or not, for TypeScript.
89
108
*/
90
- function createStoreCheckerForTS (
91
- tools : TSTools ,
92
- ) : ( node : ESTree . Expression ) => boolean {
109
+ function createStoreCheckerForTS ( tools : TSTools ) : StoreCheckerWithOptions {
93
110
const { service } = tools
94
111
const checker = service . program . getTypeChecker ( )
95
112
const tsNodeMap = service . esTreeNodeToTSNodeMap
96
113
97
- return ( node ) => {
114
+ return ( node , options ) => {
98
115
const tsNode = tsNodeMap . get ( node )
99
116
if ( ! tsNode ) {
100
117
return false
@@ -107,57 +124,73 @@ function createStoreCheckerForTS(
107
124
* Checks whether the given type is a store or not
108
125
*/
109
126
function isStoreType ( type : TS . Type ) : boolean {
110
- if ( type . isUnion ( ) ) {
111
- return type . types . some ( isStoreType )
112
- }
113
- const subscribe = type . getProperty ( "subscribe" )
114
- if ( subscribe === undefined ) {
115
- return false
116
- }
117
- const subscribeType = checker . getTypeOfSymbolAtLocation (
118
- subscribe ,
119
- tsNode ! ,
120
- )
121
- return isStoreSubscribeSignatureType ( subscribeType )
127
+ return eachTypeCheck ( type , options , ( type ) => {
128
+ const subscribe = type . getProperty ( "subscribe" )
129
+ if ( ! subscribe ) {
130
+ return false
131
+ }
132
+ const subscribeType = checker . getTypeOfSymbolAtLocation (
133
+ subscribe ,
134
+ tsNode ! ,
135
+ )
136
+ return isStoreSubscribeSignatureType ( subscribeType )
137
+ } )
122
138
}
123
139
124
140
/**
125
141
* Checks whether the given type is a store's subscribe or not
126
142
*/
127
143
function isStoreSubscribeSignatureType ( type : TS . Type ) : boolean {
128
- if ( type . isUnion ( ) ) {
129
- return type . types . some ( isStoreSubscribeSignatureType )
130
- }
131
- for ( const signature of type . getCallSignatures ( ) ) {
132
- if (
133
- signature . parameters . length >= 2 &&
134
- isFunctionSymbol ( signature . parameters [ 0 ] ) &&
135
- isFunctionSymbol ( signature . parameters [ 1 ] )
136
- ) {
137
- return true
144
+ return eachTypeCheck ( type , options , ( type ) => {
145
+ for ( const signature of type . getCallSignatures ( ) ) {
146
+ if (
147
+ signature . parameters . length >= 2 &&
148
+ maybeFunctionSymbol ( signature . parameters [ 0 ] ) &&
149
+ maybeFunctionSymbol ( signature . parameters [ 1 ] )
150
+ ) {
151
+ return true
152
+ }
138
153
}
139
- }
140
- return false
154
+ return false
155
+ } )
141
156
}
142
157
143
158
/**
144
- * Checks whether the given symbol is a function param or not
159
+ * Checks whether the given symbol maybe function param or not
145
160
*/
146
- function isFunctionSymbol ( param : TS . Symbol ) : boolean {
161
+ function maybeFunctionSymbol ( param : TS . Symbol ) : boolean {
147
162
const type : TS . Type | undefined = checker . getApparentType (
148
163
checker . getTypeOfSymbolAtLocation ( param , tsNode ! ) ,
149
164
)
150
- return isFunctionType ( type )
165
+ return maybeFunctionType ( type )
166
+ }
167
+
168
+ /**
169
+ * Checks whether the given type is maybe function param or not
170
+ */
171
+ function maybeFunctionType ( type : TS . Type ) : boolean {
172
+ return eachTypeCheck ( type , { consistent : false } , ( type ) => {
173
+ return type . getCallSignatures ( ) . length > 0
174
+ } )
151
175
}
152
176
}
177
+ }
153
178
154
- /**
155
- * Checks whether the given symbol is a function param or not
156
- */
157
- function isFunctionType ( type : TS . Type ) : boolean {
158
- if ( type . isUnion ( ) ) {
159
- return type . types . some ( isFunctionType )
179
+ /**
180
+ * Check the given type with the given check function.
181
+ * For union types, `options.consistent: true` requires all types to pass the check function.
182
+ * `options.consistent: false` considers a match if any type passes the check function.
183
+ */
184
+ function eachTypeCheck (
185
+ type : TS . Type ,
186
+ options : { consistent : boolean } ,
187
+ check : ( t : TS . Type ) => boolean ,
188
+ ) : boolean {
189
+ if ( type . isUnion ( ) ) {
190
+ if ( options . consistent ) {
191
+ return type . types . every ( ( t ) => eachTypeCheck ( t , options , check ) )
160
192
}
161
- return type . getCallSignatures ( ) . length > 0
193
+ return type . types . some ( ( t ) => eachTypeCheck ( t , options , check ) )
162
194
}
195
+ return check ( type )
163
196
}
0 commit comments