From d2a76c90ff7088cfddef6866bb1671a3e8eef866 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Fri, 6 Mar 2020 12:58:33 +0900 Subject: [PATCH 1/3] New: Add `vue/no-watch-after-await` rule --- docs/rules/README.md | 1 + docs/rules/no-watch-after-await.md | 47 ++++++++++ lib/index.js | 1 + lib/rules/no-watch-after-await.js | 107 ++++++++++++++++++++++ tests/lib/rules/no-watch-after-await.js | 115 ++++++++++++++++++++++++ 5 files changed, 271 insertions(+) create mode 100644 docs/rules/no-watch-after-await.md create mode 100644 lib/rules/no-watch-after-await.js create mode 100644 tests/lib/rules/no-watch-after-await.js diff --git a/docs/rules/README.md b/docs/rules/README.md index cc2a97313..4f099696c 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -273,6 +273,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 | | diff --git a/docs/rules/no-watch-after-await.md b/docs/rules/no-watch-after-await.md new file mode 100644 index 000000000..a0b2d9b61 --- /dev/null +++ b/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. + + + +```vue + +``` + + + +## :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) diff --git a/lib/index.js b/lib/index.js index 10e2e8766..1b35670e3 100644 --- a/lib/index.js +++ b/lib/index.js @@ -71,6 +71,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'), diff --git a/lib/rules/no-watch-after-await.js b/lib/rules/no-watch-after-await.js new file mode 100644 index 000000000..11d33ec3a --- /dev/null +++ b/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' + }) + } + } + }) + ) + } +} diff --git a/tests/lib/rules/no-watch-after-await.js b/tests/lib/rules/no-watch-after-await.js new file mode 100644 index 000000000..5baa0f1af --- /dev/null +++ b/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: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + } + ], + invalid: [ + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: 'The `watch` after `await` expression are forbidden.', + line: 8, + column: 11, + endLine: 8, + endColumn: 37 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'forbidden', + line: 8 + }, + { + messageId: 'forbidden', + line: 12 + } + ] + } + ] +}) From a503ea13194a9d82c588cb0f85dfe9e72e28d109 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Sat, 7 Mar 2020 18:10:13 +0900 Subject: [PATCH 2/3] Add check for watchEffect. --- docs/rules/README.md | 2 +- docs/rules/no-watch-after-await.md | 2 ++ lib/configs/vue3-essential.js | 1 + lib/rules/no-watch-after-await.js | 5 ++- tests/lib/rules/no-watch-after-await.js | 41 +++++++++++++++++++++++++ 5 files changed, 49 insertions(+), 2 deletions(-) diff --git a/docs/rules/README.md b/docs/rules/README.md index 4f099696c..aed5bef27 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -58,6 +58,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 `` 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 | | @@ -273,7 +274,6 @@ 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 | | diff --git a/docs/rules/no-watch-after-await.md b/docs/rules/no-watch-after-await.md index a0b2d9b61..999c2afc9 100644 --- a/docs/rules/no-watch-after-await.md +++ b/docs/rules/no-watch-after-await.md @@ -7,6 +7,8 @@ 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. diff --git a/lib/configs/vue3-essential.js b/lib/configs/vue3-essential.js index 3e45ddd2c..6a41aebbc 100644 --- a/lib/configs/vue3-essential.js +++ b/lib/configs/vue3-essential.js @@ -26,6 +26,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', diff --git a/lib/rules/no-watch-after-await.js b/lib/rules/no-watch-after-await.js index 11d33ec3a..7ffd04099 100644 --- a/lib/rules/no-watch-after-await.js +++ b/lib/rules/no-watch-after-await.js @@ -11,7 +11,7 @@ module.exports = { type: 'suggestion', docs: { description: 'disallow asynchronously registered `watch`', - category: undefined, + categories: ['vue3-essential'], url: 'https://eslint.vuejs.org/rules/no-watch-after-await.html' }, fixable: null, @@ -45,6 +45,9 @@ module.exports = { [ReferenceTracker.ESM]: true, watch: { [ReferenceTracker.CALL]: true + }, + watchEffect: { + [ReferenceTracker.CALL]: true } } } diff --git a/tests/lib/rules/no-watch-after-await.js b/tests/lib/rules/no-watch-after-await.js index 5baa0f1af..1adc15aab 100644 --- a/tests/lib/rules/no-watch-after-await.js +++ b/tests/lib/rules/no-watch-after-await.js @@ -41,6 +41,21 @@ tester.run('no-watch-after-await', rule, { ` }, + { + filename: 'test.vue', + code: ` + + ` + }, { filename: 'test.vue', code: ` @@ -82,6 +97,32 @@ tester.run('no-watch-after-await', rule, { } ] }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'forbidden', + line: 8 + }, + { + messageId: 'forbidden', + line: 9 + } + ] + }, { filename: 'test.vue', code: ` From 7d22e7688785f82b508aac3a26482a1a5fd87a46 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Sat, 18 Apr 2020 18:29:39 +0900 Subject: [PATCH 3/3] Update vue/no-watch-after-await --- docs/rules/no-watch-after-await.md | 28 +++++++++++++++++-- lib/rules/no-watch-after-await.js | 29 ++++++++++++++++++- tests/lib/rules/no-watch-after-await.js | 37 +++++++++++++++++++------ 3 files changed, 83 insertions(+), 11 deletions(-) diff --git a/docs/rules/no-watch-after-await.md b/docs/rules/no-watch-after-await.md index 999c2afc9..679dbcbfc 100644 --- a/docs/rules/no-watch-after-await.md +++ b/docs/rules/no-watch-after-await.md @@ -22,12 +22,35 @@ import { watch } from 'vue' export default { async setup() { /* ✓ GOOD */ - watch(() => { /* ... */ }) + watch(watchSource, () => { /* ... */ }) await doSomething() /* ✗ BAD */ - watch(() => { /* ... */ }) + watch(watchSource, () => { /* ... */ }) + } +} + +``` + + + +This rule is not reported when using the stop handle. + + + +```vue + @@ -42,6 +65,7 @@ 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 diff --git a/lib/rules/no-watch-after-await.js b/lib/rules/no-watch-after-await.js index 7ffd04099..f65a459de 100644 --- a/lib/rules/no-watch-after-await.js +++ b/lib/rules/no-watch-after-await.js @@ -6,6 +6,33 @@ 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', @@ -82,7 +109,7 @@ module.exports = { return } - if (watchCallNodes.has(node)) { + if (watchCallNodes.has(node) && !isMaybeUsedStopHandle(node)) { addForbiddenNode(setupFunctionData.setupProperty, node) } }, diff --git a/tests/lib/rules/no-watch-after-await.js b/tests/lib/rules/no-watch-after-await.js index 1adc15aab..9d91fbb2b 100644 --- a/tests/lib/rules/no-watch-after-await.js +++ b/tests/lib/rules/no-watch-after-await.js @@ -20,7 +20,7 @@ tester.run('no-watch-after-await', rule, { import {watch} from 'vue' export default { async setup() { - watch(() => { /* ... */ }) // ok + watch(foo, () => { /* ... */ }) // ok await doSomething() } @@ -35,7 +35,7 @@ tester.run('no-watch-after-await', rule, { import {watch} from 'vue' export default { async setup() { - watch(() => { /* ... */ }) + watch(foo, () => { /* ... */ }) } } @@ -49,7 +49,7 @@ tester.run('no-watch-after-await', rule, { export default { async setup() { watchEffect(() => { /* ... */ }) - watch(() => { /* ... */ }) + watch(foo, () => { /* ... */ }) await doSomething() } } @@ -70,6 +70,27 @@ tester.run('no-watch-after-await', rule, { } ` + }, + { + filename: 'test.vue', + code: ` + + ` } ], invalid: [ @@ -82,7 +103,7 @@ tester.run('no-watch-after-await', rule, { async setup() { await doSomething() - watch(() => { /* ... */ }) // error + watch(foo, () => { /* ... */ }) // error } } @@ -93,7 +114,7 @@ tester.run('no-watch-after-await', rule, { line: 8, column: 11, endLine: 8, - endColumn: 37 + endColumn: 42 } ] }, @@ -107,7 +128,7 @@ tester.run('no-watch-after-await', rule, { await doSomething() watchEffect(() => { /* ... */ }) - watch(() => { /* ... */ }) + watch(foo, () => { /* ... */ }) } } @@ -132,11 +153,11 @@ tester.run('no-watch-after-await', rule, { async setup() { await doSomething() - watch(() => { /* ... */ }) + watch(foo, () => { /* ... */ }) await doSomething() - watch(() => { /* ... */ }) + watch(foo, () => { /* ... */ }) } }