Skip to content

Commit 9df5565

Browse files
committedMar 13, 2025·
refactor(linter): improve unicorn/filename-case (#9762)
Related to #6050 - Improve the documentation - Better code style
1 parent b0b1f18 commit 9df5565

File tree

1 file changed

+88
-56
lines changed

1 file changed

+88
-56
lines changed
 

‎crates/oxc_linter/src/rules/unicorn/filename_case.rs

+88-56
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,12 @@
11
use convert_case::{Boundary, Case, Converter};
2+
use cow_utils::CowUtils;
23
use oxc_diagnostics::OxcDiagnostic;
34
use oxc_macros::declare_oxc_lint;
45
use oxc_span::Span;
56
use serde_json::Value;
67

78
use crate::{context::LintContext, rule::Rule};
89

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-
4610
#[derive(Debug, Clone)]
4711
pub struct FilenameCase {
4812
/// Whether kebab case is allowed.
@@ -68,7 +32,9 @@ declare_oxc_lint!(
6832
///
6933
/// ### Why is this bad?
7034
///
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.
7238
///
7339
/// ### Cases
7440
///
@@ -97,6 +63,41 @@ declare_oxc_lint!(
9763
/// - `SomeFileName.js`
9864
/// - `SomeFileName.Test.js`
9965
/// - `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+
/// ```
100101
FilenameCase,
101102
unicorn,
102103
style
@@ -150,30 +151,60 @@ impl Rule for FilenameCase {
150151
let filename = filename.trim_matches('_');
151152

152153
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"),
157158
];
158-
let mut enabled_cases = cases.iter().filter(|(enabled, _, _)| *enabled);
159159

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+
}
173173
}
174+
175+
let filename = file_path.file_name().unwrap().to_string_lossy();
176+
ctx.diagnostic(filename_case_diagnostic(&filename, valid));
174177
}
175178
}
176179

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+
177208
#[test]
178209
fn test() {
179210
use std::path::PathBuf;
@@ -277,6 +308,7 @@ fn test() {
277308
test_cases("src/foo/FooBar.js", ["kebabCase", "pascalCase"]),
278309
test_cases("src/foo/___foo_bar.js", ["snakeCase", "pascalCase"]),
279310
];
311+
280312
let fail = vec![
281313
test_case("src/foo/foo_bar.js", ""),
282314
// todo: linter does not support uppercase JS files

0 commit comments

Comments
 (0)
Please sign in to comment.