Skip to content

Commit 2c92d3d

Browse files
authored
New: Add vue/no-setup-props-destructure rule (#1066)
* Add no-setup-props-destructure rule * update * Add testcases * update * update * update doc
1 parent 11c9a94 commit 2c92d3d

File tree

6 files changed

+572
-0
lines changed

6 files changed

+572
-0
lines changed

docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ For example:
170170
| [vue/no-ref-as-operand](./no-ref-as-operand.md) | disallow use of value wrapped by `ref()` (Composition API) as an operand | |
171171
| [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions | |
172172
| [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax | |
173+
| [vue/no-setup-props-destructure](./no-setup-props-destructure.md) | disallow destructuring of `props` passed to `setup` | |
173174
| [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes | |
174175
| [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: |
175176
| [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: |
+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/no-setup-props-destructure
5+
description: disallow destructuring of `props` passed to `setup`
6+
---
7+
# vue/no-setup-props-destructure
8+
> disallow destructuring of `props` passed to `setup`
9+
10+
## :book: Rule Details
11+
12+
This rule reports the destructuring of `props` passed to `setup` causing the value to lose reactivity.
13+
14+
<eslint-code-block :rules="{'vue/no-setup-props-destructure': ['error']}">
15+
16+
```vue
17+
<script>
18+
export default {
19+
/* ✓ GOOD */
20+
setup(props) {
21+
watch(() => {
22+
console.log(props.count)
23+
})
24+
25+
return () => {
26+
return h('div', props.count)
27+
}
28+
}
29+
}
30+
</script>
31+
```
32+
33+
</eslint-code-block>
34+
35+
Destructuring the `props` passed to `setup` will cause the value to lose reactivity.
36+
37+
<eslint-code-block :rules="{'vue/no-setup-props-destructure': ['error']}">
38+
39+
```vue
40+
<script>
41+
export default {
42+
/* ✗ BAD */
43+
setup({ count }) {
44+
watch(() => {
45+
console.log(count) // not going to detect changes
46+
})
47+
48+
return () => {
49+
return h('div', count) // not going to update
50+
}
51+
}
52+
}
53+
</script>
54+
```
55+
56+
</eslint-code-block>
57+
58+
Also, destructuring in root scope of `setup()` should error, but ok inside nested callbacks or returned render functions:
59+
60+
<eslint-code-block :rules="{'vue/no-setup-props-destructure': ['error']}">
61+
62+
```vue
63+
<script>
64+
export default {
65+
setup(props) {
66+
/* ✗ BAD */
67+
const { count } = props
68+
69+
watch(() => {
70+
/* ✓ GOOD */
71+
const { count } = props
72+
console.log(count)
73+
})
74+
75+
return () => {
76+
/* ✓ GOOD */
77+
const { count } = props
78+
return h('div', count)
79+
}
80+
}
81+
}
82+
</script>
83+
```
84+
85+
</eslint-code-block>
86+
87+
## :wrench: Options
88+
89+
Nothing.
90+
91+
## :books: Further reading
92+
93+
- [Vue RFCs - 0013-composition-api](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0013-composition-api.md)
94+
95+
## :mag: Implementation
96+
97+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-setup-props-destructure.js)
98+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-setup-props-destructure.js)

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ module.exports = {
5656
'no-reserved-component-names': require('./rules/no-reserved-component-names'),
5757
'no-reserved-keys': require('./rules/no-reserved-keys'),
5858
'no-restricted-syntax': require('./rules/no-restricted-syntax'),
59+
'no-setup-props-destructure': require('./rules/no-setup-props-destructure'),
5960
'no-shared-component-data': require('./rules/no-shared-component-data'),
6061
'no-side-effects-in-computed-properties': require('./rules/no-side-effects-in-computed-properties'),
6162
'no-spaces-around-equal-signs-in-attribute': require('./rules/no-spaces-around-equal-signs-in-attribute'),
+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/**
2+
* @author Yosuke Ota
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
const { findVariable } = require('eslint-utils')
7+
const utils = require('../utils')
8+
9+
module.exports = {
10+
meta: {
11+
type: 'suggestion',
12+
docs: {
13+
description: 'disallow destructuring of `props` passed to `setup`',
14+
category: undefined,
15+
url: 'https://eslint.vuejs.org/rules/no-setup-props-destructure.html'
16+
},
17+
fixable: null,
18+
schema: [],
19+
messages: {
20+
destructuring: 'Destructuring the `props` will cause the value to lose reactivity.',
21+
getProperty: 'Getting a value from the `props` in root scope of `setup()` will cause the value to lose reactivity.'
22+
}
23+
},
24+
create (context) {
25+
const setupFunctions = new Map()
26+
const forbiddenNodes = new Map()
27+
28+
function addForbiddenNode (property, node, messageId) {
29+
let list = forbiddenNodes.get(property)
30+
if (!list) {
31+
list = []
32+
forbiddenNodes.set(property, list)
33+
}
34+
list.push({
35+
node,
36+
messageId
37+
})
38+
}
39+
40+
function verify (left, right, { propsReferenceIds, setupProperty }) {
41+
if (!right) {
42+
return
43+
}
44+
45+
if (left.type === 'ArrayPattern' || left.type === 'ObjectPattern') {
46+
if (propsReferenceIds.has(right)) {
47+
addForbiddenNode(setupProperty, left, 'getProperty')
48+
}
49+
} else if (left.type === 'Identifier' && right.type === 'MemberExpression') {
50+
if (propsReferenceIds.has(right.object)) {
51+
addForbiddenNode(setupProperty, right, 'getProperty')
52+
}
53+
}
54+
}
55+
56+
let scopeStack = { upper: null, functionNode: null }
57+
58+
return Object.assign(
59+
{
60+
'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node) {
61+
if (utils.getStaticPropertyName(node) !== 'setup') {
62+
return
63+
}
64+
const param = node.value.params[0]
65+
if (!param) {
66+
// no arguments
67+
return
68+
}
69+
if (param.type === 'RestElement') {
70+
// cannot check
71+
return
72+
}
73+
if (param.type === 'ArrayPattern' || param.type === 'ObjectPattern') {
74+
addForbiddenNode(node, param, 'destructuring')
75+
return
76+
}
77+
setupFunctions.set(node.value, {
78+
setupProperty: node,
79+
propsParam: param,
80+
propsReferenceIds: new Set()
81+
})
82+
},
83+
':function' (node) {
84+
scopeStack = { upper: scopeStack, functionNode: node }
85+
},
86+
':function>*' (node) {
87+
const setupFunctionData = setupFunctions.get(node.parent)
88+
if (!setupFunctionData || setupFunctionData.propsParam !== node) {
89+
return
90+
}
91+
const variable = findVariable(context.getScope(), node)
92+
if (!variable) {
93+
return
94+
}
95+
const { propsReferenceIds } = setupFunctionData
96+
for (const reference of variable.references) {
97+
if (!reference.isRead()) {
98+
continue
99+
}
100+
101+
propsReferenceIds.add(reference.identifier)
102+
}
103+
},
104+
'VariableDeclarator' (node) {
105+
const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
106+
if (!setupFunctionData) {
107+
return
108+
}
109+
verify(node.id, node.init, setupFunctionData)
110+
},
111+
'AssignmentExpression' (node) {
112+
const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
113+
if (!setupFunctionData) {
114+
return
115+
}
116+
verify(node.left, node.right, setupFunctionData)
117+
},
118+
':function:exit' (node) {
119+
scopeStack = scopeStack.upper
120+
121+
setupFunctions.delete(node)
122+
}
123+
},
124+
utils.executeOnVue(context, obj => {
125+
const reportsList = obj.properties
126+
.map(item => forbiddenNodes.get(item))
127+
.filter(reports => !!reports)
128+
for (const reports of reportsList) {
129+
for (const report of reports) {
130+
context.report(report)
131+
}
132+
}
133+
})
134+
)
135+
}
136+
}

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"test:base": "mocha \"tests/lib/**/*.js\" --reporter dot",
99
"test": "nyc npm run test:base -- \"tests/integrations/*.js\" --timeout 60000",
1010
"debug": "mocha --inspect-brk \"tests/lib/**/*.js\" --reporter dot --timeout 60000",
11+
"cover:report": "nyc report --reporter=html",
1112
"lint": "eslint . --rulesdir eslint-internal-rules",
1213
"pretest": "npm run lint",
1314
"preversion": "npm test && npm run update && git add .",

0 commit comments

Comments
 (0)