Skip to content

Commit 6b4f397

Browse files
committed
Add no-mutating-props rule.
1 parent 6032f21 commit 6b4f397

File tree

6 files changed

+576
-7
lines changed

6 files changed

+576
-7
lines changed

Diff for: docs/rules/no-mutating-props.md

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# disallow mutation of component props (vue/no-mutating-props)
2+
3+
This rule reports mutation of component props.
4+
5+
## Rule Details
6+
7+
:-1: Examples of **incorrect** code for this rule:
8+
9+
```html
10+
<template>
11+
<div>
12+
<input v-model="value" @click="openModal">
13+
</div>
14+
</template>
15+
<script>
16+
export default {
17+
props: {
18+
value: {
19+
type: String,
20+
required: true
21+
}
22+
},
23+
methods: {
24+
openModal() {
25+
this.value = 'test'
26+
}
27+
}
28+
}
29+
</script>
30+
```
31+
32+
:+1: Examples of **correct** code for this rule:
33+
34+
```html
35+
<template>
36+
<div>
37+
<input :value="value" @input="$emit('input', $event.target.value)" @click="openModal">
38+
</div>
39+
</template>
40+
<script>
41+
export default {
42+
props: {
43+
value: {
44+
type: String,
45+
required: true
46+
}
47+
},
48+
methods: {
49+
openModal() {
50+
this.$emit('input', 'test')
51+
}
52+
}
53+
}
54+
</script>
55+
```
56+
57+
## :wrench: Options
58+
59+
Nothing.
60+
61+
## Related links
62+
63+
- [Vue - Prop Mutation - deprecated](https://vuejs.org/v2/guide/migration.html#Prop-Mutation-deprecated)
64+
- [Style guide - Implicit parent-child communication](hhttps://vuejs.org/v2/style-guide/#Implicit-parent-child-communication-use-with-caution)

Diff for: lib/rules/no-mutating-props.js

+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/**
2+
* @fileoverview disallow mutation component props
3+
* @author 2018 Armano
4+
*/
5+
'use strict'
6+
7+
const utils = require('../utils')
8+
9+
// ------------------------------------------------------------------------------
10+
// Rule Definition
11+
// ------------------------------------------------------------------------------
12+
13+
module.exports = {
14+
meta: {
15+
docs: {
16+
description: 'disallow mutation of component props',
17+
category: undefined,
18+
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.3/docs/rules/no-mutating-props.md'
19+
},
20+
fixable: null, // or "code" or "whitespace"
21+
schema: [
22+
// fill in your schema
23+
]
24+
},
25+
26+
create (context) {
27+
let mutatedNodes = []
28+
let props = []
29+
let scope = {
30+
parent: null,
31+
nodes: []
32+
}
33+
34+
function checkForMutations () {
35+
if (mutatedNodes.length > 0) {
36+
for (const prop of props) {
37+
for (const node of mutatedNodes) {
38+
if (prop === node.name) {
39+
context.report({
40+
node: node.node,
41+
message: 'Unexpected mutation of "{{key}}" prop.',
42+
data: {
43+
key: node.name
44+
}
45+
})
46+
}
47+
}
48+
}
49+
}
50+
mutatedNodes = []
51+
}
52+
53+
function isInScope (name) {
54+
return scope.nodes.some(node => node.name === name)
55+
}
56+
57+
function checkExpression (node, expression) {
58+
if (expression[0] === 'this') {
59+
mutatedNodes.push({ name: expression[1], node })
60+
} else {
61+
const name = expression[0]
62+
if (!isInScope(name)) {
63+
mutatedNodes.push({ name, node })
64+
}
65+
}
66+
}
67+
68+
function checkTemplateProperty (node) {
69+
if (node.type === 'MemberExpression') {
70+
checkExpression(node, utils.parseMemberExpression(node))
71+
} else if (node.type === 'Identifier') {
72+
if (!isInScope(node.name)) {
73+
mutatedNodes.push({
74+
name: node.name,
75+
node
76+
})
77+
}
78+
}
79+
}
80+
81+
return Object.assign({},
82+
{
83+
// this.xxx <=|+=|-=>
84+
'AssignmentExpression' (node) {
85+
if (node.left.type !== 'MemberExpression') return
86+
const expression = utils.parseMemberExpression(node.left)
87+
if (expression[0] === 'this') {
88+
mutatedNodes.push({
89+
name: expression[1],
90+
node
91+
})
92+
}
93+
},
94+
// this.xxx <++|-->
95+
'UpdateExpression > MemberExpression' (node) {
96+
const expression = utils.parseMemberExpression(node)
97+
if (expression[0] === 'this') {
98+
mutatedNodes.push({
99+
name: expression[1],
100+
node
101+
})
102+
}
103+
},
104+
// this.xxx.func()
105+
'CallExpression' (node) {
106+
const expression = utils.parseMemberOrCallExpression(node)
107+
const code = expression.join('.').replace(/\.\[/g, '[')
108+
const MUTATION_REGEX = /(this.)((?!(concat|slice|map|filter)\().)[^\)]*((push|pop|shift|unshift|reverse|splice|sort|copyWithin|fill)\()/g
109+
110+
if (MUTATION_REGEX.test(code)) {
111+
if (expression[0] === 'this') {
112+
mutatedNodes.push({
113+
name: expression[1],
114+
node
115+
})
116+
}
117+
}
118+
}
119+
},
120+
utils.executeOnVue(context, (obj) => {
121+
props = utils.getComponentProps(obj)
122+
.filter(cp => cp.key)
123+
.map(cp => utils.getStaticPropertyName(cp.key))
124+
checkForMutations()
125+
}),
126+
127+
utils.defineTemplateBodyVisitor(context, {
128+
VElement (node) {
129+
scope = {
130+
parent: scope,
131+
nodes: scope.nodes.slice() // make copy
132+
}
133+
134+
if (node.variables) {
135+
for (const variable of node.variables) {
136+
scope.nodes.push(variable.id)
137+
}
138+
}
139+
},
140+
'VElement:exit' () {
141+
scope = scope.parent
142+
},
143+
'VExpressionContainer AssignmentExpression' (node) {
144+
checkTemplateProperty(node.left)
145+
},
146+
// this.xxx <++|-->
147+
'VExpressionContainer UpdateExpression' (node) {
148+
checkTemplateProperty(node.argument)
149+
},
150+
// this.xxx.func()
151+
'VExpressionContainer CallExpression' (node) {
152+
const expression = utils.parseMemberOrCallExpression(node)
153+
const code = expression.join('.').replace(/\.\[/g, '[')
154+
const MUTATION_REGEX = /(this.)?((?!(concat|slice|map|filter)\().)[^\)]*((push|pop|shift|unshift|reverse|splice|sort|copyWithin|fill)\()/g
155+
156+
if (MUTATION_REGEX.test(code)) {
157+
checkExpression(node, expression)
158+
}
159+
},
160+
"VAttribute[directive=true][key.name='model'] VExpressionContainer" (node) {
161+
checkTemplateProperty(node.expression)
162+
},
163+
"VElement[name='template']:exit" () {
164+
checkForMutations()
165+
}
166+
})
167+
)
168+
}
169+
}

Diff for: lib/rules/no-side-effects-in-computed-properties.js

+7-4
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ module.exports = {
4242
// this.xxx.func()
4343
'CallExpression' (node) {
4444
const code = utils.parseMemberOrCallExpression(node)
45+
.join('.')
46+
.replace(/\.\[/g, '[')
47+
4548
const MUTATION_REGEX = /(this.)((?!(concat|slice|map|filter)\().)[^\)]*((push|pop|shift|unshift|reverse|splice|sort|copyWithin|fill)\()/g
4649

4750
if (MUTATION_REGEX.test(code)) {
@@ -52,8 +55,8 @@ module.exports = {
5255
utils.executeOnVue(context, (obj) => {
5356
const computedProperties = utils.getComputedProperties(obj)
5457

55-
computedProperties.forEach(cp => {
56-
forbiddenNodes.forEach(node => {
58+
for (const cp of computedProperties) {
59+
for (const node of forbiddenNodes) {
5760
if (
5861
cp.value &&
5962
node.loc.start.line >= cp.value.loc.start.line &&
@@ -65,8 +68,8 @@ module.exports = {
6568
data: { key: cp.key }
6669
})
6770
}
68-
})
69-
})
71+
}
72+
}
7073
})
7174
)
7275
}

Diff for: lib/utils/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -747,7 +747,7 @@ module.exports = {
747747
parsedCallee.push('this')
748748
}
749749

750-
return parsedCallee.reverse().join('.').replace(/\.\[/g, '[')
750+
return parsedCallee.reverse()
751751
},
752752

753753
/**

0 commit comments

Comments
 (0)