diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py index fff7aae96c..f9c4fe8b1d 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/string.py @@ -108,3 +108,13 @@ test_particular = [ '1.0000000000000000000000000000000000000000000010000' #... '0000000000000000000000000000000000000000025', ] + +# Parenthesized string continuation with messed up indentation +{ + "key": ( + [], + 'a' + 'b' + 'c' + ) +} diff --git a/crates/ruff_python_formatter/src/builders.rs b/crates/ruff_python_formatter/src/builders.rs index 962707a160..97a70dbe47 100644 --- a/crates/ruff_python_formatter/src/builders.rs +++ b/crates/ruff_python_formatter/src/builders.rs @@ -242,23 +242,25 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { } pub(crate) fn finish(&mut self) -> FormatResult<()> { - if let Some(last_end) = self.last_end.take() { - if_group_breaks(&text(",")).fmt(self.fmt)?; + self.result.and_then(|_| { + if let Some(last_end) = self.last_end.take() { + if_group_breaks(&text(",")).fmt(self.fmt)?; - if self.fmt.options().magic_trailing_comma().is_respect() - && matches!( - first_non_trivia_token(last_end, self.fmt.context().contents()), - Some(Token { - kind: TokenKind::Comma, - .. - }) - ) - { - expand_parent().fmt(self.fmt)?; + if self.fmt.options().magic_trailing_comma().is_respect() + && matches!( + first_non_trivia_token(last_end, self.fmt.context().contents()), + Some(Token { + kind: TokenKind::Comma, + .. + }) + ) + { + expand_parent().fmt(self.fmt)?; + } } - } - Ok(()) + Ok(()) + }) } } diff --git a/crates/ruff_python_formatter/src/expression/string.rs b/crates/ruff_python_formatter/src/expression/string.rs index 055b5d3642..0362aff005 100644 --- a/crates/ruff_python_formatter/src/expression/string.rs +++ b/crates/ruff_python_formatter/src/expression/string.rs @@ -8,7 +8,7 @@ use ruff_formatter::{format_args, write, FormatError}; use ruff_python_ast::str::is_implicit_concatenation; use ruff_text_size::{TextLen, TextRange, TextSize}; use rustpython_parser::ast::{ExprConstant, Ranged}; -use rustpython_parser::lexer::lex_starts_at; +use rustpython_parser::lexer::{lex_starts_at, LexicalError, LexicalErrorType}; use rustpython_parser::{Mode, Tok}; use std::borrow::Cow; @@ -17,7 +17,7 @@ pub enum StringLayout { Default(Option), /// Enforces that implicit continuation strings are printed on a single line even if they exceed - /// the configured line width. + /// the configured line width. Flat, } @@ -83,7 +83,7 @@ impl Format> for FormatStringContinuation<'_> { // Call into the lexer to extract the individual chunks and format each string on its own. // This code does not yet implement the automatic joining of strings that fit on the same line // because this is a black preview style. - let lexer = lex_starts_at(string_content, Mode::Module, string_range.start()); + let lexer = lex_starts_at(string_content, Mode::Expression, string_range.start()); let separator = format_with(|f| match self.layout { StringLayout::Default(_) => soft_line_break_or_space().fmt(f), @@ -93,7 +93,31 @@ impl Format> for FormatStringContinuation<'_> { let mut joiner = f.join_with(separator); for token in lexer { - let (token, token_range) = token.map_err(|_| FormatError::SyntaxError)?; + let (token, token_range) = match token { + Ok(spanned) => spanned, + Err(LexicalError { + error: LexicalErrorType::IndentationError, + .. + }) => { + // This can happen if the string continuation appears anywhere inside of a parenthesized expression + // because the lexer doesn't know about the parentheses. For example, the following snipped triggers an Indentation error + // ```python + // { + // "key": ( + // [], + // 'a' + // 'b' + // 'c' + // ) + // } + // ``` + // Ignoring the error here is *safe* because we know that the program once parsed to a valid AST. + continue; + } + Err(_) => { + return Err(FormatError::SyntaxError); + } + }; match token { Tok::String { .. } => { diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap index 94fc0a6049..7bdc05dbb5 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap @@ -114,6 +114,16 @@ test_particular = [ '1.0000000000000000000000000000000000000000000010000' #... '0000000000000000000000000000000000000000025', ] + +# Parenthesized string continuation with messed up indentation +{ + "key": ( + [], + 'a' + 'b' + 'c' + ) +} ``` ## Outputs @@ -259,6 +269,9 @@ test_particular = [ "1.0000000000000000000000000000000000000000000010000" # ... "0000000000000000000000000000000000000000025", ] + +# Parenthesized string continuation with messed up indentation +{"key": ([], "a" "b" "c")} ``` @@ -404,6 +417,9 @@ test_particular = [ '1.0000000000000000000000000000000000000000000010000' # ... '0000000000000000000000000000000000000000025', ] + +# Parenthesized string continuation with messed up indentation +{'key': ([], 'a' 'b' 'c')} ```