Skip to content

Commit

Permalink
fix(await-async-events): false positive reports on awaited expression…
Browse files Browse the repository at this point in the history
…s evaluating to promise (#890)
  • Loading branch information
Chamion committed Apr 12, 2024
1 parent 6b39e60 commit 767f1be
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 24 deletions.
71 changes: 47 additions & 24 deletions lib/node-utils/index.ts
Expand Up @@ -222,40 +222,63 @@ export function isPromiseHandled(nodeIdentifier: TSESTree.Identifier): boolean {
nodeIdentifier,
true
);
const callRootExpression =
closestCallExpressionNode == null
? null
: getRootExpression(closestCallExpressionNode);

const suspiciousNodes = [nodeIdentifier, closestCallExpressionNode].filter(
Boolean
const suspiciousNodes = [nodeIdentifier, callRootExpression].filter(
(node): node is NonNullable<typeof node> => node != null
);

for (const node of suspiciousNodes) {
if (!node?.parent) {
continue;
}
if (ASTUtils.isAwaitExpression(node.parent)) {
return true;
}

return suspiciousNodes.some((node) => {
if (!node.parent) return false;
if (ASTUtils.isAwaitExpression(node.parent)) return true;
if (
isArrowFunctionExpression(node.parent) ||
isReturnStatement(node.parent)
) {
return true;
}

if (hasClosestExpectResolvesRejects(node.parent)) {
return true;
}

if (hasChainedThen(node)) {
)
return true;
}
if (hasClosestExpectResolvesRejects(node.parent)) return true;
if (hasChainedThen(node)) return true;
if (isPromisesArrayResolved(node)) return true;
});
}

if (isPromisesArrayResolved(node)) {
return true;
/**
* For an expression in a parent that evaluates to the expression or another child returns the parent node recursively.
*/
function getRootExpression(
expression: TSESTree.Expression
): TSESTree.Expression {
const { parent } = expression;
if (parent == null) return expression;
switch (parent.type) {
case AST_NODE_TYPES.ConditionalExpression:
return getRootExpression(parent);
case AST_NODE_TYPES.LogicalExpression: {
let rootExpression;
switch (parent.operator) {
case '??':
case '||':
rootExpression = getRootExpression(parent);
break;
case '&&':
rootExpression =
parent.right === expression
? getRootExpression(parent)
: expression;
break;
}
return rootExpression ?? expression;
}
case AST_NODE_TYPES.SequenceExpression:
return parent.expressions[parent.expressions.length - 1] === expression
? getRootExpression(parent)
: expression;
default:
return expression;
}

return false;
}

export function getVariableReferences(
Expand Down
99 changes: 99 additions & 0 deletions tests/lib/rules/await-async-events.test.ts
Expand Up @@ -311,6 +311,19 @@ ruleTester.run(RULE_NAME, rule, {
await triggerEvent()
})
`,
options: [{ eventModule: 'userEvent' }] as const,
})),
...USER_EVENT_ASYNC_FUNCTIONS.map((eventMethod) => ({
code: `
import userEvent from '${testingFramework}'
test('await expression that evaluates to promise is valid', async () => {
await (null, userEvent.${eventMethod}(getByLabelText('username')));
await (condition ? null : userEvent.${eventMethod}(getByLabelText('username')));
await (condition && userEvent.${eventMethod}(getByLabelText('username')));
await (userEvent.${eventMethod}(getByLabelText('username')) || userEvent.${eventMethod}(getByLabelText('username')));
await (userEvent.${eventMethod}(getByLabelText('username')) ?? userEvent.${eventMethod}(getByLabelText('username')));
})
`,
options: [{ eventModule: 'userEvent' }] as const,
})),
Expand Down Expand Up @@ -960,6 +973,92 @@ ruleTester.run(RULE_NAME, rule, {
}
triggerEvent()
`,
} as const)
),
...USER_EVENT_ASYNC_FUNCTIONS.map(
(eventMethod) =>
({
code: `
import userEvent from '${testingFramework}'
test('unhandled expression that evaluates to promise is invalid', () => {
condition ? null : (null, true && userEvent.${eventMethod}(getByLabelText('username')));
});
`,
errors: [
{
line: 4,
column: 38,
messageId: 'awaitAsyncEvent',
data: { name: eventMethod },
},
],
options: [{ eventModule: 'userEvent' }],
output: `
import userEvent from '${testingFramework}'
test('unhandled expression that evaluates to promise is invalid', async () => {
condition ? null : (null, true && await userEvent.${eventMethod}(getByLabelText('username')));
});
`,
} as const)
),
...USER_EVENT_ASYNC_FUNCTIONS.map(
(eventMethod) =>
({
code: `
import userEvent from '${testingFramework}'
test('handled AND expression with left promise is invalid', async () => {
await (userEvent.${eventMethod}(getByLabelText('username')) && userEvent.${eventMethod}(getByLabelText('username')));
});
`,
errors: [
{
line: 4,
column: 11,
messageId: 'awaitAsyncEvent',
data: { name: eventMethod },
},
],
options: [{ eventModule: 'userEvent' }],
output: `
import userEvent from '${testingFramework}'
test('handled AND expression with left promise is invalid', async () => {
await (await userEvent.${eventMethod}(getByLabelText('username')) && userEvent.${eventMethod}(getByLabelText('username')));
});
`,
} as const)
),
...USER_EVENT_ASYNC_FUNCTIONS.map(
(eventMethod) =>
({
code: `
import userEvent from '${testingFramework}'
test('voided promise is invalid', async () => {
await void userEvent.${eventMethod}(getByLabelText('username'));
await (userEvent.${eventMethod}(getByLabelText('username')), null);
});
`,
errors: [
{
line: 4,
column: 15,
messageId: 'awaitAsyncEvent',
data: { name: eventMethod },
},
{
line: 5,
column: 11,
messageId: 'awaitAsyncEvent',
data: { name: eventMethod },
},
],
options: [{ eventModule: 'userEvent' }],
output: `
import userEvent from '${testingFramework}'
test('voided promise is invalid', async () => {
await void await userEvent.${eventMethod}(getByLabelText('username'));
await (await userEvent.${eventMethod}(getByLabelText('username')), null);
});
`,
} as const)
),
Expand Down
27 changes: 27 additions & 0 deletions tests/lib/rules/await-async-utils.test.ts
Expand Up @@ -418,6 +418,33 @@ ruleTester.run(RULE_NAME, rule, {
doSomethingElse(aPromise);
${asyncUtil}(() => getByLabelText('email'));
});
`,
errors: [
{
line: 4,
column: 28,
messageId: 'awaitAsyncUtil',
data: { name: asyncUtil },
},
{
line: 6,
column: 11,
messageId: 'awaitAsyncUtil',
data: { name: asyncUtil },
},
],
} as const)
),
...ASYNC_UTILS.map(
(asyncUtil) =>
({
code: `
import { ${asyncUtil} } from '${testingFramework}';
test('unhandled expression that evaluates to promise is invalid', () => {
const aPromise = ${asyncUtil}(() => getByLabelText('username'));
doSomethingElse(aPromise);
${asyncUtil}(() => getByLabelText('email'));
});
`,
errors: [
{
Expand Down

0 comments on commit 767f1be

Please sign in to comment.