Skip to content

Commit a34dcba

Browse files
committed
Gracefully handle lint panics
Create a diagnostics for files that panic during linting rather than crashing Ruff.
1 parent d5700d7 commit a34dcba

File tree

5 files changed

+95
-10
lines changed

5 files changed

+95
-10
lines changed

Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,7 @@ textwrap = { version = "0.16.0" }
4848
toml = { version = "0.7.2" }
4949

5050
[profile.release]
51-
panic = "abort"
52-
lto = "thin"
51+
lto = "fat"
5352
codegen-units = 1
5453
opt-level = 3
5554

crates/ruff_cli/src/commands/linter.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
use std::fmt::Write;
12
use std::io;
23
use std::io::BufWriter;
3-
use std::io::Write;
44

55
use anyhow::Result;
66
use itertools::Itertools;
@@ -41,7 +41,7 @@ pub fn linter(format: HelpFormat) -> Result<()> {
4141
.join("/"),
4242
prefix => prefix.to_string(),
4343
};
44-
output.push_str(&format!("{:>4} {}\n", prefix, linter.name()));
44+
write!(output, "{:>4} {}\n", prefix, linter.name()).unwrap();
4545
}
4646
}
4747

@@ -65,7 +65,7 @@ pub fn linter(format: HelpFormat) -> Result<()> {
6565
}
6666
}
6767

68-
write!(stdout, "{output}")?;
68+
io::Write::write_fmt(&mut stdout, format_args!("{output}"))?;
6969

7070
Ok(())
7171
}

crates/ruff_cli/src/commands/run.rs

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11
use std::io;
2-
use std::path::PathBuf;
2+
use std::path::{Path, PathBuf};
33
use std::time::Instant;
44

55
use anyhow::Result;
66
use colored::Colorize;
77
use ignore::Error;
8-
use log::{debug, error};
8+
use log::{debug, error, warn};
99
#[cfg(not(target_family = "wasm"))]
1010
use rayon::prelude::*;
1111

12+
use crate::panic::catch_unwind;
1213
use ruff::message::{Location, Message};
1314
use ruff::registry::Rule;
1415
use ruff::resolver::PyprojectDiscovery;
15-
use ruff::settings::flags;
16+
use ruff::settings::{flags, AllSettings};
1617
use ruff::{fix, fs, packaging, resolver, warn_user_once, IOError, Range};
1718
use ruff_diagnostics::Diagnostic;
1819

1920
use crate::args::Overrides;
2021
use crate::cache;
21-
use crate::diagnostics::{lint_path, Diagnostics};
22+
use crate::diagnostics::Diagnostics;
2223

2324
/// Run the linter over a collection of files.
2425
pub fn run(
@@ -83,6 +84,7 @@ pub fn run(
8384
.and_then(|parent| package_roots.get(parent))
8485
.and_then(|package| *package);
8586
let settings = resolver.resolve_all(path, pyproject_strategy);
87+
8688
lint_path(path, package, settings, cache, noqa, autofix)
8789
.map_err(|e| (Some(path.to_owned()), e.to_string()))
8890
}
@@ -135,3 +137,39 @@ pub fn run(
135137

136138
Ok(diagnostics)
137139
}
140+
141+
/// Wraps [`lint_path`](crate::diagnostics::lint_path) in a [`catch_unwind`](std::panic::catch_unwind) and emits
142+
/// a diagnostic if the linting the file panics.
143+
fn lint_path(
144+
path: &Path,
145+
package: Option<&Path>,
146+
settings: &AllSettings,
147+
cache: flags::Cache,
148+
noqa: flags::Noqa,
149+
autofix: fix::FixMode,
150+
) -> Result<Diagnostics> {
151+
let result = catch_unwind(|| {
152+
crate::diagnostics::lint_path(path, package, settings, cache, noqa, autofix)
153+
});
154+
155+
match result {
156+
Ok(inner) => inner,
157+
Err(error) => {
158+
let message = r#"This indicates a bug in `ruff`. If you could open an issue at:
159+
160+
https://github.com/charliermarsh/ruff/issues/new?title=%5BLinter%20panic%5D
161+
162+
with the relevant file contents, the `pyproject.toml` settings, and the following stack trace, we'd be very appreciative!
163+
"#;
164+
165+
warn!(
166+
"{}{}{} {message}\n{error}",
167+
"Linting panicked ".bold(),
168+
fs::relativize_path(path).bold(),
169+
":".bold()
170+
);
171+
172+
Ok(Diagnostics::default())
173+
}
174+
}
175+
}

crates/ruff_cli/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub mod args;
1818
mod cache;
1919
mod commands;
2020
mod diagnostics;
21+
mod panic;
2122
mod printer;
2223
mod resolve;
2324

@@ -46,7 +47,6 @@ pub fn run(
4647
log_level_args,
4748
}: Args,
4849
) -> Result<ExitStatus> {
49-
#[cfg(not(debug_assertions))]
5050
{
5151
use colored::Colorize;
5252

crates/ruff_cli/src/panic.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#[derive(Default, Debug)]
2+
pub(crate) struct PanicError {
3+
pub info: String,
4+
pub backtrace: Option<std::backtrace::Backtrace>,
5+
}
6+
7+
impl std::fmt::Display for PanicError {
8+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
9+
writeln!(f, "{}", self.info)?;
10+
if let Some(backtrace) = &self.backtrace {
11+
writeln!(f, "Backtrace: {backtrace}")
12+
} else {
13+
Ok(())
14+
}
15+
}
16+
}
17+
18+
thread_local! {
19+
static LAST_PANIC: std::cell::Cell<Option<PanicError>> = std::cell::Cell::new(None);
20+
}
21+
22+
/// Take and set a specific panic hook before calling `f` inside a `catch_unwind`, then
23+
/// resetting the old panic hook.
24+
///
25+
/// If `f` panicks an `Error` with the panic message plus backtrace (only if `RUST_BACKTRACE` is set) will be returned.
26+
pub(crate) fn catch_unwind<F, R>(f: F) -> Result<R, PanicError>
27+
where
28+
F: FnOnce() -> R + std::panic::UnwindSafe,
29+
{
30+
let prev = std::panic::take_hook();
31+
std::panic::set_hook(Box::new(|info| {
32+
let info = info.to_string();
33+
let backtrace = std::backtrace::Backtrace::force_capture();
34+
LAST_PANIC.with(|cell| {
35+
cell.set(Some(PanicError {
36+
info,
37+
backtrace: Some(backtrace),
38+
}))
39+
})
40+
}));
41+
42+
let result = std::panic::catch_unwind(f)
43+
.map_err(|_| LAST_PANIC.with(|cell| cell.take()).unwrap_or_default());
44+
45+
std::panic::set_hook(prev);
46+
47+
result
48+
}

0 commit comments

Comments
 (0)