diff --git a/docs/rules/no-mutating-props.md b/docs/rules/no-mutating-props.md
new file mode 100644
index 000000000..7c021a398
--- /dev/null
+++ b/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
+
+
+
+
+
+
+```
+
+:+1: Examples of **correct** code for this rule:
+
+```html
+
+
+
+
+
+
+```
+
+## :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)
diff --git a/lib/rules/no-mutating-props.js b/lib/rules/no-mutating-props.js
new file mode 100644
index 000000000..726f5abca
--- /dev/null
+++ b/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()
+ }
+ })
+ )
+ }
+}
diff --git a/lib/rules/no-side-effects-in-computed-properties.js b/lib/rules/no-side-effects-in-computed-properties.js
index 627e9ab4d..d68ced413 100644
--- a/lib/rules/no-side-effects-in-computed-properties.js
+++ b/lib/rules/no-side-effects-in-computed-properties.js
@@ -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)) {
@@ -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 &&
@@ -66,8 +69,8 @@ module.exports = {
data: { key: cp.key }
})
}
- })
- })
+ }
+ }
})
)
}
diff --git a/lib/utils/index.js b/lib/utils/index.js
index cf58a0fce..665aac88f 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -782,7 +782,7 @@ module.exports = {
parsedCallee.push('this')
}
- return parsedCallee.reverse().join('.').replace(/\.\[/g, '[')
+ return parsedCallee.reverse()
},
/**
diff --git a/tests/lib/rules/no-mutating-props.js b/tests/lib/rules/no-mutating-props.js
new file mode 100644
index 000000000..3b141d6c3
--- /dev/null
+++ b/tests/lib/rules/no-mutating-props.js
@@ -0,0 +1,333 @@
+/**
+ * @fileoverview disallow mutation of component props
+ * @author 2018 Armano
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/no-mutating-props')
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({
+ parser: 'vue-eslint-parser',
+ parserOptions: {
+ ecmaVersion: 2018,
+ sourceType: 'module'
+ }
+})
+
+ruleTester.run('no-mutating-props', rule, {
+
+ valid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ }
+ ],
+
+ invalid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `,
+ errors: [
+ {
+ message: 'Unexpected mutation of "prop1" prop.',
+ line: 4
+ },
+ {
+ message: 'Unexpected mutation of "prop2" prop.',
+ line: 5
+ },
+ {
+ message: 'Unexpected mutation of "prop3" prop.',
+ line: 6
+ },
+ {
+ message: 'Unexpected mutation of "prop4" prop.',
+ line: 7
+ },
+ {
+ message: 'Unexpected mutation of "prop5" prop.',
+ line: 8
+ },
+ {
+ message: 'Unexpected mutation of "prop6" prop.',
+ line: 9
+ },
+ {
+ message: 'Unexpected mutation of "prop7" prop.',
+ line: 10
+ },
+ {
+ message: 'Unexpected mutation of "prop8" prop.',
+ line: 11
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+
+
+
+
+ `,
+ errors: [
+ {
+ message: 'Unexpected mutation of "prop1" prop.',
+ line: 4
+ },
+ {
+ message: 'Unexpected mutation of "prop2" prop.',
+ line: 5
+ },
+ {
+ message: 'Unexpected mutation of "prop3" prop.',
+ line: 6
+ },
+ {
+ message: 'Unexpected mutation of "prop4" prop.',
+ line: 7
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ message: 'Unexpected mutation of "items" prop.',
+ line: 16
+ },
+ {
+ message: 'Unexpected mutation of "todo" prop.',
+ line: 17
+ },
+ {
+ message: 'Unexpected mutation of "items" prop.',
+ line: 18
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ errors: [
+ {
+ message: 'Unexpected mutation of "prop" prop.',
+ line: 8
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `,
+ errors: [
+ {
+ message: 'Unexpected mutation of "prop" prop.',
+ line: 5
+ },
+ {
+ message: 'Unexpected mutation of "prop" prop.',
+ line: 7
+ }
+ ]
+ }
+ ]
+})
diff --git a/tests/lib/utils/index.js b/tests/lib/utils/index.js
index 0b8c07e2d..e17536dcc 100644
--- a/tests/lib/utils/index.js
+++ b/tests/lib/utils/index.js
@@ -199,13 +199,13 @@ describe('parseMemberOrCallExpression', () => {
it('should parse CallExpression', () => {
node = parse(`const test = this.lorem['ipsum'].map(d => d.id).filter((a, b) => a > b).reduce((acc, d) => acc + d, 0)`)
const parsed = utils.parseMemberOrCallExpression(node)
- assert.equal(parsed, 'this.lorem[].map().filter().reduce()')
+ assert.deepEqual(parsed, ['this', 'lorem', '[]', 'map()', 'filter()', 'reduce()'])
})
it('should parse MemberExpression', () => {
node = parse(`const test = this.lorem['ipsum'][0].map(d => d.id).dolor.reduce((acc, d) => acc + d, 0).sit`)
const parsed = utils.parseMemberOrCallExpression(node)
- assert.equal(parsed, 'this.lorem[][].map().dolor.reduce().sit')
+ assert.deepEqual(parsed, ['this', 'lorem', '[]', '[]', 'map()', 'dolor', 'reduce()', 'sit'])
})
})