From 6f490ab1948b23ac2cb2ed9c7fd703ce310987a5 Mon Sep 17 00:00:00 2001 From: Armano Date: Sat, 24 Nov 2018 21:21:52 +0100 Subject: [PATCH 1/4] Add `no-mutating-props` rule. --- docs/rules/no-mutating-props.md | 64 +++++ lib/rules/no-mutating-props.js | 170 ++++++++++++++ lib/utils/index.js | 2 +- tests/lib/rules/no-mutating-props.js | 333 +++++++++++++++++++++++++++ tests/lib/utils/index.js | 4 +- 5 files changed, 570 insertions(+), 3 deletions(-) create mode 100644 docs/rules/no-mutating-props.md create mode 100644 lib/rules/no-mutating-props.js create mode 100644 tests/lib/rules/no-mutating-props.js 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/utils/index.js b/lib/utils/index.js index c31ed0e6e..1b7072989 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -847,7 +847,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 f698a98ef..b64a31425 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']) }) }) From e768ecd259c11e5dcd9c99e51bc67002e49e10ca Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Wed, 20 May 2020 17:59:08 +0900 Subject: [PATCH 2/4] update --- docs/rules/README.md | 1 + docs/rules/no-mutating-props.md | 34 ++- lib/index.js | 1 + lib/rules/no-mutating-props.js | 319 +++++++++++++++++---------- lib/utils/index.js | 23 ++ tests/lib/rules/no-mutating-props.js | 54 ++++- 6 files changed, 304 insertions(+), 128 deletions(-) diff --git a/docs/rules/README.md b/docs/rules/README.md index 83eda5692..58b176f65 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -284,6 +284,7 @@ For example: | [vue/no-boolean-default](./no-boolean-default.md) | disallow boolean defaults | :wrench: | | [vue/no-empty-pattern](./no-empty-pattern.md) | disallow empty destructuring patterns | | | [vue/no-irregular-whitespace](./no-irregular-whitespace.md) | disallow irregular whitespace | | +| [vue/no-mutating-props](./no-mutating-props.md) | disallow mutation of component props | | | [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions | | | [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax | | | [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes | | diff --git a/docs/rules/no-mutating-props.md b/docs/rules/no-mutating-props.md index 7c021a398..38a0df14e 100644 --- a/docs/rules/no-mutating-props.md +++ b/docs/rules/no-mutating-props.md @@ -1,12 +1,20 @@ -# disallow mutation of component props (vue/no-mutating-props) +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/no-mutating-props +description: disallow mutation of component props +--- +# vue/no-mutating-props +> disallow mutation of component props -This rule reports mutation of component props. +## :book: Rule Details -## Rule Details +This rule reports mutation of component props. -:-1: Examples of **incorrect** code for this rule: + -```html +```vue +