Skip to content

Commit 9c6a762

Browse files
committed
Add no-mutating-props rule.
1 parent 1b5a799 commit 9c6a762

File tree

5 files changed

+519
-8
lines changed

5 files changed

+519
-8
lines changed

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

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/**
2+
* @fileoverview Check if component props are not mutated
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 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+
30+
function checkForMutations () {
31+
for (const prop of props) {
32+
const propName = utils.getStaticPropertyName(prop.key)
33+
34+
for (const node of mutatedNodes) {
35+
if (propName === node.name) {
36+
context.report({
37+
node: node.node,
38+
message: 'Unexpected mutation of "{{key}}" prop.',
39+
data: { key: node.name }
40+
})
41+
}
42+
}
43+
}
44+
mutatedNodes = []
45+
}
46+
47+
function checkTemplateProperty (node) {
48+
if (node.type === 'MemberExpression') {
49+
const expression = utils.parseMemberExpression(node)
50+
mutatedNodes.push({
51+
name: expression[0] === 'this' ? expression[1] : expression[0],
52+
node
53+
})
54+
} else if (node.type === 'Identifier') {
55+
mutatedNodes.push({
56+
name: node.name,
57+
node
58+
})
59+
}
60+
}
61+
62+
return Object.assign({},
63+
{
64+
// this.xxx <=|+=|-=>
65+
'AssignmentExpression' (node) {
66+
if (node.left.type !== 'MemberExpression') return
67+
const expression = utils.parseMemberExpression(node.left)
68+
if (expression[0] === 'this') {
69+
mutatedNodes.push({
70+
name: expression[1],
71+
node
72+
})
73+
}
74+
},
75+
// this.xxx <++|-->
76+
'UpdateExpression > MemberExpression' (node) {
77+
const expression = utils.parseMemberExpression(node)
78+
if (expression[0] === 'this') {
79+
mutatedNodes.push({
80+
name: expression[1],
81+
node
82+
})
83+
}
84+
},
85+
// this.xxx.func()
86+
'CallExpression' (node) {
87+
const expression = utils.parseMemberOrCallExpression(node)
88+
const code = expression.join('.').replace(/\.\[/g, '[')
89+
const MUTATION_REGEX = /(this.)((?!(concat|slice|map|filter)\().)[^\)]*((push|pop|shift|unshift|reverse|splice|sort|copyWithin|fill)\()/g
90+
91+
if (MUTATION_REGEX.test(code)) {
92+
if (expression[0] === 'this') {
93+
mutatedNodes.push({
94+
name: expression[1],
95+
node
96+
})
97+
}
98+
}
99+
}
100+
},
101+
utils.executeOnVue(context, (obj) => {
102+
props = utils.getComponentProps(obj)
103+
.filter(cp => cp.key)
104+
checkForMutations()
105+
}),
106+
107+
utils.defineTemplateBodyVisitor(context, {
108+
'VExpressionContainer AssignmentExpression' (node) {
109+
checkTemplateProperty(node.left)
110+
},
111+
// this.xxx <++|-->
112+
'VExpressionContainer UpdateExpression' (node) {
113+
checkTemplateProperty(node.argument)
114+
},
115+
// this.xxx.func()
116+
'VExpressionContainer CallExpression' (node) {
117+
const expression = utils.parseMemberOrCallExpression(node)
118+
const code = expression.join('.').replace(/\.\[/g, '[')
119+
const MUTATION_REGEX = /(this.)?((?!(concat|slice|map|filter)\().)[^\)]*((push|pop|shift|unshift|reverse|splice|sort|copyWithin|fill)\()/g
120+
121+
if (MUTATION_REGEX.test(code)) {
122+
mutatedNodes.push({
123+
name: expression[0] === 'this' ? expression[1] : expression[0],
124+
node
125+
})
126+
}
127+
},
128+
129+
"VAttribute[directive=true][key.name='model'] VExpressionContainer" (node) {
130+
checkTemplateProperty(node.expression)
131+
},
132+
133+
"VElement[name='template']:exit" () {
134+
checkForMutations()
135+
}
136+
})
137+
)
138+
}
139+
}

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

+39-2
Original file line numberDiff line numberDiff line change
@@ -365,9 +365,46 @@ module.exports = {
365365
return null
366366
},
367367

368+
/**
369+
* Get all props by looking at all component's properties
370+
* @param {ObjectExpression} componentObject Object with component definition
371+
* @return {Array} Array of component props in format: [{key?: String, value?: ASTNode, node: ASTNod}]
372+
*/
373+
getComponentProps (componentObject) {
374+
const propsNode = componentObject.properties
375+
.find(p =>
376+
p.type === 'Property' &&
377+
p.key.type === 'Identifier' &&
378+
p.key.name === 'props' &&
379+
(p.value.type === 'ObjectExpression' || p.value.type === 'ArrayExpression')
380+
)
381+
382+
if (!propsNode) {
383+
return []
384+
}
385+
386+
let props
387+
388+
if (propsNode.value.type === 'ObjectExpression') {
389+
props = propsNode.value.properties
390+
.filter(cp => cp.type === 'Property')
391+
.map(cp => {
392+
return { key: cp.key, value: this.unwrapTypes(cp.value), node: cp }
393+
})
394+
} else {
395+
props = propsNode.value.elements
396+
.map(cp => {
397+
const key = cp.type === 'Literal' && typeof cp.value === 'string' ? cp : null
398+
return { key, value: null, node: cp }
399+
})
400+
}
401+
402+
return props
403+
},
404+
368405
/**
369406
* Get all computed properties by looking at all component's properties
370-
* @param {ObjectExpression} Object with component definition
407+
* @param {ObjectExpression} componentObject Object with component definition
371408
* @return {Array} Array of computed properties in format: [{key: String, value: ASTNode}]
372409
*/
373410
getComputedProperties (componentObject) {
@@ -710,7 +747,7 @@ module.exports = {
710747
parsedCallee.push('this')
711748
}
712749

713-
return parsedCallee.reverse().join('.').replace(/\.\[/g, '[')
750+
return parsedCallee.reverse()
714751
},
715752

716753
/**

0 commit comments

Comments
 (0)