4
4
*/
5
5
'use strict'
6
6
7
+ /**
8
+ * @typedef {{name?: string, set: Set<string>} } PropsInfo
9
+ */
10
+
7
11
const utils = require ( '../utils' )
8
12
const { findVariable } = require ( '@eslint-community/eslint-utils' )
9
13
@@ -84,6 +88,19 @@ function isVmReference(node) {
84
88
return false
85
89
}
86
90
91
+ /**
92
+ * @param { object } options
93
+ * @param { boolean } options.shallowOnly Enables mutating the value of a prop but leaving the reference the same
94
+ */
95
+ function parseOptions ( options ) {
96
+ return Object . assign (
97
+ {
98
+ shallowOnly : false
99
+ } ,
100
+ options
101
+ )
102
+ }
103
+
87
104
module . exports = {
88
105
meta : {
89
106
type : 'suggestion' ,
@@ -94,12 +111,21 @@ module.exports = {
94
111
} ,
95
112
fixable : null , // or "code" or "whitespace"
96
113
schema : [
97
- // fill in your schema
114
+ {
115
+ type : 'object' ,
116
+ properties : {
117
+ shallowOnly : {
118
+ type : 'boolean'
119
+ }
120
+ } ,
121
+ additionalProperties : false
122
+ }
98
123
]
99
124
} ,
100
125
/** @param {RuleContext } context */
101
126
create ( context ) {
102
- /** @type {Map<ObjectExpression|CallExpression, Set<string>> } */
127
+ const { shallowOnly } = parseOptions ( context . options [ 0 ] )
128
+ /** @type {Map<ObjectExpression|CallExpression, PropsInfo> } */
103
129
const propsMap = new Map ( )
104
130
/** @type { { type: 'export' | 'mark' | 'definition', object: ObjectExpression } | { type: 'setup', object: CallExpression } | null } */
105
131
let vueObjectData = null
@@ -138,10 +164,11 @@ module.exports = {
138
164
/**
139
165
* @param {MemberExpression|Identifier } props
140
166
* @param {string } name
167
+ * @param {boolean } isRootProps
141
168
*/
142
- function verifyMutating ( props , name ) {
169
+ function verifyMutating ( props , name , isRootProps = false ) {
143
170
const invalid = utils . findMutating ( props )
144
- if ( invalid ) {
171
+ if ( invalid && isShallowOnlyInvalid ( invalid , isRootProps ) ) {
145
172
report ( invalid . node , name )
146
173
}
147
174
}
@@ -210,6 +237,9 @@ module.exports = {
210
237
continue
211
238
}
212
239
let name
240
+ if ( ! isShallowOnlyInvalid ( invalid , path . length === 0 ) ) {
241
+ continue
242
+ }
213
243
if ( path . length === 0 ) {
214
244
if ( invalid . pathNodes . length === 0 ) {
215
245
continue
@@ -246,26 +276,43 @@ module.exports = {
246
276
}
247
277
}
248
278
279
+ /**
280
+ * Is shallowOnly false or the prop reassigned
281
+ * @param {Exclude<ReturnType<typeof utils.findMutating>, null> } invalid
282
+ * @param {boolean } isRootProps
283
+ * @return {boolean }
284
+ */
285
+ function isShallowOnlyInvalid ( invalid , isRootProps ) {
286
+ return (
287
+ ! shallowOnly ||
288
+ ( invalid . pathNodes . length === ( isRootProps ? 1 : 0 ) &&
289
+ [ 'assignment' , 'update' ] . includes ( invalid . kind ) )
290
+ )
291
+ }
292
+
249
293
return utils . compositingVisitors (
250
294
{ } ,
251
295
utils . defineScriptSetupVisitor ( context , {
252
296
onDefinePropsEnter ( node , props ) {
253
297
const defineVariableNames = new Set ( extractDefineVariableNames ( ) )
254
298
255
- const propsSet = new Set (
256
- props
257
- . map ( ( p ) => p . propName )
258
- . filter (
259
- /**
260
- * @returns {propName is string }
261
- */
262
- ( propName ) =>
263
- utils . isDef ( propName ) &&
264
- ! GLOBALS_WHITE_LISTED . has ( propName ) &&
265
- ! defineVariableNames . has ( propName )
266
- )
267
- )
268
- propsMap . set ( node , propsSet )
299
+ const propsInfo = {
300
+ name : '' ,
301
+ set : new Set (
302
+ props
303
+ . map ( ( p ) => p . propName )
304
+ . filter (
305
+ /**
306
+ * @returns {propName is string }
307
+ */
308
+ ( propName ) =>
309
+ utils . isDef ( propName ) &&
310
+ ! GLOBALS_WHITE_LISTED . has ( propName ) &&
311
+ ! defineVariableNames . has ( propName )
312
+ )
313
+ )
314
+ }
315
+ propsMap . set ( node , propsInfo )
269
316
vueObjectData = {
270
317
type : 'setup' ,
271
318
object : node
@@ -294,22 +341,25 @@ module.exports = {
294
341
target . parent . id ,
295
342
[ ]
296
343
) ) {
344
+ if ( path . length === 0 ) {
345
+ propsInfo . name = prop . name
346
+ } else {
347
+ propsInfo . set . add ( prop . name )
348
+ }
297
349
verifyPropVariable ( prop , path )
298
- propsSet . add ( prop . name )
299
350
}
300
351
}
301
352
} ) ,
302
353
utils . defineVueVisitor ( context , {
303
354
onVueObjectEnter ( node ) {
304
- propsMap . set (
305
- node ,
306
- new Set (
355
+ propsMap . set ( node , {
356
+ set : new Set (
307
357
utils
308
358
. getComponentPropsFromOptions ( node )
309
359
. map ( ( p ) => p . propName )
310
360
. filter ( utils . isDef )
311
361
)
312
- )
362
+ } )
313
363
} ,
314
364
onVueObjectExit ( node , { type } ) {
315
365
if (
@@ -359,7 +409,7 @@ module.exports = {
359
409
const name = utils . getStaticPropertyName ( mem )
360
410
if (
361
411
name &&
362
- /** @type {Set<string> } */ ( propsMap . get ( vueNode ) ) . has ( name )
412
+ /** @type {PropsInfo } */ ( propsMap . get ( vueNode ) ) . set . has ( name )
363
413
) {
364
414
verifyMutating ( mem , name )
365
415
}
@@ -378,9 +428,9 @@ module.exports = {
378
428
const name = utils . getStaticPropertyName ( mem )
379
429
if (
380
430
name &&
381
- /** @type {Set<string> } */ ( propsMap . get ( vueObjectData . object ) ) . has (
382
- name
383
- )
431
+ /** @type {PropsInfo } */ (
432
+ propsMap . get ( vueObjectData . object )
433
+ ) . set . has ( name )
384
434
) {
385
435
verifyMutating ( mem , name )
386
436
}
@@ -393,14 +443,18 @@ module.exports = {
393
443
if ( ! isVmReference ( node ) ) {
394
444
return
395
445
}
396
- const name = node . name
397
- if (
398
- name &&
399
- /** @type {Set<string> } */ ( propsMap . get ( vueObjectData . object ) ) . has (
400
- name
401
- )
402
- ) {
403
- verifyMutating ( node , name )
446
+ const propsInfo = /** @type {PropsInfo } */ (
447
+ propsMap . get ( vueObjectData . object )
448
+ )
449
+ const isRootProps = ! ! node . name && propsInfo . name === node . name
450
+ const parent = node . parent
451
+ const name =
452
+ ( isRootProps &&
453
+ parent . type === 'MemberExpression' &&
454
+ utils . getStaticPropertyName ( parent ) ) ||
455
+ node . name
456
+ if ( name && ( propsInfo . set . has ( name ) || isRootProps ) ) {
457
+ verifyMutating ( node , name , isRootProps )
404
458
}
405
459
} ,
406
460
/** @param {ESNode } node */
@@ -423,28 +477,45 @@ module.exports = {
423
477
return
424
478
}
425
479
480
+ const propsInfo = /** @type {PropsInfo } */ (
481
+ propsMap . get ( vueObjectData . object )
482
+ )
483
+
426
484
const nodes = utils . getMemberChaining ( node )
427
485
const first = nodes [ 0 ]
428
486
let name
429
487
if ( isVmReference ( first ) ) {
430
- name = first . name
488
+ if ( first . name === propsInfo . name ) {
489
+ // props variable
490
+ if ( shallowOnly && nodes . length > 2 ) {
491
+ return
492
+ }
493
+ name = ( nodes [ 1 ] && getPropertyNameText ( nodes [ 1 ] ) ) || first . name
494
+ } else {
495
+ if ( shallowOnly && nodes . length > 1 ) {
496
+ return
497
+ }
498
+ name = first . name
499
+ if ( ! name || ! propsInfo . set . has ( name ) ) {
500
+ return
501
+ }
502
+ }
431
503
} else if ( first . type === 'ThisExpression' ) {
504
+ if ( shallowOnly && nodes . length > 2 ) {
505
+ return
506
+ }
432
507
const mem = nodes [ 1 ]
433
508
if ( ! mem ) {
434
509
return
435
510
}
436
511
name = utils . getStaticPropertyName ( mem )
512
+ if ( ! name || ! propsInfo . set . has ( name ) ) {
513
+ return
514
+ }
437
515
} else {
438
516
return
439
517
}
440
- if (
441
- name &&
442
- /** @type {Set<string> } */ ( propsMap . get ( vueObjectData . object ) ) . has (
443
- name
444
- )
445
- ) {
446
- report ( node , name )
447
- }
518
+ report ( node , name )
448
519
}
449
520
} )
450
521
)
0 commit comments