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
58use oxc_formatter:: get_supported_source_type;
69use oxc_span:: SourceType ;
710
811pub struct Walk {
912 inner : ignore:: WalkParallel ,
10- with_node_modules : bool ,
1113}
1214
1315impl 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+
60160pub struct WalkEntry {
61161 pub path : PathBuf ,
62162 pub source_type : SourceType ,
63163}
64164
65165struct WalkBuilder {
66166 sender : mpsc:: Sender < WalkEntry > ,
67- with_node_modules : bool ,
68167}
69168
70169impl < ' 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
79175struct 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
99179impl 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