Skip to content

Commit

Permalink
Add composition api's computed function support to vue/no-async-in-co…
Browse files Browse the repository at this point in the history
…mputed-properties refs #1393 (#1398)

* Add composition api's computed function support to vue/no-async-in-computed-properties refs #1393

* rename getComputedGetterBody to getGetterBodyFromComputedFunction

* add testcase without return

* use array instead of set
  • Loading branch information
sapphi-red committed Jan 3, 2021
1 parent 1b75e28 commit b5b6347
Show file tree
Hide file tree
Showing 4 changed files with 455 additions and 52 deletions.
49 changes: 45 additions & 4 deletions docs/rules/no-async-in-computed-properties.md
Expand Up @@ -2,21 +2,21 @@
pageClass: rule-details
sidebarDepth: 0
title: vue/no-async-in-computed-properties
description: disallow asynchronous actions in computed properties
description: disallow asynchronous actions in computed properties and functions
since: v3.8.0
---
# vue/no-async-in-computed-properties

> disallow asynchronous actions in computed properties
> disallow asynchronous actions in computed properties and functions
- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.

Computed properties should be synchronous. Asynchronous actions inside them may not work as expected and can lead to an unexpected behaviour, that's why you should avoid them.
Computed properties and functions should be synchronous. Asynchronous actions inside them may not work as expected and can lead to an unexpected behaviour, that's why you should avoid them.
If you need async computed properties you might want to consider using additional plugin [vue-async-computed]

## :book: Rule Details

This rule is aimed at preventing asynchronous methods from being called in computed properties.
This rule is aimed at preventing asynchronous methods from being called in computed properties and functions.

<eslint-code-block :rules="{'vue/no-async-in-computed-properties': ['error']}">

