66
77#![ cfg( feature = "ruledocs" ) ]
88
9+ use cow_utils:: CowUtils ;
10+ use rustc_hash:: FxHashSet ;
11+
12+ // Recursively scan rule files
13+ fn scan_rules_dir (
14+ dir : & std:: path:: Path ,
15+ base_dir : & std:: path:: Path ,
16+ results : & mut Vec < String > ,
17+ ) -> std:: io:: Result < ( ) > {
18+ for entry in std:: fs:: read_dir ( dir) ? {
19+ let entry = entry?;
20+ let path = entry. path ( ) ;
21+
22+ if path. is_dir ( ) {
23+ scan_rules_dir ( & path, base_dir, results) ?;
24+ } else if path. extension ( ) . and_then ( |s| s. to_str ( ) ) == Some ( "rs" ) {
25+ let content = std:: fs:: read_to_string ( & path) ?;
26+
27+ let has_declare_lint = content. contains ( "declare_oxc_lint!(" ) ;
28+ let has_from_config = content. contains ( "fn from_configuration(" ) ;
29+ // Look for "config =" as a parameter in declare_oxc_lint macro
30+ // Use regex to be more precise
31+ let has_config_param = content. contains ( "declare_oxc_lint!" )
32+ && content. split ( "declare_oxc_lint!" ) . any ( |section| {
33+ // Check if this section (after declare_oxc_lint!) contains "config ="
34+ // before the closing of the macro (the semicolon after the paren)
35+ if let Some ( macro_end) = section. find ( ");" ) {
36+ let macro_content = & section[ ..macro_end] ;
37+ macro_content. contains ( "config =" )
38+ } else {
39+ false
40+ }
41+ } ) ;
42+
43+ if has_declare_lint && has_from_config && !has_config_param {
44+ let rel_path = path. strip_prefix ( base_dir) . unwrap ( ) ;
45+ results. push ( rel_path. to_string_lossy ( ) . to_string ( ) ) ;
46+ }
47+ }
48+ }
49+ Ok ( ( ) )
50+ }
51+
952/// Test to ensure that all rules with `from_configuration` implementations
1053/// also have a schema and proper documentation.
1154///
@@ -75,7 +118,7 @@ fn test_rules_with_custom_configuration_have_schema() {
75118 "vue/define-props-declaration" ,
76119 ] ;
77120
78- let exception_set: std :: collections :: HashSet < & str > = exceptions. iter ( ) . copied ( ) . collect ( ) ;
121+ let exception_set: FxHashSet < & str > = exceptions. iter ( ) . copied ( ) . collect ( ) ;
79122
80123 // Step 1: Scan source code to find rules with from_configuration but no config =
81124 let workspace_root =
@@ -84,46 +127,6 @@ fn test_rules_with_custom_configuration_have_schema() {
84127
85128 let mut rules_with_from_config_no_schema = Vec :: new ( ) ;
86129
87- // Recursively scan rule files
88- fn scan_rules_dir (
89- dir : & std:: path:: Path ,
90- base_dir : & std:: path:: Path ,
91- results : & mut Vec < String > ,
92- ) -> std:: io:: Result < ( ) > {
93- for entry in std:: fs:: read_dir ( dir) ? {
94- let entry = entry?;
95- let path = entry. path ( ) ;
96-
97- if path. is_dir ( ) {
98- scan_rules_dir ( & path, base_dir, results) ?;
99- } else if path. extension ( ) . and_then ( |s| s. to_str ( ) ) == Some ( "rs" ) {
100- let content = std:: fs:: read_to_string ( & path) ?;
101-
102- let has_declare_lint = content. contains ( "declare_oxc_lint!(" ) ;
103- let has_from_config = content. contains ( "fn from_configuration(" ) ;
104- // Look for "config =" as a parameter in declare_oxc_lint macro
105- // Use regex to be more precise
106- let has_config_param = content. contains ( "declare_oxc_lint!" )
107- && content. split ( "declare_oxc_lint!" ) . any ( |section| {
108- // Check if this section (after declare_oxc_lint!) contains "config ="
109- // before the closing of the macro (the semicolon after the paren)
110- if let Some ( macro_end) = section. find ( ");" ) {
111- let macro_content = & section[ ..macro_end] ;
112- macro_content. contains ( "config =" )
113- } else {
114- false
115- }
116- } ) ;
117-
118- if has_declare_lint && has_from_config && !has_config_param {
119- let rel_path = path. strip_prefix ( base_dir) . unwrap ( ) ;
120- results. push ( rel_path. to_string_lossy ( ) . to_string ( ) ) ;
121- }
122- }
123- }
124- Ok ( ( ) )
125- }
126-
127130 scan_rules_dir ( & rules_dir, & rules_dir, & mut rules_with_from_config_no_schema)
128131 . expect ( "Failed to scan rules directory" ) ;
129132
@@ -133,7 +136,11 @@ fn test_rules_with_custom_configuration_have_schema() {
133136 . filter_map ( |path| {
134137 // Path format: "plugin/rule_file.rs" or "plugin/rule_name/mod.rs"
135138 // Always use '/' as separator regardless of OS
136- let normalized_path = path. replace ( std:: path:: MAIN_SEPARATOR , "/" ) ;
139+ let normalized_path = if std:: path:: MAIN_SEPARATOR == '/' {
140+ path. as_str ( )
141+ } else {
142+ & path. cow_replace ( std:: path:: MAIN_SEPARATOR , "/" )
143+ } ;
137144 let parts: Vec < & str > = normalized_path. split ( '/' ) . collect ( ) ;
138145 if parts. len ( ) >= 2 {
139146 let plugin = parts[ 0 ] ;
@@ -145,8 +152,12 @@ fn test_rules_with_custom_configuration_have_schema() {
145152 parts[ 1 ]
146153 } ;
147154 // Convert underscores to hyphens for rule name
148- let rule_name = rule_file. replace ( '_' , "-" ) ;
149- Some ( format ! ( "{}/{}" , plugin, rule_name) )
155+ let rule_name = if rule_file. contains ( '_' ) {
156+ rule_file. cow_replace ( '_' , "-" ) . into_owned ( )
157+ } else {
158+ rule_file. to_string ( )
159+ } ;
160+ Some ( format ! ( "{plugin}/{rule_name}" ) )
150161 } else {
151162 None
152163 }
@@ -184,9 +195,11 @@ fn test_rules_with_custom_configuration_have_schema() {
184195 . output ( )
185196 . expect ( "Failed to generate documentation" ) ;
186197
187- if !output. status . success ( ) {
188- panic ! ( "Failed to generate documentation:\n {}" , String :: from_utf8_lossy( & output. stderr) ) ;
189- }
198+ assert ! (
199+ output. status. success( ) ,
200+ "Failed to generate documentation:\n {}" ,
201+ String :: from_utf8_lossy( & output. stderr)
202+ ) ;
190203
191204 // Step 3: Check generated documentation for Configuration sections
192205 for rule_name in & rules_needing_schema {
@@ -196,7 +209,7 @@ fn test_rules_with_custom_configuration_have_schema() {
196209 }
197210
198211 // Read the generated markdown file
199- let doc_file = temp_dir. join ( format ! ( "{}.md" , rule_name ) ) ;
212+ let doc_file = temp_dir. join ( format ! ( "{rule_name }.md" ) ) ;
200213
201214 if !doc_file. exists ( ) {
202215 failures. push ( format ! (
0 commit comments