66const { findVariable } = require ( '@eslint-community/eslint-utils' )
77const utils = require ( '../utils' )
88
9+ /**
10+ * @typedef {'props'|'prop' } PropIdKind
11+ * - `'props'`: A node is a container object that has props.
12+ * - `'prop'`: A node is a variable with one prop.
13+ */
14+ /**
15+ * @typedef {object } PropId
16+ * @property {Pattern } node
17+ * @property {PropIdKind } kind
18+ */
19+ /**
20+ * Iterates over Prop identifiers by parsing the given pattern
21+ * in the left operand of defineProps().
22+ * @param {Pattern } node
23+ * @returns {IterableIterator<PropId> }
24+ */
25+ function * iteratePropIds ( node ) {
26+ switch ( node . type ) {
27+ case 'ObjectPattern' : {
28+ for ( const prop of node . properties ) {
29+ yield prop . type === 'Property'
30+ ? {
31+ // e.g. `const { prop } = defineProps()`
32+ node : unwrapAssignment ( prop . value ) ,
33+ kind : 'prop'
34+ }
35+ : {
36+ // RestElement
37+ // e.g. `const { x, ...prop } = defineProps()`
38+ node : unwrapAssignment ( prop . argument ) ,
39+ kind : 'props'
40+ }
41+ }
42+ break
43+ }
44+ default : {
45+ // e.g. `const props = defineProps()`
46+ yield { node : unwrapAssignment ( node ) , kind : 'props' }
47+ }
48+ }
49+ }
50+
51+ /**
52+ * @template {Pattern} T
53+ * @param {T } node
54+ * @returns {Pattern }
55+ */
56+ function unwrapAssignment ( node ) {
57+ if ( node . type === 'AssignmentPattern' ) {
58+ return node . left
59+ }
60+ return node
61+ }
62+
963module . exports = {
1064 meta : {
1165 type : 'suggestion' ,
@@ -31,7 +85,9 @@ module.exports = {
3185 create ( context ) {
3286 /**
3387 * @typedef {object } ScopePropsReferences
34- * @property {Set<Identifier> } refs
88+ * @property {object } refs
89+ * @property {Set<Identifier> } refs.props A set of references to container objects with multiple props.
90+ * @property {Set<Identifier> } refs.prop A set of references a variable with one property.
3591 * @property {string } scopeName
3692 */
3793 /** @type {Map<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program, ScopePropsReferences> } */
@@ -72,70 +128,72 @@ module.exports = {
72128 wrapperExpressionTypes . has ( rightNode . type ) &&
73129 isPropsMemberAccessed ( rightNode , propsReferences )
74130 ) {
75- return report ( rightNode , 'getProperty' , propsReferences . scopeName )
76- }
77-
78- if (
79- left . type !== 'ArrayPattern' &&
80- left . type !== 'ObjectPattern' &&
81- rightNode . type !== 'MemberExpression' &&
82- rightNode . type !== 'ConditionalExpression' &&
83- rightNode . type !== 'TemplateLiteral'
84- ) {
131+ // e.g. `const foo = { x: props.x }`
132+ report ( rightNode , 'getProperty' , propsReferences . scopeName )
85133 return
86134 }
87135
88- if ( rightNode . type === 'TemplateLiteral' ) {
89- rightNode . expressions . some ( ( expression ) =>
90- checkMemberAccess ( expression , propsReferences , left , right )
91- )
92- } else {
93- checkMemberAccess ( rightNode , propsReferences , left , right )
136+ // Get the expression that provides the value.
137+ /** @type {Expression | Super } */
138+ let expression = rightNode
139+ while ( expression . type === 'MemberExpression' ) {
140+ expression = utils . skipChainExpression ( expression . object )
94141 }
95- }
142+ /** A list of expression nodes to verify */
143+ const expressions =
144+ expression . type === 'TemplateLiteral'
145+ ? expression . expressions
146+ : expression . type === 'ConditionalExpression'
147+ ? [ expression . test , expression . consequent , expression . alternate ]
148+ : expression . type === 'Identifier'
149+ ? [ expression ]
150+ : [ ]
96151
97- /**
98- * @param {Expression | Super } rightId
99- * @param {ScopePropsReferences } propsReferences
100- * @param {Pattern } left
101- * @param {Expression } right
102- * @return {boolean }
103- */
104- function checkMemberAccess ( rightId , propsReferences , left , right ) {
105- while ( rightId . type === 'MemberExpression' ) {
106- rightId = utils . skipChainExpression ( rightId . object )
107- }
108- if ( rightId . type === 'Identifier' && propsReferences . refs . has ( rightId ) ) {
109- report ( left , 'getProperty' , propsReferences . scopeName )
110- return true
111- }
112152 if (
113- rightId . type === 'ConditionalExpression' &&
114- ( isPropsMemberAccessed ( rightId . test , propsReferences ) ||
115- isPropsMemberAccessed ( rightId . consequent , propsReferences ) ||
116- isPropsMemberAccessed ( rightId . alternate , propsReferences ) )
153+ ( left . type === 'ArrayPattern' || left . type === 'ObjectPattern' ) &&
154+ expressions . some (
155+ ( expr ) =>
156+ expr . type === 'Identifier' && propsReferences . refs . props . has ( expr )
157+ )
117158 ) {
118- report ( right , 'getProperty' , propsReferences . scopeName )
119- return true
159+ // e.g. `const {foo} = props`
160+ report ( left , 'getProperty' , propsReferences . scopeName )
161+ return
162+ }
163+
164+ const reportNode = expressions . find ( ( expr ) =>
165+ isPropsMemberAccessed ( expr , propsReferences )
166+ )
167+ if ( reportNode ) {
168+ report ( reportNode , 'getProperty' , propsReferences . scopeName )
120169 }
121- return false
122170 }
123171
124172 /**
125- * @param {Expression } node
173+ * @param {Expression | Super } node
126174 * @param {ScopePropsReferences } propsReferences
127175 */
128176 function isPropsMemberAccessed ( node , propsReferences ) {
129- const propRefs = [ ...propsReferences . refs . values ( ) ]
130-
131- return propRefs . some ( ( props ) => {
177+ for ( const props of propsReferences . refs . props ) {
132178 const isPropsInExpressionRange = utils . inRange ( node . range , props )
133179 const isPropsMemberExpression =
134180 props . parent . type === 'MemberExpression' &&
135181 props . parent . object === props
136182
137- return isPropsInExpressionRange && isPropsMemberExpression
138- } )
183+ if ( isPropsInExpressionRange && isPropsMemberExpression ) {
184+ return true
185+ }
186+ }
187+
188+ // Checks for actual member access using prop destructuring.
189+ for ( const prop of propsReferences . refs . prop ) {
190+ const isPropsInExpressionRange = utils . inRange ( node . range , prop )
191+ if ( isPropsInExpressionRange ) {
192+ return true
193+ }
194+ }
195+
196+ return false
139197 }
140198
141199 /**
@@ -149,16 +207,12 @@ module.exports = {
149207 let scopeStack = null
150208
151209 /**
152- * @param {Pattern | null } node
210+ * @param {PropId } propId
153211 * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program } scopeNode
154212 * @param {import('eslint').Scope.Scope } currentScope
155213 * @param {string } scopeName
156214 */
157- function processPattern ( node , scopeNode , currentScope , scopeName ) {
158- if ( ! node ) {
159- // no arguments
160- return
161- }
215+ function processPropId ( { node, kind } , scopeNode , currentScope , scopeName ) {
162216 if (
163217 node . type === 'RestElement' ||
164218 node . type === 'AssignmentPattern' ||
@@ -176,7 +230,19 @@ module.exports = {
176230 if ( ! variable ) {
177231 return
178232 }
179- const propsReferenceIds = new Set ( )
233+
234+ let scopePropsReferences = setupScopePropsReferenceIds . get ( scopeNode )
235+ if ( ! scopePropsReferences ) {
236+ scopePropsReferences = {
237+ refs : {
238+ props : new Set ( ) ,
239+ prop : new Set ( )
240+ } ,
241+ scopeName
242+ }
243+ setupScopePropsReferenceIds . set ( scopeNode , scopePropsReferences )
244+ }
245+ const propsReferenceIds = scopePropsReferences . refs [ kind ]
180246 for ( const reference of variable . references ) {
181247 // If reference is in another scope, we can't check it.
182248 if ( reference . from !== currentScope ) {
@@ -189,11 +255,8 @@ module.exports = {
189255
190256 propsReferenceIds . add ( reference . identifier )
191257 }
192- setupScopePropsReferenceIds . set ( scopeNode , {
193- refs : propsReferenceIds ,
194- scopeName
195- } )
196258 }
259+
197260 return utils . compositingVisitors (
198261 {
199262 /**
@@ -287,20 +350,29 @@ module.exports = {
287350 } else if ( target . parent . type === 'AssignmentExpression' ) {
288351 id = target . parent . right === target ? target . parent . left : null
289352 }
353+ if ( ! id ) return
290354 const currentScope = utils . getScope ( context , node )
291- processPattern (
292- id ,
293- context . getSourceCode ( ) . ast ,
294- currentScope ,
295- '<script setup>'
296- )
355+ for ( const propId of iteratePropIds ( id ) ) {
356+ processPropId (
357+ propId ,
358+ context . getSourceCode ( ) . ast ,
359+ currentScope ,
360+ '<script setup>'
361+ )
362+ }
297363 }
298364 } ) ,
299365 utils . defineVueVisitor ( context , {
300366 onSetupFunctionEnter ( node ) {
301367 const currentScope = utils . getScope ( context , node )
302368 const propsParam = utils . skipDefaultParamValue ( node . params [ 0 ] )
303- processPattern ( propsParam , node , currentScope , 'setup()' )
369+ if ( ! propsParam ) return
370+ processPropId (
371+ { node : propsParam , kind : 'props' } ,
372+ node ,
373+ currentScope ,
374+ 'setup()'
375+ )
304376 }
305377 } )
306378 )
0 commit comments