Skip to content

Commit 0a556c5

Browse files
committed
feat(no-async-in-computed-properties): add ignoredObjectNames option
1 parent a5127b0 commit 0a556c5

File tree

3 files changed

+240
-13
lines changed

3 files changed

+240
-13
lines changed

docs/rules/no-async-in-computed-properties.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,42 @@ export default {
108108

109109
## :wrench: Options
110110

111-
Nothing.
111+
```js
112+
{
113+
"vue/no-async-in-computed-properties": ["error", {
114+
"ignoredObjectNames": []
115+
}]
116+
}
117+
```
118+
119+
- `ignoredObjectNames`: An array of object names that should be ignored when used with promise-like methods (`.then()`, `.catch()`, `.finally()`). This is useful for validation libraries like Zod that use these method names for non-promise purposes.
120+
121+
### `"ignoredObjectNames": ["z"]`
122+
123+
<eslint-code-block :rules="{'vue/no-async-in-computed-properties': ['error', {ignoredObjectNames: ['z']}]}">
124+
125+
```vue
126+
<script setup>
127+
import { computed } from 'vue'
128+
import { z } from 'zod'
129+
130+
/* ✓ GOOD */
131+
const schema1 = computed(() => {
132+
return z.string().catch('default')
133+
})
134+
135+
const schema2 = computed(() => {
136+
return z.catch(z.string().min(2), 'fallback')
137+
})
138+
139+
/* ✗ BAD */
140+
const fetchData = computed(() => {
141+
return myFunc().then(res => res.json())
142+
})
143+
</script>
144+
```
145+
146+
</eslint-code-block>
112147

113148
## :books: Further Reading
114149

lib/rules/no-async-in-computed-properties.js

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,22 +38,60 @@ function isTimedFunction(node) {
3838
)
3939
}
4040

