Catch panics in formatter (#7377)

## Summary

This PR ensures that we catch and render panics in the formatter
identically to other kinds of errors. It also improves the consistency
in error rendering throughout and makes a few stylistic changes to the
messages.

Closes https://github.com/astral-sh/ruff/issues/7247.

## Test Plan

I created a file `foo.py` with a syntax error, and a file `bar.py` with
an intentional panic.

<img width="1624" alt="Screen Shot 2023-09-13 at 10 25 22 PM"
src="605c2839-ad02-4376-a2e9-d5a593ab660f">

<img width="1624" alt="Screen Shot 2023-09-13 at 10 25 24 PM"
src="b1381909-157c-48cb-9630-d0bbfcb1b640">
This commit is contained in:
Charlie Marsh 2023-09-14 11:44:16 -04:00 committed by GitHub
parent ec2f229a45
commit 5d21b9c22e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 61 additions and 32 deletions

View file

@ -36,9 +36,6 @@ use crate::settings::{flags, Settings};
use crate::source_kind::SourceKind; use crate::source_kind::SourceKind;
use crate::{directives, fs}; use crate::{directives, fs};
const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME");
const CARGO_PKG_REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY");
/// A [`Result`]-like type that returns both data and an error. Used to return /// A [`Result`]-like type that returns both data and an error. Used to return
/// diagnostics even in the face of parse errors, since many diagnostics can be /// diagnostics even in the face of parse errors, since many diagnostics can be
/// generated without a full AST. /// generated without a full AST.
@ -543,8 +540,9 @@ fn report_failed_to_converge_error(path: &Path, transformed: &str, diagnostics:
let codes = collect_rule_codes(diagnostics.iter().map(|diagnostic| diagnostic.kind.rule())); let codes = collect_rule_codes(diagnostics.iter().map(|diagnostic| diagnostic.kind.rule()));
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
eprintln!( eprintln!(
"{}: Failed to converge after {} iterations in `{}` with rule codes {}:---\n{}\n---", "{}{} Failed to converge after {} iterations in `{}` with rule codes {}:---\n{}\n---",
"debug error".red().bold(), "debug error".red().bold(),
":".bold(),
MAX_ITERATIONS, MAX_ITERATIONS,
fs::relativize_path(path), fs::relativize_path(path),
codes, codes,
@ -553,18 +551,17 @@ fn report_failed_to_converge_error(path: &Path, transformed: &str, diagnostics:
} else { } else {
eprintln!( eprintln!(
r#" r#"
{}: Failed to converge after {} iterations. {}{} Failed to converge after {} iterations.
This indicates a bug in `{}`. If you could open an issue at: This indicates a bug in Ruff. If you could open an issue at:
{}/issues/new?title=%5BInfinite%20loop%5D https://github.com/astral-sh/ruff/issues/new?title=%5BInfinite%20loop%5D
...quoting the contents of `{}`, the rule codes {}, along with the `pyproject.toml` settings and executed command, we'd be very appreciative! ...quoting the contents of `{}`, the rule codes {}, along with the `pyproject.toml` settings and executed command, we'd be very appreciative!
"#, "#,
"error".red().bold(), "error".red().bold(),
":".bold(),
MAX_ITERATIONS, MAX_ITERATIONS,
CARGO_PKG_NAME,
CARGO_PKG_REPOSITORY,
fs::relativize_path(path), fs::relativize_path(path),
codes codes
); );
@ -581,8 +578,9 @@ fn report_autofix_syntax_error(
let codes = collect_rule_codes(rules); let codes = collect_rule_codes(rules);
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
eprintln!( eprintln!(
"{}: Autofix introduced a syntax error in `{}` with rule codes {}: {}\n---\n{}\n---", "{}{} Autofix introduced a syntax error in `{}` with rule codes {}: {}\n---\n{}\n---",
"error".red().bold(), "error".red().bold(),
":".bold(),
fs::relativize_path(path), fs::relativize_path(path),
codes, codes,
error, error,
@ -591,17 +589,16 @@ fn report_autofix_syntax_error(
} else { } else {
eprintln!( eprintln!(
r#" r#"
{}: Autofix introduced a syntax error. Reverting all changes. {}{} Autofix introduced a syntax error. Reverting all changes.
This indicates a bug in `{}`. If you could open an issue at: This indicates a bug in Ruff. If you could open an issue at:
{}/issues/new?title=%5BAutofix%20error%5D https://github.com/astral-sh/ruff/issues/new?title=%5BAutofix%20error%5D
...quoting the contents of `{}`, the rule codes {}, along with the `pyproject.toml` settings and executed command, we'd be very appreciative! ...quoting the contents of `{}`, the rule codes {}, along with the `pyproject.toml` settings and executed command, we'd be very appreciative!
"#, "#,
"error".red().bold(), "error".red().bold(),
CARGO_PKG_NAME, ":".bold(),
CARGO_PKG_REPOSITORY,
fs::relativize_path(path), fs::relativize_path(path),
codes, codes,
); );

View file

@ -211,16 +211,16 @@ fn lint_path(
match result { match result {
Ok(inner) => inner, Ok(inner) => inner,
Err(error) => { Err(error) => {
let message = r#"This indicates a bug in `ruff`. If you could open an issue at: let message = r#"This indicates a bug in Ruff. If you could open an issue at:
https://github.com/astral-sh/ruff/issues/new?title=%5BLinter%20panic%5D https://github.com/astral-sh/ruff/issues/new?title=%5BLinter%20panic%5D
with the relevant file contents, the `pyproject.toml` settings, and the following stack trace, we'd be very appreciative! ...with the relevant file contents, the `pyproject.toml` settings, and the following stack trace, we'd be very appreciative!
"#; "#;
warn!( error!(
"{}{}{} {message}\n{error}", "{}{}{} {message}\n{error}",
"Linting panicked ".bold(), "Panicked while linting ".bold(),
fs::relativize_path(path).bold(), fs::relativize_path(path).bold(),
":".bold() ":".bold()
); );

View file

@ -6,6 +6,7 @@ use std::time::Instant;
use anyhow::Result; use anyhow::Result;
use colored::Colorize; use colored::Colorize;
use log::error;
use rayon::iter::Either::{Left, Right}; use rayon::iter::Either::{Left, Right};
use rayon::iter::{IntoParallelIterator, ParallelIterator}; use rayon::iter::{IntoParallelIterator, ParallelIterator};
use thiserror::Error; use thiserror::Error;
@ -22,6 +23,7 @@ use ruff_source_file::{find_newline, LineEnding};
use ruff_workspace::resolver::python_files_in_path; use ruff_workspace::resolver::python_files_in_path;
use crate::args::{FormatArguments, Overrides}; use crate::args::{FormatArguments, Overrides};
use crate::panic::{catch_unwind, PanicError};
use crate::resolve::resolve; use crate::resolve::resolve;
use crate::ExitStatus; use crate::ExitStatus;
@ -85,7 +87,13 @@ pub(crate) fn format(
.with_line_width(LineWidth::from(NonZeroU16::from(line_length))) .with_line_width(LineWidth::from(NonZeroU16::from(line_length)))
.with_preview(preview); .with_preview(preview);
debug!("Formatting {} with {:?}", path.display(), options); debug!("Formatting {} with {:?}", path.display(), options);
Some(format_path(path, options, mode))
Some(match catch_unwind(|| format_path(path, options, mode)) {
Ok(inner) => inner,
Err(error) => {
Err(FormatCommandError::Panic(Some(path.to_path_buf()), error))
}
})
} }
Err(err) => Some(Err(FormatCommandError::Ignore(err))), Err(err) => Some(Err(FormatCommandError::Ignore(err))),
} }
@ -95,22 +103,20 @@ pub(crate) fn format(
Err(err) => Right(err), Err(err) => Right(err),
}); });
let duration = start.elapsed(); let duration = start.elapsed();
debug!( debug!(
"Formatted {} files in {:.2?}", "Formatted {} files in {:.2?}",
results.len() + errors.len(), results.len() + errors.len(),
duration duration
); );
let summary = FormatResultSummary::new(results, mode);
// Report on any errors. // Report on any errors.
if !errors.is_empty() {
warn!("Encountered {} errors while formatting:", errors.len());
for error in &errors { for error in &errors {
warn!("{error}"); error!("{error}");
}
} }
let summary = FormatResultSummary::new(results, mode);
// Report on the formatting changes. // Report on the formatting changes.
if log_level >= LogLevel::Default { if log_level >= LogLevel::Default {
#[allow(clippy::print_stdout)] #[allow(clippy::print_stdout)]
@ -255,6 +261,7 @@ pub(crate) enum FormatCommandError {
Read(Option<PathBuf>, io::Error), Read(Option<PathBuf>, io::Error),
Write(Option<PathBuf>, io::Error), Write(Option<PathBuf>, io::Error),
FormatModule(Option<PathBuf>, FormatModuleError), FormatModule(Option<PathBuf>, FormatModuleError),
Panic(Option<PathBuf>, PanicError),
} }
impl Display for FormatCommandError { impl Display for FormatCommandError {
@ -320,6 +327,29 @@ impl Display for FormatCommandError {
write!(f, "{}{} {err}", "Failed to format".bold(), ":".bold()) write!(f, "{}{} {err}", "Failed to format".bold(), ":".bold())
} }
} }
Self::Panic(path, err) => {
let message = r#"This indicates a bug in Ruff. If you could open an issue at:
https://github.com/astral-sh/ruff/issues/new?title=%5BFormatter%20panic%5D
...with the relevant file contents, the `pyproject.toml` settings, and the following stack trace, we'd be very appreciative!
"#;
if let Some(path) = path {
write!(
f,
"{}{}{} {message}\n{err}",
"Panicked while formatting ".bold(),
fs::relativize_path(path).bold(),
":".bold()
)
} else {
write!(
f,
"{} {message}\n{err}",
"Panicked while formatting.".bold()
)
}
}
} }
} }
} }

View file

@ -111,13 +111,15 @@ pub fn run(
{ {
eprintln!( eprintln!(
r#" r#"
{}: `ruff` crashed. This indicates a bug in `ruff`. If you could open an issue at: {}{} {} If you could open an issue at:
https://github.com/astral-sh/ruff/issues/new?title=%5BPanic%5D https://github.com/astral-sh/ruff/issues/new?title=%5BPanic%5D
quoting the executed command, along with the relevant file contents and `pyproject.toml` settings, we'd be very appreciative! ...quoting the executed command, along with the relevant file contents and `pyproject.toml` settings, we'd be very appreciative!
"#, "#,
"error".red().bold(), "error".red().bold(),
":".bold(),
"Ruff crashed.".bold(),
); );
} }
default_panic_hook(info); default_panic_hook(info);

View file

@ -120,7 +120,7 @@ impl From<ParseError> for FormatModuleError {
} }
} }
#[tracing::instrument(level=Level::TRACE, skip_all, err)] #[tracing::instrument(level=Level::TRACE, skip_all)]
pub fn format_module( pub fn format_module(
contents: &str, contents: &str,
options: PyFormatOptions, options: PyFormatOptions,