Skip to content

Commit

Permalink
New: Add vue/no-watch-after-await rule (#1068)
Browse files Browse the repository at this point in the history
* New: Add `vue/no-watch-after-await` rule

* Add check for watchEffect.

* Update vue/no-watch-after-await
  • Loading branch information
ota-meshi authored Apr 21, 2020
1 parent 35efedc commit f4de98e
Show file tree
Hide file tree
Showing 6 changed files with 390 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
| [vue/no-unused-components](./no-unused-components.md) | disallow registering components that are not used inside templates | |
| [vue/no-unused-vars](./no-unused-vars.md) | disallow unused variable definitions of v-for directives or scope attributes | |
| [vue/no-use-v-if-with-v-for](./no-use-v-if-with-v-for.md) | disallow use v-if on the same element as v-for | |
| [vue/no-watch-after-await](./no-watch-after-await.md) | disallow asynchronously registered `watch` | |
| [vue/require-component-is](./require-component-is.md) | require `v-bind:is` of `<component>` elements | |
| [vue/require-prop-type-constructor](./require-prop-type-constructor.md) | require prop type to be a constructor | :wrench: |
| [vue/require-render-return](./require-render-return.md) | enforce render function to always return value | |
Expand Down
73 changes: 73 additions & 0 deletions docs/rules/no-watch-after-await.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/no-watch-after-await
description: disallow asynchronously registered `watch`
---
# vue/no-watch-after-await
> disallow asynchronously registered `watch`
- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.

## :book: Rule Details

This rule reports the `watch()` after `await` expression.
In `setup()` function, `watch()` should be registered synchronously.

<eslint-code-block :rules="{'vue/no-watch-after-await': ['error']}">

```vue
<script>
import { watch } from 'vue'
export default {
async setup() {
/* ✓ GOOD */
watch(watchSource, () => { /* ... */ })
await doSomething()
/* ✗ BAD */
watch(watchSource, () => { /* ... */ })
}
}
</script>
```

</eslint-code-block>

This rule is not reported when using the stop handle.

<eslint-code-block :rules="{'vue/no-watch-after-await': ['error']}">

```vue
<script>
import { watch } from 'vue'
export default {
async setup() {
await doSomething()
/* ✓ GOOD */
const stopHandle = watch(watchSource, () => { /* ... */ })
// later
stopHandle()
}
}
</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)
- [Vue Composition API - API Reference - Stopping the Watcher](https://composition-api.vuejs.org/api.html#stopping-the-watcher)

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-watch-after-await.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-watch-after-await.js)
1 change: 1 addition & 0 deletions lib/configs/vue3-essential.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ module.exports = {
'vue/no-unused-components': 'error',
'vue/no-unused-vars': 'error',
'vue/no-use-v-if-with-v-for': 'error',
'vue/no-watch-after-await': 'error',
'vue/require-component-is': 'error',
'vue/require-prop-type-constructor': 'error',
'vue/require-render-return': 'error',
Expand Down
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ module.exports = {
'no-use-v-if-with-v-for': require('./rules/no-use-v-if-with-v-for'),
'no-v-html': require('./rules/no-v-html'),
'no-v-model-argument': require('./rules/no-v-model-argument'),
'no-watch-after-await': require('./rules/no-watch-after-await'),
'object-curly-spacing': require('./rules/object-curly-spacing'),
'order-in-components': require('./rules/order-in-components'),
'padding-line-between-blocks': require('./rules/padding-line-between-blocks'),
Expand Down
137 changes: 137 additions & 0 deletions lib/rules/no-watch-after-await.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
const { ReferenceTracker } = require('eslint-utils')
const utils = require('../utils')

function isMaybeUsedStopHandle (node) {
const parent = node.parent
if (parent) {
if (parent.type === 'VariableDeclarator') {
// var foo = watch()
return true
}
if (parent.type === 'AssignmentExpression') {
// foo = watch()
return true
}
if (parent.type === 'CallExpression') {
// foo(watch())
return true
}
if (parent.type === 'Property') {
// {foo: watch()}
return true
}
if (parent.type === 'ArrayExpression') {
// [watch()]
return true
}
}
return false
}

module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow asynchronously registered `watch`',
categories: ['vue3-essential'],
url: 'https://eslint.vuejs.org/rules/no-watch-after-await.html'
},
fixable: null,
schema: [],
messages: {
forbidden: 'The `watch` after `await` expression are forbidden.'
}
},
create (context) {
const watchCallNodes = new Set()
const setupFunctions = new Map()
const forbiddenNodes = new Map()

function addForbiddenNode (property, node) {
let list = forbiddenNodes.get(property)
if (!list) {
list = []
forbiddenNodes.set(property, list)
}
list.push(node)
}

let scopeStack = { upper: null, functionNode: null }

return Object.assign(
{
'Program' () {
const tracker = new ReferenceTracker(context.getScope())
const traceMap = {
vue: {
[ReferenceTracker.ESM]: true,
watch: {
[ReferenceTracker.CALL]: true
},
watchEffect: {
[ReferenceTracker.CALL]: true
}
}
}

for (const { node } of tracker.iterateEsmReferences(traceMap)) {
watchCallNodes.add(node)
}
},
'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node) {
if (utils.getStaticPropertyName(node) !== 'setup') {
return
}

setupFunctions.set(node.value, {
setupProperty: node,
afterAwait: false
})
},
':function' (node) {
scopeStack = { upper: scopeStack, functionNode: node }
},
'AwaitExpression' () {
const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
if (!setupFunctionData) {
return
}
setupFunctionData.afterAwait = true
},
'CallExpression' (node) {
const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
if (!setupFunctionData || !setupFunctionData.afterAwait) {
return
}

if (watchCallNodes.has(node) && !isMaybeUsedStopHandle(node)) {
addForbiddenNode(setupFunctionData.setupProperty, node)
}
},
':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 node of reports) {
context.report({
node,
messageId: 'forbidden'
})
}
}
})
)
}
}
Loading

0 comments on commit f4de98e

Please sign in to comment.