Skip to content

Commit

Permalink
Add no-mutating-props rule.
Browse files Browse the repository at this point in the history
  • Loading branch information
armano2 committed Nov 7, 2018
1 parent 6d257cb commit 834c027
Show file tree
Hide file tree
Showing 5 changed files with 380 additions and 7 deletions.
139 changes: 139 additions & 0 deletions lib/rules/no-mutating-props.js
@@ -0,0 +1,139 @@
/**
* @fileoverview Check if component props are not mutated
* @author 2018 Armano
*/
'use strict'

const utils = require('../utils')

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

module.exports = {
meta: {
docs: {
description: 'disallow mutation of props',
category: undefined,
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.3/docs/rules/no-mutating-props.md'
},
fixable: null, // or "code" or "whitespace"
schema: [
// fill in your schema
]
},

create (context) {
let mutatedNodes = []
let props = []

function checkForMutations () {
for (const prop of props) {
const propName = utils.getStaticPropertyName(prop.key)

for (const node of mutatedNodes) {
if (propName === node.name) {
context.report({
node: node.node,
message: 'Unexpected mutation of "{{key}}" prop.',
data: { key: node.name }
})
}
}
}
mutatedNodes = []
}

function checkTemplateProperty (node) {
if (node.type === 'MemberExpression') {
const expression = utils.parseMemberExpression(node)
mutatedNodes.push({
name: expression[0] === 'this' ? expression[1] : expression[0],
node
})
} else if (node.type === 'Identifier') {
mutatedNodes.push({
name: node.name,
node
})
}
}

return Object.assign({},
{
// this.xxx <=|+=|-=>
'AssignmentExpression' (node) {
if (node.left.type !== 'MemberExpression') return
const expression = utils.parseMemberExpression(node.left)
if (expression[0] === 'this') {
mutatedNodes.push({
name: expression[1],
node
})
}
},
// this.xxx <++|-->
'UpdateExpression > MemberExpression' (node) {
const expression = utils.parseMemberExpression(node)
if (expression[0] === 'this') {
mutatedNodes.push({
name: expression[1],
node
})
}
},
// this.xxx.func()
'CallExpression' (node) {
const expression = utils.parseMemberOrCallExpression(node)
const code = expression.join('.').replace(/\.\[/g, '[')
const MUTATION_REGEX = /(this.)((?!(concat|slice|map|filter)\().)[^\)]*((push|pop|shift|unshift|reverse|splice|sort|copyWithin|fill)\()/g

if (MUTATION_REGEX.test(code)) {
if (expression[0] === 'this') {
mutatedNodes.push({
name: expression[1],
node
})
}
}
}
},
utils.executeOnVue(context, (obj) => {
props = utils.getComponentProps(obj)
.filter(cp => cp.key)
checkForMutations()
}),

utils.defineTemplateBodyVisitor(context, {
'VExpressionContainer AssignmentExpression' (node) {
checkTemplateProperty(node.left)
},
// this.xxx <++|-->
'VExpressionContainer UpdateExpression' (node) {
checkTemplateProperty(node.argument)
},
// this.xxx.func()
'VExpressionContainer CallExpression' (node) {
const expression = utils.parseMemberOrCallExpression(node)
const code = expression.join('.').replace(/\.\[/g, '[')
const MUTATION_REGEX = /(this.)?((?!(concat|slice|map|filter)\().)[^\)]*((push|pop|shift|unshift|reverse|splice|sort|copyWithin|fill)\()/g

if (MUTATION_REGEX.test(code)) {
mutatedNodes.push({
name: expression[0] === 'this' ? expression[1] : expression[0],
node
})
}
},

"VAttribute[directive=true][key.name='model'] VExpressionContainer" (node) {
checkTemplateProperty(node.expression)
},

"VElement[name='template']:exit" () {
checkForMutations()
}
})
)
}
}
11 changes: 7 additions & 4 deletions lib/rules/no-side-effects-in-computed-properties.js
Expand Up @@ -42,6 +42,9 @@ module.exports = {
// this.xxx.func()
'CallExpression' (node) {
const code = utils.parseMemberOrCallExpression(node)
.join('.')
.replace(/\.\[/g, '[')

const MUTATION_REGEX = /(this.)((?!(concat|slice|map|filter)\().)[^\)]*((push|pop|shift|unshift|reverse|splice|sort|copyWithin|fill)\()/g

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

computedProperties.forEach(cp => {
forbiddenNodes.forEach(node => {
for (const cp of computedProperties) {
for (const node of forbiddenNodes) {
if (
cp.value &&
node.loc.start.line >= cp.value.loc.start.line &&
Expand All @@ -65,8 +68,8 @@ module.exports = {
data: { key: cp.key }
})
}
})
})
}
}
})
)
}
Expand Down
2 changes: 1 addition & 1 deletion lib/utils/index.js
Expand Up @@ -747,7 +747,7 @@ module.exports = {
parsedCallee.push('this')
}

return parsedCallee.reverse().join('.').replace(/\.\[/g, '[')
return parsedCallee.reverse()
},

/**
Expand Down

0 comments on commit 834c027

Please sign in to comment.