diff --git a/crates/typos-cli/src/file.rs b/crates/typos-cli/src/file.rs index fd57e2a2..421ddc90 100644 --- a/crates/typos-cli/src/file.rs +++ b/crates/typos-cli/src/file.rs @@ -960,7 +960,14 @@ fn walk_entry( (path, cwd) } else { let path = entry.path(); - let abs_path = report_result(path.canonicalize(), Some(path), reporter)?; + let abs_path = match path.canonicalize() { + Ok(abs_path) => abs_path, + Err(err) => { + report_error(err, Some(path), reporter)?; + // Avoid a failed `engine.policy` lookup + return Ok(()); + } + }; (path, abs_path) }; let policy = engine.policy(&lookup_path); diff --git a/crates/typos-cli/tests/walk_canonicalize.rs b/crates/typos-cli/tests/walk_canonicalize.rs new file mode 100644 index 00000000..214141a1 --- /dev/null +++ b/crates/typos-cli/tests/walk_canonicalize.rs @@ -0,0 +1,73 @@ +//! Regression test for +//! +//! `walk_entry` canonicalizes each entry's path to look up its policy. When that failed, +//! `report_result` reported the error and then handed back `PathBuf::default()`, an empty +//! path that is never a key in `ConfigEngine`'s directory map, so the `policy()` call +//! right after it panicked with `` `walk()` should be called first ``. +#![cfg(unix)] + +struct CollectingReporter { + errors: std::sync::Mutex>, +} + +impl CollectingReporter { + fn new() -> Self { + Self { + errors: std::sync::Mutex::new(Vec::new()), + } + } +} + +impl typos_cli::report::Report for CollectingReporter { + fn report(&self, msg: typos_cli::report::Message<'_>) -> Result<(), std::io::Error> { + if msg.is_error() { + self.errors.lock().unwrap().push(format!("{msg:?}")); + } + Ok(()) + } +} + +#[test] +fn walk_path_reports_file_removed_mid_walk() { + let temp = assert_fs::TempDir::new().unwrap(); + let vanishing = temp.path().join("vanishing.txt"); + std::fs::write(&vanishing, b"helllo world\n").unwrap(); + + let storage = typos_cli::policy::ConfigStorage::new(); + let mut engine = typos_cli::policy::ConfigEngine::new(&storage); + engine.set_isolated(true); + engine.set_overrides(typos_cli::config::Config::default()); + let cwd = temp.path().canonicalize().unwrap(); + engine.init_dir(&cwd).unwrap(); + + // Remove the file as the walker visits it. `ignore` still yields it as an `Ok` entry, + // but the `canonicalize()` in `walk_entry` then fails with `NotFound` -- the same + // outcome as a file that a concurrent process removes mid-scan. + let mut builder = ignore::WalkBuilder::new(temp.path()); + let target = vanishing.clone(); + let removed = std::sync::atomic::AtomicBool::new(false); + builder.filter_entry(move |entry| { + if entry.path() == target && !removed.swap(true, std::sync::atomic::Ordering::SeqCst) { + std::fs::remove_file(&target).unwrap(); + } + true + }); + let reporter = CollectingReporter::new(); + + let result = typos_cli::file::walk_path( + builder.build(), + &typos_cli::file::Typos, + &engine, + &reporter, + false, + ); + + assert!( + result.is_ok(), + "walk_path should not surface an ignore::Error: {result:?}" + ); + assert!( + !reporter.errors.lock().unwrap().is_empty(), + "expected the vanished file's canonicalize() failure to be reported" + ); +}