41+
/**
42+
* Get the root object name from a member expression chain
43+
* @param {MemberExpression} memberExpr
44+
* @returns {string|null}
45+
*/
46+
function getRootObjectName(memberExpr) {
47+
let current = memberExpr.object
48+
49+
while (current) {
50+
if (current.type === 'MemberExpression') {
51+
current = utils.skipChainExpression(current.object)
52+
} else if (current.type === 'CallExpression') {
53+
const calleeExpr = utils.skipChainExpression(current.callee)
54+
if (calleeExpr.type === 'MemberExpression') {
55+
current = calleeExpr.object
56+
} else if (calleeExpr.type === 'Identifier') {
57+
return calleeExpr.name
58+
} else {
59+
break
60+
}
61+
} else if (current.type === 'Identifier') {
62+
return current.name
63+
} else {
64+
break
65+
}
66+
}
67+
68+
return null
69+
}
70+
4171
/**
4272
* @param {CallExpression} node
73+
* @param {Set<string>} ignoredObjectNames
4374
*/
44-
function isPromise(node) {
75+
function isPromise(node, ignoredObjectNames) {
4576
const callee = utils.skipChainExpression(node.callee)
4677
if (callee.type === 'MemberExpression') {
4778
const name = utils.getStaticPropertyName(callee)
48-
return (
49-
name &&
50-
// hello.PROMISE_FUNCTION()
51-
(PROMISE_FUNCTIONS.has(name) ||
52-
// Promise.PROMISE_METHOD()
53-
(callee.object.type === 'Identifier' &&
54-
callee.object.name === 'Promise' &&
55-
PROMISE_METHODS.has(name)))
56-
)
79+
if (!name) return false
80+
81+
const isPromiseMethod =
82+
PROMISE_FUNCTIONS.has(name) ||
83+
(callee.object.type === 'Identifier' &&
84+
callee.object.name === 'Promise' &&
85+
PROMISE_METHODS.has(name))
86+
87+
if (!isPromiseMethod) return false
88+
89+
const rootObjectName = getRootObjectName(callee)
90+
if (rootObjectName && ignoredObjectNames.has(rootObjectName)) {
91+
return false
92+
}
93+
94+
return true
5795
}
5896
return false
5997
}
@@ -85,7 +123,20 @@ module.exports = {
85123
url: 'https://eslint.vuejs.org/rules/no-async-in-computed-properties.html'
86124
},
87125
fixable: null,
88-
schema: [],
126+
schema: [
127+
{
128+
type: 'object',
129+
properties: {
130+
ignoredObjectNames: {
131+
type: 'array',
132+
items: { type: 'string' },
133+
uniqueItems: true,
134+
additionalItems: false
135+
}
136+
},
137+
additionalProperties: false
138+
}
139+
],
89140
messages: {
90141
unexpectedInFunction:
91142
'Unexpected {{expressionName}} in computed function.',
@@ -95,6 +146,9 @@ module.exports = {
95146
},
96147
/** @param {RuleContext} context */
97148
create(context) {
149+
const options = context.options[0] || {}
150+
const ignoredObjectNames = new Set(options.ignoredObjectNames || [])
151+
98152
/** @type {Map<ObjectExpression, ComponentComputedProperty[]>} */
99153
const computedPropertiesMap = new Map()
100154
/** @type {(FunctionExpression | ArrowFunctionExpression)[]} */
@@ -217,7 +271,7 @@ module.exports = {
217271
if (!scopeStack) {
218272
return
219273
}
220-
if (isPromise(node)) {
274+
if (isPromise(node, ignoredObjectNames)) {
221275
verify(
222276
node,
223277
scopeStack.body,

tests/lib/rules/no-async-in-computed-properties.js

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,52 @@ ruleTester.run('no-async-in-computed-properties', rule, {
324324
sourceType: 'module',
325325
ecmaVersion: 2020
326326
}
327+
},
328+
{
329+
filename: 'test.vue',
330+
code: `
331+
export default {
332+
computed: {
333+
foo: function () {
334+
return z.catch(
335+
z.string().check(z.minLength(2)),
336+
'default'
337+
).then(val => val).finally(() => {})
338+
}
339+
}
340+
}
341+
`,
342+
options: [{ ignoredObjectNames: ['z'] }],
343+
languageOptions
344+
},
345+
{
346+
filename: 'test.vue',
347+
code: `
348+
<script setup>
349+
import { computed } from 'vue'
350+
351+
const numberWithCatch = computed(() => z.number().catch(42))
352+
</script>`,
353+
options: [{ ignoredObjectNames: ['z'] }],
354+
languageOptions: {
355+
parser,
356+
sourceType: 'module',
357+
ecmaVersion: 2020
358+
}
359+
},
360+
{
361+
filename: 'test.vue',
362+
code: `
363+
export default {
364+
computed: {
365+
foo: function () {
366+
return z.a.b.c.d.e.f.method().catch(err => err).finally(() => {})
367+
}
368+
}
369+
}
370+
`,
371+
options: [{ ignoredObjectNames: ['z'] }],
372+
languageOptions
327373
}
328374
],
329375

@@ -1542,6 +1588,98 @@ ruleTester.run('no-async-in-computed-properties', rule, {
15421588
endColumn: 8
15431589
}
15441590
]
1591+
},
1592+
{
1593+
filename: 'test.vue',
1594+
code: `
1595+
export default {
1596+
computed: {
1597+
foo: function () {
1598+
return myFunc().catch('default')
1599+
}
1600+
}
1601+
}
1602+
`,
1603+
languageOptions,
1604+
errors: [
1605+
{
1606+
message: 'Unexpected asynchronous action in "foo" computed property.',
1607+
line: 5,
1608+
column: 22,
1609+
endLine: 5,
1610+
endColumn: 47
1611+
}
1612+
]
1613+
},
1614+
{
1615+
filename: 'test.vue',
1616+
code: `
1617+
export default {
1618+
computed: {
1619+
foo: function () {
1620+
return z.number().catch(42)
1621+
}
1622+
}
1623+
}
1624+
`,
1625+
languageOptions,
1626+
errors: [
1627+
{
1628+
message: 'Unexpected asynchronous action in "foo" computed property.',
1629+
line: 5,
1630+
column: 22,
1631+
endLine: 5,
1632+
endColumn: 42
1633+
}
1634+
]
1635+
},
1636+
{
1637+
filename: 'test.vue',
1638+
code: `
1639+
export default {
1640+
computed: {
1641+
foo: function () {
1642+
return someLib.string().catch(42)
1643+
}
1644+
}
1645+
}
1646+
`,
1647+
options: [{ ignoredObjectNames: ['z'] }],
1648+
languageOptions,
1649+
errors: [
1650+
{
1651+
message: 'Unexpected asynchronous action in "foo" computed property.',
1652+
line: 5,
1653+
column: 22,
1654+
endLine: 5,
1655+
endColumn: 48
1656+
}
1657+
]
1658+
},
1659+
{
1660+
filename: 'test.vue',
1661+
code: `
1662+
<script setup>
1663+
import {computed} from 'vue'
1664+
1665+
const deepCall = computed(() => z.a.b.c.d().e().f().catch())
1666+
</script>
1667+
`,
1668+
options: [{ ignoredObjectNames: ['a'] }],
1669+
languageOptions: {
1670+
parser,
1671+
sourceType: 'module',
1672+
ecmaVersion: 2020
1673+
},
1674+
errors: [
1675+
{
1676+
message: 'Unexpected asynchronous action in computed function.',
1677+
line: 5,
1678+
column: 41,
1679+
endLine: 5,
1680+
endColumn: 68
1681+
}
1682+
]
15451683
}
15461684
]
15471685
})

0 commit comments

Comments
 (0)