diff --git a/docs/rules/README.md b/docs/rules/README.md index 3e37fe70b..bb8d92e8b 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -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 `` 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 | | diff --git a/docs/rules/no-watch-after-await.md b/docs/rules/no-watch-after-await.md new file mode 100644 index 000000000..679dbcbfc --- /dev/null +++ b/docs/rules/no-watch-after-await.md @@ -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. + + + +```vue + +``` + + + +This rule is not reported when using the stop handle. + + + +```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) +- [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) diff --git a/lib/configs/vue3-essential.js b/lib/configs/vue3-essential.js index f7c35fecf..d668791c7 100644 --- a/lib/configs/vue3-essential.js +++ b/lib/configs/vue3-essential.js @@ -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', diff --git a/lib/index.js b/lib/index.js index 03fd799a2..fec192830 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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'), diff --git a/lib/rules/no-watch-after-await.js b/lib/rules/no-watch-after-await.js new file mode 100644 index 000000000..f65a459de --- /dev/null +++ b/lib/rules/no-watch-after-await.js @@ -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' + }) + } + } + }) + ) + } +} 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..9d91fbb2b --- /dev/null +++ b/tests/lib/rules/no-watch-after-await.js @@ -0,0 +1,177 @@ +/** + * @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: ` + + ` + }, + { + 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: 42 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'forbidden', + line: 8 + }, + { + messageId: 'forbidden', + line: 9 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'forbidden', + line: 8 + }, + { + messageId: 'forbidden', + line: 12 + } + ] + } + ] +})