1
1
import { TSESLint , TSESTree } from '@typescript-eslint/experimental-utils' ;
2
2
3
+ export type TestingLibrarySettings = {
4
+ 'testing-library/module' ?: string ;
5
+ } ;
6
+
7
+ export type TestingLibraryContext <
8
+ TOptions extends readonly unknown [ ] ,
9
+ TMessageIds extends string
10
+ > = Readonly <
11
+ TSESLint . RuleContext < TMessageIds , TOptions > & {
12
+ settings : TestingLibrarySettings ;
13
+ }
14
+ > ;
15
+
16
+ export type EnhancedRuleCreate <
17
+ TOptions extends readonly unknown [ ] ,
18
+ TMessageIds extends string ,
19
+ TRuleListener extends TSESLint . RuleListener = TSESLint . RuleListener
20
+ > = (
21
+ context : TestingLibraryContext < TOptions , TMessageIds > ,
22
+ optionsWithDefault : Readonly < TOptions > ,
23
+ detectionHelpers : Readonly < DetectionHelpers >
24
+ ) => TRuleListener ;
25
+
3
26
export type DetectionHelpers = {
4
- getIsImportingTestingLibrary : ( ) => boolean ;
27
+ getIsTestingLibraryImported : ( ) => boolean ;
28
+ canReportErrors : ( ) => boolean ;
5
29
} ;
6
30
7
31
/**
@@ -11,50 +35,91 @@ export function detectTestingLibraryUtils<
11
35
TOptions extends readonly unknown [ ] ,
12
36
TMessageIds extends string ,
13
37
TRuleListener extends TSESLint . RuleListener = TSESLint . RuleListener
14
- > (
15
- ruleCreate : (
16
- context : Readonly < TSESLint . RuleContext < TMessageIds , TOptions > > ,
17
- optionsWithDefault : Readonly < TOptions > ,
18
- detectionHelpers : Readonly < DetectionHelpers >
19
- ) => TRuleListener
20
- ) {
38
+ > ( ruleCreate : EnhancedRuleCreate < TOptions , TMessageIds , TRuleListener > ) {
21
39
return (
22
- context : Readonly < TSESLint . RuleContext < TMessageIds , TOptions > > ,
40
+ context : TestingLibraryContext < TOptions , TMessageIds > ,
23
41
optionsWithDefault : Readonly < TOptions >
24
- ) : TRuleListener => {
25
- let isImportingTestingLibrary = false ;
42
+ ) : TSESLint . RuleListener => {
43
+ let isImportingTestingLibraryModule = false ;
44
+ let isImportingCustomModule = false ;
26
45
27
- // TODO: init here options based on shared ESLint config
46
+ // Init options based on shared ESLint settings
47
+ const customModule = context . settings [ 'testing-library/module' ] ;
28
48
29
- // helpers for Testing Library detection
49
+ // Helpers for Testing Library detection.
30
50
const helpers : DetectionHelpers = {
31
- getIsImportingTestingLibrary ( ) {
32
- return isImportingTestingLibrary ;
51
+ /**
52
+ * Gets if Testing Library is considered as imported or not.
53
+ *
54
+ * By default, it is ALWAYS considered as imported. This is what we call
55
+ * "aggressive reporting" so we don't miss TL utils reexported from
56
+ * custom modules.
57
+ *
58
+ * However, there is a setting to customize the module where TL utils can
59
+ * be imported from: "testing-library/module". If this setting is enabled,
60
+ * then this method will return `true` ONLY IF a testing-library package
61
+ * or custom module are imported.
62
+ */
63
+ getIsTestingLibraryImported ( ) {
64
+ if ( ! customModule ) {
65
+ return true ;
66
+ }
67
+
68
+ return isImportingTestingLibraryModule || isImportingCustomModule ;
69
+ } ,
70
+
71
+ /**
72
+ * Wraps all conditions that must be met to report rules.
73
+ */
74
+ canReportErrors ( ) {
75
+ return this . getIsTestingLibraryImported ( ) ;
33
76
} ,
34
77
} ;
35
78
36
- // instructions for Testing Library detection
79
+ // Instructions for Testing Library detection.
37
80
const detectionInstructions : TSESLint . RuleListener = {
81
+ /**
82
+ * This ImportDeclaration rule listener will check if Testing Library related
83
+ * modules are loaded. Since imports happen first thing in a file, it's
84
+ * safe to use `isImportingTestingLibraryModule` and `isImportingCustomModule`
85
+ * since they will have corresponding value already updated when reporting other
86
+ * parts of the file.
87
+ */
38
88
ImportDeclaration ( node : TSESTree . ImportDeclaration ) {
39
- isImportingTestingLibrary = / t e s t i n g - l i b r a r y / g. test (
40
- node . source . value as string
41
- ) ;
89
+ if ( ! isImportingTestingLibraryModule ) {
90
+ // check only if testing library import not found yet so we avoid
91
+ // to override isImportingTestingLibraryModule after it's found
92
+ isImportingTestingLibraryModule = / t e s t i n g - l i b r a r y / g. test (
93
+ node . source . value as string
94
+ ) ;
95
+ }
96
+
97
+ if ( ! isImportingCustomModule ) {
98
+ // check only if custom module import not found yet so we avoid
99
+ // to override isImportingCustomModule after it's found
100
+ const importName = String ( node . source . value ) ;
101
+ isImportingCustomModule = importName . endsWith ( customModule ) ;
102
+ }
42
103
} ,
43
104
} ;
44
105
45
106
// update given rule to inject Testing Library detection
46
107
const ruleInstructions = ruleCreate ( context , optionsWithDefault , helpers ) ;
47
- const enhancedRuleInstructions = Object . assign ( { } , ruleInstructions ) ;
108
+ const enhancedRuleInstructions : TSESLint . RuleListener = { } ;
109
+
110
+ const allKeys = new Set (
111
+ Object . keys ( detectionInstructions ) . concat ( Object . keys ( ruleInstructions ) )
112
+ ) ;
48
113
49
- Object . keys ( detectionInstructions ) . forEach ( ( instruction ) => {
50
- ( enhancedRuleInstructions as TSESLint . RuleListener ) [ instruction ] = (
51
- node
52
- ) => {
114
+ // Iterate over ALL instructions keys so we can override original rule instructions
115
+ // to prevent their execution if conditions to report errors are not met.
116
+ allKeys . forEach ( ( instruction ) => {
117
+ enhancedRuleInstructions [ instruction ] = ( node ) => {
53
118
if ( instruction in detectionInstructions ) {
54
119
detectionInstructions [ instruction ] ( node ) ;
55
120
}
56
121
57
- if ( ruleInstructions [ instruction ] ) {
122
+ if ( helpers . canReportErrors ( ) && ruleInstructions [ instruction ] ) {
58
123
return ruleInstructions [ instruction ] ( node ) ;
59
124
}
60
125
} ;
0 commit comments