Skip to content

Commit e0f7c4a

Browse files
committed
Add propProps option to vue/no-mutating-props (vuejs#1371)
1 parent 6916db0 commit e0f7c4a

File tree

4 files changed

+398
-83
lines changed

4 files changed

+398
-83
lines changed

docs/rules/no-mutating-props.md

+71-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ This rule reports mutation of component props.
2222
<template>
2323
<div>
2424
<input v-model="value" @click="openModal">
25+
<button @click="pushItem">Push Item</button>
26+
<button @click="changeId">Change ID</button>
2527
</div>
2628
</template>
2729
<script>
@@ -30,11 +32,25 @@ This rule reports mutation of component props.
3032
value: {
3133
type: String,
3234
required: true
35+
},
36+
list: {
37+
type: Array,
38+
required: true
39+
},
40+
user: {
41+
type: Object,
42+
required: true
3343
}
3444
},
3545
methods: {
3646
openModal() {
3747
this.value = 'test'
48+
},
49+
pushItem() {
50+
this.list.push(0)
51+
},
52+
changeId() {
53+
this.user.id = 1
3854
}
3955
}
4056
}
@@ -50,6 +66,8 @@ This rule reports mutation of component props.
5066
<template>
5167
<div>
5268
<input :value="value" @input="$emit('input', $event.target.value)" @click="openModal">
69+
<button @click="pushItem">Push Item</button>
70+
<button @click="changeId">Change ID</button>
5371
</div>
5472
</template>
5573
<script>
@@ -58,11 +76,25 @@ This rule reports mutation of component props.
5876
value: {
5977
type: String,
6078
required: true
79+
},
80+
list: {
81+
type: Array,
82+
required: true
83+
},
84+
user: {
85+
type: Object,
86+
required: true
6187
}
6288
},
6389
methods: {
6490
openModal() {
6591
this.$emit('input', 'test')
92+
},
93+
pushItem() {
94+
this.$emit('push', 0)
95+
},
96+
changeId() {
97+
this.$emit('change-id', 1)
6698
}
6799
}
68100
}
@@ -88,7 +120,45 @@ This rule reports mutation of component props.
88120

89121
## :wrench: Options
90122

91-
Nothing.
123+
```json
124+
{
125+
"vue/no-mutating-props": ["error", {
126+
"propProps": true
127+
}]
128+
}
129+
```
130+
131+
- "propProps" (`boolean`) Avoid mutating the value of a prop but leaving the reference the same. Default is `true`.
132+
133+
### "propProps": false
134+
135+
<eslint-code-block :rules="{'vue/no-mutating-props': ['error', {propProps: false}]}">
136+
137+
```vue
138+
<!-- ✓ GOOD -->
139+
<template>
140+
<div>
141+
<input v-model="value.id" @click="openModal">
142+
</div>
143+
</template>
144+
<script>
145+
export default {
146+
props: {
147+
value: {
148+
type: Object,
149+
required: true
150+
}
151+
},
152+
methods: {
153+
openModal() {
154+
this.value.visible = true
155+
}
156+
}
157+
}
158+
</script>
159+
```
160+
161+
</eslint-code-block>
92162

93163
## :books: Further Reading
94164

lib/rules/no-mutating-props.js

+95-47
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
*/
55
'use strict'
66

