Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New: Add vue/no-setup-props-destructure rule #1066

Merged
merged 6 commits into from Mar 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -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: |
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 @@ -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'),
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)
}
}
})
)
}
}
6 changes: 4 additions & 2 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 Expand Up @@ -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",
Expand Down