1use super::{BoxedTraversal, ErrorKind, File, FileSystemDiagnostic};
3use crate::fs::OpenOptions;
4use crate::{
5 fs::{TraversalContext, TraversalScope},
6 BiomePath, FileSystem,
7};
8use biome_diagnostics::{adapters::IoError, DiagnosticExt, Error, Severity};
9use oxc_resolver::{Resolution, ResolveError, ResolveOptions, Resolver};
10use rayon::{scope, Scope};
11use std::ffi::OsStr;
12use std::fs::{DirEntry, FileType};
13use std::panic::AssertUnwindSafe;
14use std::process::Command;
15use std::{
16 env, fs,
17 io::{self, ErrorKind as IoErrorKind, Read, Seek, Write},
18 mem,
19 path::{Path, PathBuf},
20};
21
22const MAX_SYMLINK_DEPTH: u8 = 3;
23
24pub struct OsFileSystem {
26 pub working_directory: Option<PathBuf>,
27 pub configuration_resolver: AssertUnwindSafe<Resolver>,
28}
29
30impl OsFileSystem {
31 pub fn new(working_directory: PathBuf) -> Self {
32 Self {
33 working_directory: Some(working_directory),
34 configuration_resolver: AssertUnwindSafe(Resolver::new(ResolveOptions {
35 condition_names: vec!["node".to_string(), "import".to_string()],
36 extensions: vec!["*.json".to_string()],
37 ..ResolveOptions::default()
38 })),
39 }
40 }
41}
42
43impl Default for OsFileSystem {
44 fn default() -> Self {
45 Self {
46 working_directory: env::current_dir().ok(),
47 configuration_resolver: AssertUnwindSafe(Resolver::new(ResolveOptions {
48 condition_names: vec!["node".to_string(), "import".to_string()],
49 extensions: vec!["*.json".to_string(), "*.jsonc".to_string()],
50 ..ResolveOptions::default()
51 })),
52 }
53 }
54}
55
56impl FileSystem for OsFileSystem {
57 fn open_with_options(&self, path: &Path, options: OpenOptions) -> io::Result<Box<dyn File>> {
58 tracing::debug_span!("OsFileSystem::open_with_options", path = ?path, options = ?options)
59 .in_scope(move || -> io::Result<Box<dyn File>> {
60 let mut fs_options = fs::File::options();
61 Ok(Box::new(OsFile {
62 inner: options.into_fs_options(&mut fs_options).open(path)?,
63 version: 0,
64 }))
65 })
66 }
67
68 fn traversal(&self, func: BoxedTraversal) {
69 OsTraversalScope::with(move |scope| {
70 func(scope);
71 })
72 }
73
74 fn working_directory(&self) -> Option<PathBuf> {
75 self.working_directory.clone()
76 }
77
78 fn path_exists(&self, path: &Path) -> bool {
79 path.exists()
80 }
81
82 fn path_is_file(&self, path: &Path) -> bool {
83 path.is_file()
84 }
85
86 fn resolve_configuration(&self, specifier: &str) -> Result<Resolution, ResolveError> {
87 self.configuration_resolver
88 .resolve(self.working_directory().unwrap(), specifier)
89 }
90
91 fn get_changed_files(&self, base: &str) -> io::Result<Vec<String>> {
92 let output = Command::new("git")
93 .arg("diff")
94 .arg("--name-only")
95 .arg("--diff-filter=ACMR")
101 .arg(format!("{}...HEAD", base))
102 .output()?;
103
104 Ok(String::from_utf8_lossy(&output.stdout)
105 .lines()
106 .map(|l| l.to_string())
107 .collect())
108 }
109}
110
111struct OsFile {
112 inner: fs::File,
113 version: i32,
114}
115
116impl File for OsFile {
117 fn read_to_string(&mut self, buffer: &mut String) -> io::Result<()> {
118 tracing::debug_span!("OsFile::read_to_string").in_scope(move || {
119 self.inner.rewind()?;
121 self.inner.read_to_string(buffer)?;
123 Ok(())
124 })
125 }
126
127 fn set_content(&mut self, content: &[u8]) -> io::Result<()> {
128 tracing::trace_span!("OsFile::set_content").in_scope(move || {
129 self.inner.set_len(0)?;
131 self.inner.rewind()?;
133 self.inner.write_all(content)?;
135 self.version += 1;
137 Ok(())
138 })
139 }
140
141 fn file_version(&self) -> i32 {
142 self.version
143 }
144}
145
146#[repr(transparent)]
147pub struct OsTraversalScope<'scope> {
148 scope: Scope<'scope>,
149}
150
151impl<'scope> OsTraversalScope<'scope> {
152 pub(crate) fn with<F>(func: F)
153 where
154 F: FnOnce(&Self) + Send,
155 {
156 scope(move |scope| func(Self::from_rayon(scope)))
157 }
158
159 fn from_rayon<'a>(scope: &'a Scope<'scope>) -> &'a Self {
160 unsafe { mem::transmute(scope) }
164 }
165}
166
167impl<'scope> TraversalScope<'scope> for OsTraversalScope<'scope> {
168 fn spawn(&self, ctx: &'scope dyn TraversalContext, path: PathBuf) {
169 let file_type = match path.metadata() {
170 Ok(meta) => meta.file_type(),
171 Err(err) => {
172 ctx.push_diagnostic(
173 IoError::from(err).with_file_path(path.to_string_lossy().to_string()),
174 );
175 return;
176 }
177 };
178 handle_any_file(&self.scope, ctx, path, file_type, None);
179 }
180}
181
182const DEFAULT_IGNORE: &[&str; 5] = &[".git", ".svn", ".hg", ".yarn", "node_modules"];
186
187fn handle_dir<'scope>(
189 scope: &Scope<'scope>,
190 ctx: &'scope dyn TraversalContext,
191 path: &Path,
192 origin_path: Option<PathBuf>,
194) {
195 if let Some(file_name) = path.file_name().and_then(OsStr::to_str) {
196 if DEFAULT_IGNORE.contains(&file_name) {
197 return;
198 }
199 }
200 let iter = match fs::read_dir(path) {
201 Ok(iter) => iter,
202 Err(err) => {
203 ctx.push_diagnostic(IoError::from(err).with_file_path(path.display().to_string()));
204 return;
205 }
206 };
207
208 for entry in iter {
209 match entry {
210 Ok(entry) => handle_dir_entry(scope, ctx, entry, origin_path.clone()),
211 Err(err) => {
212 ctx.push_diagnostic(IoError::from(err).with_file_path(path.display().to_string()));
213 }
214 }
215 }
216}
217
218fn handle_dir_entry<'scope>(
221 scope: &Scope<'scope>,
222 ctx: &'scope dyn TraversalContext,
223 entry: DirEntry,
224 origin_path: Option<PathBuf>,
226) {
227 let path = entry.path();
228 let file_type = match entry.file_type() {
229 Ok(file_type) => file_type,
230 Err(err) => {
231 ctx.push_diagnostic(
232 IoError::from(err).with_file_path(path.to_string_lossy().to_string()),
233 );
234 return;
235 }
236 };
237 handle_any_file(scope, ctx, path, file_type, origin_path);
238}
239
240fn handle_any_file<'scope>(
241 scope: &Scope<'scope>,
242 ctx: &'scope dyn TraversalContext,
243 mut path: PathBuf,
244 mut file_type: FileType,
245 mut origin_path: Option<PathBuf>,
247) {
248 if !ctx.interner().intern_path(path.clone()) {
249 return;
252 }
253
254 if file_type.is_symlink() {
255 if !ctx.can_handle(&BiomePath::new(path.clone())) {
256 return;
257 }
258 let Ok((target_path, target_file_type)) = expand_symbolic_link(path.clone(), ctx) else {
259 return;
260 };
261
262 if !ctx.interner().intern_path(target_path.clone()) {
263 return;
266 }
267
268 if target_file_type.is_dir() {
269 scope.spawn(move |scope| {
270 handle_dir(scope, ctx, &target_path, Some(path));
271 });
272 return;
273 }
274
275 path = target_path;
276 file_type = target_file_type;
277 }
278
279 let biome_path = if let Some(old_origin_path) = &origin_path {
283 if let Some(file_name) = path.file_name() {
284 let new_origin_path = old_origin_path.join(file_name);
285 origin_path = Some(new_origin_path.clone());
286 BiomePath::new(new_origin_path)
287 } else {
288 ctx.push_diagnostic(Error::from(FileSystemDiagnostic {
289 path: path.to_string_lossy().to_string(),
290 error_kind: ErrorKind::UnknownFileType,
291 severity: Severity::Warning,
292 }));
293 return;
294 }
295 } else {
296 BiomePath::new(&path)
297 };
298
299 if !ctx.can_handle(&biome_path) {
305 return;
306 }
307
308 if file_type.is_dir() {
309 scope.spawn(move |scope| {
310 handle_dir(scope, ctx, &path, origin_path);
311 });
312 return;
313 }
314
315 if file_type.is_file() {
316 scope.spawn(move |_| {
317 ctx.handle_file(&path);
318 });
319 return;
320 }
321
322 ctx.push_diagnostic(Error::from(FileSystemDiagnostic {
323 path: path.to_string_lossy().to_string(),
324 error_kind: ErrorKind::from(file_type),
325 severity: Severity::Warning,
326 }));
327}
328
329struct SymlinkExpansionError;
335
336fn expand_symbolic_link(
343 mut path: PathBuf,
344 ctx: &dyn TraversalContext,
345) -> Result<(PathBuf, FileType), SymlinkExpansionError> {
346 let mut symlink_depth = 0;
347 loop {
348 symlink_depth += 1;
349 if symlink_depth > MAX_SYMLINK_DEPTH {
350 let path = path.to_string_lossy().to_string();
351 ctx.push_diagnostic(Error::from(FileSystemDiagnostic {
352 path: path.clone(),
353 error_kind: ErrorKind::DeeplyNestedSymlinkExpansion(path),
354 severity: Severity::Warning,
355 }));
356 return Err(SymlinkExpansionError);
357 }
358
359 let (target_path, target_file_type) = follow_symlink(&path, ctx)?;
360
361 if target_file_type.is_symlink() {
362 path = target_path;
363 continue;
364 }
365
366 return Ok((target_path, target_file_type));
367 }
368}
369
370fn follow_symlink(
371 path: &Path,
372 ctx: &dyn TraversalContext,
373) -> Result<(PathBuf, FileType), SymlinkExpansionError> {
374 tracing::info!("Translating symlink: {path:?}");
375
376 let target_path = fs::read_link(path).map_err(|err| {
377 ctx.push_diagnostic(IoError::from(err).with_file_path(path.to_string_lossy().to_string()));
378 SymlinkExpansionError
379 })?;
380
381 let target_path = path
383 .parent()
384 .map(|parent_dir| parent_dir.join(&target_path))
385 .unwrap_or(target_path);
386
387 let target_file_type = match fs::symlink_metadata(&target_path) {
388 Ok(meta) => meta.file_type(),
389 Err(err) => {
390 if err.kind() == IoErrorKind::NotFound {
391 let path = path.to_string_lossy().to_string();
392 ctx.push_diagnostic(Error::from(FileSystemDiagnostic {
393 path: path.clone(),
394 error_kind: ErrorKind::DereferencedSymlink(path),
395 severity: Severity::Warning,
396 }));
397 } else {
398 ctx.push_diagnostic(
399 IoError::from(err).with_file_path(path.to_string_lossy().to_string()),
400 );
401 }
402 return Err(SymlinkExpansionError);
403 }
404 };
405
406 Ok((target_path, target_file_type))
407}
408
409impl From<FileType> for ErrorKind {
410 fn from(_: FileType) -> Self {
411 Self::UnknownFileType
412 }
413}