diff --git a/docs/rules/README.md b/docs/rules/README.md index ab1f5b220..bf70de4b1 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -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 | | 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 02e65ba96..22d9a799a 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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'), 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/package.json b/package.json index 1744009a5..dac43a010 100644 --- a/package.json +++ b/package.json @@ -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 .", @@ -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", 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 + } + ] + } + ] +})