Skip to content

Commit 02f59ba

Browse files
committed
fix(oxfmt): Always respect ignored files even specified (#16632)
Fixes #16621
1 parent 37c1a06 commit 02f59ba

File tree

4 files changed

+121
-76
lines changed

4 files changed

+121
-76
lines changed

apps/oxfmt/src/cli/format.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,16 @@ impl FormatRunner {
109109
ignore_options.with_node_modules,
110110
&ignore_patterns,
111111
) {
112-
Ok(walker) => walker,
112+
Ok(Some(walker)) => walker,
113+
// All target paths are ignored
114+
Ok(None) => {
115+
if misc_options.no_error_on_unmatched_pattern {
116+
print_and_flush(stderr, "No files found matching the given patterns.\n");
117+
return CliRunResult::None;
118+
}
119+
print_and_flush(stderr, "Expected at least one target file\n");
120+
return CliRunResult::NoFilesFound;
121+
}
113122
Err(err) => {
114123
print_and_flush(
115124
stderr,

apps/oxfmt/src/cli/walk.rs

Lines changed: 78 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::{
33
sync::mpsc,
44
};
55

6-
use ignore::{gitignore::GitignoreBuilder, overrides::OverrideBuilder};
6+
use ignore::gitignore::GitignoreBuilder;
77

88
use crate::core::FormatFileSource;
99

@@ -18,38 +18,49 @@ impl Walk {
1818
ignore_paths: &[PathBuf],
1919
with_node_modules: bool,
2020
ignore_patterns: &[String],
21-
) -> Result<Self, String> {
22-
let (target_paths, exclude_patterns) = normalize_paths(cwd, paths);
23-
24-
// Add all non-`!` prefixed paths to the walker base
25-
let mut inner = ignore::WalkBuilder::new(
26-
target_paths
27-
.first()
28-
.expect("`target_paths` never be empty, should have at least `cwd`"),
29-
);
30-
if let Some(paths) = target_paths.get(1..) {
31-
for path in paths {
32-
inner.add(path);
21+
) -> Result<Option<Self>, String> {
22+
//
23+
// Classify and normalize specified paths
24+
//
25+
let mut target_paths = vec![];
26+
let mut exclude_patterns = vec![];
27+
for path in paths {
28+
let path_str = path.to_string_lossy();
29+
30+
// Instead of `oxlint`'s `--ignore-pattern=PAT`,
31+
// `oxfmt` supports `!` prefix in paths like Prettier.
32+
if path_str.starts_with('!') {
33+
exclude_patterns.push(path_str.to_string());
34+
continue;
3335
}
34-
}
3536

36-
// NOTE: We are using `OverrideBuilder` only for exclusion.
37-
// This means there is no way to "re-include" a file once ignored.
37+
// Otherwise, treat as target path
3838

39-
// Treat all `!` prefixed patterns as overrides to exclude
40-
if !exclude_patterns.is_empty() {
41-
let mut builder = OverrideBuilder::new(cwd);
42-
for pattern_str in exclude_patterns {
43-
builder
44-
.add(&pattern_str)
45-
.map_err(|_| format!("{pattern_str} is not a valid glob for override."))?;
39+
if path.is_absolute() {
40+
target_paths.push(path.clone());
41+
continue;
4642
}
47-
let overrides = builder.build().map_err(|_| "Failed to build overrides".to_string())?;
48-
inner.overrides(overrides);
43+
44+
// NOTE: `.` and cwd behave differently, need to normalize
45+
let path = if path_str == "." {
46+
cwd.to_path_buf()
47+
} else if let Some(stripped) = path_str.strip_prefix("./") {
48+
cwd.join(stripped)
49+
} else {
50+
cwd.join(path)
51+
};
52+
target_paths.push(path);
53+
}
54+
// Default to cwd if no target paths are provided
55+
if target_paths.is_empty() {
56+
target_paths.push(cwd.to_path_buf());
4957
}
5058

51-
// Handle ignore files
59+
//
60+
// Build ignores
61+
//
5262
let mut builder = GitignoreBuilder::new(cwd);
63+
// Handle ignore files
5364
for ignore_path in &load_ignore_paths(cwd, ignore_paths) {
5465
if builder.add(ignore_path).is_some() {
5566
return Err(format!("Failed to add ignore file: {}", ignore_path.display()));
@@ -58,11 +69,47 @@ impl Walk {
5869
// Handle `config.ignorePatterns`
5970
for pattern in ignore_patterns {
6071
if builder.add_line(None, pattern).is_err() {
61-
return Err(format!("Failed to add ignore pattern `{pattern}`"));
72+
return Err(format!(
73+
"Failed to add ignore pattern `{pattern}` from `.ignorePatterns`"
74+
));
75+
}
76+
}
77+
// Handle `!` prefixed paths as ignore patterns too
78+
for pattern in &exclude_patterns {
79+
// Remove the leading `!` because `GitignoreBuilder` uses `!` as negation
80+
let pattern =
81+
pattern.strip_prefix('!').expect("There should be a `!` prefix, already checked");
82+
if builder.add_line(None, pattern).is_err() {
83+
return Err(format!("Failed to add ignore pattern `{pattern}` from `!` prefix"));
6284
}
6385
}
6486
let ignores = builder.build().map_err(|_| "Failed to build ignores".to_string())?;
6587

88+
//
89+
// Filter paths by ignores
90+
//
91+
// NOTE: Base paths passed to `WalkBuilder` are not filtered by `filter_entry()`,
92+
// so we need to filter them here before passing to the walker.
93+
let target_paths: Vec<_> = target_paths
94+
.into_iter()
95+
.filter(|path| {
96+
let matched = ignores.matched(path, path.is_dir());
97+
!matched.is_ignore() || matched.is_whitelist()
98+
})
99+
.collect();
100+
101+
// If no target paths remain after filtering, return `None`.
102+
// Not an error, but nothing to format, leave it to the caller how to handle.
103+
let Some(first_path) = target_paths.first() else {
104+
return Ok(None);
105+
};
106+
107+
// Add all non-`!` prefixed paths to the walker base
108+
let mut inner = ignore::WalkBuilder::new(first_path);
109+
for path in target_paths.iter().skip(1) {
110+
inner.add(path);
111+
}
112+
66113
// NOTE: If return `false` here, it will not be `visit()`ed at all
67114
inner.filter_entry(move |entry| {
68115
let Some(file_type) = entry.file_type() else {
@@ -115,7 +162,7 @@ impl Walk {
115162
.git_ignore(false)
116163
.git_exclude(false)
117164
.build_parallel();
118-
Ok(Self { inner })
165+
Ok(Some(Self { inner }))
119166
}
120167

121168
/// Stream entries through a channel as they are discovered
@@ -133,49 +180,6 @@ impl Walk {
133180
}
134181
}
135182

136-
/// Normalize user input paths into `target_paths` and `exclude_patterns`.
137-
/// - `target_paths`: Absolute paths to format
138-
/// - `exclude_patterns`: Pattern strings to exclude (with `!` prefix)
139-
fn normalize_paths(cwd: &Path, input_paths: &[PathBuf]) -> (Vec<PathBuf>, Vec<String>) {
140-
let mut target_paths = vec![];
141-
let mut exclude_patterns = vec![];
142-
143-
for path in input_paths {
144-
let path_str = path.to_string_lossy();
145-
146-
// Instead of `oxlint`'s `--ignore-pattern=PAT`,
147-
// `oxfmt` supports `!` prefix in paths like Prettier.
148-
if path_str.starts_with('!') {
149-
exclude_patterns.push(path_str.to_string());
150-
continue;
151-
}
152-
153-
// Otherwise, treat as target path
154-
155-
if path.is_absolute() {
156-
target_paths.push(path.clone());
157-
continue;
158-
}
159-
160-
// NOTE: `.` and cwd behaves differently, need to normalize
161-
let path = if path_str == "." {
162-
cwd.to_path_buf()
163-
} else if let Some(stripped) = path_str.strip_prefix("./") {
164-
cwd.join(stripped)
165-
} else {
166-
cwd.join(path)
167-
};
168-
target_paths.push(path);
169-
}
170-
171-
// Default to cwd if no `target_paths` are provided
172-
if target_paths.is_empty() {
173-
target_paths.push(cwd.into());
174-
}
175-
176-
(target_paths, exclude_patterns)
177-
}
178-
179183
fn load_ignore_paths(cwd: &Path, ignore_paths: &[PathBuf]) -> Vec<PathBuf> {
180184
// If specified, just resolves absolute paths
181185
if !ignore_paths.is_empty() {
@@ -187,12 +191,12 @@ fn load_ignore_paths(cwd: &Path, ignore_paths: &[PathBuf]) -> Vec<PathBuf> {
187191

188192
// Else, search for default ignore files in cwd
189193
[".gitignore", ".prettierignore"]
190-
.iter()
194+
.into_iter()
191195
.filter_map(|file_name| {
192196
let path = cwd.join(file_name);
193-
if path.exists() { Some(path) } else { None }
197+
path.exists().then_some(path)
194198
})
195-
.collect::<Vec<_>>()
199+
.collect()
196200
}
197201

198202
// ---

apps/oxfmt/test/__snapshots__/ignore_and_override.test.ts.snap

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,24 @@ arguments: --check --ignore-path ignore1 should_format/ok.js
3030
working directory: fixtures/ignore_and_override
3131
exit code: 1
3232
--- STDOUT ---------
33+
34+
--- STDERR ---------
35+
Expected at least one target file
36+
--------------------
37+
--------------------
38+
arguments: --check --ignore-path ignore1 should_format/ok.js --no-error-on-unmatched-pattern
39+
working directory: fixtures/ignore_and_override
40+
exit code: 0
41+
--- STDOUT ---------
42+
43+
--- STDERR ---------
44+
No files found matching the given patterns.
45+
--------------------
46+
--------------------
47+
arguments: --check --ignore-path ignore2
48+
working directory: fixtures/ignore_and_override
49+
exit code: 1
50+
--- STDOUT ---------
3351
Checking formatting...
3452
3553
should_format/ok.js (<variable>ms)
@@ -40,7 +58,7 @@ Finished in <variable>ms on 1 files using 1 threads.
4058
4159
--------------------
4260
--------------------
43-
arguments: --check --ignore-path ignore2
61+
arguments: --check --ignore-path ignore2 should_format/ok.js
4462
working directory: fixtures/ignore_and_override
4563
exit code: 1
4664
--- STDOUT ---------

apps/oxfmt/test/ignore_and_override.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,24 @@ describe("ignore_and_override", () => {
88
it("should handle ignore files with overrides", async () => {
99
const cwd = join(fixturesDir, "ignore_and_override");
1010
const testCases = [
11+
// Exclude `err.js` via `!` pattern, format `ok.js` only
1112
["--check", "!**/err.js"],
13+
// `ignore1` excludes all files -> no files found error
1214
["--check", "--ignore-path", "ignore1"],
15+
// Explicitly specified file is also excluded by `ignore1` -> no files found error
1316
["--check", "--ignore-path", "ignore1", "should_format/ok.js"],
17+
// Same as above, but suppress error with `--no-error-on-unmatched-pattern`
18+
[
19+
"--check",
20+
"--ignore-path",
21+
"ignore1",
22+
"should_format/ok.js",
23+
"--no-error-on-unmatched-pattern",
24+
],
25+
// `ignore2` has `!should_format/ok.js` (whitelist), so `ok.js` is formatted
1426
["--check", "--ignore-path", "ignore2"],
27+
// Whitelist + explicit file: `ok.js` is whitelisted in `ignore2` and explicitly specified
28+
["--check", "--ignore-path", "ignore2", "should_format/ok.js"],
1529
];
1630

1731
const snapshot = await runAndSnapshot(cwd, testCases);

0 commit comments

Comments
 (0)