From 7d7237c6606fa621485cf3cb982f107ce0f48db3 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda Date: Fri, 3 Oct 2025 22:36:07 +0900 Subject: [PATCH] [`ruff`] Handle argfile expansion errors gracefully (#20691) ## Summary Fixes #20655 - Guard `argfile::expand_args_from` with contextual error handling so missing @file arguments surface a friendly failure instead of panicking. - Extract existing stderr reporting into `report_error` for reuse on both CLI parsing failures and runtime errors. ## Test Plan Add a regression test to integration_test.rs. --------- Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com> --- crates/ruff/src/main.rs | 66 +++++++++++++++------------ crates/ruff/tests/integration_test.rs | 21 +++++++++ 2 files changed, 58 insertions(+), 29 deletions(-) diff --git a/crates/ruff/src/main.rs b/crates/ruff/src/main.rs index 62e4f85b6e..5ec78c9e25 100644 --- a/crates/ruff/src/main.rs +++ b/crates/ruff/src/main.rs @@ -1,6 +1,7 @@ use std::io::Write; use std::process::ExitCode; +use anyhow::Context; use clap::Parser; use colored::Colorize; @@ -39,39 +40,46 @@ pub fn main() -> ExitCode { } let args = wild::args_os(); - let args = argfile::expand_args_from(args, argfile::parse_fromfile, argfile::PREFIX).unwrap(); + let args = match argfile::expand_args_from(args, argfile::parse_fromfile, argfile::PREFIX) + .context("Failed to read CLI arguments from files") + { + Ok(args) => args, + Err(err) => return report_error(&err), + }; let args = Args::parse_from(args); match run(args) { Ok(code) => code.into(), - Err(err) => { - { - // Exit "gracefully" on broken pipe errors. - // - // See: https://github.com/BurntSushi/ripgrep/blob/bf63fe8f258afc09bae6caa48f0ae35eaf115005/crates/core/main.rs#L47C1-L61C14 - for cause in err.chain() { - if let Some(ioerr) = cause.downcast_ref::() { - if ioerr.kind() == std::io::ErrorKind::BrokenPipe { - return ExitCode::from(0); - } - } - } - - // Use `writeln` instead of `eprintln` to avoid panicking when the stderr pipe is broken. - let mut stderr = std::io::stderr().lock(); - - // This communicates that this isn't a linter error but ruff itself hard-errored for - // some reason (e.g. failed to resolve the configuration) - writeln!(stderr, "{}", "ruff failed".red().bold()).ok(); - // Currently we generally only see one error, but e.g. with io errors when resolving - // the configuration it is help to chain errors ("resolving configuration failed" -> - // "failed to read file: subdir/pyproject.toml") - for cause in err.chain() { - writeln!(stderr, " {} {cause}", "Cause:".bold()).ok(); - } - } - ExitStatus::Error.into() - } + Err(err) => report_error(&err), } } + +fn report_error(err: &anyhow::Error) -> ExitCode { + { + // Exit "gracefully" on broken pipe errors. + // + // See: https://github.com/BurntSushi/ripgrep/blob/bf63fe8f258afc09bae6caa48f0ae35eaf115005/crates/core/main.rs#L47C1-L61C14 + for cause in err.chain() { + if let Some(ioerr) = cause.downcast_ref::() { + if ioerr.kind() == std::io::ErrorKind::BrokenPipe { + return ExitCode::from(0); + } + } + } + + // Use `writeln` instead of `eprintln` to avoid panicking when the stderr pipe is broken. + let mut stderr = std::io::stderr().lock(); + + // This communicates that this isn't a linter error but ruff itself hard-errored for + // some reason (e.g. failed to resolve the configuration) + writeln!(stderr, "{}", "ruff failed".red().bold()).ok(); + // Currently we generally only see one error, but e.g. with io errors when resolving + // the configuration it is help to chain errors ("resolving configuration failed" -> + // "failed to read file: subdir/pyproject.toml") + for cause in err.chain() { + writeln!(stderr, " {} {cause}", "Cause:".bold()).ok(); + } + } + ExitStatus::Error.into() +} diff --git a/crates/ruff/tests/integration_test.rs b/crates/ruff/tests/integration_test.rs index 4babbb3986..edca13cd72 100644 --- a/crates/ruff/tests/integration_test.rs +++ b/crates/ruff/tests/integration_test.rs @@ -1688,6 +1688,27 @@ fn check_input_from_argfile() -> Result<()> { Ok(()) } +#[test] +// Regression test for https://github.com/astral-sh/ruff/issues/20655 +fn missing_argfile_reports_error() { + let mut cmd = RuffCheck::default().filename("@!.txt").build(); + insta::with_settings!({filters => vec![ + ("The system cannot find the file specified.", "No such file or directory") + ]}, { + assert_cmd_snapshot!(cmd, @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + ruff failed + Cause: Failed to read CLI arguments from files + Cause: failed to open file `!.txt` + Cause: No such file or directory (os error 2) + "); + }); +} + #[test] fn check_hints_hidden_unsafe_fixes() { let mut cmd = RuffCheck::default()