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 new file mode 100644 index 000000000..2c56d9aa2 --- /dev/null +++ b/docs/rules/no-setup-props-destructure.md @@ -0,0 +1,98 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/no-setup-props-destructure +description: disallow destructuring of `props` passed to `setup` +--- +# vue/no-setup-props-destructure +> disallow destructuring of `props` passed to `setup` + +## :book: Rule Details + +This rule reports the destructuring of `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 + +``` + + + +## :wrench: Options + +Nothing. + +## :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/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 new file mode 100644 index 000000000..af7e3754a --- /dev/null +++ b/lib/rules/no-setup-props-destructure.js @@ -0,0 +1,136 @@ +/** + * @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 of `props` passed to `setup`', + category: undefined, + url: 'https://eslint.vuejs.org/rules/no-setup-props-destructure.html' + }, + fixable: null, + schema: [], + messages: { + 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 setupFunctions = new Map() + const forbiddenNodes = new Map() + + function addForbiddenNode (property, node, messageId) { + let list = forbiddenNodes.get(property) + if (!list) { + list = [] + forbiddenNodes.set(property, list) + } + list.push({ + node, + messageId + }) + } + + function verify (left, right, { propsReferenceIds, setupProperty }) { + 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 } + + 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, 'destructuring') + return + } + setupFunctions.set(node.value, { + setupProperty: node, + propsParam: param, + propsReferenceIds: new Set() + }) + }, + ':function' (node) { + scopeStack = { upper: scopeStack, functionNode: node } + }, + ':function>*' (node) { + const setupFunctionData = setupFunctions.get(node.parent) + if (!setupFunctionData || setupFunctionData.propsParam !== node) { + return + } + const variable = findVariable(context.getScope(), node) + if (!variable) { + return + } + const { propsReferenceIds } = setupFunctionData + for (const reference of variable.references) { + if (!reference.isRead()) { + continue + } + + propsReferenceIds.add(reference.identifier) + } + }, + 'VariableDeclarator' (node) { + const setupFunctionData = setupFunctions.get(scopeStack.functionNode) + if (!setupFunctionData) { + return + } + verify(node.id, node.init, setupFunctionData) + }, + 'AssignmentExpression' (node) { + const setupFunctionData = setupFunctions.get(scopeStack.functionNode) + if (!setupFunctionData) { + return + } + verify(node.left, node.right, setupFunctionData) + }, + ':function:exit' (node) { + scopeStack = scopeStack.upper + + setupFunctions.delete(node) + } + }, + utils.executeOnVue(context, obj => { + const reportsList = obj.properties + .map(item => forbiddenNodes.get(item)) + .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 1744009a5..dac43a010 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", "lint": "eslint . --rulesdir eslint-internal-rules", "pretest": "npm run lint", "preversion": "npm test && npm run update && git add .", @@ -47,9 +48,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..2f3bccc51 --- /dev/null +++ b/tests/lib/rules/no-setup-props-destructure.js @@ -0,0 +1,335 @@ +/** + * @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: [ + { + messageId: 'destructuring', + line: 4, + column: 15, + endLine: 4, + endColumn: 24 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'getProperty', + line: 5, + column: 17, + endLine: 5, + endColumn: 26 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'getProperty', + line: 5 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'getProperty', + line: 5 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'getProperty', + line: 5 + }, + { + messageId: 'getProperty', + line: 11 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'getProperty', + line: 5 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'getProperty', + line: 5 + }, + { + messageId: 'getProperty', + line: 6 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'getProperty', + line: 5 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'getProperty', + line: 6 + } + ] + } + ] +})