Skip to content

Commit e14c293

Browse files
committed
refactor: collect settling expressions in single AST pass
1 parent 647d457 commit e14c293

1 file changed

Lines changed: 65 additions & 71 deletions

File tree

src/rules/no-unsettled-absence-query.ts

Lines changed: 65 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { ASTUtils } from '@typescript-eslint/utils';
2-
31
import { createTestingLibraryRule } from '../create-testing-library-rule';
42
import {
53
findClosestCallNode,
@@ -21,52 +19,6 @@ export type Options = [];
2119

2220
const NEGATED_ABSENCE_MATCHERS = ['toBeVisible'];
2321

24-
function isNestedFunction(node: TSESTree.Node): boolean {
25-
return (
26-
isArrowFunctionExpression(node) ||
27-
isFunctionExpression(node) ||
28-
isFunctionDeclaration(node)
29-
);
30-
}
31-
32-
function containsNode(
33-
node: TSESTree.Node,
34-
predicate: (n: TSESTree.Node) => boolean
35-
): boolean {
36-
if (predicate(node)) {
37-
return true;
38-
}
39-
40-
if (isNestedFunction(node)) {
41-
return false;
42-
}
43-
44-
for (const key of Object.keys(node)) {
45-
if (key === 'parent') continue;
46-
const child = (node as unknown as Record<string, unknown>)[key];
47-
if (child && typeof child === 'object') {
48-
if (Array.isArray(child)) {
49-
for (const item of child) {
50-
if (
51-
item &&
52-
typeof item === 'object' &&
53-
'type' in item &&
54-
containsNode(item as TSESTree.Node, predicate)
55-
) {
56-
return true;
57-
}
58-
}
59-
} else if (
60-
'type' in child &&
61-
containsNode(child as TSESTree.Node, predicate)
62-
) {
63-
return true;
64-
}
65-
}
66-
}
67-
return false;
68-
}
69-
7022
export default createTestingLibraryRule<Options, MessageIds>({
7123
name: RULE_NAME,
7224
meta: {
@@ -93,6 +45,13 @@ export default createTestingLibraryRule<Options, MessageIds>({
9345
defaultOptions: [],
9446

9547
create(context, _, helpers) {
48+
const earliestSettlingIndex = new WeakMap<TSESTree.Statement[], number>();
49+
const candidates: Array<{
50+
identifier: TSESTree.Identifier;
51+
body: TSESTree.Statement[];
52+
stmtIndex: number;
53+
}> = [];
54+
9655
function isAbsenceAssertion(node: TSESTree.MemberExpression): boolean {
9756
if (helpers.isAbsenceAssert(node)) {
9857
return true;
@@ -148,6 +107,24 @@ export default createTestingLibraryRule<Options, MessageIds>({
148107
return null;
149108
}
150109

110+
function findImmediateFunctionBody(
111+
node: TSESTree.Node
112+
): TSESTree.Statement[] | null {
113+
let current: TSESTree.Node | undefined = node.parent;
114+
115+
while (current) {
116+
if (
117+
isArrowFunctionExpression(current) ||
118+
isFunctionExpression(current) ||
119+
isFunctionDeclaration(current)
120+
) {
121+
return isBlockStatement(current.body) ? current.body.body : null;
122+
}
123+
current = current.parent;
124+
}
125+
return null;
126+
}
127+
151128
function findAncestorStatement(
152129
node: TSESTree.Node,
153130
statements: TSESTree.Statement[]
@@ -162,19 +139,28 @@ export default createTestingLibraryRule<Options, MessageIds>({
162139
return null;
163140
}
164141

165-
function hasSettlingExpression(statement: TSESTree.Statement): boolean {
166-
const hasAwait = containsNode(statement, (n) =>
167-
ASTUtils.isAwaitExpression(n)
168-
);
169-
const hasGetQuery = containsNode(
170-
statement,
171-
(n) => ASTUtils.isIdentifier(n) && helpers.isGetQueryVariant(n)
172-
);
173-
return hasAwait || hasGetQuery;
142+
function recordSettling(node: TSESTree.Node): void {
143+
const body = findImmediateFunctionBody(node);
144+
if (!body) return;
145+
const stmt = findAncestorStatement(node, body);
146+
if (!stmt) return;
147+
const stmtIndex = body.indexOf(stmt);
148+
const prev = earliestSettlingIndex.get(body);
149+
if (prev === undefined || stmtIndex < prev) {
150+
earliestSettlingIndex.set(body, stmtIndex);
151+
}
174152
}
175153

176154
return {
155+
AwaitExpression(node: TSESTree.AwaitExpression) {
156+
recordSettling(node);
157+
},
177158
'CallExpression Identifier'(node: TSESTree.Identifier) {
159+
if (helpers.isGetQueryVariant(node)) {
160+
recordSettling(node);
161+
return;
162+
}
163+
178164
if (!helpers.isQueryQueryVariant(node)) {
179165
return;
180166
}
@@ -200,26 +186,34 @@ export default createTestingLibraryRule<Options, MessageIds>({
200186
return;
201187
}
202188

203-
const functionBody = findEnclosingFunctionBody(node);
204-
if (!functionBody) {
189+
const body = findEnclosingFunctionBody(node);
190+
if (!body) {
205191
return;
206192
}
207193

208-
const containingStatement = findAncestorStatement(node, functionBody);
209-
if (!containingStatement) {
194+
const stmt = findAncestorStatement(node, body);
195+
if (!stmt) {
210196
return;
211197
}
212198

213-
const stmtIndex = functionBody.indexOf(containingStatement);
214-
const precedingStatements = functionBody.slice(0, stmtIndex);
215-
const hasSettled = precedingStatements.some(hasSettlingExpression);
216-
217-
if (!hasSettled) {
218-
context.report({
219-
node,
220-
messageId: 'noUnsettledAbsenceQuery',
221-
data: { queryMethod: node.name },
222-
});
199+
candidates.push({
200+
identifier: node,
201+
body,
202+
stmtIndex: body.indexOf(stmt),
203+
});
204+
},
205+
'Program:exit'() {
206+
for (const candidate of candidates) {
207+
const earliest = earliestSettlingIndex.get(candidate.body);
208+
const settled =
209+
earliest !== undefined && earliest < candidate.stmtIndex;
210+
if (!settled) {
211+
context.report({
212+
node: candidate.identifier,
213+
messageId: 'noUnsettledAbsenceQuery',
214+
data: { queryMethod: candidate.identifier.name },
215+
});
216+
}
223217
}
224218
},
225219
};

0 commit comments

Comments
 (0)