1
1
use convert_case:: { Boundary , Case , Converter } ;
2
+ use cow_utils:: CowUtils ;
2
3
use oxc_diagnostics:: OxcDiagnostic ;
3
4
use oxc_macros:: declare_oxc_lint;
4
5
use oxc_span:: Span ;
5
6
use serde_json:: Value ;
6
7
7
8
use crate :: { context:: LintContext , rule:: Rule } ;
8
9
9
- fn join_strings_disjunction ( strings : & [ String ] ) -> String {
10
- let mut list = String :: new ( ) ;
11
- for ( i, s) in strings. iter ( ) . enumerate ( ) {
12
- if i == 0 {
13
- list. push_str ( s) ;
14
- } else if i == strings. len ( ) - 1 {
15
- list. push_str ( & format ! ( ", or {s}" ) ) ;
16
- } else {
17
- list. push_str ( & format ! ( ", {s}" ) ) ;
18
- }
19
- }
20
- list
21
- }
22
-
23
- fn filename_case_diagnostic ( filename : & str , valid_cases : & [ ( & str , Case ) ] ) -> OxcDiagnostic {
24
- let case_names = valid_cases. iter ( ) . map ( |( name, _) | format ! ( "{name} case" ) ) . collect :: < Vec < _ > > ( ) ;
25
- let message = format ! ( "Filename should be in {}" , join_strings_disjunction( & case_names) ) ;
26
-
27
- let trimmed_filename = filename. trim_matches ( '_' ) ;
28
- let converted_filenames = valid_cases
29
- . iter ( )
30
- . map ( |( _, case) | {
31
- let converter =
32
- Converter :: new ( ) . remove_boundaries ( & [ Boundary :: LOWER_DIGIT , Boundary :: DIGIT_LOWER ] ) ;
33
- // get the leading characters that were trimmed, if any, else empty string
34
- let leading = filename. chars ( ) . take_while ( |c| c == & '_' ) . collect :: < String > ( ) ;
35
- let trailing = filename. chars ( ) . rev ( ) . take_while ( |c| c == & '_' ) . collect :: < String > ( ) ;
36
- format ! ( "'{leading}{}{trailing}'" , converter. to_case( * case) . convert( trimmed_filename) )
37
- } )
38
- . collect :: < Vec < _ > > ( ) ;
39
-
40
- let help_message =
41
- format ! ( "Rename the file to {}" , join_strings_disjunction( & converted_filenames) ) ;
42
-
43
- OxcDiagnostic :: warn ( message) . with_label ( Span :: default ( ) ) . with_help ( help_message)
44
- }
45
-
46
10
#[ derive( Debug , Clone ) ]
47
11
pub struct FilenameCase {
48
12
/// Whether kebab case is allowed.
@@ -68,7 +32,9 @@ declare_oxc_lint!(
68
32
///
69
33
/// ### Why is this bad?
70
34
///
71
- /// Inconsistent file naming conventions can make it harder to locate files or to create new ones.
35
+ /// Inconsistent file naming conventions make it harder to locate files, navigate projects, and enforce
36
+ /// consistency across a codebase. Standardizing naming conventions improves readability, reduces cognitive
37
+ /// overhead, and aligns with best practices in large-scale development.
72
38
///
73
39
/// ### Cases
74
40
///
@@ -97,6 +63,41 @@ declare_oxc_lint!(
97
63
/// - `SomeFileName.js`
98
64
/// - `SomeFileName.Test.js`
99
65
/// - `SomeFileName.TestUtils.js`
66
+ ///
67
+ /// ### Options
68
+ ///
69
+ /// Use `kebabCase` as the default option.
70
+ ///
71
+ /// #### case
72
+ ///
73
+ /// `{ type: 'kebabCase' | 'camelCase' | 'snakeCase' | 'pascalCase' }`
74
+ ///
75
+ /// You can set the case option like this:
76
+ /// ```json
77
+ /// "unicorn/filename-case": [
78
+ /// "error",
79
+ /// {
80
+ /// "case": "kebabCase"
81
+ /// }
82
+ /// ]
83
+ /// ```
84
+ ///
85
+ /// #### cases
86
+ ///
87
+ /// `{ type: { [key in 'kebabCase' | 'camelCase' | 'snakeCase' | 'pascalCase']?: boolean } }`
88
+ ///
89
+ /// You can set the case option like this:
90
+ /// ```json
91
+ /// "unicorn/filename-case": [
92
+ /// "error",
93
+ /// {
94
+ /// "cases": {
95
+ /// "camelCase": true,
96
+ /// "pascalCase": true
97
+ /// }
98
+ /// }
99
+ /// ]
100
+ /// ```
100
101
FilenameCase ,
101
102
unicorn,
102
103
style
@@ -150,30 +151,60 @@ impl Rule for FilenameCase {
150
151
let filename = filename. trim_matches ( '_' ) ;
151
152
152
153
let cases = [
153
- ( self . camel_case , Case :: Camel , "camel" ) ,
154
- ( self . kebab_case , Case :: Kebab , "kebab" ) ,
155
- ( self . snake_case , Case :: Snake , "snake" ) ,
156
- ( self . pascal_case , Case :: Pascal , "pascal" ) ,
154
+ ( self . camel_case , Case :: Camel , "camel case " ) ,
155
+ ( self . kebab_case , Case :: Kebab , "kebab case " ) ,
156
+ ( self . snake_case , Case :: Snake , "snake case " ) ,
157
+ ( self . pascal_case , Case :: Pascal , "pascal case " ) ,
157
158
] ;
158
- let mut enabled_cases = cases. iter ( ) . filter ( |( enabled, _, _) | * enabled) ;
159
159
160
- if !enabled_cases . any ( | ( _ , case , _ ) | {
161
- let converter =
162
- Converter :: new ( ) . remove_boundaries ( & [ Boundary :: LOWER_DIGIT , Boundary :: DIGIT_LOWER ] ) ;
163
- converter. to_case ( * case ) . convert ( filename ) == filename
164
- } ) {
165
- let valid_cases = cases
166
- . iter ( )
167
- . filter_map (
168
- | ( enabled , case , name ) | if * enabled { Some ( ( * name , * case ) ) } else { None } ,
169
- )
170
- . collect :: < Vec < _ > > ( ) ;
171
- let filename = file_path . file_name ( ) . unwrap ( ) . to_string_lossy ( ) ;
172
- ctx . diagnostic ( filename_case_diagnostic ( & filename , & valid_cases ) ) ;
160
+ let mut valid = Vec :: new ( ) ;
161
+ for ( enabled , case , name ) in cases {
162
+ if enabled {
163
+ let converter = Converter :: new ( )
164
+ . remove_boundaries ( & [ Boundary :: LOWER_DIGIT , Boundary :: DIGIT_LOWER ] ) ;
165
+ let converter = converter . to_case ( case ) ;
166
+
167
+ if converter . convert ( filename ) == filename {
168
+ return ;
169
+ }
170
+
171
+ valid . push ( ( converter , name ) ) ;
172
+ }
173
173
}
174
+
175
+ let filename = file_path. file_name ( ) . unwrap ( ) . to_string_lossy ( ) ;
176
+ ctx. diagnostic ( filename_case_diagnostic ( & filename, valid) ) ;
174
177
}
175
178
}
176
179
180
+ fn filename_case_diagnostic ( filename : & str , valid_cases : Vec < ( Converter , & str ) > ) -> OxcDiagnostic {
181
+ let trimmed_filename = filename. trim_matches ( '_' ) ;
182
+ let valid_cases_len = valid_cases. len ( ) ;
183
+
184
+ let mut message = String :: from ( "Filename should be in " ) ;
185
+ let mut help_message = String :: from ( "Rename the file to " ) ;
186
+
187
+ for ( i, ( converter, name) ) in valid_cases. into_iter ( ) . enumerate ( ) {
188
+ let filename = format ! (
189
+ "'{}'" ,
190
+ filename. cow_replace( trimmed_filename, & converter. convert( trimmed_filename) )
191
+ ) ;
192
+
193
+ let ( name, filename) = if i == 0 {
194
+ ( name, filename. as_ref ( ) )
195
+ } else if i == valid_cases_len - 1 {
196
+ ( & * format ! ( ", or {name}" ) , & * format ! ( ", or {filename}" ) )
197
+ } else {
198
+ ( & * format ! ( ", {name}" ) , & * format ! ( ", {filename}" ) )
199
+ } ;
200
+
201
+ message. push_str ( name) ;
202
+ help_message. push_str ( filename) ;
203
+ }
204
+
205
+ OxcDiagnostic :: warn ( message) . with_label ( Span :: default ( ) ) . with_help ( help_message)
206
+ }
207
+
177
208
#[ test]
178
209
fn test ( ) {
179
210
use std:: path:: PathBuf ;
@@ -277,6 +308,7 @@ fn test() {
277
308
test_cases( "src/foo/FooBar.js" , [ "kebabCase" , "pascalCase" ] ) ,
278
309
test_cases( "src/foo/___foo_bar.js" , [ "snakeCase" , "pascalCase" ] ) ,
279
310
] ;
311
+
280
312
let fail = vec ! [
281
313
test_case( "src/foo/foo_bar.js" , "" ) ,
282
314
// todo: linter does not support uppercase JS files
0 commit comments