From 3cc5ac00cb0db328c30b9d689502319d35f2e7c5 Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Sat, 14 Mar 2020 17:10:58 +0900 Subject: [PATCH] New: Add `vue/no-lifecycle-after-await` rule (#1067) --- docs/rules/README.md | 1 + docs/rules/no-lifecycle-after-await.md | 47 +++++ lib/index.js | 1 + lib/rules/no-lifecycle-after-await.js | 111 ++++++++++++ tests/lib/rules/no-lifecycle-after-await.js | 180 ++++++++++++++++++++ 5 files changed, 340 insertions(+) create mode 100644 docs/rules/no-lifecycle-after-await.md create mode 100644 lib/rules/no-lifecycle-after-await.js create mode 100644 tests/lib/rules/no-lifecycle-after-await.js diff --git a/docs/rules/README.md b/docs/rules/README.md index 86b51d484..5b21a0840 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -167,6 +167,7 @@ For example: | [vue/no-deprecated-v-bind-sync](./no-deprecated-v-bind-sync.md) | disallow use of deprecated `.sync` modifier on `v-bind` directive (in Vue.js 3.0.0+) | :wrench: | | [vue/no-empty-pattern](./no-empty-pattern.md) | disallow empty destructuring patterns | | | [vue/no-irregular-whitespace](./no-irregular-whitespace.md) | disallow irregular whitespace | | +| [vue/no-lifecycle-after-await](./no-lifecycle-after-await.md) | disallow asynchronously registered lifecycle hooks | | | [vue/no-ref-as-operand](./no-ref-as-operand.md) | disallow use of value wrapped by `ref()` (Composition API) as an operand | | | [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions | | | [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax | | diff --git a/docs/rules/no-lifecycle-after-await.md b/docs/rules/no-lifecycle-after-await.md new file mode 100644 index 000000000..aaa521f7c --- /dev/null +++ b/docs/rules/no-lifecycle-after-await.md @@ -0,0 +1,47 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/no-lifecycle-after-await +description: disallow asynchronously registered lifecycle hooks +--- +# vue/no-lifecycle-after-await +> disallow asynchronously registered lifecycle hooks + +## :book: Rule Details + +This rule reports the lifecycle hooks after `await` expression. +In `setup()` function, `onXXX` lifecycle hooks 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-lifecycle-after-await.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-lifecycle-after-await.js) diff --git a/lib/index.js b/lib/index.js index e772dc590..55d753058 100644 --- a/lib/index.js +++ b/lib/index.js @@ -49,6 +49,7 @@ module.exports = { 'no-duplicate-attributes': require('./rules/no-duplicate-attributes'), 'no-empty-pattern': require('./rules/no-empty-pattern'), 'no-irregular-whitespace': require('./rules/no-irregular-whitespace'), + 'no-lifecycle-after-await': require('./rules/no-lifecycle-after-await'), 'no-multi-spaces': require('./rules/no-multi-spaces'), 'no-multiple-template-root': require('./rules/no-multiple-template-root'), 'no-parsing-error': require('./rules/no-parsing-error'), diff --git a/lib/rules/no-lifecycle-after-await.js b/lib/rules/no-lifecycle-after-await.js new file mode 100644 index 000000000..1f78165e5 --- /dev/null +++ b/lib/rules/no-lifecycle-after-await.js @@ -0,0 +1,111 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' +const { ReferenceTracker } = require('eslint-utils') +const utils = require('../utils') + +const LIFECYCLE_HOOKS = ['onBeforeMount', 'onBeforeUnmount', 'onBeforeUpdate', 'onErrorCaptured', 'onMounted', 'onRenderTracked', 'onRenderTriggered', 'onUnmounted', 'onUpdated', 'onActivated', 'onDeactivated'] + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'disallow asynchronously registered lifecycle hooks', + category: undefined, + url: 'https://eslint.vuejs.org/rules/no-lifecycle-after-await.html' + }, + fixable: null, + schema: [], + messages: { + forbidden: 'The lifecycle hooks after `await` expression are forbidden.' + } + }, + create (context) { + const lifecycleHookCallNodes = 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 + } + } + for (const lifecycleHook of LIFECYCLE_HOOKS) { + traceMap.vue[lifecycleHook] = { + [ReferenceTracker.CALL]: true + } + } + + for (const { node } of tracker.iterateEsmReferences(traceMap)) { + lifecycleHookCallNodes.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 (lifecycleHookCallNodes.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-lifecycle-after-await.js b/tests/lib/rules/no-lifecycle-after-await.js new file mode 100644 index 000000000..eaa99f843 --- /dev/null +++ b/tests/lib/rules/no-lifecycle-after-await.js @@ -0,0 +1,180 @@ +/** + * @author Yosuke Ota + */ +'use strict' + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/no-lifecycle-after-await') + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { ecmaVersion: 2019, sourceType: 'module' } +}) + +tester.run('no-lifecycle-after-await', rule, { + valid: [ + { + filename: 'test.vue', + code: ` + + ` + }, { + filename: 'test.vue', + code: ` + + ` + }, { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + } + ], + invalid: [ + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: 'The lifecycle hooks after `await` expression are forbidden.', + line: 8, + column: 11, + endLine: 8, + endColumn: 41 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'forbidden', + line: 8 + }, + { + messageId: 'forbidden', + line: 9 + }, + { + messageId: 'forbidden', + line: 10 + }, + { + messageId: 'forbidden', + line: 11 + }, + { + messageId: 'forbidden', + line: 12 + }, + { + messageId: 'forbidden', + line: 13 + }, + { + messageId: 'forbidden', + line: 14 + }, + { + messageId: 'forbidden', + line: 15 + }, + { + messageId: 'forbidden', + line: 16 + }, + { + messageId: 'forbidden', + line: 17 + }, + { + messageId: 'forbidden', + line: 18 + } + ] + } + ] +})