Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 79d6a31

Browse files
waynzhFloEdelmann
andauthored
Added ignoredObjectNames option to vue/no-async-in-computed-properties (#2927)
Co-authored-by: Flo Edelmann <git@flo-edelmann.de>
1 parent 3e122e5 commit 79d6a31

File tree

4 files changed

+294
-13
lines changed

4 files changed

+294
-13
lines changed

‎.changeset/cute-bears-sneeze.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'eslint-plugin-vue': minor
3+
---
4+
5+
Added `ignoredObjectNames` option to `vue/no-async-in-computed-properties`

‎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 (e.g. [`z.catch()`](https://zod.dev/api#catch)).
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: 94 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,22 +38,88 @@ function isTimedFunction(node) {
3838
)
3939
}
4040

41+
/**
42+
* @param {*} node
43+
* @returns {*}
44+
*/
45+
function skipWrapper(node) {
46+
while (node && node.expression) {
47+
node = node.expression
48+
}
49+
return node
50+
}
51+
52+
/**
53+
* Get the root object name from a member expression chain
54+
* @param {MemberExpression} memberExpr
55+
* @returns {string|null}
56+
*/
57+
function getRootObjectName(memberExpr) {
58+
let current = skipWrapper(memberExpr.object)
59+
60+
while (current) {
61+
switch (current.type) {
62+
case 'MemberExpression': {
63+
current = skipWrapper(current.object)
64+
break
65+
}
66+
case 'CallExpression': {
67+
const calleeExpr = skipWrapper(current.callee)
68+
if (calleeExpr.type === 'MemberExpression') {
69+
current = skipWrapper(calleeExpr.object)
70+
} else if (calleeExpr.type === 'Identifier') {
71+
return calleeExpr.name
72+
} else {
73+
return null
74+
}
75+
break
76+
}
77+
case 'Identifier': {
78+
return current.name
79+
}
80+
default: {
81+
return null
82+
}
83+
}
84+
}
85+
86+
return null
87+
}
88+
89+
/**
90+
* @param {string} name
91+
* @param {*} callee
92+
* @returns {boolean}
93+
*/
94+
function isPromiseMethod(name, callee) {
95+
return (
96+
// hello.PROMISE_FUNCTION()
97+
PROMISE_FUNCTIONS.has(name) ||
98+
// Promise.PROMISE_METHOD()
99+
(callee.object.type === 'Identifier' &&
100+
callee.object.name === 'Promise' &&
101+
PROMISE_METHODS.has(name))
102+
)
103+
}
104+
41105
/**
42106
* @param {CallExpression} node
107+
* @param {Set<string>} ignoredObjectNames
43108
*/
44-
function isPromise(node) {
109+
function isPromise(node,ignoredObjectNames) {
45110
const callee = utils.skipChainExpression(node.callee)
46111
if (callee.type === 'MemberExpression') {
47112
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-
)
113+
if (!name || !isPromiseMethod(name, callee)) {
114+
return false
115+
}
116+
117+
const rootObjectName = getRootObjectName(callee)
118+
if (rootObjectName && ignoredObjectNames.has(rootObjectName)) {
119+
return false
120+
}
121+
122+
return true
57123
}
58124
return false
59125
}
@@ -85,7 +151,20 @@ module.exports = {
85151
url: 'https://eslint.vuejs.org/rules/no-async-in-computed-properties.html'
86152
},
87153
fixable: null,
88-
schema: [],
154+
schema: [
155+
{
156+
type: 'object',
157+
properties: {
158+
ignoredObjectNames: {
159+
type: 'array',
160+
items: { type: 'string' },
161+
uniqueItems: true,
162+
additionalItems: false
163+
}
164+
},
165+
additionalProperties: false
166+
}
167+
],
89168
messages: {
90169
unexpectedInFunction:
91170
'Unexpected {{expressionName}} in computed function.',
@@ -95,6 +174,9 @@ module.exports = {
95174
},
96175
/** @param {RuleContext} context */
97176
create(context) {
177+
const options = context.options[0] || {}
178+
const ignoredObjectNames = new Set(options.ignoredObjectNames || [])
179+
98180
/** @type {Map<ObjectExpression, ComponentComputedProperty[]>} */
99181
const computedPropertiesMap = new Map()
100182
/** @type {(FunctionExpression | ArrowFunctionExpression)[]} */
@@ -217,7 +299,7 @@ module.exports = {
217299
if (!scopeStack) {
218300
return
219301
}
220-
if (isPromise(node)) {
302+
if (isPromise(node,ignoredObjectNames)) {
221303
verify(
222304
node,
223305
scopeStack.body,

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

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,73 @@ 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.method().catch(err => err).finally(() => {})
367+
}
368+
}
369+
}
370+
`,
371+
options: [{ ignoredObjectNames: ['z'] }],
372+
languageOptions: {
373+
parser,
374+
sourceType: 'module',
375+
ecmaVersion: 2020
376+
}
377+
},
378+
{
379+
filename: 'test.vue',
380+
code: `
381+
<script setup lang="ts">
382+
import { computed } from 'vue'
383+
import { z } from 'zod'
384+
385+
const foo = computed(() => z.a?.['b'].c!.d.method().catch(err => err).finally(() => {}))
386+
</script>`,
387+
options: [{ ignoredObjectNames: ['z'] }],
388+
languageOptions: {
389+
parser: require('vue-eslint-parser'),
390+
parserOptions: {
391+
parser: require.resolve('@typescript-eslint/parser')
392+
}
393+
}
327394
}
328395
],
329396

@@ -1542,6 +1609,98 @@ ruleTester.run('no-async-in-computed-properties', rule, {
15421609
endColumn: 8
15431610
}
15441611
]
1612+
},
1613+
{
1614+
filename: 'test.vue',
1615+
code: `
1616+
export default {
1617+
computed: {
1618+
foo: function () {
1619+
return myFunc().catch('default')
1620+
}
1621+
}
1622+
}
1623+
`,
1624+
languageOptions,
1625+
errors: [
1626+
{
1627+
message: 'Unexpected asynchronous action in "foo" computed property.',
1628+
line: 5,
1629+
column: 22,
1630+
endLine: 5,
1631+
endColumn: 47
1632+
}
1633+
]
1634+
},
1635+
{
1636+
filename: 'test.vue',
1637+
code: `
1638+
export default {
1639+
computed: {
1640+
foo: function () {
1641+
return z.number().catch(42)
1642+
}
1643+
}
1644+
}
1645+
`,
1646+
languageOptions,
1647+
errors: [
1648+
{
1649+
message: 'Unexpected asynchronous action in "foo" computed property.',
1650+
line: 5,
1651+
column: 22,
1652+
endLine: 5,
1653+
endColumn: 42
1654+
}
1655+
]
1656+
},
1657+
{
1658+
filename: 'test.vue',
1659+
code: `
1660+
export default {
1661+
computed: {
1662+
foo: function () {
1663+
return someLib.string().catch(42)
1664+
}
1665+
}
1666+
}
1667+
`,
1668+
options: [{ ignoredObjectNames: ['z'] }],
1669+
languageOptions,
1670+
errors: [
1671+
{
1672+
message: 'Unexpected asynchronous action in "foo" computed property.',
1673+
line: 5,
1674+
column: 22,
1675+
endLine: 5,
1676+
endColumn: 48
1677+
}
1678+
]
1679+
},
1680+
{
1681+
filename: 'test.vue',
1682+
code: `
1683+
<script setup>
1684+
import {computed} from 'vue'
1685+
1686+
const deepCall = computed(() => z.a.b.c.d().e().f().catch())
1687+
</script>
1688+
`,
1689+
options: [{ ignoredObjectNames: ['a'] }],
1690+
languageOptions: {
1691+
parser,
1692+
sourceType: 'module',
1693+
ecmaVersion: 2020
1694+
},
1695+
errors: [
1696+
{
1697+
message: 'Unexpected asynchronous action in computed function.',
1698+
line: 5,
1699+
column: 41,
1700+
endLine: 5,
1701+
endColumn: 68
1702+
}
1703+
]
15451704
}
15461705
]
15471706
})

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /