44 */
55'use strict'
66
7+ /**
8+ * @typedef {{name?: string, set: Set<string>} } PropsInfo
9+ */
10+
711const utils = require ( '../utils' )
812const { findVariable } = require ( '@eslint-community/eslint-utils' )
913
@@ -84,6 +88,19 @@ function isVmReference(node) {
8488 return false
8589}
8690
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+
87104module . exports = {
88105 meta : {
89106 type : 'suggestion' ,
@@ -94,12 +111,21 @@ module.exports = {
94111 } ,
95112 fixable : null , // or "code" or "whitespace"
96113 schema : [
97- // fill in your schema
114+ {
115+ type : 'object' ,
116+ properties : {
117+ shallowOnly : {
118+ type : 'boolean'
119+ }
120+ } ,
121+ additionalProperties : false
122+ }
98123 ]
99124 } ,
100125 /** @param {RuleContext } context */
101126 create ( context ) {
102- /** @type {Map<ObjectExpression|CallExpression, Set<string>> } */
127+ const { shallowOnly } = parseOptions ( context . options [ 0 ] )
128+ /** @type {Map<ObjectExpression|CallExpression, PropsInfo> } */
103129 const propsMap = new Map ( )
104130 /** @type { { type: 'export' | 'mark' | 'definition', object: ObjectExpression } | { type: 'setup', object: CallExpression } | null } */
105131 let vueObjectData = null
@@ -138,10 +164,11 @@ module.exports = {
138164 /**
139165 * @param {MemberExpression|Identifier } props
140166 * @param {string } name
167+ * @param {boolean } isRootProps
141168 */
142- function verifyMutating ( props , name ) {
169+ function verifyMutating ( props , name , isRootProps = false ) {
143170 const invalid = utils . findMutating ( props )
144- if ( invalid ) {
171+ if ( invalid && isShallowOnlyInvalid ( invalid , isRootProps ) ) {
145172 report ( invalid . node , name )
146173 }
147174 }
@@ -210,6 +237,9 @@ module.exports = {
210237 continue
211238 }
212239 let name
240+ if ( ! isShallowOnlyInvalid ( invalid , path . length === 0 ) ) {
241+ continue
242+ }
213243 if ( path . length === 0 ) {
214244 if ( invalid . pathNodes . length === 0 ) {
215245 continue
@@ -246,26 +276,43 @@ module.exports = {
246276 }
247277 }
248278
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+
249293 return utils . compositingVisitors (
250294 { } ,
251295 utils . defineScriptSetupVisitor ( context , {
252296 onDefinePropsEnter ( node , props ) {
253297 const defineVariableNames = new Set ( extractDefineVariableNames ( ) )
254298
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 )
269316 vueObjectData = {
270317 type : 'setup' ,
271318 object : node
@@ -294,22 +341,25 @@ module.exports = {
294341 target . parent . id ,
295342 [ ]
296343 ) ) {
344+ if ( path . length === 0 ) {
345+ propsInfo . name = prop . name
346+ } else {
347+ propsInfo . set . add ( prop . name )
348+ }
297349 verifyPropVariable ( prop , path )
298- propsSet . add ( prop . name )
299350 }
300351 }
301352 } ) ,
302353 utils . defineVueVisitor ( context , {
303354 onVueObjectEnter ( node ) {
304- propsMap . set (
305- node ,
306- new Set (
355+ propsMap . set ( node , {
356+ set : new Set (
307357 utils
308358 . getComponentPropsFromOptions ( node )
309359 . map ( ( p ) => p . propName )
310360 . filter ( utils . isDef )
311361 )
312- )
362+ } )
313363 } ,
314364 onVueObjectExit ( node , { type } ) {
315365 if (
@@ -359,7 +409,7 @@ module.exports = {
359409 const name = utils . getStaticPropertyName ( mem )
360410 if (
361411 name &&
362- /** @type {Set<string> } */ ( propsMap . get ( vueNode ) ) . has ( name )
412+ /** @type {PropsInfo } */ ( propsMap . get ( vueNode ) ) . set . has ( name )
363413 ) {
364414 verifyMutating ( mem , name )
365415 }
@@ -378,9 +428,9 @@ module.exports = {
378428 const name = utils . getStaticPropertyName ( mem )
379429 if (
380430 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 )
384434 ) {
385435 verifyMutating ( mem , name )
386436 }
@@ -393,14 +443,18 @@ module.exports = {
393443 if ( ! isVmReference ( node ) ) {
394444 return
395445 }
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 )
404458 }
405459 } ,
406460 /** @param {ESNode } node */
@@ -423,28 +477,45 @@ module.exports = {
423477 return
424478 }
425479
480+ const propsInfo = /** @type {PropsInfo } */ (
481+ propsMap . get ( vueObjectData . object )
482+ )
483+
426484 const nodes = utils . getMemberChaining ( node )
427485 const first = nodes [ 0 ]
428486 let name
429487 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+ }
431503 } else if ( first . type === 'ThisExpression' ) {
504+ if ( shallowOnly && nodes . length > 2 ) {
505+ return
506+ }
432507 const mem = nodes [ 1 ]
433508 if ( ! mem ) {
434509 return
435510 }
436511 name = utils . getStaticPropertyName ( mem )
512+ if ( ! name || ! propsInfo . set . has ( name ) ) {
513+ return
514+ }
437515 } else {
438516 return
439517 }
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 )
448519 }
449520 } )
450521 )
0 commit comments