diff --git a/crates/ruff_cli/src/commands/format.rs b/crates/ruff_cli/src/commands/format.rs index 334dda8ae3..8299c05f6b 100644 --- a/crates/ruff_cli/src/commands/format.rs +++ b/crates/ruff_cli/src/commands/format.rs @@ -17,13 +17,14 @@ use tracing::debug; use ruff_diagnostics::SourceMap; use ruff_linter::fs; -use ruff_linter::logging::LogLevel; +use ruff_linter::logging::{DisplayParseError, LogLevel}; use ruff_linter::registry::Rule; use ruff_linter::rules::flake8_quotes::settings::Quote; use ruff_linter::source_kind::{SourceError, SourceKind}; use ruff_linter::warn_user_once; use ruff_python_ast::{PySourceType, SourceType}; use ruff_python_formatter::{format_module_source, FormatModuleError, QuoteStyle}; +use ruff_source_file::{LineIndex, SourceCode}; use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_workspace::resolver::{ match_exclusion, python_files_in_path, PyprojectConfig, ResolvedFile, Resolver, @@ -244,7 +245,7 @@ pub(crate) fn format_path( // Extract the sources from the file. let unformatted = match SourceKind::from_path(path, source_type) { Ok(Some(source_kind)) => source_kind, - // Non Python Jupyter notebook + // Non-Python Jupyter notebook. Ok(None) => return Ok(FormatResult::Skipped), Err(err) => { return Err(FormatCommandError::Read(Some(path.to_path_buf()), err)); @@ -321,12 +322,13 @@ pub(crate) fn format_source( path: Option<&Path>, settings: &FormatterSettings, ) -> Result { - match source_kind { + match &source_kind { SourceKind::Python(unformatted) => { let options = settings.to_format_options(source_type, unformatted); - let formatted = format_module_source(unformatted, options) - .map_err(|err| FormatCommandError::Format(path.map(Path::to_path_buf), err))?; + let formatted = format_module_source(unformatted, options).map_err(|err| { + FormatCommandError::Format(path.map(Path::to_path_buf), source_kind.clone(), err) + })?; let formatted = formatted.into_code(); if formatted.len() == unformatted.len() && formatted == *unformatted { @@ -352,8 +354,14 @@ pub(crate) fn format_source( let unformatted = ¬ebook.source_code()[range]; // Format the cell. - let formatted = format_module_source(unformatted, options.clone()) - .map_err(|err| FormatCommandError::Format(path.map(Path::to_path_buf), err))?; + let formatted = + format_module_source(unformatted, options.clone()).map_err(|err| { + FormatCommandError::Format( + path.map(Path::to_path_buf), + source_kind.clone(), + err, + ) + })?; // If the cell is unchanged, skip it. let formatted = formatted.as_code(); @@ -565,7 +573,7 @@ pub(crate) enum FormatCommandError { Ignore(#[from] ignore::Error), Panic(Option, PanicError), Read(Option, SourceError), - Format(Option, FormatModuleError), + Format(Option, SourceKind, FormatModuleError), Write(Option, SourceError), Diff(Option, io::Error), } @@ -582,7 +590,7 @@ impl FormatCommandError { } Self::Panic(path, _) | Self::Read(path, _) - | Self::Format(path, _) + | Self::Format(path, _, _) | Self::Write(path, _) | Self::Diff(path, _) => path.as_deref(), } @@ -639,7 +647,22 @@ impl Display for FormatCommandError { write!(f, "{}{} {err}", "Failed to write".bold(), ":".bold()) } } - Self::Format(path, err) => { + Self::Format(path, source_kind, FormatModuleError::ParseError(err)) => { + write!( + f, + "{}", + DisplayParseError::new( + err, + &SourceCode::new( + source_kind.source_code(), + &LineIndex::from_source_text(source_kind.source_code()), + ), + source_kind, + path.as_deref(), + ) + ) + } + Self::Format(path, _source_kind, err) => { if let Some(path) = path { write!( f, diff --git a/crates/ruff_cli/src/diagnostics.rs b/crates/ruff_cli/src/diagnostics.rs index 581dad4560..a1bbf5c171 100644 --- a/crates/ruff_cli/src/diagnostics.rs +++ b/crates/ruff_cli/src/diagnostics.rs @@ -360,13 +360,13 @@ pub(crate) fn lint_path( error!( "{}", DisplayParseError::new( - err, - SourceCode::new( + &err, + &SourceCode::new( source_kind.source_code(), &LineIndex::from_source_text(source_kind.source_code()) ), &source_kind, - path, + Some(path), ) ); } diff --git a/crates/ruff_cli/tests/format.rs b/crates/ruff_cli/tests/format.rs index b4f175a348..f31b5bde78 100644 --- a/crates/ruff_cli/tests/format.rs +++ b/crates/ruff_cli/tests/format.rs @@ -348,7 +348,7 @@ from module import = ----- stdout ----- ----- stderr ----- - error: Failed to format main.py: source contains syntax errors: invalid syntax. Got unexpected token '=' at byte offset 20 + error: Failed to parse main.py:2:20: Unexpected token '=' "###); Ok(()) diff --git a/crates/ruff_dev/src/format_dev.rs b/crates/ruff_dev/src/format_dev.rs index 5589254b05..381f67bdca 100644 --- a/crates/ruff_dev/src/format_dev.rs +++ b/crates/ruff_dev/src/format_dev.rs @@ -35,6 +35,7 @@ use ruff_linter::settings::types::{FilePattern, FilePatternSet}; use ruff_python_formatter::{ format_module_source, FormatModuleError, MagicTrailingComma, PreviewMode, PyFormatOptions, }; +use ruff_python_parser::ParseError; use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile, Resolver}; /// Find files that ruff would check so we can format them. Adapted from `ruff_cli`. @@ -742,11 +743,11 @@ enum CheckFileError { reformatted: String, }, /// The input file was already invalid (not a bug) - SyntaxErrorInInput(FormatModuleError), + SyntaxErrorInInput(ParseError), /// The formatter introduced a syntax error SyntaxErrorInOutput { formatted: String, - error: FormatModuleError, + error: ParseError, }, /// The formatter failed (bug) FormatError(FormatError), @@ -796,7 +797,7 @@ fn format_dev_file( let start = Instant::now(); let printed = match format_module_source(&content, options.clone()) { Ok(printed) => printed, - Err(err @ (FormatModuleError::LexError(_) | FormatModuleError::ParseError(_))) => { + Err(FormatModuleError::ParseError(err)) => { return Err(CheckFileError::SyntaxErrorInInput(err)); } Err(FormatModuleError::FormatError(err)) => { @@ -823,7 +824,7 @@ fn format_dev_file( if stability_check { let reformatted = match format_module_source(formatted, options) { Ok(reformatted) => reformatted, - Err(err @ (FormatModuleError::LexError(_) | FormatModuleError::ParseError(_))) => { + Err(FormatModuleError::ParseError(err)) => { return Err(CheckFileError::SyntaxErrorInOutput { formatted: formatted.to_string(), error: err, diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index 9197677013..ba08ef2399 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -339,7 +339,7 @@ pub fn add_noqa_to_path( if let Some(error) = error { error!( "{}", - DisplayParseError::new(error, locator.to_source_code(), source_kind, path) + DisplayParseError::new(&error, &locator.to_source_code(), source_kind, Some(path)) ); } diff --git a/crates/ruff_linter/src/logging.rs b/crates/ruff_linter/src/logging.rs index 2332c66b09..46b8485332 100644 --- a/crates/ruff_linter/src/logging.rs +++ b/crates/ruff_linter/src/logging.rs @@ -136,19 +136,21 @@ pub fn set_up_logging(level: &LogLevel) -> Result<()> { Ok(()) } +/// A wrapper around [`ParseError`] to translate byte offsets to user-facing +/// source code locations (typically, line and column numbers). pub struct DisplayParseError<'a> { - error: ParseError, - source_code: SourceCode<'a, 'a>, + error: &'a ParseError, + source_code: &'a SourceCode<'a, 'a>, source_kind: &'a SourceKind, - path: &'a Path, + path: Option<&'a Path>, } impl<'a> DisplayParseError<'a> { pub fn new( - error: ParseError, - source_code: SourceCode<'a, 'a>, + error: &'a ParseError, + source_code: &'a SourceCode<'a, 'a>, source_kind: &'a SourceKind, - path: &'a Path, + path: Option<&'a Path>, ) -> Self { Self { error, @@ -161,13 +163,22 @@ impl<'a> DisplayParseError<'a> { impl Display for DisplayParseError<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{header} {path}{colon}", - header = "Failed to parse".bold(), - path = fs::relativize_path(self.path).bold(), - colon = ":".cyan(), - )?; + if let Some(path) = self.path { + write!( + f, + "{header} {path}{colon}", + header = "Failed to parse".bold(), + path = fs::relativize_path(path).bold(), + colon = ":".cyan(), + )?; + } else { + write!( + f, + "{header}{colon}", + header = "Failed to parse".bold(), + colon = ":".cyan(), + )?; + } let source_location = self.source_code.source_location(self.error.offset); diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index 8213537db6..73d98a6161 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -6,8 +6,7 @@ use ruff_formatter::{format, FormatError, Formatted, PrintError, Printed, Source use ruff_python_ast::AstNode; use ruff_python_ast::Mod; use ruff_python_index::tokens_and_ranges; -use ruff_python_parser::lexer::LexicalError; -use ruff_python_parser::{parse_ok_tokens, AsMode, ParseError}; +use ruff_python_parser::{parse_ok_tokens, AsMode, ParseError, ParseErrorType}; use ruff_python_trivia::CommentRanges; use ruff_source_file::Locator; @@ -108,35 +107,25 @@ where #[derive(Error, Debug)] pub enum FormatModuleError { - #[error("source contains syntax errors: {0}")] - LexError(LexicalError), - #[error("source contains syntax errors: {0}")] - ParseError(ParseError), + #[error(transparent)] + ParseError(#[from] ParseError), #[error(transparent)] FormatError(#[from] FormatError), #[error(transparent)] PrintError(#[from] PrintError), } -impl From for FormatModuleError { - fn from(value: LexicalError) -> Self { - Self::LexError(value) - } -} - -impl From for FormatModuleError { - fn from(value: ParseError) -> Self { - Self::ParseError(value) - } -} - #[tracing::instrument(name = "format", level = Level::TRACE, skip_all)] pub fn format_module_source( source: &str, options: PyFormatOptions, ) -> Result { let source_type = options.source_type(); - let (tokens, comment_ranges) = tokens_and_ranges(source, source_type)?; + let (tokens, comment_ranges) = + tokens_and_ranges(source, source_type).map_err(|err| ParseError { + offset: err.location, + error: ParseErrorType::Lexical(err.error), + })?; let module = parse_ok_tokens(tokens, source, source_type.as_mode())?; let formatted = format_module_ast(&module, &comment_ranges, source, options)?; Ok(formatted.print()?) @@ -180,7 +169,6 @@ mod tests { use ruff_python_ast::PySourceType; use ruff_python_index::tokens_and_ranges; - use ruff_python_parser::{parse_ok_tokens, AsMode}; use crate::{format_module_ast, format_module_source, PyFormatOptions}; diff --git a/crates/ruff_python_formatter/src/string/docstring.rs b/crates/ruff_python_formatter/src/string/docstring.rs index 336c51434d..d10fbe128a 100644 --- a/crates/ruff_python_formatter/src/string/docstring.rs +++ b/crates/ruff_python_formatter/src/string/docstring.rs @@ -4,8 +4,8 @@ use std::{borrow::Cow, collections::VecDeque}; +use ruff_python_parser::ParseError; use {once_cell::sync::Lazy, regex::Regex}; - use { ruff_formatter::{write, FormatOptions, IndentStyle, LineWidth, Printed}, ruff_python_trivia::{is_python_whitespace, PythonWhitespace}, @@ -499,11 +499,7 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> { let printed = match docstring_format_source(options, self.quote_char, &codeblob) { Ok(printed) => printed, Err(FormatModuleError::FormatError(err)) => return Err(err), - Err( - FormatModuleError::LexError(_) - | FormatModuleError::ParseError(_) - | FormatModuleError::PrintError(_), - ) => { + Err(FormatModuleError::ParseError(_) | FormatModuleError::PrintError(_)) => { return Ok(None); } }; @@ -1518,7 +1514,8 @@ fn docstring_format_source( use ruff_python_parser::AsMode; let source_type = options.source_type(); - let (tokens, comment_ranges) = ruff_python_index::tokens_and_ranges(source, source_type)?; + let (tokens, comment_ranges) = + ruff_python_index::tokens_and_ranges(source, source_type).map_err(ParseError::from)?; let module = ruff_python_parser::parse_ok_tokens(tokens, source, source_type.as_mode())?; let source_code = ruff_formatter::SourceCode::new(source); let comments = crate::Comments::from_ast(&module, source_code, &comment_ranges); diff --git a/crates/ruff_python_parser/src/parser.rs b/crates/ruff_python_parser/src/parser.rs index 21491d21f7..5f3a5f95ca 100644 --- a/crates/ruff_python_parser/src/parser.rs +++ b/crates/ruff_python_parser/src/parser.rs @@ -391,6 +391,15 @@ impl ParseErrorType { } } +impl From for ParseError { + fn from(error: LexicalError) -> Self { + ParseError { + error: ParseErrorType::Lexical(error.error), + offset: error.location, + } + } +} + /// An expression that may be parenthesized. #[derive(Clone, Debug)] pub(super) struct ParenthesizedExpr {