6
6
const { findVariable } = require ( '@eslint-community/eslint-utils' )
7
7
const utils = require ( '../utils' )
8
8
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
+
9
63
module . exports = {
10
64
meta : {
11
65
type : 'suggestion' ,
@@ -31,7 +85,9 @@ module.exports = {
31
85
create ( context ) {
32
86
/**
33
87
* @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.
35
91
* @property {string } scopeName
36
92
*/
37
93
/** @type {Map<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program, ScopePropsReferences> } */
@@ -72,70 +128,72 @@ module.exports = {
72
128
wrapperExpressionTypes . has ( rightNode . type ) &&
73
129
isPropsMemberAccessed ( rightNode , propsReferences )
74
130
) {
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 )
85
133
return
86
134
}
87
135
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 )
94
141
}
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
+ : [ ]
96
151
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
- }
112
152
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
+ )
117
158
) {
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 )
120
169
}
121
- return false
122
170
}
123
171
124
172
/**
125
- * @param {Expression } node
173
+ * @param {Expression | Super } node
126
174
* @param {ScopePropsReferences } propsReferences
127
175
*/
128
176
function isPropsMemberAccessed ( node , propsReferences ) {
129
- const propRefs = [ ...propsReferences . refs . values ( ) ]
130
-
131
- return propRefs . some ( ( props ) => {
177
+ for ( const props of propsReferences . refs . props ) {
132
178
const isPropsInExpressionRange = utils . inRange ( node . range , props )
133
179
const isPropsMemberExpression =
134
180
props . parent . type === 'MemberExpression' &&
135
181
props . parent . object === props
136
182
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
139
197
}
140
198
141
199
/**
@@ -149,16 +207,12 @@ module.exports = {
149
207
let scopeStack = null
150
208
151
209
/**
152
- * @param {Pattern | null } node
210
+ * @param {PropId } propId
153
211
* @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program } scopeNode
154
212
* @param {import('eslint').Scope.Scope } currentScope
155
213
* @param {string } scopeName
156
214
*/
157
- function processPattern ( node , scopeNode , currentScope , scopeName ) {
158
- if ( ! node ) {
159
- // no arguments
160
- return
161
- }
215
+ function processPropId ( { node, kind } , scopeNode , currentScope , scopeName ) {
162
216
if (
163
217
node . type === 'RestElement' ||
164
218
node . type === 'AssignmentPattern' ||
@@ -176,7 +230,19 @@ module.exports = {
176
230
if ( ! variable ) {
177
231
return
178
232
}
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 ]
180
246
for ( const reference of variable . references ) {
181
247
// If reference is in another scope, we can't check it.
182
248
if ( reference . from !== currentScope ) {
@@ -189,11 +255,8 @@ module.exports = {
189
255
190
256
propsReferenceIds . add ( reference . identifier )
191
257
}
192
- setupScopePropsReferenceIds . set ( scopeNode , {
193
- refs : propsReferenceIds ,
194
- scopeName
195
- } )
196
258
}
259
+
197
260
return utils . compositingVisitors (
198
261
{
199
262
/**
@@ -287,20 +350,29 @@ module.exports = {
287
350
} else if ( target . parent . type === 'AssignmentExpression' ) {
288
351
id = target . parent . right === target ? target . parent . left : null
289
352
}
353
+ if ( ! id ) return
290
354
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
+ }
297
363
}
298
364
} ) ,
299
365
utils . defineVueVisitor ( context , {
300
366
onSetupFunctionEnter ( node ) {
301
367
const currentScope = utils . getScope ( context , node )
302
368
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
+ )
304
376
}
305
377
} )
306
378
)
0 commit comments