11import { createRule } from '../utils/index.js' ;
2- import { getSourceCode } from '../utils/compat.js' ;
32import { getTypeScriptTools } from '../utils/ts-utils/index.js' ;
43import type { TSESTree } from '@typescript-eslint/types' ;
4+ import type ts from 'typescript' ;
5+ import { findVariable } from '../utils/ast-utils.js' ;
56
6- type TSInterfaceDeclarationWithId = TSESTree . TSInterfaceDeclaration & {
7- id : TSESTree . Identifier ;
8- body : TSESTree . TSInterfaceBody ;
9- } ;
7+ const unknown = Symbol ( 'unknown' ) ;
108
119export default createRule ( 'no-unused-props' , {
1210 meta : {
@@ -28,202 +26,93 @@ export default createRule('no-unused-props', {
2826 ]
2927 } ,
3028 create ( context ) {
31- const sourceCode = getSourceCode ( context ) ;
32- const scopeManager = sourceCode . scopeManager ;
33-
34- // Get TypeScript tools using the provided utility.
35- const _tsTools = getTypeScriptTools ( context ) ;
36-
37- // Property names obtained from the Props interface.
38- const declaredProps = new Map < string , TSESTree . Node > ( ) ;
39- let hasIndexSignature = false ;
40-
41- // Track used properties and rest usage.
42- const usedProps = new Set < string > ( ) ;
43- const restUsage = new Map < string , boolean > ( ) ; // variableName -> hasRest
44-
45- // Track renamed variables.
46- const renamedVars = new Map < string , string > ( ) ;
29+ const tools = getTypeScriptTools ( context ) ;
30+ if ( ! tools ) {
31+ return { } ;
32+ }
4733
48- // Track used variables.
49- const usedVars = new Set < string > ( ) ;
34+ const typeChecker = tools . service . program . getTypeChecker ( ) ;
35+ if ( ! typeChecker ) {
36+ return { } ;
37+ }
5038
51- // Track $props variables by name and its identifier node.
52- const propsVars = new Set < string > ( ) ;
53- const propsNodes = new Map < string , TSESTree . Node > ( ) ;
39+ function getUsedPropertyNames ( node : TSESTree . Identifier ) : ( string | typeof unknown ) [ ] {
40+ const variable = findVariable ( context , node ) ;
41+ if ( ! variable ) {
42+ return [ unknown ] ;
43+ }
5444
55- // Track processed interfaces to avoid infinite recursion.
56- const processedInterfaces = new Set < string > ( ) ;
45+ const usedProps = new Set < string | typeof unknown > ( ) ;
5746
58- // Collect declared properties from the Props interface.
59- function collectPropsFromInterface ( node : TSInterfaceDeclarationWithId ) {
60- if ( ! node . id || node . id . name !== 'Props' ) return ;
61- processedInterfaces . add ( node . id . name ) ;
62- for ( const m of node . body . body ) {
63- if ( m . type === 'TSPropertySignature' && m . key . type === 'Identifier' ) {
64- declaredProps . set ( m . key . name , m ) ;
65- // For nested properties, collect property paths (e.g., "nested.a").
66- if ( m . typeAnnotation ?. typeAnnotation . type === 'TSTypeLiteral' ) {
67- const nestedProps = m . typeAnnotation . typeAnnotation . members ;
68- for ( const nestedProp of nestedProps ) {
69- if (
70- nestedProp . type === 'TSPropertySignature' &&
71- nestedProp . key . type === 'Identifier'
72- ) {
73- const fullPath = `${ m . key . name } .${ nestedProp . key . name } ` ;
74- declaredProps . set ( fullPath , nestedProp ) ;
75- }
76- }
77- }
78- }
79- if ( m . type === 'TSIndexSignature' ) {
80- hasIndexSignature = true ;
81- }
82- }
83- // Handle extends clause.
84- if ( node . extends ) {
85- for ( const heritage of node . extends ) {
86- if ( heritage . expression . type === 'Identifier' ) {
87- const baseName = heritage . expression . name ;
88- if ( baseName === 'Props' || processedInterfaces . has ( baseName ) ) continue ;
89- const program = sourceCode . ast ;
90- for ( const scriptElem of program . body ) {
91- if ( scriptElem . type === 'SvelteScriptElement' ) {
92- const scriptProgram = scriptElem . body ;
93- if ( scriptProgram && 'body' in scriptProgram ) {
94- const statements = scriptProgram . body as TSESTree . Statement [ ] ;
95- for ( const stmt of statements ) {
96- if (
97- stmt . type === 'TSInterfaceDeclaration' &&
98- stmt . id . type === 'Identifier' &&
99- stmt . id . name === baseName
100- ) {
101- collectPropsFromInterface ( stmt as TSInterfaceDeclarationWithId ) ;
102- break ;
103- }
104- }
105- }
106- }
107- }
108- }
109- }
110- }
111- }
47+ for ( const reference of variable . references ) {
48+ const parent = reference . identifier . parent ;
49+ if ( ! parent ) continue ;
11250
113- // Collect used property names from an object destructuring pattern.
114- function markDestructuredProps ( pattern : TSESTree . ObjectPattern , variableName : string ) {
115- for ( const prop of pattern . properties ) {
116- if ( prop . type === 'Property' && prop . key . type === 'Identifier' ) {
117- const originalName = prop . key . name ;
118- if ( prop . value . type === 'Identifier' ) {
119- const renamedVar = prop . value . name ;
120- renamedVars . set ( renamedVar , originalName ) ;
51+ if ( parent . type === 'MemberExpression' && parent . object === reference . identifier ) {
52+ if ( parent . property . type === 'Identifier' && ! parent . computed ) {
53+ usedProps . add ( parent . property . name ) ;
12154 } else {
122- usedProps . add ( originalName ) ;
123- }
124- }
125- if ( prop . type === 'RestElement' ) {
126- restUsage . set ( variableName , true ) ;
127- if ( hasIndexSignature ) {
128- for ( const [ propName ] of declaredProps ) {
129- usedProps . add ( propName ) ;
130- }
55+ usedProps . add ( unknown ) ;
13156 }
132- }
133- }
134- }
135-
136- function isPropsCall ( initNode : TSESTree . Expression ) : boolean {
137- return (
138- initNode . type === 'CallExpression' &&
139- initNode . callee . type === 'Identifier' &&
140- initNode . callee . name === '$props'
141- ) ;
142- }
143-
144- // Traverse scopes and record variables assigned from $props().
145- function analyzePropsUsageInScopes ( ) {
146- for ( const scope of scopeManager . scopes ) {
147- for ( const variable of scope . variables ) {
148- const def = variable . defs . find ( ( d ) => d . type === 'Variable' ) ;
149- if ( ! def || ! def . node || ! def . node . init ) continue ;
150- const typeAnn = def . node . id . typeAnnotation ?. typeAnnotation ;
57+ } else if ( parent . type === 'CallExpression' || parent . type === 'NewExpression' ) {
15158 if (
152- isPropsCall ( def . node . init ) &&
153- typeAnn ?. type === 'TSTypeReference' &&
154- typeAnn . typeName . type === 'Identifier' &&
155- typeAnn . typeName . name === 'Props'
59+ 'arguments' in parent &&
60+ Array . isArray ( parent . arguments ) &&
61+ parent . arguments . some ( ( arg ) : arg is TSESTree . Identifier => arg === reference . identifier )
15662 ) {
157- if ( def . node . id . type === 'Identifier' ) {
158- propsVars . add ( def . node . id . name ) ;
159- propsNodes . set ( def . node . id . name , def . node . id ) ;
160- }
161- if ( def . node . id . type === 'ObjectPattern' ) {
162- markDestructuredProps ( def . node . id , variable . name ) ;
163- }
63+ usedProps . add ( unknown ) ;
16464 }
65+ } else if ( parent . type === 'AssignmentExpression' || parent . type === 'AssignmentPattern' ) {
66+ usedProps . add ( unknown ) ;
67+ } else if ( parent . type === 'SpreadElement' ) {
68+ usedProps . add ( unknown ) ;
16569 }
16670 }
71+
72+ return Array . from ( usedProps ) ;
16773 }
16874
16975 return {
170- TSInterfaceDeclaration ( node : TSESTree . Node ) {
76+ 'VariableDeclaration > VariableDeclarator' : ( node : TSESTree . VariableDeclarator ) => {
17177 if (
172- node . type === 'TSInterfaceDeclaration' &&
173- node . id &&
174- node . id . type === 'Identifier' &&
175- node . id . name === 'Props'
78+ node . init ?. type !== 'CallExpression' ||
79+ node . init . callee . type !== 'Identifier' ||
80+ node . init . callee . name !== '$props'
17681 ) {
177- collectPropsFromInterface ( node as TSInterfaceDeclarationWithId ) ;
178- }
179- } ,
180- Program ( ) {
181- analyzePropsUsageInScopes ( ) ;
182- } ,
183- MemberExpression ( node : TSESTree . MemberExpression ) {
184- let current : TSESTree . Expression | TSESTree . Super = node . object ;
185- const parts : string [ ] = [ ] ;
186- if ( node . property . type === 'Identifier' ) {
187- parts . push ( node . property . name ) ;
188- }
189- while ( current . type === 'MemberExpression' ) {
190- if ( current . property . type === 'Identifier' ) {
191- parts . unshift ( current . property . name ) ;
192- }
193- current = current . object ;
194- }
195- if ( current . type === 'Identifier' && propsVars . has ( current . name ) ) {
196- let path = '' ;
197- for ( const part of parts ) {
198- path = path ? `${ path } .${ part } ` : part ;
199- usedProps . add ( path ) ;
200- }
201- }
202- } ,
203- Identifier ( node : TSESTree . Identifier ) {
204- usedVars . add ( node . name ) ;
205- const originalName = renamedVars . get ( node . name ) ;
206- if ( originalName && usedVars . has ( node . name ) ) {
207- usedProps . add ( originalName ) ;
82+ return ;
20883 }
209- } ,
210- 'Program:exit' ( ) {
211- let hasRestWithIndexSignature = false ;
212- for ( const [ _ , hasRest ] of restUsage ) {
213- if ( hasRest && hasIndexSignature ) {
214- hasRestWithIndexSignature = true ;
215- break ;
84+
85+ const tsNode = tools . service . esTreeNodeToTSNodeMap . get ( node ) as ts . VariableDeclaration ;
86+ if ( ! tsNode || ! tsNode . type ) return ;
87+ const checker = tools . service . program . getTypeChecker ( ) ;
88+ const propType = checker . getTypeFromTypeNode ( tsNode . type ) ;
89+ const properties = checker . getPropertiesOfType ( propType ) ;
90+ const propNames = properties . map ( ( p ) => p . getName ( ) ) ;
91+
92+ const usedNames : ( string | typeof unknown ) [ ] = [ ] ;
93+ if ( node . id . type === 'ObjectPattern' ) {
94+ for ( const prop of node . id . properties ) {
95+ if ( prop . type === 'Property' && prop . key . type === 'Identifier' ) {
96+ usedNames . push ( prop . key . name ) ;
97+ } else if ( prop . type === 'RestElement' && prop . argument . type === 'Identifier' ) {
98+ usedNames . push ( ...getUsedPropertyNames ( prop . argument ) ) ;
99+ }
216100 }
101+ } else if ( node . id . type === 'Identifier' && node . id . typeAnnotation ) {
102+ usedNames . push ( ...getUsedPropertyNames ( node . id ) ) ;
217103 }
218- if ( hasRestWithIndexSignature ) {
104+
105+ if ( usedNames . includes ( unknown ) ) {
219106 return ;
220107 }
221- for ( const [ propName , propNode ] of declaredProps ) {
222- if ( ! usedProps . has ( propName ) ) {
108+ for ( const propName of propNames ) {
109+ if ( ! usedNames . includes ( propName ) ) {
223110 context . report ( {
224- node : propNode ,
111+ node : node . id ,
225112 messageId : 'unusedProp' ,
226- data : { name : propName }
113+ data : {
114+ name : propName
115+ }
227116 } ) ;
228117 }
229118 }
0 commit comments