Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

computed values maybe better?
I'm not sure how I should call it.

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