Skip to content

Commit 173168b

Browse files
committed
refactor(oxfmt): Refactor walk.rs and format.rs relationship (#14795)
Also a pure refactor, but using `filter_entry()` theoretically improves performance.
1 parent aea9d79 commit 173168b

File tree

2 files changed

+153
-135
lines changed

2 files changed

+153
-135
lines changed

apps/oxfmt/src/format.rs

Lines changed: 27 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ use std::{
66
time::Instant,
77
};
88

9-
use ignore::overrides::OverrideBuilder;
10-
119
use oxc_diagnostics::DiagnosticService;
1210
use oxc_formatter::{FormatOptions, Oxfmtrc};
1311

@@ -62,22 +60,16 @@ impl FormatRunner {
6260
}
6361
};
6462

65-
// Normalize user input paths
66-
let (target_paths, exclude_patterns) = normalize_paths(&cwd, &paths);
67-
68-
// Build exclude patterns if any exist
69-
let override_builder = (!exclude_patterns.is_empty())
70-
.then(|| {
71-
let mut builder = OverrideBuilder::new(&cwd);
72-
for pattern_str in exclude_patterns {
73-
builder.add(&pattern_str).ok()?;
74-
}
75-
builder.build().ok()
76-
})
77-
.flatten();
78-
79-
// TODO: Support ignoring files
80-
let walker = Walk::new(&target_paths, override_builder, ignore_options.with_node_modules);
63+
let walker = match Walk::build(&cwd, &paths, ignore_options.with_node_modules) {
64+
Ok(walker) => walker,
65+
Err(err) => {
66+
print_and_flush_stdout(
67+
stdout,
68+
&format!("Failed to parse target paths or ignore settings.\n{err}\n"),
69+
);
70+
return CliRunResult::InvalidOptionConfig;
71+
}
72+
};
8173

8274
// Get the receiver for streaming entries
8375
let rx_entry = walker.stream_entries();
@@ -169,81 +161,32 @@ impl FormatRunner {
169161
}
170162
}
171163

