Skip to content

Commit 5a6644f

Browse files
authored
refactor(no-dom-import): use createTestingLibraryRule (#247)
* feat: new setting for customizing file name pattern to report * test: add custom rule tester for testing library * refactor: use common rule tester config * refactor(no-dom-import): use createTestingLibraryRule * feat(detection-helpers): check imports with require * test(no-dom-import): include test cases for custom module setting * test(no-dom-import): include test cases for custom module setting * chore: fix merge * refactor(no-dom-import): extract detection helpers for import nodes * test: increase coverage * refactor: rename setting for filename pattern
1 parent d1f0388 commit 5a6644f

9 files changed

+397
-85
lines changed

.eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
coverage/
2+
dist/

.lintstagedrc

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
2-
"*.{js,ts}": [
2+
"*.{js,ts}": [
33
"eslint --fix",
44
"prettier --write",
5-
"jest --findRelatedTests",
5+
"jest --findRelatedTests"
66
],
77
"*.md": ["prettier --write"]
88
}

lib/detect-testing-library-utils.ts

+75-26
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
2+
import { isLiteral } from './node-utils';
23

34
export type TestingLibrarySettings = {
45
'testing-library/module'?: string;
5-
'testing-library/file-name'?: string;
6+
'testing-library/filename-pattern'?: string;
67
};
78

89
export type TestingLibraryContext<
@@ -24,13 +25,20 @@ export type EnhancedRuleCreate<
2425
detectionHelpers: Readonly<DetectionHelpers>
2526
) => TRuleListener;
2627

28+
type ModuleImportation =
29+
| TSESTree.ImportDeclaration
30+
| TSESTree.CallExpression
31+
| null;
32+
2733
export type DetectionHelpers = {
34+
getTestingLibraryImportNode: () => ModuleImportation;
35+
getCustomModuleImportNode: () => ModuleImportation;
2836
getIsTestingLibraryImported: () => boolean;
29-
getIsValidFileName: () => boolean;
37+
getIsValidFilename: () => boolean;
3038
canReportErrors: () => boolean;
3139
};
3240

33-
const DEFAULT_FILE_NAME_PATTERN = '^.*\\.(test|spec)\\.[jt]sx?$';
41+
const DEFAULT_FILENAME_PATTERN = '^.*\\.(test|spec)\\.[jt]sx?$';
3442

3543
/**
3644
* Enhances a given rule `create` with helpers to detect Testing Library utils.
@@ -44,17 +52,23 @@ export function detectTestingLibraryUtils<
4452
context: TestingLibraryContext<TOptions, TMessageIds>,
4553
optionsWithDefault: Readonly<TOptions>
4654
): TSESLint.RuleListener => {
47-
let isImportingTestingLibraryModule = false;
48-
let isImportingCustomModule = false;
55+
let importedTestingLibraryNode: ModuleImportation = null;
56+
let importedCustomModuleNode: ModuleImportation = null;
4957

5058
// Init options based on shared ESLint settings
5159
const customModule = context.settings['testing-library/module'];
52-
const fileNamePattern =
53-
context.settings['testing-library/file-name'] ??
54-
DEFAULT_FILE_NAME_PATTERN;
60+
const filenamePattern =
61+
context.settings['testing-library/filename-pattern'] ??
62+
DEFAULT_FILENAME_PATTERN;
5563

5664
// Helpers for Testing Library detection.
5765
const helpers: DetectionHelpers = {
66+
getTestingLibraryImportNode() {
67+
return importedTestingLibraryNode;
68+
},
69+
getCustomModuleImportNode() {
70+
return importedCustomModuleNode;
71+
},
5872
/**
5973
* Gets if Testing Library is considered as imported or not.
6074
*
@@ -72,50 +86,85 @@ export function detectTestingLibraryUtils<
7286
return true;
7387
}
7488

75-
return isImportingTestingLibraryModule || isImportingCustomModule;
89+
return !!importedTestingLibraryNode || !!importedCustomModuleNode;
7690
},
7791

7892
/**
79-
* Gets if name of the file being analyzed is valid or not.
93+
* Gets if filename being analyzed is valid or not.
8094
*
81-
* This is based on "testing-library/file-name" setting.
95+
* This is based on "testing-library/filename-pattern" setting.
8296
*/
83-
getIsValidFileName() {
97+
getIsValidFilename() {
8498
const fileName = context.getFilename();
85-
return !!fileName.match(fileNamePattern);
99+
return !!fileName.match(filenamePattern);
86100
},
87101

88102
/**
89103
* Wraps all conditions that must be met to report rules.
90104
*/
91105
canReportErrors() {
92-
return this.getIsTestingLibraryImported() && this.getIsValidFileName();
106+
return this.getIsTestingLibraryImported() && this.getIsValidFilename();
93107
},
94108
};
95109

