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
+ }
+ ]
+ }
+ ]
+})