Skip to content

Commit 06e3db9

Browse files
committedMar 29, 2025·
feat(linter): support multipleFileExtensions option for unicorn/filename-case (#10118)
closes #9765
1 parent 09c0ac6 commit 06e3db9

File tree

2 files changed

+61
-18
lines changed

2 files changed

+61
-18
lines changed
 

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

+47-14
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,14 @@ pub struct FilenameCaseConfig {
3434
/// Whether pascal case is allowed.
3535
pascal_case: bool,
3636
ignore: Option<Regex>,
37+
multi_extensions: bool,
3738
}
3839

3940
declare_oxc_lint!(
4041
/// ### What it does
4142
///
42-
/// Enforces specific case styles for filenames. By default, kebab case is enforced.
43+
/// Enforces a consistent case style for filenames to improve project organization and maintainability.
44+
/// By default, `kebab-case` is enforced, but other styles can be configured.
4345
///
4446
/// ### Why is this bad?
4547
///
@@ -77,8 +79,6 @@ declare_oxc_lint!(
7779
///
7880
/// ### Options
7981
///
80-
/// Use `kebabCase` as the default option.
81-
///
8282
/// #### case
8383
///
8484
/// `{ type: 'kebabCase' | 'camelCase' | 'snakeCase' | 'pascalCase' }`
@@ -112,7 +112,9 @@ declare_oxc_lint!(
112112
///
113113
/// #### ignore
114114
///
115-
/// `{ type: String (must be a valid regular expression) }`
115+
/// `{ type: string }`
116+
///
117+
/// Specifies a regular expression pattern for filenames that should be ignored by this rule.
116118
///
117119
/// You can set the `ignore` option like this:
118120
/// ```json
@@ -123,18 +125,28 @@ declare_oxc_lint!(
123125
/// }
124126
/// ]
125127
/// ```
128+
///
129+
/// #### multipleFileExtensions
130+
///
131+
/// `{ type: boolean, default: true }`
132+
///
133+
/// Whether to treat additional, `.`-separated parts of a filename as parts of the extension rather than parts of the filename.
126134
FilenameCase,
127135
unicorn,
128136
style
129137
);
130138

131139
impl Rule for FilenameCase {
132140
fn from_configuration(value: serde_json::Value) -> Self {
133-
let mut config = FilenameCaseConfig::default();
141+
let mut config = FilenameCaseConfig { multi_extensions: true, ..Default::default() };
134142

135143
if let Some(value) = value.get(0) {
136-
if let Some(Value::String(pat)) = value.get("ignore") {
137-
config.ignore = RegexBuilder::new(pat).build().ok();
144+
if let Some(Value::String(val)) = value.get("ignore") {
145+
config.ignore = RegexBuilder::new(val).build().ok();
146+
}
147+
148+
if let Some(Value::Bool(val)) = value.get("multipleFileExtensions") {
149+
config.multi_extensions = *val;
138150
}
139151

140152
if let Some(Value::String(s)) = value.get("case") {
@@ -176,7 +188,12 @@ impl Rule for FilenameCase {
176188
return;
177189
}
178190

179-
let filename = raw_filename.rsplit_once('.').map(|(before, _)| before);
191+
let filename = if self.multi_extensions {
192+
raw_filename.split('.').next()
193+
} else {
194+
raw_filename.rsplit_once('.').map(|(before, _)| before)
195+
};
196+
180197
let filename = filename.unwrap_or(raw_filename);
181198
let trimmed_filename = filename.trim_matches('_');
182199

@@ -286,29 +303,29 @@ fn test() {
286303
test_case("src/foo/fooBar.js", "camelCase"),
287304
test_case("src/foo/bar.test.js", "camelCase"),
288305
test_case("src/foo/fooBar.test.js", "camelCase"),
289-
// test_case("src/foo/fooBar.test-utils.js", "camelCase"),
290-
// test_case("src/foo/fooBar.test_utils.js", "camelCase"),
306+
test_case("src/foo/fooBar.test-utils.js", "camelCase"),
307+
test_case("src/foo/fooBar.test_utils.js", "camelCase"),
291308
test_case("src/foo/.test_utils.js", "camelCase"),
292309
test_case("src/foo/foo.js", "snakeCase"),
293310
test_case("src/foo/foo_bar.js", "snakeCase"),
294311
test_case("src/foo/foo.test.js", "snakeCase"),
295312
test_case("src/foo/foo_bar.test.js", "snakeCase"),
296313
test_case("src/foo/foo_bar.test_utils.js", "snakeCase"),
297-
// test_case("src/foo/foo_bar.test-utils.js", "snakeCase"),
314+
test_case("src/foo/foo_bar.test-utils.js", "snakeCase"),
298315
test_case("src/foo/.test-utils.js", "snakeCase"),
299316
test_case("src/foo/foo.js", "kebabCase"),
300317
test_case("src/foo/foo-bar.js", "kebabCase"),
301318
test_case("src/foo/foo.test.js", "kebabCase"),
302319
test_case("src/foo/foo-bar.test.js", "kebabCase"),
303320
test_case("src/foo/foo-bar.test-utils.js", "kebabCase"),
304-
// test_case("src/foo/foo-bar.test_utils.js", "kebabCase"),
321+
test_case("src/foo/foo-bar.test_utils.js", "kebabCase"),
305322
test_case("src/foo/.test_utils.js", "kebabCase"),
306323
test_case("src/foo/Foo.js", "pascalCase"),
307324
test_case("src/foo/FooBar.js", "pascalCase"),
308325
test_case("src/foo/Foo.test.js", "pascalCase"),
309326
test_case("src/foo/FooBar.test.js", "pascalCase"),
310-
// test_case("src/foo/FooBar.test-utils.js", "pascalCase"),
311-
// test_case("src/foo/FooBar.test_utils.js", "pascalCase"),
327+
test_case("src/foo/FooBar.test-utils.js", "pascalCase"),
328+
test_case("src/foo/FooBar.test_utils.js", "pascalCase"),
312329
test_case("src/foo/.test_utils.js", "pascalCase"),
313330
test_case("spec/iss47Spec.js", "camelCase"),
314331
test_case("spec/iss47Spec100.js", "camelCase"),
@@ -355,6 +372,14 @@ fn test() {
355372
"src/foo/BAR.js",
356373
serde_json::json!([{ "case": "kebabCase", "ignore": r"FOO.js|BAR.js" }]),
357374
),
375+
test_case_with_options(
376+
"src/foo/fooBar.testUtils.js",
377+
serde_json::json!([{ "case": "camelCase", "multipleFileExtensions": false }]),
378+
),
379+
test_case_with_options(
380+
"src/foo/foo_bar.test_utils.js",
381+
serde_json::json!([{ "case": "snakeCase", "multipleFileExtensions": false }]),
382+
),
358383
];
359384

360385
let fail = vec![
@@ -392,6 +417,14 @@ fn test() {
392417
"src/foo/FOOBAR.js",
393418
serde_json::json!([{ "case": "kebabCase", "ignore": r"foobar.js" }]),
394419
),
420+
test_case_with_options(
421+
"src/foo/fooBar.TestUtils.js",
422+
serde_json::json!([{ "case": "camelCase", "multipleFileExtensions": false }]),
423+
),
424+
test_case_with_options(
425+
"src/foo/foo_bar.test-utils.js",
426+
serde_json::json!([{ "case": "snakeCase", "multipleFileExtensions": false }]),
427+
),
395428
];
396429

397430
Tester::new(FilenameCase::NAME, FilenameCase::PLUGIN, pass, fail).test_and_snapshot();

‎crates/oxc_linter/src/snapshots/unicorn_filename_case.snap

+14-4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ source: crates/oxc_linter/src/tester.rs
1414
eslint-plugin-unicorn(filename-case): Filename should be in camel case
1515
╭─[filename_case.tsx:1:1]
1616
╰────
17-
help: Rename the file to 'fooBar.testUtils.js'
17+
help: Rename the file to 'fooBar.test_utils.js'
1818

1919
eslint-plugin-unicorn(filename-case): Filename should be in snake case
2020
╭─[filename_case.tsx:1:1]
@@ -29,7 +29,7 @@ source: crates/oxc_linter/src/tester.rs
2929
eslint-plugin-unicorn(filename-case): Filename should be in snake case
3030
╭─[filename_case.tsx:1:1]
3131
╰────
32-
help: Rename the file to 'foo_bar.test_utils.js'
32+
help: Rename the file to 'foo_bar.testUtils.js'
3333

3434
eslint-plugin-unicorn(filename-case): Filename should be in kebab case
3535
╭─[filename_case.tsx:1:1]
@@ -44,7 +44,7 @@ source: crates/oxc_linter/src/tester.rs
4444
eslint-plugin-unicorn(filename-case): Filename should be in kebab case
4545
╭─[filename_case.tsx:1:1]
4646
╰────
47-
help: Rename the file to 'foo-bar.test-utils.js'
47+
help: Rename the file to 'foo-bar.testUtils.js'
4848

4949
eslint-plugin-unicorn(filename-case): Filename should be in pascal case
5050
╭─[filename_case.tsx:1:1]
@@ -59,7 +59,7 @@ source: crates/oxc_linter/src/tester.rs
5959
eslint-plugin-unicorn(filename-case): Filename should be in pascal case
6060
╭─[filename_case.tsx:1:1]
6161
╰────
62-
help: Rename the file to 'FooBar.testUtils.js'
62+
help: Rename the file to 'FooBar.test-utils.js'
6363

6464
eslint-plugin-unicorn(filename-case): Filename should be in camel case
6565
╭─[filename_case.tsx:1:1]
@@ -145,3 +145,13 @@ source: crates/oxc_linter/src/tester.rs
145145
╭─[filename_case.tsx:1:1]
146146
╰────
147147
help: Rename the file to 'foobar.js'
148+
149+
eslint-plugin-unicorn(filename-case): Filename should be in camel case
150+
╭─[filename_case.tsx:1:1]
151+
╰────
152+
help: Rename the file to 'fooBar.testUtils.js'
153+
154+
eslint-plugin-unicorn(filename-case): Filename should be in snake case
155+
╭─[filename_case.tsx:1:1]
156+
╰────
157+
help: Rename the file to 'foo_bar.test_utils.js'

0 commit comments

Comments
 (0)
Please sign in to comment.