Skip to content

Commit 192a37e

Browse files
authored
refactor: second round of tweaks (#281)
* refactor(shared-settings): rename utils-module Rename testing-library/module to testing-library/utils-module * refactor(detection-helpers): improve fn type definitions * test(filename-pattern): simplify settings patterns * fix: check member expression properly within isRenderUtil helper * test: improve create-testing-library-rule test cases * refactor: check if coming from Testing Library within isAsyncUtil * refactor: extract common method for determining if node is TL util * refactor: improve TL util node detection from identifier * refactor: rename getIdentifierNode to clarify its behavior * fix: improve check for determining if node coming from TL * test: add async util test cases to fake rule * docs: format jsdoc
1 parent 4e9e585 commit 192a37e

21 files changed

+655
-317
lines changed

lib/detect-testing-library-utils.ts

Lines changed: 121 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import {
55
} from '@typescript-eslint/experimental-utils';
66
import {
77
getAssertNodeInfo,
8-
getIdentifierNode,
98
getImportModuleName,
9+
getPropertyIdentifierNode,
10+
getReferenceNode,
1011
ImportModuleNode,
1112
isImportDeclaration,
1213
isImportNamespaceSpecifier,
@@ -23,7 +24,7 @@ import {
2324
} from './utils';
2425

2526
export type TestingLibrarySettings = {
26-
'testing-library/module'?: string;
27+
'testing-library/utils-module'?: string;
2728
'testing-library/filename-pattern'?: string;
2829
'testing-library/custom-renders'?: string[];
2930
};
@@ -47,32 +48,54 @@ export type EnhancedRuleCreate<
4748
detectionHelpers: Readonly<DetectionHelpers>
4849
) => TRuleListener;
4950

50-
export type DetectionHelpers = {
51-
getTestingLibraryImportNode: () => ImportModuleNode | null;
52-
getCustomModuleImportNode: () => ImportModuleNode | null;
53-
getTestingLibraryImportName: () => string | undefined;
54-
getCustomModuleImportName: () => string | undefined;
55-
isTestingLibraryImported: () => boolean;
56-
isValidFilename: () => boolean;
57-
isGetQueryVariant: (node: TSESTree.Identifier) => boolean;
58-
isQueryQueryVariant: (node: TSESTree.Identifier) => boolean;
59-
isFindQueryVariant: (node: TSESTree.Identifier) => boolean;
60-
isSyncQuery: (node: TSESTree.Identifier) => boolean;
61-
isAsyncQuery: (node: TSESTree.Identifier) => boolean;
62-
isCustomQuery: (node: TSESTree.Identifier) => boolean;
63-
isAsyncUtil: (node: TSESTree.Identifier) => boolean;
64-
isFireEventMethod: (node: TSESTree.Identifier) => boolean;
65-
isRenderUtil: (node: TSESTree.Node) => boolean;
66-
isPresenceAssert: (node: TSESTree.MemberExpression) => boolean;
67-
isAbsenceAssert: (node: TSESTree.MemberExpression) => boolean;
68-
canReportErrors: () => boolean;
69-
findImportedUtilSpecifier: (
70-
specifierName: string
71-
) => TSESTree.ImportClause | TSESTree.Identifier | undefined;
72-
isNodeComingFromTestingLibrary: (
73-
node: TSESTree.MemberExpression | TSESTree.Identifier
74-
) => boolean;
75-
};
51+
// Helpers methods
52+
type GetTestingLibraryImportNodeFn = () => ImportModuleNode | null;
53+
type GetCustomModuleImportNodeFn = () => ImportModuleNode | null;
54+
type GetTestingLibraryImportNameFn = () => string | undefined;
55+
type GetCustomModuleImportNameFn = () => string | undefined;
56+
type IsTestingLibraryImportedFn = () => boolean;
57+
type IsValidFilenameFn = () => boolean;
58+
type IsGetQueryVariantFn = (node: TSESTree.Identifier) => boolean;
59+
type IsQueryQueryVariantFn = (node: TSESTree.Identifier) => boolean;
60+
type IsFindQueryVariantFn = (node: TSESTree.Identifier) => boolean;
61+
type IsSyncQueryFn = (node: TSESTree.Identifier) => boolean;
62+
type IsAsyncQueryFn = (node: TSESTree.Identifier) => boolean;
63+
type IsCustomQueryFn = (node: TSESTree.Identifier) => boolean;
64+
type IsAsyncUtilFn = (node: TSESTree.Identifier) => boolean;
65+
type IsFireEventMethodFn = (node: TSESTree.Identifier) => boolean;
66+
type IsRenderUtilFn = (node: TSESTree.Identifier) => boolean;
67+
type IsPresenceAssertFn = (node: TSESTree.MemberExpression) => boolean;
68+
type IsAbsenceAssertFn = (node: TSESTree.MemberExpression) => boolean;
69+
type CanReportErrorsFn = () => boolean;
70+
type FindImportedUtilSpecifierFn = (
71+
specifierName: string
72+
) => TSESTree.ImportClause | TSESTree.Identifier | undefined;
73+
type IsNodeComingFromTestingLibraryFn = (
74+
node: TSESTree.MemberExpression | TSESTree.Identifier
75+
) => boolean;
76+
77+
export interface DetectionHelpers {
78+
getTestingLibraryImportNode: GetTestingLibraryImportNodeFn;
79+
getCustomModuleImportNode: GetCustomModuleImportNodeFn;
80+
getTestingLibraryImportName: GetTestingLibraryImportNameFn;
81+
getCustomModuleImportName: GetCustomModuleImportNameFn;
82+
isTestingLibraryImported: IsTestingLibraryImportedFn;
83+
isValidFilename: IsValidFilenameFn;
84+
isGetQueryVariant: IsGetQueryVariantFn;
85+
isQueryQueryVariant: IsQueryQueryVariantFn;
86+
isFindQueryVariant: IsFindQueryVariantFn;
87+
isSyncQuery: IsSyncQueryFn;
88+
isAsyncQuery: IsAsyncQueryFn;
89+
isCustomQuery: IsCustomQueryFn;
90+
isAsyncUtil: IsAsyncUtilFn;
91+
isFireEventMethod: IsFireEventMethodFn;
92+
isRenderUtil: IsRenderUtilFn;
93+
isPresenceAssert: IsPresenceAssertFn;
94+
isAbsenceAssert: IsAbsenceAssertFn;
95+
canReportErrors: CanReportErrorsFn;
96+
findImportedUtilSpecifier: FindImportedUtilSpecifierFn;
97+
isNodeComingFromTestingLibrary: IsNodeComingFromTestingLibraryFn;
98+
}
7699

77100
const DEFAULT_FILENAME_PATTERN = '^.*\\.(test|spec)\\.[jt]sx?$';
78101

@@ -95,12 +118,33 @@ export function detectTestingLibraryUtils<
95118
let importedCustomModuleNode: ImportModuleNode | null = null;
96119

97120
// Init options based on shared ESLint settings
98-
const customModule = context.settings['testing-library/module'];
121+
const customModule = context.settings['testing-library/utils-module'];
99122
const filenamePattern =
100123
context.settings['testing-library/filename-pattern'] ??
101124
DEFAULT_FILENAME_PATTERN;
102125
const customRenders = context.settings['testing-library/custom-renders'];
103126

127+
/**
128+
* Small method to extract common checks to determine whether a node is
129+
* related to Testing Library or not.
130+
*/
131+
function isTestingLibraryUtil(
132+
node: TSESTree.Identifier,
133+
isUtilCallback: (identifierNode: TSESTree.Identifier) => boolean
134+
): boolean {
135+
if (!isUtilCallback(node)) {
136+
return false;
137+
}
138+
139+
const referenceNode = getReferenceNode(node);
140+
const referenceNodeIdentifier = getPropertyIdentifierNode(referenceNode);
141+
142+
return (
143+
isAggressiveModuleReportingEnabled() ||
144+
isNodeComingFromTestingLibrary(referenceNodeIdentifier)
145+
);
146+
}
147+
104148
/**
105149
* Determines whether aggressive module reporting is enabled or not.
106150
*
@@ -126,21 +170,22 @@ export function detectTestingLibraryUtils<
126170
!Array.isArray(customRenders) || customRenders.length === 0;
127171

128172
// Helpers for Testing Library detection.
129-
const getTestingLibraryImportNode: DetectionHelpers['getTestingLibraryImportNode'] = () => {
173+
const getTestingLibraryImportNode: GetTestingLibraryImportNodeFn = () => {
130174
return importedTestingLibraryNode;
131175
};
132176

133-
const getCustomModuleImportNode: DetectionHelpers['getCustomModuleImportNode'] = () => {
177+
const getCustomModuleImportNode: GetCustomModuleImportNodeFn = () => {
134178
return importedCustomModuleNode;
135179
};
136180

137-
const getTestingLibraryImportName: DetectionHelpers['getTestingLibraryImportName'] = () => {
181+
const getTestingLibraryImportName: GetTestingLibraryImportNameFn = () => {
138182
return getImportModuleName(importedTestingLibraryNode);
139183
};
140184

141-
const getCustomModuleImportName: DetectionHelpers['getCustomModuleImportName'] = () => {
185+
const getCustomModuleImportName: GetCustomModuleImportNameFn = () => {
142186
return getImportModuleName(importedCustomModuleNode);
143187
};
188+
144189
/**
145190
* Determines whether Testing Library utils are imported or not for
146191
* current file being analyzed.
@@ -150,84 +195,92 @@ export function detectTestingLibraryUtils<
150195
* custom modules.
151196
*
152197
* However, there is a setting to customize the module where TL utils can
153-
* be imported from: "testing-library/module". If this setting is enabled,
198+
* be imported from: "testing-library/utils-module". If this setting is enabled,
154199
* then this method will return `true` ONLY IF a testing-library package
155200
* or custom module are imported.
156201
*/
157-
const isTestingLibraryImported: DetectionHelpers['isTestingLibraryImported'] = () => {
158-
if (isAggressiveModuleReportingEnabled()) {
159-
return true;
160-
}
161-
162-
return !!importedTestingLibraryNode || !!importedCustomModuleNode;
202+
const isTestingLibraryImported: IsTestingLibraryImportedFn = () => {
203+
return (
204+
isAggressiveModuleReportingEnabled() ||
205+
!!importedTestingLibraryNode ||
206+
!!importedCustomModuleNode
207+
);
163208
};
164209

165210
/**
166211
* Determines whether filename is valid or not for current file
167212
* being analyzed based on "testing-library/filename-pattern" setting.
168213
*/
169-
const isValidFilename: DetectionHelpers['isValidFilename'] = () => {
214+
const isValidFilename: IsValidFilenameFn = () => {
170215
const fileName = context.getFilename();
171216
return !!fileName.match(filenamePattern);
172217
};
173218

174219
/**
175220
* Determines whether a given node is `get*` query variant or not.
176221
*/
177-
const isGetQueryVariant: DetectionHelpers['isGetQueryVariant'] = (node) => {
222+
const isGetQueryVariant: IsGetQueryVariantFn = (node) => {
178223
return /^get(All)?By.+$/.test(node.name);
179224
};
180225

181226
/**
182227
* Determines whether a given node is `query*` query variant or not.
183228
*/
184-
const isQueryQueryVariant: DetectionHelpers['isQueryQueryVariant'] = (
185-
node
186-
) => {
229+
const isQueryQueryVariant: IsQueryQueryVariantFn = (node) => {
187230
return /^query(All)?By.+$/.test(node.name);
188231
};
189232

190233
/**
191234
* Determines whether a given node is `find*` query variant or not.
192235
*/
193-
const isFindQueryVariant: DetectionHelpers['isFindQueryVariant'] = (
194-
node
195-
) => {
236+
const isFindQueryVariant: IsFindQueryVariantFn = (node) => {
196237
return /^find(All)?By.+$/.test(node.name);
197238
};
198239

199240
/**
200241
* Determines whether a given node is sync query or not.
201242
*/
202-
const isSyncQuery: DetectionHelpers['isSyncQuery'] = (node) => {
243+
const isSyncQuery: IsSyncQueryFn = (node) => {
203244
return isGetQueryVariant(node) || isQueryQueryVariant(node);
204245
};
205246

206247
/**
207248
* Determines whether a given node is async query or not.
208249
*/
209-
const isAsyncQuery: DetectionHelpers['isAsyncQuery'] = (node) => {
250+
const isAsyncQuery: IsAsyncQueryFn = (node) => {
210251
return isFindQueryVariant(node);
211252
};
212253

213-
const isCustomQuery: DetectionHelpers['isCustomQuery'] = (node) => {
254+
const isCustomQuery: IsCustomQueryFn = (node) => {
214255
return (
215256
(isSyncQuery(node) || isAsyncQuery(node)) &&
216257
!ALL_QUERIES_COMBINATIONS.includes(node.name)
217258
);
218259
};
219260

220261
/**
221-
* Determines whether a given node is async util or not.
262+
* Determines whether a given node is a valid async util or not.
263+
*
264+
* A node will be interpreted as a valid async util based on two conditions:
265+
* the name matches with some Testing Library async util, and the node is
266+
* coming from Testing Library module.
267+
*
268+
* The latter depends on Aggressive module reporting:
269+
* if enabled, then it doesn't matter from where the given node was imported
270+
* from as it will be considered part of Testing Library.
271+
* Otherwise, it means `custom-module` has been set up, so only those nodes
272+
* coming from Testing Library will be considered as valid.
222273
*/
223-
const isAsyncUtil: DetectionHelpers['isAsyncUtil'] = (node) => {
224-
return ASYNC_UTILS.includes(node.name);
274+
const isAsyncUtil: IsAsyncUtilFn = (node) => {
275+
return isTestingLibraryUtil(node, (identifierNode) =>
276+
ASYNC_UTILS.includes(identifierNode.name)
277+
);
225278
};
226279

227280
/**
228281
* Determines whether a given node is fireEvent method or not
229282
*/
230-
const isFireEventMethod: DetectionHelpers['isFireEventMethod'] = (node) => {
283+
const isFireEventMethod: IsFireEventMethodFn = (node) => {
231284
const fireEventUtil = findImportedUtilSpecifier(FIRE_EVENT_NAME);
232285
let fireEventUtilName: string | undefined;
233286

@@ -293,29 +346,14 @@ export function detectTestingLibraryUtils<
293346
* Testing Library. Otherwise, it means `custom-module` has been set up, so
294347
* only those nodes coming from Testing Library will be considered as valid.
295348
*/
296-
const isRenderUtil: DetectionHelpers['isRenderUtil'] = (node) => {
297-
const identifier = getIdentifierNode(node);
298-
299-
if (!identifier) {
300-
return false;
301-
}
302-
303-
const isNameMatching = (function () {
349+
const isRenderUtil: IsRenderUtilFn = (node) => {
350+
return isTestingLibraryUtil(node, (identifierNode) => {
304351
if (isAggressiveRenderReportingEnabled()) {
305-
return identifier.name.toLowerCase().includes(RENDER_NAME);
352+
return identifierNode.name.toLowerCase().includes(RENDER_NAME);
306353
}
307354

308-
return [RENDER_NAME, ...customRenders].includes(identifier.name);
309-
})();
310-
311-
if (!isNameMatching) {
312-
return false;
313-
}
314-
315-
return (
316-
isAggressiveModuleReportingEnabled() ||
317-
isNodeComingFromTestingLibrary(identifier)
318-
);
355+
return [RENDER_NAME, ...customRenders].includes(identifierNode.name);
356+
});
319357
};
320358

321359
/**
@@ -325,7 +363,7 @@ export function detectTestingLibraryUtils<
325363
* - expect(element).toBeInTheDocument()
326364
* - expect(element).not.toBeNull()
327365
*/
328-
const isPresenceAssert: DetectionHelpers['isPresenceAssert'] = (node) => {
366+
const isPresenceAssert: IsPresenceAssertFn = (node) => {
329367
const { matcher, isNegated } = getAssertNodeInfo(node);
330368

331369
if (!matcher) {
@@ -344,7 +382,7 @@ export function detectTestingLibraryUtils<
344382
* - expect(element).toBeNull()
345383
* - expect(element).not.toBeInTheDocument()
346384
*/
347-
const isAbsenceAssert: DetectionHelpers['isAbsenceAssert'] = (node) => {
385+
const isAbsenceAssert: IsAbsenceAssertFn = (node) => {
348386
const { matcher, isNegated } = getAssertNodeInfo(node);
349387

350388
if (!matcher) {
@@ -360,7 +398,7 @@ export function detectTestingLibraryUtils<
360398
* Gets a string and verifies if it was imported/required by Testing Library
361399
* related module.
362400
*/
363-
const findImportedUtilSpecifier: DetectionHelpers['findImportedUtilSpecifier'] = (
401+
const findImportedUtilSpecifier: FindImportedUtilSpecifierFn = (
364402
specifierName
365403
) => {
366404
const node = getCustomModuleImportNode() ?? getTestingLibraryImportNode();
@@ -398,32 +436,23 @@ export function detectTestingLibraryUtils<
398436
/**
399437
* Determines if file inspected meets all conditions to be reported by rules or not.
400438
*/
401-
const canReportErrors: DetectionHelpers['canReportErrors'] = () => {
439+
const canReportErrors: CanReportErrorsFn = () => {
402440
return isTestingLibraryImported() && isValidFilename();
403441
};
404442
/**
405443
* Takes a MemberExpression or an Identifier and verifies if its name comes from the import in TL
406444
* @param node a MemberExpression (in "foo.property" it would be property) or an Identifier
407445
*/
408-
const isNodeComingFromTestingLibrary: DetectionHelpers['isNodeComingFromTestingLibrary'] = (
446+
const isNodeComingFromTestingLibrary: IsNodeComingFromTestingLibraryFn = (
409447
node
410448
) => {
411-
let identifierName: string | undefined;
412-
413-
if (ASTUtils.isIdentifier(node)) {
414-
identifierName = node.name;
415-
} else if (ASTUtils.isIdentifier(node.object)) {
416-
identifierName = node.object.name;
417-
}
418-
419-
if (!identifierName) {
420-
return;
421-
}
449+
const identifierName: string | undefined = getPropertyIdentifierNode(node)
450+
.name;
422451

423452
return !!findImportedUtilSpecifier(identifierName);
424453
};
425454

426-
const helpers = {
455+
const helpers: DetectionHelpers = {
427456
getTestingLibraryImportNode,
428457
getCustomModuleImportNode,
429458
getTestingLibraryImportName,

0 commit comments

Comments
 (0)