96110
// Instructions for Testing Library detection.
97111
const detectionInstructions: TSESLint.RuleListener = {
98112
/**
99113
* This ImportDeclaration rule listener will check if Testing Library related
100-
* modules are loaded. Since imports happen first thing in a file, it's
114+
* modules are imported. Since imports happen first thing in a file, it's
101115
* safe to use `isImportingTestingLibraryModule` and `isImportingCustomModule`
102116
* since they will have corresponding value already updated when reporting other
103117
* parts of the file.
104118
*/
105119
ImportDeclaration(node: TSESTree.ImportDeclaration) {
106-
if (!isImportingTestingLibraryModule) {
107-
// check only if testing library import not found yet so we avoid
108-
// to override isImportingTestingLibraryModule after it's found
109-
isImportingTestingLibraryModule = /testing-library/g.test(
110-
node.source.value as string
111-
);
120+
// check only if testing library import not found yet so we avoid
121+
// to override importedTestingLibraryNode after it's found
122+
if (
123+
!importedTestingLibraryNode &&
124+
/testing-library/g.test(node.source.value as string)
125+
) {
126+
importedTestingLibraryNode = node;
127+
}
128+
129+
// check only if custom module import not found yet so we avoid
130+
// to override importedCustomModuleNode after it's found
131+
if (
132+
!importedCustomModuleNode &&
133+
String(node.source.value).endsWith(customModule)
134+
) {
135+
importedCustomModuleNode = node;
136+
}
137+
},
138+
139+
// Check if Testing Library related modules are loaded with required.
140+
[`CallExpression > Identifier[name="require"]`](
141+
node: TSESTree.Identifier
142+
) {
143+
const callExpression = node.parent as TSESTree.CallExpression;
144+
const { arguments: args } = callExpression;
145+
146+
if (
147+
!importedTestingLibraryNode &&
148+
args.some(
149+
(arg) =>
150+
isLiteral(arg) &&
151+
typeof arg.value === 'string' &&
152+
/testing-library/g.test(arg.value)
153+
)
154+
) {
155+
importedTestingLibraryNode = callExpression;
112156
}
113157

114-
if (!isImportingCustomModule) {
115-
// check only if custom module import not found yet so we avoid
116-
// to override isImportingCustomModule after it's found
117-
const importName = String(node.source.value);
118-
isImportingCustomModule = importName.endsWith(customModule);
158+
if (
159+
!importedCustomModuleNode &&
160+
args.some(
161+
(arg) =>
162+
isLiteral(arg) &&
163+
typeof arg.value === 'string' &&
164+
arg.value.endsWith(customModule)
165+
)
166+
) {
167+
importedCustomModuleNode = callExpression;
119168
}
120169
},
121170
};

lib/node-utils.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ export function isMemberExpression(
2727
return node && node.type === AST_NODE_TYPES.MemberExpression;
2828
}
2929

30-
export function isLiteral(node: TSESTree.Node): node is TSESTree.Literal {
31-
return node && node.type === AST_NODE_TYPES.Literal;
30+
export function isLiteral(
31+
node: TSESTree.Node | null | undefined
32+
): node is TSESTree.Literal {
33+
return node?.type === AST_NODE_TYPES.Literal;
3234
}
3335

3436
export function isImportSpecifier(

lib/rules/no-dom-import.ts

+34-26
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils';
2-
import { getDocsUrl } from '../utils';
3-
import { isLiteral, isIdentifier } from '../node-utils';
1+
import {
2+
AST_NODE_TYPES,
3+
TSESTree,
4+
} from '@typescript-eslint/experimental-utils';
5+
import { isIdentifier, isLiteral } from '../node-utils';
6+
import { createTestingLibraryRule } from '../create-testing-library-rule';
47

58
export const RULE_NAME = 'no-dom-import';
69
export type MessageIds = 'noDomImport' | 'noDomImportFramework';
@@ -11,7 +14,7 @@ const DOM_TESTING_LIBRARY_MODULES = [
1114
'@testing-library/dom',
1215
];
1316

14-
export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
17+
export default createTestingLibraryRule<Options, MessageIds>({
1518
name: RULE_NAME,
1619
meta: {
1720
type: 'problem',
@@ -35,7 +38,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
3538
},
3639
defaultOptions: [''],
3740

38-
create(context, [framework]) {
41+
create(context, [framework], helpers) {
3942
function report(
4043
node: TSESTree.ImportDeclaration | TSESTree.Identifier,
4144
moduleName: string
@@ -76,33 +79,38 @@ export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
7679
});
7780
}
7881
}
82+
7983
return {
80-
ImportDeclaration(node) {
81-
const value = node.source.value;
82-
const domModuleName = DOM_TESTING_LIBRARY_MODULES.find(
83-
(module) => module === value
84-
);
84+
'Program:exit'() {
85+
const importNode = helpers.getTestingLibraryImportNode();
8586

86-
if (domModuleName) {
87-
report(node, domModuleName);
87+
if (!importNode) {
88+
return;
8889
}
89-
},
9090

91-
[`CallExpression > Identifier[name="require"]`](
92-
node: TSESTree.Identifier
93-
) {
94-
const callExpression = node.parent as TSESTree.CallExpression;
95-
const { arguments: args } = callExpression;
91+
// import node of shape: import { foo } from 'bar'
92+
if (importNode.type === AST_NODE_TYPES.ImportDeclaration) {
93+
const domModuleName = DOM_TESTING_LIBRARY_MODULES.find(
94+
(module) => module === importNode.source.value
95+
);
96+
97+
domModuleName && report(importNode, domModuleName);
98+
}
9699

97-
const literalNodeDomModuleName = args.find(
98-
(args) =>
99-
isLiteral(args) &&
100-
typeof args.value === 'string' &&
101-
DOM_TESTING_LIBRARY_MODULES.includes(args.value)
102-
) as TSESTree.Literal;
100+
// import node of shape: const { foo } = require('bar')
101+
if (importNode.type === AST_NODE_TYPES.CallExpression) {
102+
const literalNodeDomModuleName = importNode.arguments.find(
103+
(arg) =>
104+
isLiteral(arg) &&
105+
typeof arg.value === 'string' &&
106+
DOM_TESTING_LIBRARY_MODULES.includes(arg.value)
107+
) as TSESTree.Literal;
103108

104-
if (literalNodeDomModuleName) {
105-
report(node, literalNodeDomModuleName.value as string);
109+
literalNodeDomModuleName &&
110+
report(
111+
importNode.callee as TSESTree.Identifier,
112+
literalNodeDomModuleName.value as string
113+
);
106114
}
107115
},
108116
};

0 commit comments

Comments
 (0)