7+
/**
8+
* @typedef {{name?: string, set: Set<string>}} PropsInfo
9+
*/
10+
711
const utils = require('../utils')
812
const { 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.propProps avoid mutating the value of a prop but leaving the reference the same
94+
*/
95+
function parseOptions(options) {
96+
return Object.assign(
97+
{
98+
propProps: true
99+
},
100+
options
101+
)
102+
}
103+
87104
module.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+
propProps: {
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 { propProps } = 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,9 +164,10 @@ module.exports = {
138164
/**
139165
* @param {MemberExpression|Identifier} props
140166
* @param {string} name
167+
* @param {boolean} isRootProps
141168
*/
142-
function verifyMutating(props, name) {
143-
const invalid = utils.findMutating(props)
169+
function verifyMutating(props, name, isRootProps = false) {
170+
const invalid = utils.findMutating(props, propProps, isRootProps)
144171
if (invalid) {
145172
report(invalid.node, name)
146173
}
@@ -192,8 +219,9 @@ module.exports = {
192219
/**
193220
* @param {Identifier} prop
194221
* @param {string[]} path
222+
* @param {boolean} isRootProps
195223
*/
196-
function verifyPropVariable(prop, path) {
224+
function verifyPropVariable(prop, path, isRootProps = false) {
197225
const variable = findVariable(context.getScope(), prop)
198226
if (!variable) {
199227
return
@@ -205,7 +233,7 @@ module.exports = {
205233
}
206234
const id = reference.identifier
207235

208-
const invalid = utils.findMutating(id)
236+
const invalid = utils.findMutating(id, propProps, isRootProps)
209237
if (!invalid) {
210238
continue
211239
}
@@ -252,20 +280,23 @@ module.exports = {
252280
onDefinePropsEnter(node, props) {
253281
const defineVariableNames = new Set(extractDefineVariableNames())
254282

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)
283+
const propsInfo = {
284+
name: '',
285+
set: new Set(
286+
props
287+
.map((p) => p.propName)
288+
.filter(
289+
/**
290+
* @returns {propName is string}
291+
*/
292+
(propName) =>
293+
utils.isDef(propName) &&
294+
!GLOBALS_WHITE_LISTED.has(propName) &&
295+
!defineVariableNames.has(propName)
296+
)
297+
)
298+
}
299+
propsMap.set(node, propsInfo)
269300
vueObjectData = {
270301
type: 'setup',
271302
object: node
@@ -294,22 +325,25 @@ module.exports = {
294325
target.parent.id,
295326
[]
296327
)) {
297-
verifyPropVariable(prop, path)
298-
propsSet.add(prop.name)
328+
if (path.length === 0) {
329+
propsInfo.name = prop.name
330+
} else {
331+
propsInfo.set.add(prop.name)
332+
}
333+
verifyPropVariable(prop, path, propsInfo.name === prop.name)
299334
}
300335
}
301336
}),
302337
utils.defineVueVisitor(context, {
303338
onVueObjectEnter(node) {
304-
propsMap.set(
305-
node,
306-
new Set(
339+
propsMap.set(node, {
340+
set: new Set(
307341
utils
308342
.getComponentPropsFromOptions(node)
309343
.map((p) => p.propName)
310344
.filter(utils.isDef)
311345
)
312-
)
346+
})
313347
},
314348
onVueObjectExit(node, { type }) {
315349
if (
@@ -341,7 +375,11 @@ module.exports = {
341375
propsParam,
342376
[]
343377
)) {
344-
verifyPropVariable(prop, path)
378+
verifyPropVariable(
379+
prop,
380+
path,
381+
prop.parent.type === 'FunctionExpression'
382+
)
345383
}
346384
},
347385
/** @param {(Identifier | ThisExpression) & { parent: MemberExpression } } node */
@@ -359,7 +397,7 @@ module.exports = {
359397
const name = utils.getStaticPropertyName(mem)
360398
if (
361399
name &&
362-
/** @type {Set<string>} */ (propsMap.get(vueNode)).has(name)
400+
/** @type {PropsInfo} */ (propsMap.get(vueNode)).set.has(name)
363401
) {
364402
verifyMutating(mem, name)
365403
}
@@ -378,9 +416,9 @@ module.exports = {
378416
const name = utils.getStaticPropertyName(mem)
379417
if (
380418
name &&
381-
/** @type {Set<string>} */ (propsMap.get(vueObjectData.object)).has(
382-
name
383-
)
419+
/** @type {PropsInfo} */ (
420+
propsMap.get(vueObjectData.object)
421+
).set.has(name)
384422
) {
385423
verifyMutating(mem, name)
386424
}
@@ -393,14 +431,19 @@ module.exports = {
393431
if (!isVmReference(node)) {
394432
return
395433
}
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)
434+
const propsInfo = /** @type {PropsInfo} */ (
435+
propsMap.get(vueObjectData.object)
436+
)
437+
const isRootProps = !!node.name && propsInfo.name === node.name
438+
const parent = node.parent
439+
const parentProperty =
440+
parent.type === 'MemberExpression' ? parent.property : null
441+
const name =
442+
isRootProps && parentProperty?.type === 'Identifier'
443+
? parentProperty.name
444+
: node.name
445+
if (name && (propsInfo.set.has(name) || isRootProps)) {
446+
verifyMutating(node, name, isRootProps)
404447
}
405448
},
406449
/** @param {ESNode} node */
@@ -423,12 +466,22 @@ module.exports = {
423466
return
424467
}
425468

469+
const propsInfo = /** @type {PropsInfo} */ (
470+
propsMap.get(vueObjectData.object)
471+
)
472+
426473
const nodes = utils.getMemberChaining(node)
427474
const first = nodes[0]
428475
let name
429-
if (isVmReference(first)) {
476+
if (isVmReference(first) && first.name !== propsInfo.name) {
477+
if (!propProps && nodes.length > 1) {
478+
return
479+
}
430480
name = first.name
431-
} else if (first.type === 'ThisExpression') {
481+
} else if (first.type === 'ThisExpression' || isVmReference(first)) {
482+
if (!propProps && nodes.length > 2) {
483+
return
484+
}
432485
const mem = nodes[1]
433486
if (!mem) {
434487
return
@@ -437,12 +490,7 @@ module.exports = {
437490
} else {
438491
return
439492
}
440-
if (
441-
name &&
442-
/** @type {Set<string>} */ (propsMap.get(vueObjectData.object)).has(
443-
name
444-
)
445-
) {
493+
if (name && propsInfo.set.has(name)) {
446494
report(node, name)
447495
}
448496
}

0 commit comments

Comments
 (0)