Skip to content

Commit 8a027b0

Browse files
authored
[ruff] Treat panics as fatal diagnostics, sort panics last (#20258)
- Convert panics to diagnostics with id `Panic`, severity `Fatal`, and the error as the diagnostic message, annotated with a `Span` with empty code block and no range. - Updates the post-linting message diagnostic handling to track the maximum severity seen, and then prints the "report a bug in ruff" message only if the max severity was `Fatal` This depends on the sorting changes since it creates diagnostics with no range specified.
1 parent aa63c24 commit 8a027b0

File tree

8 files changed

+203
-17
lines changed

8 files changed

+203
-17
lines changed

crates/ruff/src/commands/check.rs

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ use std::time::Instant;
66
use anyhow::Result;
77
use colored::Colorize;
88
use ignore::Error;
9-
use log::{debug, error, warn};
9+
use log::{debug, warn};
1010
#[cfg(not(target_family = "wasm"))]
1111
use rayon::prelude::*;
1212
use rustc_hash::FxHashMap;
1313

14-
use ruff_db::diagnostic::Diagnostic;
14+
use ruff_db::diagnostic::{
15+
Annotation, Diagnostic, DiagnosticId, Span, SubDiagnostic, SubDiagnosticSeverity,
16+
};
1517
use ruff_db::panic::catch_unwind;
1618
use ruff_linter::package::PackageRoot;
1719
use ruff_linter::registry::Rule;
@@ -193,21 +195,24 @@ fn lint_path(
193195
match result {
194196
Ok(inner) => inner,
195197
Err(error) => {
196-
let message = r"This indicates a bug in Ruff. If you could open an issue at:
197-
198-
https://github.com/astral-sh/ruff/issues/new?title=%5BLinter%20panic%5D
199-
200-
...with the relevant file contents, the `pyproject.toml` settings, and the following stack trace, we'd be very appreciative!
201-
";
202-
203-
error!(
204-
"{}{}{} {message}\n{error}",
205-
"Panicked while linting ".bold(),
206-
fs::relativize_path(path).bold(),
207-
":".bold()
198+
let message = match error.payload.as_str() {
199+
Some(summary) => format!("Fatal error while linting: {summary}"),
200+
_ => "Fatal error while linting".to_owned(),
201+
};
202+
let mut diagnostic = Diagnostic::new(
203+
DiagnosticId::Panic,
204+
ruff_db::diagnostic::Severity::Fatal,
205+
message,
208206
);
209-
210-
Ok(Diagnostics::default())
207+
let span = Span::from(SourceFileBuilder::new(path.to_string_lossy(), "").finish());
208+
let mut annotation = Annotation::primary(span);
209+
annotation.set_file_level(true);
210+
diagnostic.annotate(annotation);
211+
diagnostic.sub(SubDiagnostic::new(
212+
SubDiagnosticSeverity::Info,
213+
format!("{error}"),
214+
));
215+
Ok(Diagnostics::new(vec![diagnostic], FxHashMap::default()))
211216
}
212217
}
213218
}

crates/ruff/src/lib.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ use std::sync::mpsc::channel;
99
use anyhow::Result;
1010
use clap::CommandFactory;
1111
use colored::Colorize;
12-
use log::warn;
12+
use log::{error, warn};
1313
use notify::{RecursiveMode, Watcher, recommended_watcher};
1414

1515
use args::{GlobalConfigArgs, ServerCommand};
16+
use ruff_db::diagnostic::{Diagnostic, Severity};
1617
use ruff_linter::logging::{LogLevel, set_up_logging};
1718
use ruff_linter::settings::flags::FixMode;
1819
use ruff_linter::settings::types::OutputFormat;
@@ -444,6 +445,27 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
444445
}
445446

446447
if !cli.exit_zero {
448+
let max_severity = diagnostics
449+
.inner
450+
.iter()
451+
.map(Diagnostic::severity)
452+
.max()
453+
.unwrap_or(Severity::Info);
454+
if max_severity.is_fatal() {
455+
// When a panic/fatal error is reported, prompt the user to open an issue on github.
456+
// Diagnostics with severity `fatal` will be sorted to the bottom, and printing the
457+
// message here instead of attaching it to the diagnostic ensures that we only print
458+
// it once instead of repeating it for each diagnostic. Prints to stderr to prevent
459+
// the message from being captured by tools parsing the normal output.
460+
let message = "Panic during linting indicates a bug in Ruff. If you could open an issue at:
461+
462+
https://github.com/astral-sh/ruff/issues/new?title=%5BLinter%20panic%5D
463+
464+
...with the relevant file contents, the `pyproject.toml` settings, and the stack trace above, we'd be very appreciative!
465+
";
466+
error!("{message}");
467+
return Ok(ExitStatus::Error);
468+
}
447469
if cli.diff {
448470
// If we're printing a diff, we always want to exit non-zero if there are
449471
// any fixable violations (since we've printed the diff, but not applied the

crates/ruff/tests/lint.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5891,3 +5891,113 @@ fn show_fixes_in_full_output_with_preview_enabled() {
58915891
",
58925892
);
58935893
}
5894+
5895+
#[test]
5896+
fn rule_panic_mixed_results_concise() -> Result<()> {
5897+
let tempdir = TempDir::new()?;
5898+
5899+
// Create python files
5900+
let file_a_path = tempdir.path().join("normal.py");
5901+
let file_b_path = tempdir.path().join("panic.py");
5902+
fs::write(&file_a_path, b"import os")?;
5903+
fs::write(&file_b_path, b"print('hello, world!')")?;
5904+
5905+
insta::with_settings!({
5906+
filters => vec![
5907+
(tempdir_filter(&tempdir).as_str(), "[TMP]/"),
5908+
(r"\\", r"/"),
5909+
]
5910+
}, {
5911+
assert_cmd_snapshot!(
5912+
Command::new(get_cargo_bin(BIN_NAME))
5913+
.args(["check", "--select", "RUF9", "--preview", "--output-format=concise", "--no-cache"])
5914+
.args([file_a_path, file_b_path]),
5915+
@r"
5916+
success: false
5917+
exit_code: 2
5918+
----- stdout -----
5919+
[TMP]/normal.py:1:1: RUF900 Hey this is a stable test rule.
5920+
[TMP]/normal.py:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
5921+
[TMP]/normal.py:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
5922+
[TMP]/normal.py:1:1: RUF903 Hey this is a stable test rule with a display only fix.
5923+
[TMP]/normal.py:1:1: RUF911 Hey this is a preview test rule.
5924+
[TMP]/normal.py:1:1: RUF950 Hey this is a test rule that was redirected from another.
5925+
[TMP]/panic.py: panic: Fatal error while linting: This is a fake panic for testing.
5926+
Found 7 errors.
5927+
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
5928+
5929+
----- stderr -----
5930+
error: Panic during linting indicates a bug in Ruff. If you could open an issue at:
5931+
5932+
https://github.com/astral-sh/ruff/issues/new?title=%5BLinter%20panic%5D
5933+
5934+
...with the relevant file contents, the `pyproject.toml` settings, and the stack trace above, we'd be very appreciative!
5935+
");
5936+
});
5937+
Ok(())
5938+
}
5939+
5940+
#[test]
5941+
fn rule_panic_mixed_results_full() -> Result<()> {
5942+
let tempdir = TempDir::new()?;
5943+
5944+
// Create python files
5945+
let file_a_path = tempdir.path().join("normal.py");
5946+
let file_b_path = tempdir.path().join("panic.py");
5947+
fs::write(&file_a_path, b"import os")?;
5948+
fs::write(&file_b_path, b"print('hello, world!')")?;
5949+
5950+
insta::with_settings!({
5951+
filters => vec![
5952+
(tempdir_filter(&tempdir).as_str(), "[TMP]/"),
5953+
(r"\\", r"/"),
5954+
]
5955+
}, {
5956+
assert_cmd_snapshot!(
5957+
Command::new(get_cargo_bin(BIN_NAME))
5958+
.args(["check", "--select", "RUF9", "--preview", "--output-format=full", "--no-cache"])
5959+
.args([file_a_path, file_b_path]),
5960+
@r"
5961+
success: false
5962+
exit_code: 2
5963+
----- stdout -----
5964+
RUF900 Hey this is a stable test rule.
5965+
--> [TMP]/normal.py:1:1
5966+
5967+
RUF901 [*] Hey this is a stable test rule with a safe fix.
5968+
--> [TMP]/normal.py:1:1
5969+
1 + # fix from stable-test-rule-safe-fix
5970+
2 | import os
5971+
5972+
RUF902 Hey this is a stable test rule with an unsafe fix.
5973+
--> [TMP]/normal.py:1:1
5974+
5975+
RUF903 Hey this is a stable test rule with a display only fix.
5976+
--> [TMP]/normal.py:1:1
5977+
5978+
RUF911 Hey this is a preview test rule.
5979+
--> [TMP]/normal.py:1:1
5980+
5981+
RUF950 Hey this is a test rule that was redirected from another.
5982+
--> [TMP]/normal.py:1:1
5983+
5984+
panic: Fatal error while linting: This is a fake panic for testing.
5985+
--> [TMP]/panic.py:1:1
5986+
info: panicked at crates/ruff_linter/src/rules/ruff/rules/test_rules.rs:511:9:
5987+
This is a fake panic for testing.
5988+
run with `RUST_BACKTRACE=1` environment variable to display a backtrace
5989+
5990+
5991+
Found 7 errors.
5992+
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
5993+
5994+
----- stderr -----
5995+
error: Panic during linting indicates a bug in Ruff. If you could open an issue at:
5996+
5997+
https://github.com/astral-sh/ruff/issues/new?title=%5BLinter%20panic%5D
5998+
5999+
...with the relevant file contents, the `pyproject.toml` settings, and the stack trace above, we'd be very appreciative!
6000+
");
6001+
});
6002+
Ok(())
6003+
}

crates/ruff_db/src/diagnostic/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,10 +499,12 @@ impl Diagnostic {
499499
/// Panics if either diagnostic has no primary span, or if its file is not a `SourceFile`.
500500
pub fn ruff_start_ordering(&self, other: &Self) -> std::cmp::Ordering {
501501
let a = (
502+
self.severity().is_fatal(),
502503
self.expect_ruff_source_file(),
503504
self.range().map(|r| r.start()),
504505
);
505506
let b = (
507+
other.severity().is_fatal(),
506508
other.expect_ruff_source_file(),
507509
other.range().map(|r| r.start()),
508510
);

crates/ruff_linter/src/checkers/ast/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3266,6 +3266,11 @@ impl<'a> LintContext<'a> {
32663266
pub(crate) const fn settings(&self) -> &LinterSettings {
32673267
self.settings
32683268
}
3269+
3270+
#[cfg(any(feature = "test-rules", test))]
3271+
pub(crate) const fn source_file(&self) -> &SourceFile {
3272+
&self.source_file
3273+
}
32693274
}
32703275

32713276
/// An abstraction for mutating a diagnostic.

crates/ruff_linter/src/codes.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1080,6 +1080,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
10801080
(Ruff, "950") => (RuleGroup::Stable, rules::ruff::rules::RedirectedToTestRule),
10811081
#[cfg(any(feature = "test-rules", test))]
10821082
(Ruff, "960") => (RuleGroup::Removed, rules::ruff::rules::RedirectedFromPrefixTestRule),
1083+
#[cfg(any(feature = "test-rules", test))]
1084+
(Ruff, "990") => (RuleGroup::Preview, rules::ruff::rules::PanicyTestRule),
10831085

10841086

10851087
// flake8-django

crates/ruff_linter/src/linter.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,9 @@ pub fn check_path(
319319
&context,
320320
);
321321
}
322+
Rule::PanicyTestRule => {
323+
test_rules::PanicyTestRule::diagnostic(locator, comment_ranges, &context);
324+
}
322325
_ => unreachable!("All test rules must have an implementation"),
323326
}
324327
}

crates/ruff_linter/src/rules/ruff/rules/test_rules.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ pub(crate) const TEST_RULES: &[Rule] = &[
4646
Rule::RedirectedFromTestRule,
4747
Rule::RedirectedToTestRule,
4848
Rule::RedirectedFromPrefixTestRule,
49+
Rule::PanicyTestRule,
4950
];
5051

5152
pub(crate) trait TestRule {
@@ -477,3 +478,39 @@ impl TestRule for RedirectedFromPrefixTestRule {
477478
);
478479
}
479480
}
481+
482+
/// # What it does
483+
/// Fake rule for testing panics.
484+
///
485+
/// # Why is this bad?
486+
/// Panics are bad.
487+
///
488+
/// # Example
489+
/// ```python
490+
/// foo
491+
/// ```
492+
///
493+
/// Use instead:
494+
/// ```python
495+
/// bar
496+
/// ```
497+
#[derive(ViolationMetadata)]
498+
pub(crate) struct PanicyTestRule;
499+
500+
impl Violation for PanicyTestRule {
501+
const FIX_AVAILABILITY: FixAvailability = FixAvailability::None;
502+
503+
#[derive_message_formats]
504+
fn message(&self) -> String {
505+
"If you see this, maybe panic!".to_string()
506+
}
507+
}
508+
509+
impl TestRule for PanicyTestRule {
510+
fn diagnostic(_locator: &Locator, _comment_ranges: &CommentRanges, context: &LintContext) {
511+
assert!(
512+
!context.source_file().name().ends_with("panic.py"),
513+
"This is a fake panic for testing."
514+
);
515+
}
516+
}

0 commit comments

Comments
 (0)