@@ -5,6 +5,7 @@ import type ts from 'typescript';
55import { findVariable } from '../utils/ast-utils.js' ;
66import { toRegExp } from '../utils/regexp.js' ;
77import { normalize } from 'path' ;
8+ import type { AST as SvAST } from 'svelte-eslint-parser' ;
89
910type PropertyPathArray = string [ ] ;
1011type DeclaredPropertyNames = Set < { originalName : string ; aliasName : string } > ;
@@ -130,49 +131,66 @@ export default createRule('no-unused-props', {
130131 /**
131132 * Extracts property paths from member expressions.
132133 */
133- function getPropertyPath ( node : TSESTree . Identifier ) : PropertyPathArray {
134+ function getPropertyPath ( node : TSESTree . Identifier ) : {
135+ paths : PropertyPathArray ;
136+ isSpread : boolean ;
137+ } {
134138 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 ;
138143 while ( parentNode ) {
139144 if ( parentNode . type === 'MemberExpression' && parentNode . object === currentNode ) {
140145 const property = parentNode . property ;
141146 if ( property . type === 'Identifier' ) {
142147 paths . push ( property . name ) ;
143148 } else if ( property . type === 'Literal' && typeof property . value === 'string' ) {
144149 paths . push ( property . value ) ;
145- } else {
146- break ;
147150 }
151+ } else {
152+ if ( parentNode . type === 'SpreadElement' || parentNode . type === 'SvelteSpreadAttribute' ) {
153+ isSpread = true ;
154+ }
155+ break ;
148156 }
157+
149158 currentNode = parentNode ;
150- parentNode = currentNode . parent ?? null ;
159+ parentNode = ( currentNode . parent as TSESTree . Node | SvAST . SvelteSpreadAttribute ) ?? null ;
151160 }
152161
153- return paths ;
162+ return { paths, isSpread } ;
154163 }
155164
156165 /**
157166 * Finds all property access paths for a given variable.
158167 */
159- function getUsedNestedPropertyPathsArray ( node : TSESTree . Identifier ) : PropertyPathArray [ ] {
168+ function getUsedNestedPropertyPathsArray ( node : TSESTree . Identifier ) : {
169+ paths : PropertyPathArray [ ] ;
170+ spreadPaths : PropertyPathArray [ ] ;
171+ } {
160172 const variable = findVariable ( context , node ) ;
161- if ( ! variable ) return [ ] ;
173+ if ( ! variable ) return { paths : [ ] , spreadPaths : [ ] } ;
162174
163175 const pathsArray : PropertyPathArray [ ] = [ ] ;
176+ const spreadPathsArray : PropertyPathArray [ ] = [ ] ;
164177 for ( const reference of variable . references ) {
165178 if (
166179 'identifier' in reference &&
167180 reference . identifier . type === 'Identifier' &&
168181 ( reference . identifier . range [ 0 ] !== node . range [ 0 ] ||
169182 reference . identifier . range [ 1 ] !== node . range [ 1 ] )
170183 ) {
171- const referencePath = getPropertyPath ( reference . identifier ) ;
172- pathsArray . push ( referencePath ) ;
184+ const { paths, isSpread } = getPropertyPath ( reference . identifier ) ;
185+ if ( isSpread ) {
186+ spreadPathsArray . push ( paths ) ;
187+ } else {
188+ pathsArray . push ( paths ) ;
189+ }
173190 }
174191 }
175- return pathsArray ;
192+
193+ return { paths : pathsArray , spreadPaths : spreadPathsArray } ;
176194 }
177195
178196 /**
@@ -239,6 +257,7 @@ export default createRule('no-unused-props', {
239257 function checkUnusedProperties ( {
240258 propsType,
241259 usedPropertyPaths,
260+ usedSpreadPropertyPaths,
242261 declaredPropertyNames,
243262 reportNode,
244263 parentPath,
@@ -247,6 +266,7 @@ export default createRule('no-unused-props', {
247266 } : {
248267 propsType : ts . Type ;
249268 usedPropertyPaths : string [ ] ;
269+ usedSpreadPropertyPaths : string [ ] ;
250270 declaredPropertyNames : DeclaredPropertyNames ;
251271 reportNode : TSESTree . Node ;
252272 parentPath : string [ ] ;
@@ -273,6 +293,7 @@ export default createRule('no-unused-props', {
273293 checkUnusedProperties ( {
274294 propsType : propsBaseType ,
275295 usedPropertyPaths,
296+ usedSpreadPropertyPaths,
276297 declaredPropertyNames,
277298 reportNode,
278299 parentPath,
@@ -290,13 +311,17 @@ export default createRule('no-unused-props', {
290311 if ( shouldIgnoreProperty ( propName ) ) continue ;
291312
292313 const currentPath = [ ...parentPath , propName ] ;
293- const currentPathStr = [ ... parentPath , propName ] . join ( '.' ) ;
314+ const currentPathStr = currentPath . join ( '.' ) ;
294315
295316 if ( reportedPropertyPaths . has ( currentPathStr ) ) continue ;
296317
297318 const propType = typeChecker . getTypeOfSymbol ( prop ) ;
298319
299- const isUsedThisInPath = usedPropertyPaths . includes ( currentPathStr ) ;
320+ const isUsedThisInPath =
321+ usedPropertyPaths . includes ( currentPathStr ) ||
322+ usedSpreadPropertyPaths . some ( ( path ) => {
323+ return path === '' || path === currentPathStr || path . startsWith ( `${ currentPathStr } .` ) ;
324+ } ) ;
300325 const isUsedInPath = usedPropertyPaths . some ( ( path ) => {
301326 return path . startsWith ( `${ currentPathStr } .` ) ;
302327 } ) ;
@@ -330,6 +355,7 @@ export default createRule('no-unused-props', {
330355 checkUnusedProperties ( {
331356 propsType : propType ,
332357 usedPropertyPaths,
358+ usedSpreadPropertyPaths,
333359 declaredPropertyNames,
334360 reportNode,
335361 parentPath : currentPath ,
@@ -370,7 +396,6 @@ export default createRule('no-unused-props', {
370396 ) : PropertyPathArray [ ] {
371397 const normalized : PropertyPathArray [ ] = [ ] ;
372398 for ( const path of paths . sort ( ( a , b ) => a . length - b . length ) ) {
373- if ( path . length === 0 ) continue ;
374399 if ( normalized . some ( ( p ) => p . every ( ( part , idx ) => part === path [ idx ] ) ) ) {
375400 continue ;
376401 }
@@ -398,7 +423,8 @@ export default createRule('no-unused-props', {
398423 if ( ! tsNode || ! tsNode . type ) return ;
399424
400425 const propsType = typeChecker . getTypeFromTypeNode ( tsNode . type ) ;
401- let usedPropertyPathsArray : PropertyPathArray [ ] = [ ] ;
426+ const usedPropertyPathsArray : PropertyPathArray [ ] = [ ] ;
427+ const usedSpreadPropertyPathsArray : PropertyPathArray [ ] = [ ] ;
402428 let declaredPropertyNames : DeclaredPropertyNames = new Set ( ) ;
403429
404430 if ( node . id . type === 'ObjectPattern' ) {
@@ -416,21 +442,28 @@ export default createRule('no-unused-props', {
416442 }
417443 }
418444 for ( const identifier of identifiers ) {
419- const paths = getUsedNestedPropertyPathsArray ( identifier ) ;
445+ const { paths, spreadPaths } = getUsedNestedPropertyPathsArray ( identifier ) ;
420446 usedPropertyPathsArray . push ( ...paths . map ( ( path ) => [ identifier . name , ...path ] ) ) ;
447+ usedSpreadPropertyPathsArray . push (
448+ ...spreadPaths . map ( ( path ) => [ identifier . name , ...path ] )
449+ ) ;
421450 }
422451 } else if ( node . id . type === 'Identifier' ) {
423- usedPropertyPathsArray = getUsedNestedPropertyPathsArray ( node . id ) ;
452+ const { paths, spreadPaths } = getUsedNestedPropertyPathsArray ( node . id ) ;
453+ usedPropertyPathsArray . push ( ...paths ) ;
454+ usedSpreadPropertyPathsArray . push ( ...spreadPaths ) ;
455+ }
456+
457+ function runNormalizeUsedPaths ( paths : PropertyPathArray [ ] ) {
458+ return normalizeUsedPaths ( paths , options . allowUnusedNestedProperties ) . map ( ( pathArray ) => {
459+ return pathArray . join ( '.' ) ;
460+ } ) ;
424461 }
425462
426463 checkUnusedProperties ( {
427464 propsType,
428- usedPropertyPaths : normalizeUsedPaths (
429- usedPropertyPathsArray ,
430- options . allowUnusedNestedProperties
431- ) . map ( ( pathArray ) => {
432- return pathArray . join ( '.' ) ;
433- } ) ,
465+ usedPropertyPaths : runNormalizeUsedPaths ( usedPropertyPathsArray ) ,
466+ usedSpreadPropertyPaths : runNormalizeUsedPaths ( usedSpreadPropertyPathsArray ) ,
434467 declaredPropertyNames,
435468 reportNode : node . id ,
436469 parentPath : [ ] ,
0 commit comments