Skip to content

Commit

Permalink
New: Add vue/no-setup-props-destructure rule (#1066)
Browse files Browse the repository at this point in the history
* Add no-setup-props-destructure rule

* update

* Add testcases

* update

* update

* update doc
  • Loading branch information
ota-meshi committed Mar 14, 2020
1 parent 11c9a94 commit 2c92d3d
Show file tree
Hide file tree
Showing 6 changed files with 572 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -170,6 +170,7 @@ For example:
| [vue/no-ref-as-operand](./no-ref-as-operand.md) | disallow use of value wrapped by `ref()` (Composition API) as an operand | |
| [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: |
Expand Down
98 changes: 98 additions & 0 deletions 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.

<eslint-code-block :rules="{'vue/no-setup-props-destructure': ['error']}">

```vue
<script>
export default {
/* ✓ GOOD */
setup(props) {
watch(() => {
console.log(props.count)
})
return () => {
return h('div', props.count)
}
}
}
</script>
```

</eslint-code-block>

Destructuring the `props` passed to `setup` will cause the value to lose reactivity.

<eslint-code-block :rules="{'vue/no-setup-props-destructure': ['error']}">

```vue
<script>
export default {
/* ✗ BAD */
setup({ count }) {
watch(() => {
console.log(count) // not going to detect changes
})
return () => {
return h('div', count) // not going to update
}
}
}
</script>
```

</eslint-code-block>

Also, destructuring in root scope of `setup()` should error, but ok inside nested callbacks or returned render functions:

<eslint-code-block :rules="{'vue/no-setup-props-destructure': ['error']}">

```vue
<script>
export default {
setup(props) {
/* ✗ BAD */
const { count } = props
watch(() => {
/* ✓ GOOD */
const { count } = props
console.log(count)
})
return () => {
/* ✓ GOOD */
const { count } = props
return h('div', count)
}
}
}
</script>
```

</eslint-code-block>

## :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)
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -56,6 +56,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'),
Expand Down
136 changes: 136 additions & 0 deletions 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)
}
}
})
)
}
}
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -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 .",
Expand Down

0 comments on commit 2c92d3d

Please sign in to comment.