Skip to content

Commit 34a0a55

Browse files
authored
fix(await-async-utils): false positive when destructuring (#722)
1 parent e2c1a6f commit 34a0a55

File tree

2 files changed

+306
-39
lines changed

2 files changed

+306
-39
lines changed

lib/rules/await-async-utils.ts

+97-39
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
import { TSESTree } from '@typescript-eslint/utils';
1+
import { TSESTree, ASTUtils } from '@typescript-eslint/utils';
22

33
import { createTestingLibraryRule } from '../create-testing-library-rule';
44
import {
55
findClosestCallExpressionNode,
6+
getDeepestIdentifierNode,
67
getFunctionName,
78
getInnermostReturningFunction,
89
getVariableReferences,
10+
isObjectPattern,
911
isPromiseHandled,
12+
isProperty,
1013
} from '../node-utils';
1114

1215
export const RULE_NAME = 'await-async-utils';
@@ -47,59 +50,114 @@ export default createTestingLibraryRule<Options, MessageIds>({
4750
}
4851
}
4952

53+
/*
54+
Example:
55+
`const { myAsyncWrapper: myRenamedValue } = someObject`;
56+
Detects `myRenamedValue` and adds it to the known async wrapper names.
57+
*/
58+
function detectDestructuredAsyncUtilWrapperAliases(
59+
node: TSESTree.ObjectPattern
60+
) {
61+
for (const property of node.properties) {
62+
if (!isProperty(property)) {
63+
continue;
64+
}
65+
66+
if (
67+
!ASTUtils.isIdentifier(property.key) ||
68+
!ASTUtils.isIdentifier(property.value)
69+
) {
70+
continue;
71+
}
72+
73+
if (functionWrappersNames.includes(property.key.name)) {
74+
const isDestructuredAsyncWrapperPropertyRenamed =
75+
property.key.name !== property.value.name;
76+
77+
if (isDestructuredAsyncWrapperPropertyRenamed) {
78+
functionWrappersNames.push(property.value.name);
79+
}
80+
}
81+
}
82+
}
83+
84+
/*
85+
Either we report a direct usage of an async util or a usage of a wrapper
86+
around an async util
87+
*/
88+
const getMessageId = (node: TSESTree.Identifier): MessageIds => {
89+
if (helpers.isAsyncUtil(node)) {
90+
return 'awaitAsyncUtil';
91+
}
92+
93+
return 'asyncUtilWrapper';
94+
};
95+
5096
return {
97+
VariableDeclarator(node: TSESTree.VariableDeclarator) {
98+
if (isObjectPattern(node.id)) {
99+
detectDestructuredAsyncUtilWrapperAliases(node.id);
100+
return;
101+
}
102+
103+
const isAssigningKnownAsyncFunctionWrapper =
104+
ASTUtils.isIdentifier(node.id) &&
105+
node.init !== null &&
106+
functionWrappersNames.includes(
107+
getDeepestIdentifierNode(node.init)?.name ?? ''
108+
);
109+
110+
if (isAssigningKnownAsyncFunctionWrapper) {
111+
functionWrappersNames.push((node.id as TSESTree.Identifier).name);
112+
}
113+
},
51114
'CallExpression Identifier'(node: TSESTree.Identifier) {
115+
const isAsyncUtilOrKnownAliasAroundIt =
116+
helpers.isAsyncUtil(node) ||
117+
functionWrappersNames.includes(node.name);
118+
if (!isAsyncUtilOrKnownAliasAroundIt) {
119+
return;
120+
}
121+
122+
// detect async query used within wrapper function for later analysis
52123
if (helpers.isAsyncUtil(node)) {
53-
// detect async query used within wrapper function for later analysis
54124
detectAsyncUtilWrapper(node);
125+
}
55126

56-
const closestCallExpression = findClosestCallExpressionNode(
57-
node,
58-
true
59-
);
127+
const closestCallExpression = findClosestCallExpressionNode(node, true);
60128

61-
if (!closestCallExpression?.parent) {
62-
return;
63-
}
129+
if (!closestCallExpression?.parent) {
130+
return;
131+
}
64132

65-
const references = getVariableReferences(
66-
context,
67-
closestCallExpression.parent
68-
);
133+
const references = getVariableReferences(
134+
context,
135+
closestCallExpression.parent
136+
);
69137

70-
if (references.length === 0) {
71-
if (!isPromiseHandled(node)) {
138+
if (references.length === 0) {
139+
if (!isPromiseHandled(node)) {
140+
context.report({
141+
node,
142+
messageId: getMessageId(node),
143+
data: {
144+
name: node.name,
145+
},
146+
});
147+
}
148+
} else {
149+
for (const reference of references) {
150+
const referenceNode = reference.identifier as TSESTree.Identifier;
151+
if (!isPromiseHandled(referenceNode)) {
72152
context.report({
73153
node,
74-
messageId: 'awaitAsyncUtil',
154+
messageId: getMessageId(node),
75155
data: {
76156
name: node.name,
77157
},
78158
});
159+
return;
79160
}
80-
} else {
81-
for (const reference of references) {
82-
const referenceNode = reference.identifier as TSESTree.Identifier;
83-
if (!isPromiseHandled(referenceNode)) {
84-
context.report({
85-
node,
86-
messageId: 'awaitAsyncUtil',
87-
data: {
88-
name: node.name,
89-
},
90-
});
91-
return;
92-
}
93-
}
94-
}
95-
} else if (functionWrappersNames.includes(node.name)) {
96-
// check async queries used within a wrapper previously detected
97-
if (!isPromiseHandled(node)) {
98-
context.report({
99-
node,
100-
messageId: 'asyncUtilWrapper',
101-
data: { name: node.name },
102-
});
103161
}
104162
}
105163
},

tests/lib/rules/await-async-utils.test.ts

+209
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,40 @@ ruleTester.run(RULE_NAME, rule, {
260260
})
261261
`,
262262
},
263+
...ASYNC_UTILS.map((asyncUtil) => ({
264+
code: `
265+
function setup() {
266+
const utils = render(<MyComponent />);
267+
268+
const waitForAsyncUtil = () => {
269+
return ${asyncUtil}(screen.queryByTestId('my-test-id'));
270+
};
271+
272+
return { waitForAsyncUtil, ...utils };
273+
}
274+
275+
test('destructuring an async function wrapper & handling it later is valid', () => {
276+
const { user, waitForAsyncUtil } = setup();
277+
await waitForAsyncUtil();
278+
279+
const myAlias = waitForAsyncUtil;
280+
const myOtherAlias = myAlias;
281+
await myAlias();
282+
await myOtherAlias();
283+
284+
const { ...clone } = setup();
285+
await clone.waitForAsyncUtil();
286+
287+
const { waitForAsyncUtil: myDestructuredAlias } = setup();
288+
await myDestructuredAlias();
289+
290+
const { user, ...rest } = setup();
291+
await rest.waitForAsyncUtil();
292+
293+
await setup().waitForAsyncUtil();
294+
});
295+
`,
296+
})),
263297
]),
264298
invalid: SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [
265299
...ASYNC_UTILS.map(
@@ -441,6 +475,7 @@ ruleTester.run(RULE_NAME, rule, {
441475
],
442476
} as const)
443477
),
478+
444479
...ASYNC_UTILS.map(
445480
(asyncUtil) =>
446481
({
@@ -463,5 +498,179 @@ ruleTester.run(RULE_NAME, rule, {
463498
],
464499
} as const)
465500
),
501+
...ASYNC_UTILS.map(
502+
(asyncUtil) =>
503+
({
504+
code: `
505+
function setup() {
506+
const utils = render(<MyComponent />);
507+
508+
const waitForAsyncUtil = () => {
509+
return ${asyncUtil}(screen.queryByTestId('my-test-id'));
510+
};
511+
512+
return { waitForAsyncUtil, ...utils };
513+
}
514+
515+
test('unhandled promise from destructed property of async function wrapper is invalid', () => {
516+
const { user, waitForAsyncUtil } = setup();
517+
waitForAsyncUtil();
518+
});
519+
`,
520+
errors: [
521+
{
522+
line: 14,
523+
column: 11,
524+
messageId: 'asyncUtilWrapper',
525+
data: { name: 'waitForAsyncUtil' },
526+
},
527+
],
528+
} as const)
529+
),
530+
...ASYNC_UTILS.map(
531+
(asyncUtil) =>
532+
({
533+
code: `
534+
function setup() {
535+
const utils = render(<MyComponent />);
536+
537+
const waitForAsyncUtil = () => {
538+
return ${asyncUtil}(screen.queryByTestId('my-test-id'));
539+
};
540+
541+
return { waitForAsyncUtil, ...utils };
542+
}
543+
544+
test('unhandled promise from destructed property of async function wrapper is invalid', () => {
545+
const { user, waitForAsyncUtil } = setup();
546+
const myAlias = waitForAsyncUtil;
547+
myAlias();
548+
});
549+
`,
550+
errors: [
551+
{
552+
line: 15,
553+
column: 11,
554+
messageId: 'asyncUtilWrapper',
555+
data: { name: 'myAlias' },
556+
},
557+
],
558+
} as const)
559+
),
560+
...ASYNC_UTILS.map(
561+
(asyncUtil) =>
562+
({
563+
code: `
564+
function setup() {
565+
const utils = render(<MyComponent />);
566+
567+
const waitForAsyncUtil = () => {
568+
return ${asyncUtil}(screen.queryByTestId('my-test-id'));
569+
};
570+
571+
return { waitForAsyncUtil, ...utils };
572+
}
573+
574+
test('unhandled promise from destructed property of async function wrapper is invalid', () => {
575+
const { ...clone } = setup();
576+
clone.waitForAsyncUtil();
577+
});
578+
`,
579+
errors: [
580+
{
581+
line: 14,
582+
column: 17,
583+
messageId: 'asyncUtilWrapper',
584+
data: { name: 'waitForAsyncUtil' },
585+
},
586+
],
587+
} as const)
588+
),
589+
...ASYNC_UTILS.map(
590+
(asyncUtil) =>
591+
({
592+
code: `
593+
function setup() {
594+
const utils = render(<MyComponent />);
595+
596+
const waitForAsyncUtil = () => {
597+
return ${asyncUtil}(screen.queryByTestId('my-test-id'));
598+
};
599+
600+
return { waitForAsyncUtil, ...utils };
601+
}
602+
603+
test('unhandled promise from destructed property of async function wrapper is invalid', () => {
604+
const { waitForAsyncUtil: myAlias } = setup();
605+
myAlias();
606+
});
607+
`,
608+
errors: [
609+
{
610+
line: 14,
611+
column: 11,
612+
messageId: 'asyncUtilWrapper',
613+
data: { name: 'myAlias' },
614+
},
615+
],
616+
} as const)
617+
),
618+
...ASYNC_UTILS.map(
619+
(asyncUtil) =>
620+
({
621+
code: `
622+
function setup() {
623+
const utils = render(<MyComponent />);
624+
625+
const waitForAsyncUtil = () => {
626+
return ${asyncUtil}(screen.queryByTestId('my-test-id'));
627+
};
628+
629+
return { waitForAsyncUtil, ...utils };
630+
}
631+
632+
test('unhandled promise from destructed property of async function wrapper is invalid', () => {
633+
setup().waitForAsyncUtil();
634+
});
635+
`,
636+
errors: [
637+
{
638+
line: 13,
639+
column: 19,
640+
messageId: 'asyncUtilWrapper',
641+
data: { name: 'waitForAsyncUtil' },
642+
},
643+
],
644+
} as const)
645+
),
646+
...ASYNC_UTILS.map(
647+
(asyncUtil) =>
648+
({
649+
code: `
650+
function setup() {
651+
const utils = render(<MyComponent />);
652+
653+
const waitForAsyncUtil = () => {
654+
return ${asyncUtil}(screen.queryByTestId('my-test-id'));
655+
};
656+
657+
return { waitForAsyncUtil, ...utils };
658+
}
659+
660+
test('unhandled promise from destructed property of async function wrapper is invalid', () => {
661+
const myAlias = setup().waitForAsyncUtil;
662+
myAlias();
663+
});
664+
`,
665+
errors: [
666+
{
667+
line: 14,
668+
column: 11,
669+
messageId: 'asyncUtilWrapper',
670+
data: { name: 'myAlias' },
671+
},
672+
],
673+
} as const)
674+
),
466675
]),
467676
});

0 commit comments

Comments
 (0)