Skip to content

Commit 50727e6

Browse files
authored
refactor(render-result-naming-convention): refine checks to decide if coming from Testing Library (#282)
* feat(render-result-naming-convention): detect render calls from wrappers * fix: check imported node properly when specifiers are renamed ImportNamespaceSpecifier had to be checked properly in order to detect rename imports properly like: import { a as b } from 'foo' * refactor: split checks for import matching node name in different methods * test(render-result-naming-convention): add extra invalid case for wrapped function * fix(render-result-naming-convention): cover more renamed imports
1 parent 192a37e commit 50727e6

6 files changed

+331
-45
lines changed

lib/detect-testing-library-utils.ts

+86-22
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
getImportModuleName,
99
getPropertyIdentifierNode,
1010
getReferenceNode,
11+
hasImportMatch,
1112
ImportModuleNode,
1213
isImportDeclaration,
1314
isImportNamespaceSpecifier,
@@ -127,22 +128,41 @@ export function detectTestingLibraryUtils<
127128
/**
128129
* Small method to extract common checks to determine whether a node is
129130
* related to Testing Library or not.
131+
*
132+
* To determine whether a node is a valid Testing Library util, there are
133+
* two conditions to match:
134+
* - it's named in a particular way (decided by given callback)
135+
* - it's imported from valid Testing Library module (depends on aggressive
136+
* reporting)
130137
*/
131138
function isTestingLibraryUtil(
132139
node: TSESTree.Identifier,
133-
isUtilCallback: (identifierNode: TSESTree.Identifier) => boolean
140+
isUtilCallback: (
141+
identifierNodeName: string,
142+
originalNodeName?: string
143+
) => boolean
134144
): boolean {
135-
if (!isUtilCallback(node)) {
145+
const referenceNode = getReferenceNode(node);
146+
const referenceNodeIdentifier = getPropertyIdentifierNode(referenceNode);
147+
const importedUtilSpecifier = getImportedUtilSpecifier(
148+
referenceNodeIdentifier
149+
);
150+
151+
const originalNodeName =
152+
isImportSpecifier(importedUtilSpecifier) &&
153+
importedUtilSpecifier.local.name !== importedUtilSpecifier.imported.name
154+
? importedUtilSpecifier.imported.name
155+
: undefined;
156+
157+
if (!isUtilCallback(node.name, originalNodeName)) {
136158
return false;
137159
}
138160

139-
const referenceNode = getReferenceNode(node);
140-
const referenceNodeIdentifier = getPropertyIdentifierNode(referenceNode);
161+
if (isAggressiveModuleReportingEnabled()) {
162+
return true;
163+
}
141164

142-
return (
143-
isAggressiveModuleReportingEnabled() ||
144-
isNodeComingFromTestingLibrary(referenceNodeIdentifier)
145-
);
165+
return isNodeComingFromTestingLibrary(referenceNodeIdentifier);
146166
}
147167

148168
/**
@@ -272,8 +292,8 @@ export function detectTestingLibraryUtils<
272292
* coming from Testing Library will be considered as valid.
273293
*/
274294
const isAsyncUtil: IsAsyncUtilFn = (node) => {
275-
return isTestingLibraryUtil(node, (identifierNode) =>
276-
ASYNC_UTILS.includes(identifierNode.name)
295+
return isTestingLibraryUtil(node, (identifierNodeName) =>
296+
ASYNC_UTILS.includes(identifierNodeName)
277297
);
278298
};
279299

@@ -347,13 +367,27 @@ export function detectTestingLibraryUtils<
347367
* only those nodes coming from Testing Library will be considered as valid.
348368
*/
349369
const isRenderUtil: IsRenderUtilFn = (node) => {
350-
return isTestingLibraryUtil(node, (identifierNode) => {
351-
if (isAggressiveRenderReportingEnabled()) {
352-
return identifierNode.name.toLowerCase().includes(RENDER_NAME);
370+
return isTestingLibraryUtil(
371+
node,
372+
(identifierNodeName, originalNodeName) => {
373+
if (isAggressiveRenderReportingEnabled()) {
374+
return identifierNodeName.toLowerCase().includes(RENDER_NAME);
375+
}
376+
377+
return [RENDER_NAME, ...customRenders].some((validRenderName) => {
378+
let isMatch = false;
379+
380+
if (validRenderName === identifierNodeName) {
381+
isMatch = true;
382+
}
383+
384+
if (!!originalNodeName && validRenderName === originalNodeName) {
385+
isMatch = true;
386+
}
387+
return isMatch;
388+
});
353389
}
354-
355-
return [RENDER_NAME, ...customRenders].includes(identifierNode.name);
356-
});
390+
);
357391
};
358392

359393
/**
@@ -402,25 +436,34 @@ export function detectTestingLibraryUtils<
402436
specifierName
403437
) => {
404438
const node = getCustomModuleImportNode() ?? getTestingLibraryImportNode();
439+
405440
if (!node) {
406441
return null;
407442
}
443+
408444
if (isImportDeclaration(node)) {
409-
const namedExport = node.specifiers.find(
410-
(n) => isImportSpecifier(n) && n.imported.name === specifierName
411-
);
445+
const namedExport = node.specifiers.find((n) => {
446+
return (
447+
isImportSpecifier(n) &&
448+
[n.imported.name, n.local.name].includes(specifierName)
449+
);
450+
});
451+
412452
// it is "import { foo [as alias] } from 'baz'""
413453
if (namedExport) {
414454
return namedExport;
415455
}
456+
416457
// it could be "import * as rtl from 'baz'"
417458
return node.specifiers.find((n) => isImportNamespaceSpecifier(n));
418459
} else {
419460
const requireNode = node.parent as TSESTree.VariableDeclarator;
461+
420462
if (ASTUtils.isIdentifier(requireNode.id)) {
421463
// this is const rtl = require('foo')
422464
return requireNode.id;
423465
}
466+
424467
// this should be const { something } = require('foo')
425468
const destructuring = requireNode.id as TSESTree.ObjectPattern;
426469
const property = destructuring.properties.find(
@@ -429,27 +472,48 @@ export function detectTestingLibraryUtils<
429472
ASTUtils.isIdentifier(n.key) &&
430473
n.key.name === specifierName
431474
);
475+
if (!property) {
476+
return undefined;
477+
}
432478
return (property as TSESTree.Property).key as TSESTree.Identifier;
433479
}
434480
};
435481

482+
const getImportedUtilSpecifier = (
483+
node: TSESTree.MemberExpression | TSESTree.Identifier
484+
): TSESTree.ImportClause | TSESTree.Identifier | undefined => {
485+
const identifierName: string | undefined = getPropertyIdentifierNode(node)
486+
.name;
487+
488+
return findImportedUtilSpecifier(identifierName);
489+
};
490+
436491
/**
437492
* Determines if file inspected meets all conditions to be reported by rules or not.
438493
*/
439494
const canReportErrors: CanReportErrorsFn = () => {
440495
return isTestingLibraryImported() && isValidFilename();
441496
};
497+
442498
/**
443-
* Takes a MemberExpression or an Identifier and verifies if its name comes from the import in TL
444-
* @param node a MemberExpression (in "foo.property" it would be property) or an Identifier
499+
* Determines whether a node is imported from a valid Testing Library module
500+
*
501+
* This method will try to find any import matching the given node name,
502+
* and also make sure the name is a valid match in case it's been renamed.
445503
*/
446504
const isNodeComingFromTestingLibrary: IsNodeComingFromTestingLibraryFn = (
447505
node
448506
) => {
507+
const importNode = getImportedUtilSpecifier(node);
508+
509+
if (!importNode) {
510+
return false;
511+
}
512+
449513
const identifierName: string | undefined = getPropertyIdentifierNode(node)
450514
.name;
451515

452-
return !!findImportedUtilSpecifier(identifierName);
516+
return hasImportMatch(importNode, identifierName);
453517
};
454518

455519
const helpers: DetectionHelpers = {

lib/node-utils.ts

+11
Original file line numberDiff line numberDiff line change
@@ -620,3 +620,14 @@ export function getInnermostReturningFunction(
620620

621621
return functionScope.block;
622622
}
623+
624+
export function hasImportMatch(
625+
importNode: TSESTree.ImportClause | TSESTree.Identifier,
626+
identifierName: string
627+
): boolean {
628+
if (ASTUtils.isIdentifier(importNode)) {
629+
return importNode.name === identifierName;
630+
}
631+
632+
return importNode.local.name === identifierName;
633+
}

lib/rules/render-result-naming-convention.ts

+26-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { createTestingLibraryRule } from '../create-testing-library-rule';
2-
import { getDeepestIdentifierNode, isObjectPattern } from '../node-utils';
3-
import { ASTUtils } from '@typescript-eslint/experimental-utils';
2+
import {
3+
getDeepestIdentifierNode,
4+
getFunctionName,
5+
getInnermostReturningFunction,
6+
isObjectPattern,
7+
} from '../node-utils';
8+
import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils';
49

510
export const RULE_NAME = 'render-result-naming-convention';
611
export type MessageIds = 'renderResultNamingConvention';
@@ -30,15 +35,33 @@ export default createTestingLibraryRule<Options, MessageIds>({
3035
defaultOptions: [],
3136

3237
create(context, _, helpers) {
38+
const renderWrapperNames: string[] = [];
39+
40+
function detectRenderWrapper(node: TSESTree.Identifier): void {
41+
const innerFunction = getInnermostReturningFunction(context, node);
42+
43+
if (innerFunction) {
44+
renderWrapperNames.push(getFunctionName(innerFunction));
45+
}
46+
}
47+
3348
return {
49+
'CallExpression Identifier'(node: TSESTree.Identifier) {
50+
if (helpers.isRenderUtil(node)) {
51+
detectRenderWrapper(node);
52+
}
53+
},
3454
VariableDeclarator(node) {
3555
const initIdentifierNode = getDeepestIdentifierNode(node.init);
3656

3757
if (!initIdentifierNode) {
3858
return;
3959
}
4060

41-
if (!helpers.isRenderUtil(initIdentifierNode)) {
61+
if (
62+
!helpers.isRenderUtil(initIdentifierNode) &&
63+
!renderWrapperNames.includes(initIdentifierNode.name)
64+
) {
4265
return;
4366
}
4467

tests/create-testing-library-rule.test.ts

+28-20
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,19 @@ ruleTester.run(RULE_NAME, rule, {
126126
const utils = render()
127127
`,
128128
},
129+
{
130+
settings: {
131+
'testing-library/utils-module': 'test-utils',
132+
},
133+
code: `
134+
// case (render util): aggressive reporting disabled - method with same name
135+
// as TL method but not coming from TL module is valid
136+
import { render as testingLibraryRender } from 'test-utils'
137+
import { render } from 'somewhere-else'
138+
139+
const utils = render()
140+
`,
141+
},
129142

130143
// Test Cases for presence/absence assertions
131144
// cases: asserts not related to presence/absence
@@ -287,6 +300,21 @@ ruleTester.run(RULE_NAME, rule, {
287300
waitFor()
288301
`,
289302
},
303+
{
304+
settings: {
305+
'testing-library/utils-module': 'test-utils',
306+
},
307+
code: `
308+
// case (async util): aggressive reporting disabled - method with same name
309+
// as TL method but not coming from TL module is valid
310+
import { waitFor as testingLibraryWaitFor } from 'test-utils'
311+
import { waitFor } from 'somewhere-else'
312+
313+
test('this should not be reported', () => {
314+
waitFor()
315+
});
316+
`,
317+
},
290318

291319
// Test Cases for all settings mixed
292320
{
@@ -642,26 +670,6 @@ ruleTester.run(RULE_NAME, rule, {
642670
},
643671
],
644672
},
645-
{
646-
settings: {
647-
'testing-library/utils-module': 'test-utils',
648-
},
649-
code: `
650-
// case: waitFor from object property shadowed name is checked correctly
651-
import * as tl from 'test-utils'
652-
const obj = { tl }
653-
654-
obj.module.waitFor(() => {})
655-
`,
656-
errors: [
657-
{
658-
line: 6,
659-
column: 20,
660-
messageId: 'asyncUtilError',
661-
data: { utilName: 'waitFor' },
662-
},
663-
],
664-
},
665673
{
666674
settings: {
667675
'testing-library/utils-module': 'test-utils',

tests/lib/rules/prefer-wait-for.test.ts

+15
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,21 @@ ruleTester.run(RULE_NAME, rule, {
218218
cy.wait();
219219
`,
220220
},
221+
{
222+
settings: {
223+
'testing-library/utils-module': 'test-utils',
224+
},
225+
code: `
226+
// case: aggressive reporting disabled - method named same as invalid method
227+
// but not coming from Testing Library is valid
228+
import { wait as testingLibraryWait } from 'test-utils'
229+
import { wait } from 'somewhere-else'
230+
231+
async () => {
232+
await wait();
233+
}
234+
`,
235+
},
221236
{
222237
// https://github.com/testing-library/eslint-plugin-testing-library/issues/145
223238
code: `import * as foo from 'imNoTestingLibrary';

0 commit comments

Comments
 (0)