@@ -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,69 @@ 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 if (
152+ parentNode . type === 'SpreadElement' ||
153+ parentNode . type === 'SvelteSpreadAttribute'
154+ ) {
155+ isSpread = true ;
156+ break ;
157+ } else {
158+ break ;
148159 }
160+
149161 currentNode = parentNode ;
150- parentNode = currentNode . parent ?? null ;
162+ parentNode = ( currentNode . parent as TSESTree . Node | SvAST . SvelteSpreadAttribute ) ?? null ;
151163 }
152164
153- return paths ;
165+ return { paths, isSpread } ;
154166 }
155167
156168 /**
157169 * Finds all property access paths for a given variable.
158170 */
159- function getUsedNestedPropertyPathsArray ( node : TSESTree . Identifier ) : PropertyPathArray [ ] {
171+ function getUsedNestedPropertyPathsArray ( node : TSESTree . Identifier ) : {
172+ paths : PropertyPathArray [ ] ;
173+ spreadPaths : PropertyPathArray [ ] ;
174+ } {
160175 const variable = findVariable ( context , node ) ;
161- if ( ! variable ) return [ ] ;
176+ if ( ! variable ) return { paths : [ ] , spreadPaths : [ ] } ;
162177
163178 const pathsArray : PropertyPathArray [ ] = [ ] ;
179+ const spreadPathsArray : PropertyPathArray [ ] = [ ] ;
164180 for ( const reference of variable . references ) {
165181 if (
166182 'identifier' in reference &&
167183 reference . identifier . type === 'Identifier' &&
168184 ( reference . identifier . range [ 0 ] !== node . range [ 0 ] ||
169185 reference . identifier . range [ 1 ] !== node . range [ 1 ] )
170186 ) {
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+ }
173193 }
174194 }
175- return pathsArray ;
195+
196+ return { paths : pathsArray , spreadPaths : spreadPathsArray } ;
176197 }
177198
178199 /**
@@ -239,6 +260,7 @@ export default createRule('no-unused-props', {
239260 function checkUnusedProperties ( {
240261 propsType,
241262 usedPropertyPaths,
263+ usedSpreadPropertyPaths,
242264 declaredPropertyNames,
243265 reportNode,
244266 parentPath,
@@ -247,6 +269,7 @@ export default createRule('no-unused-props', {
247269 } : {
248270 propsType : ts . Type ;
249271 usedPropertyPaths : string [ ] ;
272+ usedSpreadPropertyPaths : string [ ] ;
250273 declaredPropertyNames : DeclaredPropertyNames ;
251274 reportNode : TSESTree . Node ;
252275 parentPath : string [ ] ;
@@ -273,6 +296,7 @@ export default createRule('no-unused-props', {
273296 checkUnusedProperties ( {
274297 propsType : propsBaseType ,
275298 usedPropertyPaths,
299+ usedSpreadPropertyPaths,
276300 declaredPropertyNames,
277301 reportNode,
278302 parentPath,
@@ -290,13 +314,17 @@ export default createRule('no-unused-props', {
290314 if ( shouldIgnoreProperty ( propName ) ) continue ;
291315
292316 const currentPath = [ ...parentPath , propName ] ;
293- const currentPathStr = [ ... parentPath , propName ] . join ( '.' ) ;
317+ const currentPathStr = currentPath . join ( '.' ) ;
294318
295319 if ( reportedPropertyPaths . has ( currentPathStr ) ) continue ;
296320
297321 const propType = typeChecker . getTypeOfSymbol ( prop ) ;
298322
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+ } ) ;
300328 const isUsedInPath = usedPropertyPaths . some ( ( path ) => {
301329 return path . startsWith ( `${ currentPathStr } .` ) ;
302330 } ) ;
@@ -330,6 +358,7 @@ export default createRule('no-unused-props', {
330358 checkUnusedProperties ( {
331359 propsType : propType ,
332360 usedPropertyPaths,
361+ usedSpreadPropertyPaths,
333362 declaredPropertyNames,
334363 reportNode,
335364 parentPath : currentPath ,
@@ -370,7 +399,6 @@ export default createRule('no-unused-props', {
370399 ) : PropertyPathArray [ ] {
371400 const normalized : PropertyPathArray [ ] = [ ] ;
372401 for ( const path of paths . sort ( ( a , b ) => a . length - b . length ) ) {
373- if ( path . length === 0 ) continue ;
374402 if ( normalized . some ( ( p ) => p . every ( ( part , idx ) => part === path [ idx ] ) ) ) {
375403 continue ;
376404 }
@@ -398,7 +426,8 @@ export default createRule('no-unused-props', {
398426 if ( ! tsNode || ! tsNode . type ) return ;
399427
400428 const propsType = typeChecker . getTypeFromTypeNode ( tsNode . type ) ;
401- let usedPropertyPathsArray : PropertyPathArray [ ] = [ ] ;
429+ const usedPropertyPathsArray : PropertyPathArray [ ] = [ ] ;
430+ const usedSpreadPropertyPathsArray : PropertyPathArray [ ] = [ ] ;
402431 let declaredPropertyNames : DeclaredPropertyNames = new Set ( ) ;
403432
404433 if ( node . id . type === 'ObjectPattern' ) {
@@ -416,11 +445,16 @@ export default createRule('no-unused-props', {
416445 }
417446 }
418447 for ( const identifier of identifiers ) {
419- const paths = getUsedNestedPropertyPathsArray ( identifier ) ;
448+ const { paths, spreadPaths } = getUsedNestedPropertyPathsArray ( identifier ) ;
420449 usedPropertyPathsArray . push ( ...paths . map ( ( path ) => [ identifier . name , ...path ] ) ) ;
450+ usedSpreadPropertyPathsArray . push (
451+ ...spreadPaths . map ( ( path ) => [ identifier . name , ...path ] )
452+ ) ;
421453 }
422454 } 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 ) ;
424458 }
425459
426460 checkUnusedProperties ( {
@@ -431,6 +465,12 @@ export default createRule('no-unused-props', {
431465 ) . map ( ( pathArray ) => {
432466 return pathArray . join ( '.' ) ;
433467 } ) ,
468+ usedSpreadPropertyPaths : normalizeUsedPaths (
469+ usedSpreadPropertyPathsArray ,
470+ options . allowUnusedNestedProperties
471+ ) . map ( ( pathArray ) => {
472+ return pathArray . join ( '.' ) ;
473+ } ) ,
434474 declaredPropertyNames,
435475 reportNode : node . id ,
436476 parentPath : [ ] ,
0 commit comments