Expand Down Expand Up @@ -62,6 +62,47 @@ export default {

</eslint-code-block>

<eslint-code-block :rules="{'vue/no-async-in-computed-properties': ['error']}">

```vue
<script>
import {computed} from 'vue'
export default {
setup() {
/* ✓ GOOD */
const foo = computed(() => {
var bar = 0
try {
bar = bar / this.a
} catch (e) {
return 0
} finally {
return bar
}
})
/* ✗ BAD */
const pro = computed(() => Promise.all([new Promise((resolve, reject) => {})]))
const foo1 = computed(async () => await someFunc())
const bar = computed(() => {
return fetch(url).then(response => {})
})
const tim = computed(() => {
setTimeout(() => { }, 0)
})
const inter = computed(() => {
setInterval(() => { }, 0)
})
const anim = computed(() => {
requestAnimationFrame(() => {})
})
}
}
</script>
```

</eslint-code-block>

## :wrench: Options

Nothing.
Expand Down
139 changes: 91 additions & 48 deletions lib/rules/no-async-in-computed-properties.js
Expand Up @@ -3,7 +3,7 @@
* @author Armano
*/
'use strict'

const { ReferenceTracker } = require('eslint-utils')
const utils = require('../utils')

/**
Expand Down Expand Up @@ -77,13 +77,16 @@ module.exports = {
},
/** @param {RuleContext} context */
create(context) {
/** @type {Map<ObjectExpression, ComponentComputedProperty[]>} */
const computedPropertiesMap = new Map()
/** @type {Array<FunctionExpression | ArrowFunctionExpression>} */
const computedFunctionNodes = []

/**
* @typedef {object} ScopeStack
* @property {ScopeStack | null} upper
* @property {BlockStatement | Expression} body
*/
/** @type {Map<ObjectExpression, ComponentComputedProperty[]>} */
const computedPropertiesMap = new Map()
/** @type {ScopeStack | null} */
let scopeStack = null

Expand Down Expand Up @@ -139,63 +142,103 @@ module.exports = {
})
}
})
}
return utils.defineVueVisitor(context, {
onVueObjectEnter(node) {
computedPropertiesMap.set(node, utils.getComputedProperties(node))
},
':function': onFunctionEnter,
':function:exit': onFunctionExit,

NewExpression(node, { node: vueNode }) {
if (!scopeStack) {
return
}
computedFunctionNodes.forEach((c) => {
if (
node.callee.type === 'Identifier' &&
node.callee.name === 'Promise'
node.loc.start.line >= c.loc.start.line &&
node.loc.end.line <= c.loc.end.line &&
targetBody === c.body
) {
verify(
context.report({
node,
scopeStack.body,
'new',
computedPropertiesMap.get(vueNode)
)
message: 'Unexpected {{expressionName}} in computed function.',
data: {
expressionName: expressionTypes[type]
}
})
}
},
})
}
return Object.assign(
{
Program() {
const tracker = new ReferenceTracker(context.getScope())
const traceMap = utils.createCompositionApiTraceMap({
[ReferenceTracker.ESM]: true,
computed: {
[ReferenceTracker.CALL]: true
}
})

for (const { node } of tracker.iterateEsmReferences(traceMap)) {
if (node.type !== 'CallExpression') {
continue
}

CallExpression(node, { node: vueNode }) {
if (!scopeStack) {
return
const getter = utils.getGetterBodyFromComputedFunction(node)
if (getter) {
computedFunctionNodes.push(getter)
}
}
}
if (isPromise(node)) {
verify(
node,
scopeStack.body,
'promise',
computedPropertiesMap.get(vueNode)
)
} else if (isTimedFunction(node)) {
},
utils.defineVueVisitor(context, {
onVueObjectEnter(node) {
computedPropertiesMap.set(node, utils.getComputedProperties(node))
},
':function': onFunctionEnter,
':function:exit': onFunctionExit,

NewExpression(node, { node: vueNode }) {
if (!scopeStack) {
return
}
if (
node.callee.type === 'Identifier' &&
node.callee.name === 'Promise'
) {
verify(
node,
scopeStack.body,
'new',
computedPropertiesMap.get(vueNode)
)
}
},

CallExpression(node, { node: vueNode }) {
if (!scopeStack) {
return
}
if (isPromise(node)) {
verify(
node,
scopeStack.body,
'promise',
computedPropertiesMap.get(vueNode)
)
} else if (isTimedFunction(node)) {
verify(
node,
scopeStack.body,
'timed',
computedPropertiesMap.get(vueNode)
)
}
},

AwaitExpression(node, { node: vueNode }) {
if (!scopeStack) {
return
}
verify(
node,
scopeStack.body,
'timed',
'await',
computedPropertiesMap.get(vueNode)
)
}
},

AwaitExpression(node, { node: vueNode }) {
if (!scopeStack) {
return
}
verify(
node,
scopeStack.body,
'await',
computedPropertiesMap.get(vueNode)
)
}
})
})
)
}
}
38 changes: 38 additions & 0 deletions lib/utils/index.js
Expand Up @@ -860,6 +860,44 @@ module.exports = {
})
},

/**
* Get getter body from computed function
* @param {CallExpression} callExpression call of computed function
* @return {FunctionExpression | ArrowFunctionExpression | null} getter function
*/
getGetterBodyFromComputedFunction(callExpression) {
if (callExpression.arguments.length <= 0) {
return null
}

const arg = callExpression.arguments[0]

if (
arg.type === 'FunctionExpression' ||
arg.type === 'ArrowFunctionExpression'
) {
return arg
}

if (arg.type === 'ObjectExpression') {
const getProperty = arg.properties.find(
/**
* @param {ESNode} p
* @returns { p is (Property & { value: FunctionExpression | ArrowFunctionExpression }) }
*/
(p) =>
p.type === 'Property' &&
p.key.type === 'Identifier' &&
p.key.name === 'get' &&
(p.value.type === 'FunctionExpression' ||
p.value.type === 'ArrowFunctionExpression')
)
return getProperty ? getProperty.value : null
}

return null
},

isVueFile,

/**
Expand Down

0 comments on commit b5b6347

Please sign in to comment.