Skip to content

Commit 6018dd1

Browse files
authored
refactor: detection helpers tweaks (#254)
* refactor(extract helpers for detecting presence/absence assets): add fake rule tests for queries * refactor(prefer-presence-queries): use presence/absence helpers * refactor: rename boolean detection helpers * refactor: create helpers as separated functions
1 parent b48b286 commit 6018dd1

File tree

1 file changed

+151
-135
lines changed

1 file changed

+151
-135
lines changed

lib/detect-testing-library-utils.ts

+151-135
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ export type DetectionHelpers = {
4444
getCustomModuleImportNode: () => ImportModuleNode | null;
4545
getTestingLibraryImportName: () => string | undefined;
4646
getCustomModuleImportName: () => string | undefined;
47-
getIsTestingLibraryImported: () => boolean;
48-
getIsValidFilename: () => boolean;
47+
isTestingLibraryImported: () => boolean;
48+
isValidFilename: () => boolean;
4949
isGetByQuery: (node: TSESTree.Identifier) => boolean;
5050
isQueryByQuery: (node: TSESTree.Identifier) => boolean;
5151
isSyncQuery: (node: TSESTree.Identifier) => boolean;
@@ -81,153 +81,169 @@ export function detectTestingLibraryUtils<
8181
DEFAULT_FILENAME_PATTERN;
8282

8383
// Helpers for Testing Library detection.
84-
const helpers: DetectionHelpers = {
85-
getTestingLibraryImportNode() {
86-
return importedTestingLibraryNode;
87-
},
88-
getCustomModuleImportNode() {
89-
return importedCustomModuleNode;
90-
},
91-
getTestingLibraryImportName() {
92-
return getImportModuleName(importedTestingLibraryNode);
93-
},
94-
getCustomModuleImportName() {
95-
return getImportModuleName(importedCustomModuleNode);
96-
},
97-
/**
98-
* Determines whether Testing Library utils are imported or not for
99-
* current file being analyzed.
100-
*
101-
* By default, it is ALWAYS considered as imported. This is what we call
102-
* "aggressive reporting" so we don't miss TL utils reexported from
103-
* custom modules.
104-
*
105-
* However, there is a setting to customize the module where TL utils can
106-
* be imported from: "testing-library/module". If this setting is enabled,
107-
* then this method will return `true` ONLY IF a testing-library package
108-
* or custom module are imported.
109-
*/
110-
getIsTestingLibraryImported() {
111-
if (!customModule) {
112-
return true;
113-
}
84+
const getTestingLibraryImportNode: DetectionHelpers['getTestingLibraryImportNode'] = () => {
85+
return importedTestingLibraryNode;
86+
};
11487

115-
return !!importedTestingLibraryNode || !!importedCustomModuleNode;
116-
},
88+
const getCustomModuleImportNode: DetectionHelpers['getCustomModuleImportNode'] = () => {
89+
return importedCustomModuleNode;
90+
};
11791

118-
/**
119-
* Determines whether filename is valid or not for current file
120-
* being analyzed based on "testing-library/filename-pattern" setting.
121-
*/
122-
getIsValidFilename() {
123-
const fileName = context.getFilename();
124-
return !!fileName.match(filenamePattern);
125-
},
92+
const getTestingLibraryImportName: DetectionHelpers['getTestingLibraryImportName'] = () => {
93+
return getImportModuleName(importedTestingLibraryNode);
94+
};
12695

127-
/**
128-
* Determines whether a given node is `getBy*` or `getAllBy*` query variant or not.
129-
*/
130-
isGetByQuery(node) {
131-
return !!node.name.match(/^get(All)?By.+$/);
132-
},
96+
const getCustomModuleImportName: DetectionHelpers['getCustomModuleImportName'] = () => {
97+
return getImportModuleName(importedCustomModuleNode);
98+
};
99+
/**
100+
* Determines whether Testing Library utils are imported or not for
101+
* current file being analyzed.
102+
*
103+
* By default, it is ALWAYS considered as imported. This is what we call
104+
* "aggressive reporting" so we don't miss TL utils reexported from
105+
* custom modules.
106+
*
107+
* However, there is a setting to customize the module where TL utils can
108+
* be imported from: "testing-library/module". If this setting is enabled,
109+
* then this method will return `true` ONLY IF a testing-library package
110+
* or custom module are imported.
111+
*/
112+
const isTestingLibraryImported: DetectionHelpers['isTestingLibraryImported'] = () => {
113+
if (!customModule) {
114+
return true;
115+
}
133116

134-
/**
135-
* Determines whether a given node is `queryBy*` or `queryAllBy*` query variant or not.
136-
*/
137-
isQueryByQuery(node) {
138-
return !!node.name.match(/^query(All)?By.+$/);
139-
},
117+
return !!importedTestingLibraryNode || !!importedCustomModuleNode;
118+
};
140119

141-
/**
142-
* Determines whether a given node is sync query or not.
143-
*/
144-
isSyncQuery(node) {
145-
return this.isGetByQuery(node) || this.isQueryByQuery(node);
146-
},
120+
/**
121+
* Determines whether filename is valid or not for current file
122+
* being analyzed based on "testing-library/filename-pattern" setting.
123+
*/
124+
const isValidFilename: DetectionHelpers['isValidFilename'] = () => {
125+
const fileName = context.getFilename();
126+
return !!fileName.match(filenamePattern);
127+
};
147128

148-
/**
149-
* Determines whether a given MemberExpression node is a presence assert
150-
*
151-
* Presence asserts could have shape of:
152-
* - expect(element).toBeInTheDocument()
153-
* - expect(element).not.toBeNull()
154-
*/
155-
isPresenceAssert(node) {
156-
const { matcher, isNegated } = getAssertNodeInfo(node);
129+
/**
130+
* Determines whether a given node is `getBy*` or `getAllBy*` query variant or not.
131+
*/
132+
const isGetByQuery: DetectionHelpers['isGetByQuery'] = (node) => {
133+
return !!node.name.match(/^get(All)?By.+$/);
134+
};
157135

158-
if (!matcher) {
159-
return false;
160-
}
136+
/**
137+
* Determines whether a given node is `queryBy*` or `queryAllBy*` query variant or not.
138+
*/
139+
const isQueryByQuery: DetectionHelpers['isQueryByQuery'] = (node) => {
140+
return !!node.name.match(/^query(All)?By.+$/);
141+
};
161142

162-
return isNegated
163-
? ABSENCE_MATCHERS.includes(matcher)
164-
: PRESENCE_MATCHERS.includes(matcher);
165-
},
143+
/**
144+
* Determines whether a given node is sync query or not.
145+
*/
146+
const isSyncQuery: DetectionHelpers['isSyncQuery'] = (node) => {
147+
return isGetByQuery(node) || isQueryByQuery(node);
148+
};
166149

167-
/**
168-
* Determines whether a given MemberExpression node is an absence assert
169-
*
170-
* Absence asserts could have shape of:
171-
* - expect(element).toBeNull()
172-
* - expect(element).not.toBeInTheDocument()
173-
*/
174-
isAbsenceAssert(node) {
175-
const { matcher, isNegated } = getAssertNodeInfo(node);
150+
/**
151+
* Determines whether a given MemberExpression node is a presence assert
152+
*
153+
* Presence asserts could have shape of:
154+
* - expect(element).toBeInTheDocument()
155+
* - expect(element).not.toBeNull()
156+
*/
157+
const isPresenceAssert: DetectionHelpers['isPresenceAssert'] = (node) => {
158+
const { matcher, isNegated } = getAssertNodeInfo(node);
176159

177-
if (!matcher) {
178-
return false;
179-
}
160+
if (!matcher) {
161+
return false;
162+
}
180163

181-
return isNegated
182-
? PRESENCE_MATCHERS.includes(matcher)
183-
: ABSENCE_MATCHERS.includes(matcher);
184-
},
164+
return isNegated
165+
? ABSENCE_MATCHERS.includes(matcher)
166+
: PRESENCE_MATCHERS.includes(matcher);
167+
};
185168

186-
/**
187-
* Determines if file inspected meets all conditions to be reported by rules or not.
188-
*/
189-
canReportErrors() {
190-
return (
191-
helpers.getIsTestingLibraryImported() && helpers.getIsValidFilename()
169+
/**
170+
* Determines whether a given MemberExpression node is an absence assert
171+
*
172+
* Absence asserts could have shape of:
173+
* - expect(element).toBeNull()
174+
* - expect(element).not.toBeInTheDocument()
175+
*/
176+
const isAbsenceAssert: DetectionHelpers['isAbsenceAssert'] = (node) => {
177+
const { matcher, isNegated } = getAssertNodeInfo(node);
178+
179+
if (!matcher) {
180+
return false;
181+
}
182+
183+
return isNegated
184+
? PRESENCE_MATCHERS.includes(matcher)
185+
: ABSENCE_MATCHERS.includes(matcher);
186+
};
187+
188+
/**
189+
* Gets a string and verifies if it was imported/required by our custom module node
190+
*/
191+
const findImportedUtilSpecifier: DetectionHelpers['findImportedUtilSpecifier'] = (
192+
specifierName
193+
) => {
194+
const node = getCustomModuleImportNode() ?? getTestingLibraryImportNode();
195+
if (!node) {
196+
return null;
197+
}
198+
if (isImportDeclaration(node)) {
199+
const namedExport = node.specifiers.find(
200+
(n) => isImportSpecifier(n) && n.imported.name === specifierName
192201
);
193-
},
194-
/**
195-
* Gets a string and verifies if it was imported/required by our custom module node
196-
*/
197-
findImportedUtilSpecifier(specifierName: string) {
198-
const node =
199-
helpers.getCustomModuleImportNode() ??
200-
helpers.getTestingLibraryImportNode();
201-
if (!node) {
202-
return null;
202+
// it is "import { foo [as alias] } from 'baz'""
203+
if (namedExport) {
204+
return namedExport;
203205
}
204-
if (isImportDeclaration(node)) {
205-
const namedExport = node.specifiers.find(
206-
(n) => isImportSpecifier(n) && n.imported.name === specifierName
207-
);
208-
// it is "import { foo [as alias] } from 'baz'""
209-
if (namedExport) {
210-
return namedExport;
211-
}
212-
// it could be "import * as rtl from 'baz'"
213-
return node.specifiers.find((n) => isImportNamespaceSpecifier(n));
214-
} else {
215-
const requireNode = node.parent as TSESTree.VariableDeclarator;
216-
if (ASTUtils.isIdentifier(requireNode.id)) {
217-
// this is const rtl = require('foo')
218-
return requireNode.id;
219-
}
220-
// this should be const { something } = require('foo')
221-
const destructuring = requireNode.id as TSESTree.ObjectPattern;
222-
const property = destructuring.properties.find(
223-
(n) =>
224-
isProperty(n) &&
225-
ASTUtils.isIdentifier(n.key) &&
226-
n.key.name === specifierName
227-
);
228-
return (property as TSESTree.Property).key as TSESTree.Identifier;
206+
// it could be "import * as rtl from 'baz'"
207+
return node.specifiers.find((n) => isImportNamespaceSpecifier(n));
208+
} else {
209+
const requireNode = node.parent as TSESTree.VariableDeclarator;
210+
if (ASTUtils.isIdentifier(requireNode.id)) {
211+
// this is const rtl = require('foo')
212+
return requireNode.id;
229213
}
230-
},
214+
// this should be const { something } = require('foo')
215+
const destructuring = requireNode.id as TSESTree.ObjectPattern;
216+
const property = destructuring.properties.find(
217+
(n) =>
218+
isProperty(n) &&
219+
ASTUtils.isIdentifier(n.key) &&
220+
n.key.name === specifierName
221+
);
222+
return (property as TSESTree.Property).key as TSESTree.Identifier;
223+
}
224+
};
225+
226+
/**
227+
* Determines if file inspected meets all conditions to be reported by rules or not.
228+
*/
229+
const canReportErrors: DetectionHelpers['canReportErrors'] = () => {
230+
return isTestingLibraryImported() && isValidFilename();
231+
};
232+
233+
const helpers = {
234+
getTestingLibraryImportNode,
235+
getCustomModuleImportNode,
236+
getTestingLibraryImportName,
237+
getCustomModuleImportName,
238+
isTestingLibraryImported,
239+
isValidFilename,
240+
isGetByQuery,
241+
isQueryByQuery,
242+
isSyncQuery,
243+
isPresenceAssert,
244+
isAbsenceAssert,
245+
canReportErrors,
246+
findImportedUtilSpecifier,
231247
};
232248

233249
// Instructions for Testing Library detection.
@@ -308,7 +324,7 @@ export function detectTestingLibraryUtils<
308324
detectionInstructions[instruction](node);
309325
}
310326

311-
if (helpers.canReportErrors() && ruleInstructions[instruction]) {
327+
if (canReportErrors() && ruleInstructions[instruction]) {
312328
return ruleInstructions[instruction](node);
313329
}
314330
};

0 commit comments

Comments
 (0)