Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

⭐️New: Add no-mutating-props rule. #633

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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