From 43e498aa1b173f10c0f7b1aee2bf470b55b18653 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Fri, 6 Mar 2020 12:58:33 +0900 Subject: [PATCH 1/6] Add no-setup-props-destructure rule --- docs/rules/no-setup-props-destructure.md | 94 ++++++ lib/rules/no-setup-props-destructure.js | 134 ++++++++ package.json | 5 +- tests/lib/rules/no-setup-props-destructure.js | 297 ++++++++++++++++++ 4 files changed, 528 insertions(+), 2 deletions(-) create mode 100644 docs/rules/no-setup-props-destructure.md create mode 100644 lib/rules/no-setup-props-destructure.js create mode 100644 tests/lib/rules/no-setup-props-destructure.js diff --git a/docs/rules/no-setup-props-destructure.md b/docs/rules/no-setup-props-destructure.md new file mode 100644 index 000000000..61b57f2f5 --- /dev/null +++ b/docs/rules/no-setup-props-destructure.md @@ -0,0 +1,94 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/no-setup-props-destructure +description: disallow destructuring the `props` passed to `setup` +--- +# vue/no-setup-props-destructure +> disallow destructuring the `props` passed to `setup` + +## :book: Rule Details + +This rule reports the destructuring the `props` passed to `setup` causing the value to lose reactivity. + + + +```vue + +``` + + + +Destructuring the props passed to setup will cause the value to lose reactivity. + + + +```vue + +``` + + + +Also, destructuring in root scope of `setup()` should error, but ok inside nested callbacks or returned render functions: + + + +```vue + +``` + + + +## :books: Further reading + +- [Vue RFCs - 0013-composition-api](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0013-composition-api.md) + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-setup-props-destructure.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-setup-props-destructure.js) diff --git a/lib/rules/no-setup-props-destructure.js b/lib/rules/no-setup-props-destructure.js new file mode 100644 index 000000000..69aeb3823 --- /dev/null +++ b/lib/rules/no-setup-props-destructure.js @@ -0,0 +1,134 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' +const { findVariable } = require('eslint-utils') +const utils = require('../utils') + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'disallow destructuring the `props` passed to `setup`', + category: undefined, + url: 'https://eslint.vuejs.org/rules/no-setup-props-destructure.html' + }, + fixable: null, + schema: [], + messages: { + forbidden: 'Destructuring the `props` will cause the value to lose reactivity.' + } + }, + create (context) { + const forbiddenNodes = new Map() + + function addForbiddenNode (property, node) { + let list = forbiddenNodes.get(property) + if (!list) { + list = [] + forbiddenNodes.set(property, list) + } + list.push(node) + } + + let scopeStack = { upper: null, functionNode: null } + + let setupProperty = null + let setupFunction = null + let propsParam = null + let propsReferenceIds = null + + return Object.assign( + { + 'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node) { + if (utils.getStaticPropertyName(node) !== 'setup') { + return + } + const param = node.value.params[0] + if (!param) { + // no arguments + return + } + if (param.type === 'RestElement') { + // cannot check + return + } + if (param.type === 'ArrayPattern' || param.type === 'ObjectPattern') { + addForbiddenNode(node, param) + return + } + + setupProperty = node + setupFunction = node.value + propsParam = node.value.params[0] + }, + ':function' (node) { + scopeStack = { upper: scopeStack, functionNode: node } + }, + ':function>*' (node) { + if (propsParam !== node) { + return + } + const variable = findVariable(context.getScope(), node) + if (!variable) { + return + } + propsReferenceIds = new Set() + for (const reference of variable.references) { + if (!reference.isRead()) { + continue + } + + propsReferenceIds.add(reference.identifier) + } + }, + 'VariableDeclarator > :matches(ArrayPattern, ObjectPattern)' ( + node + ) { + if (scopeStack.functionNode !== setupFunction) { + return + } + const varNode = node.parent + if (propsReferenceIds.has(varNode.init)) { + addForbiddenNode(setupProperty, node) + } + }, + 'AssignmentExpression > :matches(ArrayPattern, ObjectPattern)' ( + node + ) { + if (scopeStack.functionNode !== setupFunction) { + return + } + const assignNode = node.parent + if (propsReferenceIds.has(assignNode.right)) { + addForbiddenNode(setupProperty, node) + } + }, + ':function:exit' (node) { + scopeStack = scopeStack.upper + + if (setupFunction === node) { + setupProperty = null + setupFunction = null + propsParam = null + propsReferenceIds = null + } + } + }, + utils.executeOnVue(context, obj => { + const nodesList = obj.properties + .map(item => forbiddenNodes.get(item)) + .filter(nodes => !!nodes) + for (const nodes of nodesList) { + for (const node of nodes) { + context.report({ + node, + messageId: 'forbidden' + }) + } + } + }) + ) + } +} diff --git a/package.json b/package.json index 1744009a5..099b182a4 100644 --- a/package.json +++ b/package.json @@ -47,9 +47,10 @@ "eslint": "^5.0.0 || ^6.0.0" }, "dependencies": { + "eslint-utils": "^2.0.0", "natural-compare": "^1.4.0", - "vue-eslint-parser": "^7.0.0", - "semver": "^5.6.0" + "semver": "^5.6.0", + "vue-eslint-parser": "^7.0.0" }, "devDependencies": { "@types/node": "^4.2.16", diff --git a/tests/lib/rules/no-setup-props-destructure.js b/tests/lib/rules/no-setup-props-destructure.js new file mode 100644 index 000000000..a1ccef858 --- /dev/null +++ b/tests/lib/rules/no-setup-props-destructure.js @@ -0,0 +1,297 @@ +/** + * @author Yosuke Ota + */ +'use strict' + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/no-setup-props-destructure') + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { ecmaVersion: 2019, sourceType: 'module' } +}) + +tester.run('no-setup-props-destructure', rule, { + valid: [ + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + 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: 'Destructuring the `props` will cause the value to lose reactivity.', + line: 4, + column: 15, + endLine: 4, + endColumn: 24 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'forbidden', + line: 5, + column: 17, + endLine: 5, + endColumn: 26 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'forbidden', + line: 5 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'forbidden', + line: 5 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'forbidden', + line: 5 + }, + { + messageId: 'forbidden', + line: 11 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'forbidden', + line: 5 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'forbidden', + line: 5 + }, + { + messageId: 'forbidden', + line: 6 + } + ] + } + ] +}) From 2a946dec0c56ac5cf031b4f1c4714764ec3a04c6 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Sat, 7 Mar 2020 13:10:16 +0900 Subject: [PATCH 2/6] update --- docs/rules/README.md | 1 + docs/rules/no-setup-props-destructure.md | 8 ++++---- lib/index.js | 1 + lib/rules/no-setup-props-destructure.js | 2 +- package.json | 1 + 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/rules/README.md b/docs/rules/README.md index ab1f5b220..cdc5f6748 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -162,6 +162,7 @@ For example: | [vue/no-irregular-whitespace](./no-irregular-whitespace.md) | disallow irregular whitespace | | | [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-setup-props-destructure](./no-setup-props-destructure.md) | disallow destructuring of `props` passed to `setup` | | | [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes | | | [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: | | [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: | diff --git a/docs/rules/no-setup-props-destructure.md b/docs/rules/no-setup-props-destructure.md index 61b57f2f5..edc287cc3 100644 --- a/docs/rules/no-setup-props-destructure.md +++ b/docs/rules/no-setup-props-destructure.md @@ -2,14 +2,14 @@ pageClass: rule-details sidebarDepth: 0 title: vue/no-setup-props-destructure -description: disallow destructuring the `props` passed to `setup` +description: disallow destructuring of `props` passed to `setup` --- # vue/no-setup-props-destructure -> disallow destructuring the `props` passed to `setup` +> disallow destructuring of `props` passed to `setup` ## :book: Rule Details -This rule reports the destructuring the `props` passed to `setup` causing the value to lose reactivity. +This rule reports the destructuring of `props` passed to `setup` causing the value to lose reactivity. @@ -32,7 +32,7 @@ export default { -Destructuring the props passed to setup will cause the value to lose reactivity. +Destructuring the `props` passed to `setup` will cause the value to lose reactivity. diff --git a/lib/index.js b/lib/index.js index 02e65ba96..8b2684c3e 100644 --- a/lib/index.js +++ b/lib/index.js @@ -51,6 +51,7 @@ module.exports = { 'no-reserved-component-names': require('./rules/no-reserved-component-names'), 'no-reserved-keys': require('./rules/no-reserved-keys'), 'no-restricted-syntax': require('./rules/no-restricted-syntax'), + 'no-setup-props-destructure': require('./rules/no-setup-props-destructure'), 'no-shared-component-data': require('./rules/no-shared-component-data'), 'no-side-effects-in-computed-properties': require('./rules/no-side-effects-in-computed-properties'), 'no-spaces-around-equal-signs-in-attribute': require('./rules/no-spaces-around-equal-signs-in-attribute'), diff --git a/lib/rules/no-setup-props-destructure.js b/lib/rules/no-setup-props-destructure.js index 69aeb3823..6af8db125 100644 --- a/lib/rules/no-setup-props-destructure.js +++ b/lib/rules/no-setup-props-destructure.js @@ -10,7 +10,7 @@ module.exports = { meta: { type: 'suggestion', docs: { - description: 'disallow destructuring the `props` passed to `setup`', + description: 'disallow destructuring of `props` passed to `setup`', category: undefined, url: 'https://eslint.vuejs.org/rules/no-setup-props-destructure.html' }, diff --git a/package.json b/package.json index 099b182a4..520e6e793 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "test:base": "mocha \"tests/lib/**/*.js\" --reporter dot", "test": "nyc npm run test:base -- \"tests/integrations/*.js\" --timeout 60000", "debug": "mocha --inspect-brk \"tests/lib/**/*.js\" --reporter dot --timeout 60000", + "cover:report": "nyc report --reporter=html && npx opener ./coverage/index.html", "lint": "eslint . --rulesdir eslint-internal-rules", "pretest": "npm run lint", "preversion": "npm test && npm run update && git add .", From 88d45a7628566e41f96fbc594e2f62227a43f6d2 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Sat, 7 Mar 2020 16:25:04 +0900 Subject: [PATCH 3/6] Add testcases --- lib/rules/no-setup-props-destructure.js | 59 +++++++++++-------- package.json | 2 +- tests/lib/rules/no-setup-props-destructure.js | 56 +++++++++++++++--- 3 files changed, 81 insertions(+), 36 deletions(-) diff --git a/lib/rules/no-setup-props-destructure.js b/lib/rules/no-setup-props-destructure.js index 6af8db125..2d1aaa215 100644 --- a/lib/rules/no-setup-props-destructure.js +++ b/lib/rules/no-setup-props-destructure.js @@ -17,19 +17,39 @@ module.exports = { fixable: null, schema: [], messages: { - forbidden: 'Destructuring the `props` will cause the value to lose reactivity.' + destructuring: 'Destructuring the `props` will cause the value to lose reactivity.', + getProperty: 'Getting a value from the `props` in root scope of `setup()` will cause the value to lose reactivity.' } }, create (context) { const forbiddenNodes = new Map() - function addForbiddenNode (property, node) { + function addForbiddenNode (property, node, messageId) { let list = forbiddenNodes.get(property) if (!list) { list = [] forbiddenNodes.set(property, list) } - list.push(node) + list.push({ + node, + messageId + }) + } + + function verify (left, right) { + if (!right) { + return + } + + if (left.type === 'ArrayPattern' || left.type === 'ObjectPattern') { + if (propsReferenceIds.has(right)) { + addForbiddenNode(setupProperty, left, 'getProperty') + } + } else if (left.type === 'Identifier' && right.type === 'MemberExpression') { + if (propsReferenceIds.has(right.object)) { + addForbiddenNode(setupProperty, right, 'getProperty') + } + } } let scopeStack = { upper: null, functionNode: null } @@ -55,7 +75,7 @@ module.exports = { return } if (param.type === 'ArrayPattern' || param.type === 'ObjectPattern') { - addForbiddenNode(node, param) + addForbiddenNode(node, param, 'destructuring') return } @@ -83,27 +103,17 @@ module.exports = { propsReferenceIds.add(reference.identifier) } }, - 'VariableDeclarator > :matches(ArrayPattern, ObjectPattern)' ( - node - ) { + 'VariableDeclarator' (node) { if (scopeStack.functionNode !== setupFunction) { return } - const varNode = node.parent - if (propsReferenceIds.has(varNode.init)) { - addForbiddenNode(setupProperty, node) - } + verify(node.id, node.init) }, - 'AssignmentExpression > :matches(ArrayPattern, ObjectPattern)' ( - node - ) { + 'AssignmentExpression' (node) { if (scopeStack.functionNode !== setupFunction) { return } - const assignNode = node.parent - if (propsReferenceIds.has(assignNode.right)) { - addForbiddenNode(setupProperty, node) - } + verify(node.left, node.right) }, ':function:exit' (node) { scopeStack = scopeStack.upper @@ -117,15 +127,12 @@ module.exports = { } }, utils.executeOnVue(context, obj => { - const nodesList = obj.properties + const reportsList = obj.properties .map(item => forbiddenNodes.get(item)) - .filter(nodes => !!nodes) - for (const nodes of nodesList) { - for (const node of nodes) { - context.report({ - node, - messageId: 'forbidden' - }) + .filter(reports => !!reports) + for (const reports of reportsList) { + for (const report of reports) { + context.report(report) } } }) diff --git a/package.json b/package.json index 520e6e793..dac43a010 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "test:base": "mocha \"tests/lib/**/*.js\" --reporter dot", "test": "nyc npm run test:base -- \"tests/integrations/*.js\" --timeout 60000", "debug": "mocha --inspect-brk \"tests/lib/**/*.js\" --reporter dot --timeout 60000", - "cover:report": "nyc report --reporter=html && npx opener ./coverage/index.html", + "cover:report": "nyc report --reporter=html", "lint": "eslint . --rulesdir eslint-internal-rules", "pretest": "npm run lint", "preversion": "npm test && npm run update && git add .", diff --git a/tests/lib/rules/no-setup-props-destructure.js b/tests/lib/rules/no-setup-props-destructure.js index a1ccef858..2f3bccc51 100644 --- a/tests/lib/rules/no-setup-props-destructure.js +++ b/tests/lib/rules/no-setup-props-destructure.js @@ -118,6 +118,7 @@ tester.run('no-setup-props-destructure', rule, { setup(props) { const {x} = noProps ({y} = noProps) + const z = noProps.z } } @@ -156,7 +157,7 @@ tester.run('no-setup-props-destructure', rule, { `, errors: [ { - message: 'Destructuring the `props` will cause the value to lose reactivity.', + messageId: 'destructuring', line: 4, column: 15, endLine: 4, @@ -177,7 +178,7 @@ tester.run('no-setup-props-destructure', rule, { `, errors: [ { - messageId: 'forbidden', + messageId: 'getProperty', line: 5, column: 17, endLine: 5, @@ -198,7 +199,7 @@ tester.run('no-setup-props-destructure', rule, { `, errors: [ { - messageId: 'forbidden', + messageId: 'getProperty', line: 5 } ] @@ -216,7 +217,7 @@ tester.run('no-setup-props-destructure', rule, { `, errors: [ { - messageId: 'forbidden', + messageId: 'getProperty', line: 5 } ] @@ -240,11 +241,11 @@ tester.run('no-setup-props-destructure', rule, { `, errors: [ { - messageId: 'forbidden', + messageId: 'getProperty', line: 5 }, { - messageId: 'forbidden', + messageId: 'getProperty', line: 11 } ] @@ -265,7 +266,7 @@ tester.run('no-setup-props-destructure', rule, { `, errors: [ { - messageId: 'forbidden', + messageId: 'getProperty', line: 5 } ] @@ -284,11 +285,48 @@ tester.run('no-setup-props-destructure', rule, { `, errors: [ { - messageId: 'forbidden', + messageId: 'getProperty', line: 5 }, { - messageId: 'forbidden', + messageId: 'getProperty', + line: 6 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'getProperty', + line: 5 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'getProperty', line: 6 } ] From 787b9cbb791106a7f3682be29bfb2a7fb64c6363 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Sat, 7 Mar 2020 16:46:14 +0900 Subject: [PATCH 4/6] update --- lib/rules/no-setup-props-destructure.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rules/no-setup-props-destructure.js b/lib/rules/no-setup-props-destructure.js index 2d1aaa215..b0c9dffd2 100644 --- a/lib/rules/no-setup-props-destructure.js +++ b/lib/rules/no-setup-props-destructure.js @@ -81,7 +81,7 @@ module.exports = { setupProperty = node setupFunction = node.value - propsParam = node.value.params[0] + propsParam = param }, ':function' (node) { scopeStack = { upper: scopeStack, functionNode: node } From 783d338432b8df62cc96cfffd77dc601e0f0655f Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Sat, 7 Mar 2020 17:12:14 +0900 Subject: [PATCH 5/6] update --- lib/rules/no-setup-props-destructure.js | 39 +++++++++++-------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/lib/rules/no-setup-props-destructure.js b/lib/rules/no-setup-props-destructure.js index b0c9dffd2..af7e3754a 100644 --- a/lib/rules/no-setup-props-destructure.js +++ b/lib/rules/no-setup-props-destructure.js @@ -22,6 +22,7 @@ module.exports = { } }, create (context) { + const setupFunctions = new Map() const forbiddenNodes = new Map() function addForbiddenNode (property, node, messageId) { @@ -36,7 +37,7 @@ module.exports = { }) } - function verify (left, right) { + function verify (left, right, { propsReferenceIds, setupProperty }) { if (!right) { return } @@ -54,11 +55,6 @@ module.exports = { let scopeStack = { upper: null, functionNode: null } - let setupProperty = null - let setupFunction = null - let propsParam = null - let propsReferenceIds = null - return Object.assign( { 'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node) { @@ -78,23 +74,25 @@ module.exports = { addForbiddenNode(node, param, 'destructuring') return } - - setupProperty = node - setupFunction = node.value - propsParam = param + setupFunctions.set(node.value, { + setupProperty: node, + propsParam: param, + propsReferenceIds: new Set() + }) }, ':function' (node) { scopeStack = { upper: scopeStack, functionNode: node } }, ':function>*' (node) { - if (propsParam !== node) { + const setupFunctionData = setupFunctions.get(node.parent) + if (!setupFunctionData || setupFunctionData.propsParam !== node) { return } const variable = findVariable(context.getScope(), node) if (!variable) { return } - propsReferenceIds = new Set() + const { propsReferenceIds } = setupFunctionData for (const reference of variable.references) { if (!reference.isRead()) { continue @@ -104,26 +102,23 @@ module.exports = { } }, 'VariableDeclarator' (node) { - if (scopeStack.functionNode !== setupFunction) { + const setupFunctionData = setupFunctions.get(scopeStack.functionNode) + if (!setupFunctionData) { return } - verify(node.id, node.init) + verify(node.id, node.init, setupFunctionData) }, 'AssignmentExpression' (node) { - if (scopeStack.functionNode !== setupFunction) { + const setupFunctionData = setupFunctions.get(scopeStack.functionNode) + if (!setupFunctionData) { return } - verify(node.left, node.right) + verify(node.left, node.right, setupFunctionData) }, ':function:exit' (node) { scopeStack = scopeStack.upper - if (setupFunction === node) { - setupProperty = null - setupFunction = null - propsParam = null - propsReferenceIds = null - } + setupFunctions.delete(node) } }, utils.executeOnVue(context, obj => { From 7a61ed6030a4537d0d1ae512a6fce8513892b347 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Sat, 7 Mar 2020 17:44:12 +0900 Subject: [PATCH 6/6] update doc --- docs/rules/no-setup-props-destructure.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/rules/no-setup-props-destructure.md b/docs/rules/no-setup-props-destructure.md index edc287cc3..2c56d9aa2 100644 --- a/docs/rules/no-setup-props-destructure.md +++ b/docs/rules/no-setup-props-destructure.md @@ -84,6 +84,10 @@ export default { +## :wrench: Options + +Nothing. + ## :books: Further reading - [Vue RFCs - 0013-composition-api](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0013-composition-api.md)