@@ -5,6 +5,7 @@ import type ts from 'typescript';
5
5
import { findVariable } from '../utils/ast-utils.js' ;
6
6
import { toRegExp } from '../utils/regexp.js' ;
7
7
import { normalize } from 'path' ;
8
+ import type { AST as SvAST } from 'svelte-eslint-parser' ;
8
9
9
10
type PropertyPathArray = string [ ] ;
10
11
type DeclaredPropertyNames = Set < { originalName : string ; aliasName : string } > ;
@@ -130,49 +131,69 @@ export default createRule('no-unused-props', {
130
131
/**
131
132
* Extracts property paths from member expressions.
132
133
*/
133
- function getPropertyPath ( node : TSESTree . Identifier ) : PropertyPathArray {
134
+ function getPropertyPath ( node : TSESTree . Identifier ) : {
135
+ paths : PropertyPathArray ;
136
+ isSpread : boolean ;
137
+ } {
134
138
const paths : PropertyPathArray = [ ] ;
135
- let currentNode : TSESTree . Node = node ;
136
- let parentNode : TSESTree . Node | null = currentNode . parent ?? null ;
137
-
139
+ let isSpread = false ;
140
+ let currentNode : TSESTree . Node | SvAST . SvelteSpreadAttribute = node ;
141
+ let parentNode : TSESTree . Node | SvAST . SvelteSpreadAttribute | null =
142
+ currentNode . parent ?? null ;
138
143
while ( parentNode ) {
139
144
if ( parentNode . type === 'MemberExpression' && parentNode . object === currentNode ) {
140
145
const property = parentNode . property ;
141
146
if ( property . type === 'Identifier' ) {
142
147
paths . push ( property . name ) ;
143
148
} else if ( property . type === 'Literal' && typeof property . value === 'string' ) {
144
149
paths . push ( property . value ) ;
145
- } else {
146
- break ;
147
150
}
151
+ } else if (
152
+ parentNode . type === 'SpreadElement' ||
153
+ parentNode . type === 'SvelteSpreadAttribute'
154
+ ) {
155
+ isSpread = true ;
156
+ break ;
157
+ } else {
158
+ break ;
148
159
}
160
+
149
161
currentNode = parentNode ;
150
- parentNode = currentNode . parent ?? null ;
162
+ parentNode = ( currentNode . parent as TSESTree . Node | SvAST . SvelteSpreadAttribute ) ?? null ;
151
163
}
152
164
153
- return paths ;
165
+ return { paths, isSpread } ;
154
166
}
155
167
156
168
/**
157
169
* Finds all property access paths for a given variable.
158
170
*/
159
- function getUsedNestedPropertyPathsArray ( node : TSESTree . Identifier ) : PropertyPathArray [ ] {
171
+ function getUsedNestedPropertyPathsArray ( node : TSESTree . Identifier ) : {
172
+ paths : PropertyPathArray [ ] ;
173
+ spreadPaths : PropertyPathArray [ ] ;
174
+ } {
160
175
const variable = findVariable ( context , node ) ;
161
- if ( ! variable ) return [ ] ;
176
+ if ( ! variable ) return { paths : [ ] , spreadPaths : [ ] } ;
162
177
163
178
const pathsArray : PropertyPathArray [ ] = [ ] ;
179
+ const spreadPathsArray : PropertyPathArray [ ] = [ ] ;
164
180
for ( const reference of variable . references ) {
165
181
if (
166
182
'identifier' in reference &&
167
183
reference . identifier . type === 'Identifier' &&
168
184
( reference . identifier . range [ 0 ] !== node . range [ 0 ] ||
169
185
reference . identifier . range [ 1 ] !== node . range [ 1 ] )
170
186
) {
171
- const referencePath = getPropertyPath ( reference . identifier ) ;
172
- pathsArray . push ( referencePath ) ;
187
+ const { paths, isSpread } = getPropertyPath ( reference . identifier ) ;
188
+ if ( isSpread ) {
189
+ spreadPathsArray . push ( paths ) ;
190
+ } else {
191
+ pathsArray . push ( paths ) ;
192
+ }
173
193
}
174
194
}
175
- return pathsArray ;
195
+
196
+ return { paths : pathsArray , spreadPaths : spreadPathsArray } ;
176
197
}
177
198
178
199
/**
@@ -239,6 +260,7 @@ export default createRule('no-unused-props', {
239
260
function checkUnusedProperties ( {
240
261
propsType,
241
262
usedPropertyPaths,
263
+ usedSpreadPropertyPaths,
242
264
declaredPropertyNames,
243
265
reportNode,
244
266
parentPath,
@@ -247,6 +269,7 @@ export default createRule('no-unused-props', {
247
269
} : {
248
270
propsType : ts . Type ;
249
271
usedPropertyPaths : string [ ] ;
272
+ usedSpreadPropertyPaths : string [ ] ;
250
273
declaredPropertyNames : DeclaredPropertyNames ;
251
274
reportNode : TSESTree . Node ;
252
275
parentPath : string [ ] ;
@@ -273,6 +296,7 @@ export default createRule('no-unused-props', {
273
296
checkUnusedProperties ( {
274
297
propsType : propsBaseType ,
275
298
usedPropertyPaths,
299
+ usedSpreadPropertyPaths,
276
300
declaredPropertyNames,
277
301
reportNode,
278
302
parentPath,
@@ -290,13 +314,17 @@ export default createRule('no-unused-props', {
290
314
if ( shouldIgnoreProperty ( propName ) ) continue ;
291
315
292
316
const currentPath = [ ...parentPath , propName ] ;
293
- const currentPathStr = [ ... parentPath , propName ] . join ( '.' ) ;
317
+ const currentPathStr = currentPath . join ( '.' ) ;
294
318
295
319
if ( reportedPropertyPaths . has ( currentPathStr ) ) continue ;
296
320
297
321
const propType = typeChecker . getTypeOfSymbol ( prop ) ;
298
322
299
- const isUsedThisInPath = usedPropertyPaths . includes ( currentPathStr ) ;
323
+ const isUsedThisInPath =
324
+ usedPropertyPaths . includes ( currentPathStr ) ||
325
+ usedSpreadPropertyPaths . some ( ( path ) => {
326
+ return path === '' || path === currentPathStr || path . startsWith ( `${ currentPathStr } .` ) ;
327
+ } ) ;
300
328
const isUsedInPath = usedPropertyPaths . some ( ( path ) => {
301
329
return path . startsWith ( `${ currentPathStr } .` ) ;
302
330
} ) ;
@@ -330,6 +358,7 @@ export default createRule('no-unused-props', {
330
358
checkUnusedProperties ( {
331
359
propsType : propType ,
332
360
usedPropertyPaths,
361
+ usedSpreadPropertyPaths,
333
362
declaredPropertyNames,
334
363
reportNode,
335
364
parentPath : currentPath ,
@@ -370,7 +399,6 @@ export default createRule('no-unused-props', {
370
399
) : PropertyPathArray [ ] {
371
400
const normalized : PropertyPathArray [ ] = [ ] ;
372
401
for ( const path of paths . sort ( ( a , b ) => a . length - b . length ) ) {
373
- if ( path . length === 0 ) continue ;
374
402
if ( normalized . some ( ( p ) => p . every ( ( part , idx ) => part === path [ idx ] ) ) ) {
375
403
continue ;
376
404
}
@@ -398,7 +426,8 @@ export default createRule('no-unused-props', {
398
426
if ( ! tsNode || ! tsNode . type ) return ;
399
427
400
428
const propsType = typeChecker . getTypeFromTypeNode ( tsNode . type ) ;
401
- let usedPropertyPathsArray : PropertyPathArray [ ] = [ ] ;
429
+ const usedPropertyPathsArray : PropertyPathArray [ ] = [ ] ;
430
+ const usedSpreadPropertyPathsArray : PropertyPathArray [ ] = [ ] ;
402
431
let declaredPropertyNames : DeclaredPropertyNames = new Set ( ) ;
403
432
404
433
if ( node . id . type === 'ObjectPattern' ) {
@@ -416,11 +445,16 @@ export default createRule('no-unused-props', {
416
445
}
417
446
}
418
447
for ( const identifier of identifiers ) {
419
- const paths = getUsedNestedPropertyPathsArray ( identifier ) ;
448
+ const { paths, spreadPaths } = getUsedNestedPropertyPathsArray ( identifier ) ;
420
449
usedPropertyPathsArray . push ( ...paths . map ( ( path ) => [ identifier . name , ...path ] ) ) ;
450
+ usedSpreadPropertyPathsArray . push (
451
+ ...spreadPaths . map ( ( path ) => [ identifier . name , ...path ] )
452
+ ) ;
421
453
}
422
454
} else if ( node . id . type === 'Identifier' ) {
423
- usedPropertyPathsArray = getUsedNestedPropertyPathsArray ( node . id ) ;
455
+ const { paths, spreadPaths } = getUsedNestedPropertyPathsArray ( node . id ) ;
456
+ usedPropertyPathsArray . push ( ...paths ) ;
457
+ usedSpreadPropertyPathsArray . push ( ...spreadPaths ) ;
424
458
}
425
459
426
460
checkUnusedProperties ( {
@@ -431,6 +465,12 @@ export default createRule('no-unused-props', {
431
465
) . map ( ( pathArray ) => {
432
466
return pathArray . join ( '.' ) ;
433
467
} ) ,
468
+ usedSpreadPropertyPaths : normalizeUsedPaths (
469
+ usedSpreadPropertyPathsArray ,
470
+ options . allowUnusedNestedProperties
471
+ ) . map ( ( pathArray ) => {
472
+ return pathArray . join ( '.' ) ;
473
+ } ) ,
434
474
declaredPropertyNames,
435
475
reportNode : node . id ,
436
476
parentPath : [ ] ,
0 commit comments