Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 3cbb1b3

Browse files
FlareZhota-meshi
andauthored
Add shallowOnly option to vue/no-mutating-props (#2135)
Co-authored-by: Yosuke Ota <otameshiyo23@gmail.com>
1 parent d58fb19 commit 3cbb1b3

File tree

3 files changed

+416
-47
lines changed

3 files changed

+416
-47
lines changed

‎docs/rules/no-mutating-props.md

Lines changed: 71 additions & 1 deletion
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+
"shallowOnly": false
127+
}]
128+
}
129+
```
130+
131+
- "shallowOnly" (`boolean`) Enables mutating the value of a prop but leaving the reference the same. Default is `false`.
132+
133+
### "shallowOnly": true
134+
135+
<eslint-code-block :rules="{'vue/no-mutating-props': ['error', {shallowOnly: true}]}">
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

Lines changed: 115 additions & 44 deletions
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.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+
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+
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

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /