Skip to content

Commit bd6ddf1

Browse files
committed
Add no-mutating-props rule.
1 parent c6bbd95 commit bd6ddf1

File tree

6 files changed

+577
-7
lines changed

6 files changed

+577
-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](https://vuejs.org/v2/style-guide/#Implicit-parent-child-communication-use-with-caution)

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

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

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

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

4851
if (MUTATION_REGEX.test(code)) {
@@ -53,8 +56,8 @@ module.exports = {
5356
utils.executeOnVue(context, (obj) => {
5457
const computedProperties = utils.getComputedProperties(obj)
5558

56-
computedProperties.forEach(cp => {
57-
forbiddenNodes.forEach(node => {
59+
for (const cp of computedProperties) {
60+
for (const node of forbiddenNodes) {
5861
if (
5962
cp.value &&
6063
node.loc.start.line >= cp.value.loc.start.line &&
@@ -66,8 +69,8 @@ module.exports = {
6669
data: { key: cp.key }
6770
})
6871
}
69-
})
70-
})
72+
}
73+
}
7174
})
7275
)
7376
}

Diff for: lib/utils/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -782,7 +782,7 @@ module.exports = {
782782
parsedCallee.push('this')
783783
}
784784

785-
return parsedCallee.reverse().join('.').replace(/\.\[/g, '[')
785+
return parsedCallee.reverse()
786786
},
787787

788788
/**

0 commit comments

Comments
 (0)