172-
const DEFAULT_OXFMTRC: &str = ".oxfmtrc.json";
173-
174164
/// # Errors
175165
///
176166
/// Returns error if:
177167
/// - Config file is specified but not found or invalid
178168
/// - Config file parsing fails
179-
fn load_config(cwd: &Path, config: Option<&PathBuf>) -> Result<FormatOptions, String> {
180-
// If `--config` is explicitly specified, use that path directly
181-
if let Some(config_path) = config {
182-
let full_path = if config_path.is_absolute() {
169+
fn load_config(cwd: &Path, config_path: Option<&PathBuf>) -> Result<FormatOptions, String> {
170+
let config_path = if let Some(config_path) = config_path {
171+
// If `--config` is explicitly specified, use that path
172+
Some(if config_path.is_absolute() {
183173
PathBuf::from(config_path)
184174
} else {
185175
cwd.join(config_path)
186-
};
187-
188-
// This will error if the file does not exist or is invalid
189-
let oxfmtrc = Oxfmtrc::from_file(&full_path)?;
190-
return oxfmtrc.into_format_options();
191-
}
192-
193-
// If `--config` is not specified, search the nearest config file from cwd upwards
194-
for dir in cwd.ancestors() {
195-
let config_path = dir.join(DEFAULT_OXFMTRC);
196-
if config_path.exists() {
197-
let oxfmtrc = Oxfmtrc::from_file(&config_path)?;
198-
return oxfmtrc.into_format_options();
199-
}
176+
})
177+
} else {
178+
// If `--config` is not specified, search the nearest config file from cwd upwards
179+
cwd.ancestors().find_map(|dir| {
180+
let config_path = dir.join(".oxfmtrc.json");
181+
if config_path.exists() { Some(config_path) } else { None }
182+
})
183+
};
184+
185+
match config_path {
186+
Some(ref path) => Oxfmtrc::from_file(path)?.into_format_options(),
187+
// Default if not specified and not found
188+
None => Ok(FormatOptions::default()),
200189
}
201-
202-
// No config file found, use defaults
203-
Ok(FormatOptions::default())
204-
}
205-
206-
/// Normalize user input paths into `target_paths` and `exclude_patterns`.
207-
/// - `target_paths`: Absolute paths to format
208-
/// - `exclude_patterns`: Pattern strings to exclude (with `!` prefix)
209-
fn normalize_paths(cwd: &Path, input_paths: &[PathBuf]) -> (Vec<PathBuf>, Vec<String>) {
210-
let mut target_paths = vec![];
211-
let mut exclude_patterns = vec![];
212-
213-
for path in input_paths {
214-
let path_str = path.to_string_lossy();
215-
216-
// Instead of `oxlint`'s `--ignore-pattern=PAT`,
217-
// `oxfmt` supports `!` prefix in paths like Prettier.
218-
if path_str.starts_with('!') {
219-
exclude_patterns.push(path_str.to_string());
220-
continue;
221-
}
222-
223-
// Otherwise, treat as target path
224-
225-
if path.is_absolute() {
226-
target_paths.push(path.clone());
227-
continue;
228-
}
229-
230-
// NOTE: `.` and cwd behaves differently, need to normalize
231-
let path = if path_str == "." {
232-
cwd.to_path_buf()
233-
} else if let Some(stripped) = path_str.strip_prefix("./") {
234-
cwd.join(stripped)
235-
} else {
236-
cwd.join(path)
237-
};
238-
target_paths.push(path);
239-
}
240-
241-
// Default to cwd if no `target_paths` are provided
242-
if target_paths.is_empty() {
243-
target_paths.push(cwd.into());
244-
}
245-
246-
(target_paths, exclude_patterns)
247190
}
248191

249192
fn print_and_flush_stdout(stdout: &mut dyn Write, message: &str) {

apps/oxfmt/src/walk.rs

Lines changed: 126 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,109 @@
1-
use std::{ffi::OsStr, path::PathBuf, sync::mpsc};
1+
use std::{
2+
path::{Path, PathBuf},
3+
sync::mpsc,
4+
};
25

3-
use ignore::overrides::Override;
6+
use ignore::overrides::OverrideBuilder;
47

58
use oxc_formatter::get_supported_source_type;
69
use oxc_span::SourceType;
710

811
pub struct Walk {
912
inner: ignore::WalkParallel,
10-
with_node_modules: bool,
1113
}
1214

1315
impl Walk {
14-
/// Will not canonicalize paths.
15-
/// # Panics
16-
pub fn new(
16+
pub fn build(
17+
cwd: &PathBuf,
1718
paths: &[PathBuf],
18-
override_builder: Option<Override>,
1919
with_node_modules: bool,
20-
) -> Self {
20+
) -> Result<Self, String> {
21+
let (target_paths, exclude_patterns) = normalize_paths(cwd, paths);
22+
23+
// Add all non-`!` prefixed paths to the walker base
2124
let mut inner = ignore::WalkBuilder::new(
22-
paths
23-
.iter()
24-
.next()
25-
.expect("Expected paths parameter to Walk::new() to contain at least one path."),
25+
target_paths
26+
.first()
27+
.expect("Expected paths parameter to Walk::build() to contain at least one path."),
2628
);
27-
28-
if let Some(paths) = paths.get(1..) {
29+
if let Some(paths) = target_paths.get(1..) {
2930
for path in paths {
3031
inner.add(path);
3132
}
3233
}
3334

34-
if let Some(override_builder) = override_builder {
35-
inner.overrides(override_builder);
35+
// Treat all `!` prefixed patterns as overrides to exclude
36+
if !exclude_patterns.is_empty() {
37+
let mut builder = OverrideBuilder::new(cwd);
38+
for pattern_str in exclude_patterns {
39+
builder
40+
.add(&pattern_str)
41+
.map_err(|_| format!("{pattern_str} is not a valid glob for override."))?;
42+
}
43+
let overrides = builder.build().map_err(|_| "Failed to build overrides".to_string())?;
44+
inner.overrides(overrides);
3645
}
3746

38-
// Do not follow symlinks like Prettier does.
39-
// See https://github.com/prettier/prettier/pull/14627
40-
let inner = inner.hidden(false).ignore(false).git_global(false).build_parallel();
41-
Self { inner, with_node_modules }
47+
// TODO: Support ignoring files
48+
// --ignore-path PATH1 --ignore-path PATH2
49+
// or default cwd/{.gitignore,.prettierignore}
50+
// if let Some(err) = inner.add_ignore(path) {
51+
// return Err(format!("Failed to add ignore file: {}", err));
52+
// }
53+
54+
// NOTE: If return `false` here, it will not be `visit()`ed at all
55+
inner.filter_entry(move |entry| {
56+
// Skip stdin for now
57+
let Some(file_type) = entry.file_type() else {
58+
return false;
59+
};
60+
61+
if file_type.is_dir() {
62+
// We are setting `.hidden(false)` on the `WalkBuilder` below,
63+
// it means we want to include hidden files and directories.
64+
// However, we (and also Prettier) still skip traversing certain directories.
65+
// https://prettier.io/docs/ignore#ignoring-files-prettierignore
66+
let is_skipped_dir = {
67+
let dir_name = entry.file_name();
68+
dir_name == ".git"
69+
|| dir_name == ".jj"
70+
|| dir_name == ".sl"
71+
|| dir_name == ".svn"
72+
|| dir_name == ".hg"
73+
|| (!with_node_modules && dir_name == "node_modules")
74+
};
75+
if is_skipped_dir {
76+
return false;
77+
}
78+
}
79+
80+
// NOTE: We can also check `get_supported_source_type()` here to skip.
81+
// But we want to pass parsed `SourceType` to `FormatService`,
82+
// so we do it later in the visitor instead.
83+
84+
true
85+
});
86+
87+
let inner = inner
88+
// Do not follow symlinks like Prettier does.
89+
// See https://github.com/prettier/prettier/pull/14627
90+
.follow_links(false)
91+
// Include hidden files and directories except those we explicitly skip above
92+
.hidden(false)
93+
// Do not respect `.gitignore` automatically, we handle it manually
94+
.ignore(false)
95+
.git_global(false)
96+
.build_parallel();
97+
Ok(Self { inner })
4298
}
4399

44100
/// Stream entries through a channel as they are discovered
45101
pub fn stream_entries(self) -> mpsc::Receiver<WalkEntry> {
46102
let (sender, receiver) = mpsc::channel::<WalkEntry>();
47-
let with_node_modules = self.with_node_modules;
48103

49104
// Spawn the walk operation in a separate thread
50105
rayon::spawn(move || {
51-
let mut builder = WalkBuilder { sender, with_node_modules };
106+
let mut builder = WalkBuilder { sender };
52107
self.inner.visit(&mut builder);
53108
// Channel will be closed when builder is dropped
54109
});
@@ -57,43 +112,68 @@ impl Walk {
57112
}
58113
}
59114

115+
/// Normalize user input paths into `target_paths` and `exclude_patterns`.
116+
/// - `target_paths`: Absolute paths to format
117+
/// - `exclude_patterns`: Pattern strings to exclude (with `!` prefix)
118+
fn normalize_paths(cwd: &Path, input_paths: &[PathBuf]) -> (Vec<PathBuf>, Vec<String>) {
119+
let mut target_paths = vec![];
120+
let mut exclude_patterns = vec![];
121+
122+
for path in input_paths {
123+
let path_str = path.to_string_lossy();
124+
125+
// Instead of `oxlint`'s `--ignore-pattern=PAT`,
126+
// `oxfmt` supports `!` prefix in paths like Prettier.
127+
if path_str.starts_with('!') {
128+
exclude_patterns.push(path_str.to_string());
129+
continue;
130+
}
131+
132+
// Otherwise, treat as target path
133+
134+
if path.is_absolute() {
135+
target_paths.push(path.clone());
136+
continue;
137+
}
138+
139+
// NOTE: `.` and cwd behaves differently, need to normalize
140+
let path = if path_str == "." {
141+
cwd.to_path_buf()
142+
} else if let Some(stripped) = path_str.strip_prefix("./") {
143+
cwd.join(stripped)
144+
} else {
145+
cwd.join(path)
146+
};
147+
target_paths.push(path);
148+
}
149+
150+
// Default to cwd if no `target_paths` are provided
151+
if target_paths.is_empty() {
152+
target_paths.push(cwd.into());
153+
}
154+
155+
(target_paths, exclude_patterns)
156+
}
157+
158+
// ---
159+
60160
pub struct WalkEntry {
61161
pub path: PathBuf,
62162
pub source_type: SourceType,
63163
}
64164

65165
struct WalkBuilder {
66166
sender: mpsc::Sender<WalkEntry>,
67-
with_node_modules: bool,
68167
}
69168

70169
impl<'s> ignore::ParallelVisitorBuilder<'s> for WalkBuilder {
71170
fn build(&mut self) -> Box<dyn ignore::ParallelVisitor + 's> {
72-
Box::new(WalkVisitor {
73-
sender: self.sender.clone(),
74-
with_node_modules: self.with_node_modules,
75-
})
171+
Box::new(WalkVisitor { sender: self.sender.clone() })
76172
}
77173
}
78174

79175
struct WalkVisitor {
80176
sender: mpsc::Sender<WalkEntry>,
81-
with_node_modules: bool,
82-
}
83-
84-
impl WalkVisitor {
85-
// We are setting `.hidden(false)` on the `WalkBuilder`,
86-
// it means we want to include hidden files and directories.
87-
// However, we (and also Prettier) still skip traversing certain directories.
88-
// https://prettier.io/docs/ignore#ignoring-files-prettierignore
89-
fn is_skipped_dir(&self, dir_name: &OsStr) -> bool {
90-
dir_name == ".git"
91-
|| dir_name == ".jj"
92-
|| dir_name == ".sl"
93-
|| dir_name == ".svn"
94-
|| dir_name == ".hg"
95-
|| (!self.with_node_modules && dir_name == "node_modules")
96-
}
97177
}
98178

99179
impl ignore::ParallelVisitor for WalkVisitor {
@@ -104,14 +184,9 @@ impl ignore::ParallelVisitor for WalkVisitor {
104184
return ignore::WalkState::Continue;
105185
};
106186

107-
if file_type.is_dir() {
108-
if self.is_skipped_dir(entry.file_name()) {
109-
return ignore::WalkState::Skip;
110-
}
111-
return ignore::WalkState::Continue;
112-
}
113-
114-
if let Some(source_type) = get_supported_source_type(entry.path()) {
187+
if !file_type.is_dir()
188+
&& let Some(source_type) = get_supported_source_type(entry.path())
189+
{
115190
let walk_entry = WalkEntry { path: entry.path().to_path_buf(), source_type };
116191
// Send each entry immediately through the channel
117192
// If send fails, the receiver has been dropped, so stop walking

0 commit comments

Comments
 (0)