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 24, 2018
1 parent c6bbd95 commit bd6ddf1
Show file tree
Hide file tree
Showing 6 changed files with 577 additions and 7 deletions.
64 changes: 64 additions & 0 deletions docs/rules/no-mutating-props.md
@@ -0,0 +1,64 @@
# disallow mutation of component props (vue/no-mutating-props)

This rule reports mutation of component props.

## Rule Details

:-1: Examples of **incorrect** code for this rule:

```html
<template>
<div>
<input v-model="value" @click="openModal">
</div>
</template>
<script>
export default {
props: {
value: {
type: String,
required: true
}
},
methods: {
openModal() {
this.value = 'test'
}
}
}
</script>
```

:+1: Examples of **correct** code for this rule:

```html
<template>
<div>
<input :value="value" @input="$emit('input', $event.target.value)" @click="openModal">
</div>
</template>
<script>
export default {
props: {
value: {
type: String,
required: true
}
},
methods: {
openModal() {
this.$emit('input', 'test')
}
}
}
</script>
```

## :wrench: Options

Nothing.

## Related links

- [Vue - Prop Mutation - deprecated](https://vuejs.org/v2/guide/migration.html#Prop-Mutation-deprecated)
- [Style guide - Implicit parent-child communication](https://vuejs.org/v2/style-guide/#Implicit-parent-child-communication-use-with-caution)
170 changes: 170 additions & 0 deletions lib/rules/no-mutating-props.js
@@ -0,0 +1,170 @@
/**
* @fileoverview disallow mutation component props
* @author 2018 Armano
*/
'use strict'

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

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

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

create (context) {
let mutatedNodes = []
let props = []
let scope = {
parent: null,
nodes: []
}

function checkForMutations () {
if (mutatedNodes.length > 0) {
for (const prop of props) {
for (const node of mutatedNodes) {
if (prop === node.name) {
context.report({
node: node.node,
message: 'Unexpected mutation of "{{key}}" prop.',
data: {
key: node.name
}
})
}
}
}
}
mutatedNodes = []
}

function isInScope (name) {
return scope.nodes.some(node => node.name === name)
}

function checkExpression (node, expression) {
if (expression[0] === 'this') {
mutatedNodes.push({ name: expression[1], node })
} else {
const name = expression[0]
if (!isInScope(name)) {
mutatedNodes.push({ name, node })
}
}
}

function checkTemplateProperty (node) {
if (node.type === 'MemberExpression') {
checkExpression(node, utils.parseMemberExpression(node))
} else if (node.type === 'Identifier') {
if (!isInScope(node.name)) {
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)
.map(cp => utils.getStaticPropertyName(cp.key))
checkForMutations()
}),

utils.defineTemplateBodyVisitor(context, {
VElement (node) {
scope = {
parent: scope,
nodes: scope.nodes.slice() // make copy
}

if (node.variables) {
for (const variable of node.variables) {
scope.nodes.push(variable.id)
}
}
},
'VElement:exit' () {
scope = scope.parent
},
'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)) {
checkExpression(node, expression)
}
},
"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 @@ -43,6 +43,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 @@ -53,8 +56,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 @@ -66,8 +69,8 @@ module.exports = {
data: { key: cp.key }
})
}
})
})
}
}
})
)
}
Expand Down
2 changes: 1 addition & 1 deletion lib/utils/index.js
Expand Up @@ -782,7 +782,7 @@ module.exports = {
parsedCallee.push('this')
}

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

/**
Expand Down

0 comments on commit bd6ddf1

Please sign in to comment.