Skip to content

Commit 4e9e585

Browse files
authored
refactor(render-result-naming-convention): migrate to v4 (#280)
* docs: add comments to main parts to be modified * refactor(render-result-naming-convention): first approach for new helper First implementation of isRenderUtil helper, and use it within this rule. * refactor(aggressive-render): update criteria to consider valid renders Before, it was checking if the name of the method started by "render". Now, it checks if the name of the method contains render. * feat(aggressive-render): keep aggressive module reporting in mind Depending on aggressive module reporting, isRenderUtil needs to check if node comes from valid Testing Library module or not. * test(render-result-naming-convention): move valid to invalid tests * docs(aggressive-reporting): improve jsdocs * test(create-testing-library-rule): cases for render
1 parent 5a5881e commit 4e9e585

5 files changed

+289
-181
lines changed

lib/detect-testing-library-utils.ts

+70-7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
} from '@typescript-eslint/experimental-utils';
66
import {
77
getAssertNodeInfo,
8+
getIdentifierNode,
89
getImportModuleName,
910
ImportModuleNode,
1011
isImportDeclaration,
@@ -24,6 +25,7 @@ import {
2425
export type TestingLibrarySettings = {
2526
'testing-library/module'?: string;
2627
'testing-library/filename-pattern'?: string;
28+
'testing-library/custom-renders'?: string[];
2729
};
2830

2931
export type TestingLibraryContext<
@@ -60,6 +62,7 @@ export type DetectionHelpers = {
6062
isCustomQuery: (node: TSESTree.Identifier) => boolean;
6163
isAsyncUtil: (node: TSESTree.Identifier) => boolean;
6264
isFireEventMethod: (node: TSESTree.Identifier) => boolean;
65+
isRenderUtil: (node: TSESTree.Node) => boolean;
6366
isPresenceAssert: (node: TSESTree.MemberExpression) => boolean;
6467
isAbsenceAssert: (node: TSESTree.MemberExpression) => boolean;
6568
canReportErrors: () => boolean;
@@ -74,6 +77,7 @@ export type DetectionHelpers = {
7477
const DEFAULT_FILENAME_PATTERN = '^.*\\.(test|spec)\\.[jt]sx?$';
7578

7679
const FIRE_EVENT_NAME = 'fireEvent';
80+
const RENDER_NAME = 'render';
7781

7882
/**
7983
* Enhances a given rule `create` with helpers to detect Testing Library utils.
@@ -95,15 +99,31 @@ export function detectTestingLibraryUtils<
9599
const filenamePattern =
96100
context.settings['testing-library/filename-pattern'] ??
97101
DEFAULT_FILENAME_PATTERN;
102+
const customRenders = context.settings['testing-library/custom-renders'];
98103

99104
/**
100-
* Determines whether aggressive reporting is enabled or not.
105+
* Determines whether aggressive module reporting is enabled or not.
101106
*
102-
* Aggressive reporting is considered as enabled when:
103-
* - custom module is not set (so we need to assume everything
104-
* matching TL utils is related to TL no matter where it was imported from)
107+
* This aggressive reporting mechanism is considered as enabled when custom
108+
* module is not set, so we need to assume everything matching Testing
109+
* Library utils is related to Testing Library no matter from where module
110+
* they are coming from. Otherwise, this aggressive reporting mechanism is
111+
* opted-out in favour to report only those utils coming from Testing
112+
* Library package or custom module set up on settings.
105113
*/
106-
const isAggressiveReportingEnabled = () => !customModule;
114+
const isAggressiveModuleReportingEnabled = () => !customModule;
115+
116+
/**
117+
* Determines whether aggressive render reporting is enabled or not.
118+
*
119+
* This aggressive reporting mechanism is considered as enabled when custom
120+
* renders are not set, so we need to assume every method containing
121+
* "render" is a valid Testing Library `render`. Otherwise, this aggressive
122+
* reporting mechanism is opted-out in favour to report only `render` or
123+
* names set up on custom renders setting.
124+
*/
125+
const isAggressiveRenderReportingEnabled = () =>
126+
!Array.isArray(customRenders) || customRenders.length === 0;
107127

108128
// Helpers for Testing Library detection.
109129
const getTestingLibraryImportNode: DetectionHelpers['getTestingLibraryImportNode'] = () => {
@@ -135,7 +155,7 @@ export function detectTestingLibraryUtils<
135155
* or custom module are imported.
136156
*/
137157
const isTestingLibraryImported: DetectionHelpers['isTestingLibraryImported'] = () => {
138-
if (isAggressiveReportingEnabled()) {
158+
if (isAggressiveModuleReportingEnabled()) {
139159
return true;
140160
}
141161

@@ -215,7 +235,7 @@ export function detectTestingLibraryUtils<
215235
fireEventUtilName = ASTUtils.isIdentifier(fireEventUtil)
216236
? fireEventUtil.name
217237
: fireEventUtil.local.name;
218-
} else if (isAggressiveReportingEnabled()) {
238+
} else if (isAggressiveModuleReportingEnabled()) {
219239
fireEventUtilName = FIRE_EVENT_NAME;
220240
}
221241

@@ -256,6 +276,48 @@ export function detectTestingLibraryUtils<
256276
return regularCall || wildcardCall;
257277
};
258278

279+
/**
280+
* Determines whether a given node is a valid render util or not.
281+
*
282+
* A node will be interpreted as a valid render based on two conditions:
283+
* the name matches with a valid "render" option, and the node is coming
284+
* from Testing Library module. This depends on:
285+
*
286+
* - Aggressive render reporting: if enabled, then every node name
287+
* containing "render" will be assumed as Testing Library render util.
288+
* Otherwise, it means `custom-modules` has been set up, so only those nodes
289+
* named as "render" or some of the `custom-modules` options will be
290+
* considered as Testing Library render util.
291+
* - Aggressive module reporting: if enabled, then it doesn't matter from
292+
* where the given node was imported from as it will be considered part of
293+
* Testing Library. Otherwise, it means `custom-module` has been set up, so
294+
* only those nodes coming from Testing Library will be considered as valid.
295+
*/
296+
const isRenderUtil: DetectionHelpers['isRenderUtil'] = (node) => {
297+
const identifier = getIdentifierNode(node);
298+
299+
if (!identifier) {
300+
return false;
301+
}
302+
303+
const isNameMatching = (function () {
304+
if (isAggressiveRenderReportingEnabled()) {
305+
return identifier.name.toLowerCase().includes(RENDER_NAME);
306+
}
307+
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+
);
319+
};
320+
259321
/**
260322
* Determines whether a given MemberExpression node is a presence assert
261323
*
@@ -376,6 +438,7 @@ export function detectTestingLibraryUtils<
376438
isCustomQuery,
377439
isAsyncUtil,
378440
isFireEventMethod,
441+
isRenderUtil,
379442
isPresenceAssert,
380443
isAbsenceAssert,
381444
canReportErrors,

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

+14-99
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,18 @@
1-
import {
2-
ESLintUtils,
3-
TSESTree,
4-
ASTUtils,
5-
} from '@typescript-eslint/experimental-utils';
6-
import { getDocsUrl, hasTestingLibraryImportModule } from '../utils';
7-
import {
8-
isCallExpression,
9-
isImportSpecifier,
10-
isMemberExpression,
11-
isObjectPattern,
12-
isRenderVariableDeclarator,
13-
} from '../node-utils';
1+
import { createTestingLibraryRule } from '../create-testing-library-rule';
2+
import { isObjectPattern } from '../node-utils';
3+
import { ASTUtils } from '@typescript-eslint/experimental-utils';
144

155
export const RULE_NAME = 'render-result-naming-convention';
166
export type MessageIds = 'renderResultNamingConvention';
17-
type Options = [{ renderFunctions?: string[] }];
7+
8+
type Options = [];
189

1910
const ALLOWED_VAR_NAMES = ['view', 'utils'];
2011
const ALLOWED_VAR_NAMES_TEXT = ALLOWED_VAR_NAMES.map(
2112
(name) => `\`${name}\``
2213
).join(', ');
2314

24-
export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
15+
export default createTestingLibraryRule<Options, MessageIds>({
2516
name: RULE_NAME,
2617
meta: {
2718
type: 'suggestion',
@@ -31,103 +22,27 @@ export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
3122
recommended: false,
3223
},
3324
messages: {
34-
renderResultNamingConvention: `\`{{ varName }}\` is not a recommended name for \`render\` returned value. Instead, you should destructure it, or call it using one of the valid choices: ${ALLOWED_VAR_NAMES_TEXT}`,
25+
renderResultNamingConvention: `\`{{ renderResultName }}\` is not a recommended name for \`render\` returned value. Instead, you should destructure it, or name it using one of: ${ALLOWED_VAR_NAMES_TEXT}`,
3526
},
3627
fixable: null,
37-
schema: [
38-
{
39-
type: 'object',
40-
properties: {
41-
renderFunctions: {
42-
type: 'array',
43-
},
44-
},
45-
},
46-
],
28+
schema: [],
4729
},
48-
defaultOptions: [
49-
{
50-
renderFunctions: [],
51-
},
52-
],
53-
54-
create(context, [options]) {
55-
const { renderFunctions } = options;
56-
let renderAlias: string | undefined;
57-
let wildcardImportName: string | undefined;
30+
defaultOptions: [],
5831

32+
create(context, _, helpers) {
5933
return {
60-
// check named imports
61-
ImportDeclaration(node: TSESTree.ImportDeclaration) {
62-
if (!hasTestingLibraryImportModule(node)) {
63-
return;
64-
}
65-
const renderImport = node.specifiers.find(
66-
(node) => isImportSpecifier(node) && node.imported.name === 'render'
67-
);
68-
69-
if (!renderImport) {
70-
return;
71-
}
72-
73-
renderAlias = renderImport.local.name;
74-
},
75-
// check wildcard imports
76-
'ImportDeclaration ImportNamespaceSpecifier'(
77-
node: TSESTree.ImportNamespaceSpecifier
78-
) {
79-
if (
80-
!hasTestingLibraryImportModule(
81-
node.parent as TSESTree.ImportDeclaration
82-
)
83-
) {
34+
VariableDeclarator(node) {
35+
if (!helpers.isRenderUtil(node.init)) {
8436
return;
8537
}
8638

87-
wildcardImportName = node.local.name;
88-
},
89-
VariableDeclarator(node: TSESTree.VariableDeclarator) {
9039
// check if destructuring return value from render
9140
if (isObjectPattern(node.id)) {
9241
return;
9342
}
9443

95-
const isValidRenderDeclarator = isRenderVariableDeclarator(node, [
96-
...renderFunctions,
97-
renderAlias,
98-
]);
99-
const isValidWildcardImport = !!wildcardImportName;
100-
101-
// check if is a Testing Library related import
102-
if (!isValidRenderDeclarator && !isValidWildcardImport) {
103-
return;
104-
}
105-
106-
const renderFunctionName =
107-
isCallExpression(node.init) &&
108-
ASTUtils.isIdentifier(node.init.callee) &&
109-
node.init.callee.name;
110-
111-
const renderFunctionObjectName =
112-
isCallExpression(node.init) &&
113-
isMemberExpression(node.init.callee) &&
114-
ASTUtils.isIdentifier(node.init.callee.property) &&
115-
ASTUtils.isIdentifier(node.init.callee.object) &&
116-
node.init.callee.property.name === 'render' &&
117-
node.init.callee.object.name;
118-
119-
const isRenderAlias = !!renderAlias;
120-
const isCustomRender = renderFunctions.includes(renderFunctionName);
121-
const isWildCardRender =
122-
renderFunctionObjectName &&
123-
renderFunctionObjectName === wildcardImportName;
124-
125-
// check if is a qualified render function
126-
if (!isRenderAlias && !isCustomRender && !isWildCardRender) {
127-
return;
128-
}
129-
13044
const renderResultName = ASTUtils.isIdentifier(node.id) && node.id.name;
45+
13146
const isAllowedRenderResultName = ALLOWED_VAR_NAMES.includes(
13247
renderResultName
13348
);
@@ -141,7 +56,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
14156
node,
14257
messageId: 'renderResultNamingConvention',
14358
data: {
144-
varName: renderResultName,
59+
renderResultName,
14560
},
14661
});
14762
},

0 commit comments

Comments
 (0)