11import { TSESLint , TSESTree } from '@typescript-eslint/experimental-utils' ;
22
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+
326export type DetectionHelpers = {
4- getIsImportingTestingLibrary : ( ) => boolean ;
27+ getIsTestingLibraryImported : ( ) => boolean ;
28+ canReportErrors : ( ) => boolean ;
529} ;
630
731/**
@@ -11,50 +35,91 @@ export function detectTestingLibraryUtils<
1135 TOptions extends readonly unknown [ ] ,
1236 TMessageIds extends string ,
1337 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 > ) {
2139 return (
22- context : Readonly < TSESLint . RuleContext < TMessageIds , TOptions > > ,
40+ context : TestingLibraryContext < TOptions , TMessageIds > ,
2341 optionsWithDefault : Readonly < TOptions >
24- ) : TRuleListener => {
25- let isImportingTestingLibrary = false ;
42+ ) : TSESLint . RuleListener => {
43+ let isImportingTestingLibraryModule = false ;
44+ let isImportingCustomModule = false ;
2645
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' ] ;
2848
29- // helpers for Testing Library detection
49+ // Helpers for Testing Library detection.
3050 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 ( ) ;
3376 } ,
3477 } ;
3578
36- // instructions for Testing Library detection
79+ // Instructions for Testing Library detection.
3780 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+ */
3888 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+ }
42103 } ,
43104 } ;
44105
45106 // update given rule to inject Testing Library detection
46107 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+ ) ;
48113
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 ) => {
53118 if ( instruction in detectionInstructions ) {
54119 detectionInstructions [ instruction ] ( node ) ;
55120 }
56121
57- if ( ruleInstructions [ instruction ] ) {
122+ if ( helpers . canReportErrors ( ) && ruleInstructions [ instruction ] ) {
58123 return ruleInstructions [ instruction ] ( node ) ;
59124 }
60125 } ;
0 commit comments