Skip to content

Commit

Permalink
fix(await-async-utils): false positive when destructuring (#722)
Browse files Browse the repository at this point in the history
  • Loading branch information
patriscus authored Feb 8, 2023
1 parent e2c1a6f commit 34a0a55
Show file tree
Hide file tree
Showing 2 changed files with 306 additions and 39 deletions.
136 changes: 97 additions & 39 deletions lib/rules/await-async-utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { TSESTree } from '@typescript-eslint/utils';
import { TSESTree, ASTUtils } from '@typescript-eslint/utils';

import { createTestingLibraryRule } from '../create-testing-library-rule';
import {
findClosestCallExpressionNode,
getDeepestIdentifierNode,
getFunctionName,
getInnermostReturningFunction,
getVariableReferences,
isObjectPattern,
isPromiseHandled,
isProperty,
} from '../node-utils';

export const RULE_NAME = 'await-async-utils';
Expand Down Expand Up @@ -47,59 +50,114 @@ export default createTestingLibraryRule<Options, MessageIds>({
}
}

/*
Example:
`const { myAsyncWrapper: myRenamedValue } = someObject`;
Detects `myRenamedValue` and adds it to the known async wrapper names.
*/
function detectDestructuredAsyncUtilWrapperAliases(
node: TSESTree.ObjectPattern
) {
for (const property of node.properties) {
if (!isProperty(property)) {
continue;
}

if (
!ASTUtils.isIdentifier(property.key) ||
!ASTUtils.isIdentifier(property.value)
) {
continue;
}

if (functionWrappersNames.includes(property.key.name)) {
const isDestructuredAsyncWrapperPropertyRenamed =
property.key.name !== property.value.name;

if (isDestructuredAsyncWrapperPropertyRenamed) {
functionWrappersNames.push(property.value.name);
}
}
}
}

/*
Either we report a direct usage of an async util or a usage of a wrapper
around an async util
*/
const getMessageId = (node: TSESTree.Identifier): MessageIds => {
if (helpers.isAsyncUtil(node)) {
return 'awaitAsyncUtil';
}

return 'asyncUtilWrapper';
};

return {
VariableDeclarator(node: TSESTree.VariableDeclarator) {
if (isObjectPattern(node.id)) {
detectDestructuredAsyncUtilWrapperAliases(node.id);
return;
}

const isAssigningKnownAsyncFunctionWrapper =
ASTUtils.isIdentifier(node.id) &&
node.init !== null &&
functionWrappersNames.includes(
getDeepestIdentifierNode(node.init)?.name ?? ''
);

if (isAssigningKnownAsyncFunctionWrapper) {
functionWrappersNames.push((node.id as TSESTree.Identifier).name);
}
},
'CallExpression Identifier'(node: TSESTree.Identifier) {
const isAsyncUtilOrKnownAliasAroundIt =
helpers.isAsyncUtil(node) ||
functionWrappersNames.includes(node.name);
if (!isAsyncUtilOrKnownAliasAroundIt) {
return;
}

// detect async query used within wrapper function for later analysis
if (helpers.isAsyncUtil(node)) {
// detect async query used within wrapper function for later analysis
detectAsyncUtilWrapper(node);
}

const closestCallExpression = findClosestCallExpressionNode(
node,
true
);
const closestCallExpression = findClosestCallExpressionNode(node, true);

if (!closestCallExpression?.parent) {
return;
}
if (!closestCallExpression?.parent) {
return;
}

const references = getVariableReferences(
context,
closestCallExpression.parent
);
const references = getVariableReferences(
context,
closestCallExpression.parent
);

if (references.length === 0) {
if (!isPromiseHandled(node)) {
if (references.length === 0) {
if (!isPromiseHandled(node)) {
context.report({
node,
messageId: getMessageId(node),
data: {
name: node.name,
},
});
}
} else {
for (const reference of references) {
const referenceNode = reference.identifier as TSESTree.Identifier;
if (!isPromiseHandled(referenceNode)) {
context.report({
node,
messageId: 'awaitAsyncUtil',
messageId: getMessageId(node),
data: {
name: node.name,
},
});
return;
}
} else {
for (const reference of references) {
const referenceNode = reference.identifier as TSESTree.Identifier;
if (!isPromiseHandled(referenceNode)) {
context.report({
node,
messageId: 'awaitAsyncUtil',
data: {
name: node.name,
},
});
return;
}
}
}
} else if (functionWrappersNames.includes(node.name)) {
// check async queries used within a wrapper previously detected
if (!isPromiseHandled(node)) {
context.report({
node,
messageId: 'asyncUtilWrapper',
data: { name: node.name },
});
}
}
},
Expand Down
209 changes: 209 additions & 0 deletions tests/lib/rules/await-async-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,40 @@ ruleTester.run(RULE_NAME, rule, {
})
`,
},
...ASYNC_UTILS.map((asyncUtil) => ({
code: `
function setup() {
const utils = render(<MyComponent />);
const waitForAsyncUtil = () => {
return ${asyncUtil}(screen.queryByTestId('my-test-id'));
};
return { waitForAsyncUtil, ...utils };
}
test('destructuring an async function wrapper & handling it later is valid', () => {
const { user, waitForAsyncUtil } = setup();
await waitForAsyncUtil();
const myAlias = waitForAsyncUtil;
const myOtherAlias = myAlias;
await myAlias();
await myOtherAlias();
const { ...clone } = setup();
await clone.waitForAsyncUtil();
const { waitForAsyncUtil: myDestructuredAlias } = setup();
await myDestructuredAlias();
const { user, ...rest } = setup();
await rest.waitForAsyncUtil();
await setup().waitForAsyncUtil();
});
`,
})),
]),
invalid: SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [
...ASYNC_UTILS.map(
Expand Down Expand Up @@ -441,6 +475,7 @@ ruleTester.run(RULE_NAME, rule, {
],
} as const)
),

...ASYNC_UTILS.map(
(asyncUtil) =>
({
Expand All @@ -463,5 +498,179 @@ ruleTester.run(RULE_NAME, rule, {
],
} as const)
),
...ASYNC_UTILS.map(
(asyncUtil) =>
({
code: `
function setup() {
const utils = render(<MyComponent />);
const waitForAsyncUtil = () => {
return ${asyncUtil}(screen.queryByTestId('my-test-id'));
};
return { waitForAsyncUtil, ...utils };
}
test('unhandled promise from destructed property of async function wrapper is invalid', () => {
const { user, waitForAsyncUtil } = setup();
waitForAsyncUtil();
});
`,
errors: [
{
line: 14,
column: 11,
messageId: 'asyncUtilWrapper',
data: { name: 'waitForAsyncUtil' },
},
],
} as const)
),
...ASYNC_UTILS.map(
(asyncUtil) =>
({
code: `
function setup() {
const utils = render(<MyComponent />);
const waitForAsyncUtil = () => {
return ${asyncUtil}(screen.queryByTestId('my-test-id'));
};
return { waitForAsyncUtil, ...utils };
}
test('unhandled promise from destructed property of async function wrapper is invalid', () => {
const { user, waitForAsyncUtil } = setup();
const myAlias = waitForAsyncUtil;
myAlias();
});
`,
errors: [
{
line: 15,
column: 11,
messageId: 'asyncUtilWrapper',
data: { name: 'myAlias' },
},
],
} as const)
),
...ASYNC_UTILS.map(
(asyncUtil) =>
({
code: `
function setup() {
const utils = render(<MyComponent />);
const waitForAsyncUtil = () => {
return ${asyncUtil}(screen.queryByTestId('my-test-id'));
};
return { waitForAsyncUtil, ...utils };
}
test('unhandled promise from destructed property of async function wrapper is invalid', () => {
const { ...clone } = setup();
clone.waitForAsyncUtil();
});
`,
errors: [
{
line: 14,
column: 17,
messageId: 'asyncUtilWrapper',
data: { name: 'waitForAsyncUtil' },
},
],
} as const)
),
...ASYNC_UTILS.map(
(asyncUtil) =>
({
code: `
function setup() {
const utils = render(<MyComponent />);
const waitForAsyncUtil = () => {
return ${asyncUtil}(screen.queryByTestId('my-test-id'));
};
return { waitForAsyncUtil, ...utils };
}
test('unhandled promise from destructed property of async function wrapper is invalid', () => {
const { waitForAsyncUtil: myAlias } = setup();
myAlias();
});
`,
errors: [
{
line: 14,
column: 11,
messageId: 'asyncUtilWrapper',
data: { name: 'myAlias' },
},
],
} as const)
),
...ASYNC_UTILS.map(
(asyncUtil) =>
({
code: `
function setup() {
const utils = render(<MyComponent />);
const waitForAsyncUtil = () => {
return ${asyncUtil}(screen.queryByTestId('my-test-id'));
};
return { waitForAsyncUtil, ...utils };
}
test('unhandled promise from destructed property of async function wrapper is invalid', () => {
setup().waitForAsyncUtil();
});
`,
errors: [
{
line: 13,
column: 19,
messageId: 'asyncUtilWrapper',
data: { name: 'waitForAsyncUtil' },
},
],
} as const)
),
...ASYNC_UTILS.map(
(asyncUtil) =>
({
code: `
function setup() {
const utils = render(<MyComponent />);
const waitForAsyncUtil = () => {
return ${asyncUtil}(screen.queryByTestId('my-test-id'));
};
return { waitForAsyncUtil, ...utils };
}
test('unhandled promise from destructed property of async function wrapper is invalid', () => {
const myAlias = setup().waitForAsyncUtil;
myAlias();
});
`,
errors: [
{
line: 14,
column: 11,
messageId: 'asyncUtilWrapper',
data: { name: 'myAlias' },
},
],
} as const)
),
]),
});

0 comments on commit 34a0a55

Please sign in to comment.