@@ -3,9 +3,10 @@ import { createRule } from '../utils';
3
3
import { TypeFlags } from 'typescript' ;
4
4
import type { TSESTree } from '@typescript-eslint/types' ;
5
5
import type { TS , TSTools } from '../utils/ts-utils' ;
6
- import { getConstrainedTypeAtLocation , getTypeName , getTypeScriptTools } from '../utils/ts-utils' ;
6
+ import { getTypeName , getTypeScriptTools } from '../utils/ts-utils' ;
7
7
import { findVariable } from '../utils/ast-utils' ;
8
8
import type { RuleContext } from '../types' ;
9
+ import type { Scope , Variable } from '@typescript-eslint/scope-manager' ;
9
10
10
11
const props = {
11
12
allowBoolean : {
@@ -38,6 +39,11 @@ type Props = {
38
39
allowUndefined : boolean ;
39
40
} ;
40
41
42
+ enum NodeType {
43
+ Unknown ,
44
+ Allowed
45
+ }
46
+
41
47
type Config = {
42
48
stringTemplateExpressions ?: Props ;
43
49
textExpressions ?: Props ;
@@ -86,6 +92,7 @@ export default createRule('restrict-mustache-expressions', {
86
92
if ( node . parent . type === 'SvelteAttribute' ) {
87
93
if ( ! node . parent . value . find ( ( n ) => n . type === 'SvelteLiteral' ) ) {
88
94
// we are rendering a non-literal attribute (eg: class:disabled={disabled}, so we allow any type
95
+ // (todo): maybe we could maybe check the expected type of the attribute here, but I think the language server already does that?
89
96
return ;
90
97
}
91
98
// we are rendering an template string attribute (eg: href="/page/{page.id}"), so we only allow stringifiable types
@@ -107,14 +114,13 @@ export default createRule('restrict-mustache-expressions', {
107
114
if ( allowNull ) allowed_types . add ( 'null' ) ;
108
115
if ( allowUndefined ) allowed_types . add ( 'undefined' ) ;
109
116
110
- const disallowed = disallowed_expression ( node . expression , allowed_types , context , tools ! ) ;
111
- if ( ! disallowed ) return ;
112
-
117
+ const type = disallowed_expression ( node . expression , allowed_types , context , tools ! ) ;
118
+ if ( NodeType . Allowed === type ) return ;
113
119
context . report ( {
114
120
node,
115
121
messageId : 'expectedStringifyableType' ,
116
122
data : {
117
- disallowed : getTypeName ( disallowed , tools ! ) ,
123
+ disallowed : type === NodeType . Unknown ? 'unknown' : getTypeName ( type , tools ! ) ,
118
124
types : [ ...allowed_types ] . map ( ( t ) => `\`${ t } \`` ) . join ( ', ' )
119
125
}
120
126
} ) ;
@@ -126,49 +132,87 @@ export default createRule('restrict-mustache-expressions', {
126
132
}
127
133
} ) ;
128
134
129
- function getNodeType (
130
- node : TSESTree . Expression | TSESTree . PrivateIdentifier | TSESTree . SpreadElement ,
131
- tools : TSTools
132
- ) : TS . Type | null {
133
- const tsNode = tools . service . esTreeNodeToTSNodeMap . get ( node ) ;
134
- return (
135
- ( tsNode && getConstrainedTypeAtLocation ( tools . service . program . getTypeChecker ( ) , tsNode ) ) || null
136
- ) ;
135
+ function getNodeType ( node : TSESTree . Node , tools : TSTools ) : TS . Type | NodeType . Unknown {
136
+ const checker = tools . service . program . getTypeChecker ( ) ;
137
+ const ts_node = tools . service . esTreeNodeToTSNodeMap . get ( node ) ;
138
+ if ( ! ts_node ) return NodeType . Unknown ;
139
+ const nodeType = checker . getTypeAtLocation ( ts_node ) ;
140
+ const constrained = checker . getBaseConstraintOfType ( nodeType ) ;
141
+ return constrained ?? nodeType ;
137
142
}
138
143
139
144
function disallowed_identifier (
140
145
expression : TSESTree . Identifier ,
141
146
allowed_types : Set < string > ,
142
147
context : RuleContext ,
143
148
tools : TSTools
144
- ) : TS . Type | null {
145
- const type = getNodeType ( expression , tools ) ;
149
+ ) : TS . Type | NodeType {
150
+ const type = get_variable_type ( expression , context , tools ) ;
146
151
147
- if ( ! type ) return null ;
152
+ if ( type === NodeType . Unknown ) return NodeType . Unknown ;
148
153
149
154
return disallowed_type ( type , allowed_types , context , tools ) ;
150
155
}
151
156
157
+ function get_variable_type (
158
+ identifier : TSESTree . Identifier ,
159
+ context : RuleContext ,
160
+ tools : TSTools
161
+ ) : TS . Type | NodeType . Unknown {
162
+ const variable = findVariable ( context , identifier ) ;
163
+
164
+ const identifiers = variable ?. identifiers [ 0 ] ;
165
+
166
+ if ( ! identifiers ) return getNodeType ( identifier , tools ) ;
167
+
168
+ const type = getNodeType ( variable . identifiers [ 0 ] , tools ) ;
169
+
170
+ if ( NodeType . Unknown === type ) return NodeType . Unknown ;
171
+
172
+ return narrow_variable_type ( identifier , type , tools ) ;
173
+ }
174
+
175
+ function narrow_variable_type (
176
+ identifier : TSESTree . Identifier ,
177
+ type : TS . Type ,
178
+ tools : TSTools
179
+ ) : TS . Type {
180
+ const checker = tools . service . program . getTypeChecker ( ) ;
181
+ let currentNode : TSESTree . Node | AST . SvelteNode | undefined = identifier as TSESTree . Node ;
182
+
183
+ while ( currentNode ) {
184
+ if ( currentNode . type === 'SvelteIfBlock' ) {
185
+ const condition = currentNode . expression ;
186
+ if ( condition . type === 'Identifier' && condition . name === identifier . name ) {
187
+ return checker . getNonNullableType ( type ) ;
188
+ }
189
+ }
190
+ currentNode = currentNode . parent as TSESTree . Node | AST . SvelteNode ;
191
+ }
192
+
193
+ return type ;
194
+ }
195
+
152
196
function disallowed_type (
153
197
type : TS . Type ,
154
198
allowed_types : Set < string > ,
155
199
context : RuleContext ,
156
200
tools : TSTools
157
- ) : TS . Type | null {
201
+ ) : TS . Type | NodeType {
158
202
if ( type . flags & TypeFlags . StringLike ) {
159
- return null ;
203
+ return NodeType . Allowed ;
160
204
}
161
205
if ( type . flags & TypeFlags . BooleanLike ) {
162
- return allowed_types . has ( 'boolean' ) ? null : type ;
206
+ return allowed_types . has ( 'boolean' ) ? NodeType . Allowed : type ;
163
207
}
164
208
if ( type . flags & TypeFlags . NumberLike ) {
165
- return allowed_types . has ( 'number' ) ? null : type ;
209
+ return allowed_types . has ( 'number' ) ? NodeType . Allowed : type ;
166
210
}
167
211
if ( type . flags & TypeFlags . Null ) {
168
- return allowed_types . has ( 'null' ) ? null : type ;
212
+ return allowed_types . has ( 'null' ) ? NodeType . Allowed : type ;
169
213
}
170
214
if ( type . flags & TypeFlags . Undefined ) {
171
- return allowed_types . has ( 'undefined' ) ? null : type ;
215
+ return allowed_types . has ( 'undefined' ) ? NodeType . Allowed : type ;
172
216
}
173
217
if ( type . isUnion ( ) ) {
174
218
for ( const sub_type of type . types ) {
@@ -177,7 +221,7 @@ function disallowed_type(
177
221
return disallowed ;
178
222
}
179
223
}
180
- return null ;
224
+ return NodeType . Allowed ;
181
225
}
182
226
183
227
return type ;
@@ -188,10 +232,10 @@ function disallowed_literal(
188
232
allowed_types : Set < string > ,
189
233
context : RuleContext ,
190
234
tools : TSTools
191
- ) : TS . Type | null {
235
+ ) : TS . Type | NodeType {
192
236
const type = getNodeType ( expression , tools ) ;
193
237
194
- if ( ! type ) return null ;
238
+ if ( NodeType . Unknown === type ) return NodeType . Unknown ;
195
239
196
240
return disallowed_type ( type , allowed_types , context , tools ) ;
197
241
}
@@ -201,7 +245,7 @@ function disallowed_expression(
201
245
allowed_types : Set < string > ,
202
246
context : RuleContext ,
203
247
tools : TSTools
204
- ) : TS . Type | null {
248
+ ) : TS . Type | NodeType {
205
249
switch ( expression . type ) {
206
250
case 'Literal' :
207
251
return disallowed_literal ( expression , allowed_types , context , tools ) ;
@@ -213,8 +257,11 @@ function disallowed_expression(
213
257
return disallowed_member_expression ( expression , allowed_types , context , tools ) ;
214
258
case 'LogicalExpression' :
215
259
return disallowed_logical_expression ( expression , allowed_types , context , tools ) ;
216
- default :
217
- return getNodeType ( expression , tools ) ;
260
+ default : {
261
+ const type = getNodeType ( expression , tools ) ;
262
+ if ( NodeType . Unknown === type ) return NodeType . Unknown ;
263
+ return disallowed_type ( type , allowed_types , context , tools ) ;
264
+ }
218
265
}
219
266
}
220
267
@@ -223,58 +270,24 @@ function disallowed_logical_expression(
223
270
allowed_types : Set < string > ,
224
271
context : RuleContext ,
225
272
tools : TSTools
226
- ) : TS . Type | null {
273
+ ) : TS . Type | NodeType {
227
274
const type = getNodeType ( expression , tools ) ;
228
275
229
- if ( ! type ) return null ;
276
+ if ( NodeType . Unknown === type ) return NodeType . Unknown ;
230
277
231
278
return disallowed_type ( type , allowed_types , context , tools ) ;
232
279
}
233
280
234
- // function disallowed_member_expression(
235
- // expression: TSESTree.MemberExpression,
236
- // allowed_types: Set<string>,
237
- // context: RuleContext,
238
- // tools: TSTools
239
- // ): TS.Type | null {
240
- // const checker = tools.service.program.getTypeChecker();
241
- // const type = getNodeType(expression, tools);
242
- // if (!type) return null;
243
-
244
- // const object = expression.object;
245
- // if (object.type === 'Identifier') {
246
- // const variable = findVariable(context, object);
247
- // if (!variable) return null;
248
- // const node_def = variable.defs[0].node;
249
- // if (node_def.type !== 'VariableDeclarator') return null;
250
- // if (!node_def.init) return null;
251
- // // let type = getNodeType(node_def.init, tools);
252
- // if (node_def.init.type !== 'ObjectExpression') return null;
253
- // if (expression.property.type !== 'Identifier') return null;
254
-
255
- // const type = getNodeType(node_def.init, tools);
256
- // if (!type) return null;
257
- // const symbol = checker.getPropertyOfType(type, expression.property.name);
258
- // if (!symbol) return null;
259
-
260
- // const prop_type = checker.getTypeOfSymbol(symbol);
261
-
262
- // return disallowed_type(prop_type, allowed_types, context, tools);
263
- // }
264
-
265
- // return disallowed_type(type, allowed_types, context, tools);
266
- // }
267
-
268
281
function disallowed_member_expression (
269
282
expression : TSESTree . MemberExpression ,
270
283
allowed_types : Set < string > ,
271
284
context : RuleContext ,
272
285
tools : TSTools
273
- ) : TS . Type | null {
286
+ ) : TS . Type | NodeType {
274
287
const checker = tools . service . program . getTypeChecker ( ) ;
275
- let objectType = getNodeType ( expression . object , tools ) ;
288
+ let objectType : TS . Type | NodeType = getNodeType ( expression . object , tools ) ;
276
289
277
- if ( ! objectType ) return null ;
290
+ if ( NodeType . Unknown === objectType ) return NodeType . Unknown ;
278
291
279
292
// Handle nested member expressions
280
293
if ( expression . object . type === 'MemberExpression' ) {
@@ -287,6 +300,8 @@ function disallowed_member_expression(
287
300
if ( nestedType ) objectType = nestedType ;
288
301
}
289
302
303
+ if ( NodeType . Allowed === objectType ) return NodeType . Allowed ;
304
+
290
305
// Handle identifiers (variables)
291
306
if ( expression . object . type === 'Identifier' ) {
292
307
const variable = findVariable ( context , expression . object ) ;
@@ -303,26 +318,14 @@ function disallowed_member_expression(
303
318
const propertyName = getPropertyName ( expression . property ) ;
304
319
if ( ! propertyName ) return objectType ;
305
320
306
- let propertyType : TS . Type | undefined ;
307
-
308
321
// Try to get property type using getPropertyOfType
309
322
const symbol = checker . getPropertyOfType ( objectType , propertyName ) ;
310
323
if ( symbol ) {
311
- propertyType = checker . getTypeOfSymbol ( symbol ) ;
312
- }
313
-
314
- // If property type is still not found, try using getTypeOfPropertyOfType
315
- if ( ! propertyType ) {
316
- const property_symbol = checker . getPropertyOfType ( objectType , propertyName ) ;
317
- if ( property_symbol ) {
318
- propertyType = checker . getTypeOfSymbol ( property_symbol ) ;
319
- }
324
+ const property_type = checker . getTypeOfSymbol ( symbol ) ;
325
+ return disallowed_type ( property_type , allowed_types , context , tools ) ;
320
326
}
321
327
322
- // If we found a property type, use it; otherwise, fall back to the object type
323
- return propertyType
324
- ? disallowed_type ( propertyType , allowed_types , context , tools )
325
- : disallowed_type ( objectType , allowed_types , context , tools ) ;
328
+ return NodeType . Unknown ;
326
329
}
327
330
328
331
function getPropertyName (
@@ -333,7 +336,7 @@ function getPropertyName(
333
336
} else if ( property . type === 'Literal' && typeof property . value === 'string' ) {
334
337
return property . value ;
335
338
} else if ( property . type === 'TemplateLiteral' && property . quasis . length === 1 ) {
336
- return property . quasis [ 0 ] . value . cooked ;
339
+ // return property.quasis[0].value.cooked;
337
340
}
338
341
return undefined ;
339
342
}
0 commit comments