Skip to content

Commit

Permalink
New: Add vue/no-lifecycle-after-await rule (#1067)
Browse files Browse the repository at this point in the history
  • Loading branch information
ota-meshi committed Mar 14, 2020
1 parent a634e3c commit 3cc5ac0
Show file tree
Hide file tree
Showing 5 changed files with 340 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -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 | |
Expand Down
47 changes: 47 additions & 0 deletions 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.

<eslint-code-block :rules="{'vue/no-lifecycle-after-await': ['error']}">

```vue
<script>
import { onMounted } from 'vue'
export default {
async setup() {
/* ✓ GOOD */
onMounted(() => { /* ... */ })
await doSomething()
/* ✗ BAD */
onMounted(() => { /* ... */ })
}
}
</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-lifecycle-after-await.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-lifecycle-after-await.js)
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -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'),
Expand Down
111 changes: 111 additions & 0 deletions 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'
})
}
}
})
)
}
}
180 changes: 180 additions & 0 deletions 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: `
<script>
import {onMounted} from 'vue'
export default {
async setup() {
onMounted(() => { /* ... */ }) // ok
await doSomething()
}
}
</script>
`
}, {
filename: 'test.vue',
code: `
<script>
import {onMounted} from 'vue'
export default {
async setup() {
onMounted(() => { /* ... */ })
}
}
</script>
`
}, {
filename: 'test.vue',
code: `
<script>
import {onBeforeMount, onBeforeUnmount, onBeforeUpdate, onErrorCaptured, onMounted, onRenderTracked, onRenderTriggered, onUnmounted, onUpdated, onActivated, onDeactivated} from 'vue'
export default {
async setup() {
onBeforeMount(() => { /* ... */ })
onBeforeUnmount(() => { /* ... */ })
onBeforeUpdate(() => { /* ... */ })
onErrorCaptured(() => { /* ... */ })
onMounted(() => { /* ... */ })
onRenderTracked(() => { /* ... */ })
onRenderTriggered(() => { /* ... */ })
onUnmounted(() => { /* ... */ })
onUpdated(() => { /* ... */ })
onActivated(() => { /* ... */ })
onDeactivated(() => { /* ... */ })
await doSomething()
}
}
</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 {onMounted} from 'vue'
export default {
async setup() {
await doSomething()
onMounted(() => { /* ... */ }) // error
}
}
</script>
`,
errors: [
{
message: 'The lifecycle hooks after `await` expression are forbidden.',
line: 8,
column: 11,
endLine: 8,
endColumn: 41
}
]
},
{
filename: 'test.vue',
code: `
<script>
import {onBeforeMount, onBeforeUnmount, onBeforeUpdate, onErrorCaptured, onMounted, onRenderTracked, onRenderTriggered, onUnmounted, onUpdated, onActivated, onDeactivated} from 'vue'
export default {
async setup() {
await doSomething()
onBeforeMount(() => { /* ... */ })
onBeforeUnmount(() => { /* ... */ })
onBeforeUpdate(() => { /* ... */ })
onErrorCaptured(() => { /* ... */ })
onMounted(() => { /* ... */ })
onRenderTracked(() => { /* ... */ })
onRenderTriggered(() => { /* ... */ })
onUnmounted(() => { /* ... */ })
onUpdated(() => { /* ... */ })
onActivated(() => { /* ... */ })
onDeactivated(() => { /* ... */ })
}
}
</script>
`,
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
}
]
}
]
})

0 comments on commit 3cc5ac0

Please sign in to comment.