Skip to content

Commit

Permalink
New: Add vue/no-watch-after-await rule
Browse files Browse the repository at this point in the history
  • Loading branch information
ota-meshi committed Mar 7, 2020
1 parent ab3bf36 commit ac342f7
Show file tree
Hide file tree
Showing 6 changed files with 275 additions and 2 deletions.
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -164,6 +164,7 @@ For example:
| [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax | |
| [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/no-watch-after-await](./no-watch-after-await.md) | disallow asynchronously registered `watch` | |
| [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: |
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: |
| [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | |
Expand Down
47 changes: 47 additions & 0 deletions docs/rules/no-watch-after-await.md
@@ -0,0 +1,47 @@
---
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`
## :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(() => { /* ... */ })
await doSomething()
/* ✗ BAD */
watch(() => { /* ... */ })
}
}
</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-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/index.js
Expand Up @@ -63,6 +63,7 @@ module.exports = {
'no-unused-vars': require('./rules/no-unused-vars'),
'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-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
107 changes: 107 additions & 0 deletions lib/rules/no-watch-after-await.js
@@ -0,0 +1,107 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
const { ReferenceTracker } = require('eslint-utils')
const utils = require('../utils')

module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow asynchronously registered `watch`',
category: undefined,
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
}
}
}

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)) {
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'
})
}
}
})
)
}
}
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
115 changes: 115 additions & 0 deletions tests/lib/rules/no-watch-after-await.js
@@ -0,0 +1,115 @@
/**
* @author Yosuke Ota
*/
'use strict'

const RuleTester = require('eslint').RuleTester
const rule = require('../../../lib/rules/no-watch-after-await')

const tester = new RuleTester({
parser: require.resolve('vue-eslint-parser'),
parserOptions: { ecmaVersion: 2019, sourceType: 'module' }
})

tester.run('no-watch-after-await', rule, {
valid: [
{
filename: 'test.vue',
code: `
<script>
import {watch} from 'vue'
export default {
async setup() {
watch(() => { /* ... */ }) // ok
await doSomething()
}
}
</script>
`
},
{
filename: 'test.vue',
code: `
<script>
import {watch} from 'vue'
export default {
async setup() {
watch(() => { /* ... */ })
}
}
</script>
`
},
{
filename: 'test.vue',
code: `
<script>
import {onMounted} from 'vue'
export default {
async _setup() {
await doSomething()
onMounted(() => { /* ... */ }) // error
}
}
</script>
`
}
],
invalid: [
{
filename: 'test.vue',
code: `
<script>
import {watch} from 'vue'
export default {
async setup() {
await doSomething()
watch(() => { /* ... */ }) // error
}
}
</script>
`,
errors: [
{
message: 'The `watch` after `await` expression are forbidden.',
line: 8,
column: 11,
endLine: 8,
endColumn: 37
}
]
},
{
filename: 'test.vue',
code: `
<script>
import {watch} from 'vue'
export default {
async setup() {
await doSomething()
watch(() => { /* ... */ })
await doSomething()
watch(() => { /* ... */ })
}
}
</script>
`,
errors: [
{
messageId: 'forbidden',
line: 8
},
{
messageId: 'forbidden',
line: 12
}
]
}
]
})

0 comments on commit ac342f7

Please sign in to comment.