diff --git a/crates/ruff_python_formatter/resources/test/fixtures/black/cases/pep_701.py b/crates/ruff_python_formatter/resources/test/fixtures/black/cases/pep_701.py index ba6a1b208c..8224f38ea2 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/black/cases/pep_701.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/black/cases/pep_701.py @@ -74,8 +74,7 @@ x = f"a{2+2:=^{foo(x+y**2):something else}one more}b" f'{(abc:=10)}' f"This is a really long string, but just make sure that you reflow fstrings { - 2+2:d -}" + 2+2:d}" f"This is a really long string, but just make sure that you reflow fstrings correctly {2+2:d}" f"{2+2=}" diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py index 6a0cbc1581..fd658cc5ce 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py @@ -278,16 +278,7 @@ x = f"aaaaaaaaa { x = !r }" # Combine conversion flags with format specifiers x = f"{x = !s - :>0 - - }" -# This is interesting. There can be a comment after the format specifier but only if it's -# on it's own line. Refer to https://github.com/astral-sh/ruff/pull/7787 for more details. -# We'll format is as trailing comments. -x = f"{x !s - :>0 - # comment 21 - }" + :>0}" x = f"{ x!s:>{ @@ -295,6 +286,13 @@ x = f"{ # comment 21-2 }}" +f"{1 + # comment 21-3 +:}" + +f"{1 # comment 21-4 +:} a" + x = f""" { # comment 22 @@ -311,14 +309,14 @@ x = f"""{"foo " + # comment 24 """ # Mix of various features. -f"{ # comment 26 +f"""{ # comment 26 foo # after foo :>{ x # after x } # comment 27 # comment 28 -} woah {x}" +} woah {x}""" f"""{foo @@ -332,8 +330,7 @@ f"""{foo f"{ # comment 31 foo - :> -}" + :>}" # Assignment statement @@ -487,13 +484,11 @@ aaaaa[aaaaaaaaaaa] = ( # This is not a multiline f-string even though it has a newline after the format specifier. aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeee{ - a:.3f - }moreeeeeeeeeeeeeeeeeetest" # comment + a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment aaaaaaaaaaaaaaaaaa = ( f"testeeeeeeeeeeeeeeeeeeeeeeeee{ - a:.3f - }moreeeeeeeeeeeeeeeeeetest" # comment + a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment ) # The newline is only considered when it's a tripled-quoted f-string. diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.py index a0321e4eb3..8e1aaf635b 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.py @@ -274,16 +274,7 @@ x = t"aaaaaaaaa { x = !r }" # Combine conversion flags with format specifiers x = t"{x = !s - :>0 - - }" -# This is interesting. There can be a comment after the format specifier but only if it's -# on it's own line. Refer to https://github.com/astral-sh/ruff/pull/7787 for more details. -# We'll format is as trailing comments. -x = t"{x !s - :>0 - # comment 21 - }" + :>0}" x = f"{ x!s:>{ @@ -291,6 +282,13 @@ x = f"{ # comment 21-2 }}" +f"{1 + # comment 21-3 +:}" + +f"{1 # comment 21-4 +:} a" + x = t""" { # comment 22 x = :.0{y # comment 23 @@ -306,14 +304,14 @@ x = t"""{"foo " + # comment 24 """ # Mix of various features. -t"{ # comment 26 +t"""{ # comment 26 foo # after foo :>{ x # after x } # comment 27 # comment 28 -} woah {x}" +} woah {x}""" # Assignment statement @@ -467,13 +465,11 @@ aaaaa[aaaaaaaaaaa] = ( # This is not a multiline t-string even though it has a newline after the format specifier. aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeee{ - a:.3f - }moreeeeeeeeeeeeeeeeeetest" # comment + a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment aaaaaaaaaaaaaaaaaa = ( t"testeeeeeeeeeeeeeeeeeeeeeeeee{ - a:.3f - }moreeeeeeeeeeeeeeeeeetest" # comment + a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment ) # The newline is only considered when it's a tripled-quoted t-string. diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index dd19db96b2..0ec2aa6d2f 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -323,27 +323,18 @@ fn handle_enclosed_comment<'a>( AnyNodeRef::TString(tstring) => CommentPlacement::dangling(tstring, comment), AnyNodeRef::InterpolatedElement(element) => { if let Some(preceding) = comment.preceding_node() { - if comment.line_position().is_own_line() && element.format_spec.is_some() { - return if comment.following_node().is_some() { - // Own line comment before format specifier - // ```py - // aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa { - // aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd - // # comment - // :.3f} cccccccccc""" - // ``` - CommentPlacement::trailing(preceding, comment) - } else { - // TODO: This can be removed once format specifiers with a newline are a syntax error. - // This is to handle cases like: - // ```py - // x = f"{x !s - // :>0 - // # comment 21 - // }" - // ``` - CommentPlacement::trailing(element, comment) - }; + // Own line comment before format specifier + // ```py + // aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa { + // aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd + // # comment + // :.3f} cccccccccc""" + // ``` + if comment.line_position().is_own_line() + && element.format_spec.is_some() + && comment.following_node().is_some() + { + return CommentPlacement::trailing(preceding, comment); } } diff --git a/crates/ruff_python_formatter/src/other/interpolated_string_element.rs b/crates/ruff_python_formatter/src/other/interpolated_string_element.rs index 3111776cf8..13526c218f 100644 --- a/crates/ruff_python_formatter/src/other/interpolated_string_element.rs +++ b/crates/ruff_python_formatter/src/other/interpolated_string_element.rs @@ -7,7 +7,7 @@ use ruff_python_ast::{ }; use ruff_text_size::{Ranged, TextSlice}; -use crate::comments::{dangling_open_parenthesis_comments, trailing_comments}; +use crate::comments::dangling_open_parenthesis_comments; use crate::context::{ InterpolatedStringState, NodeLevel, WithInterpolatedStringState, WithNodeLevel, }; @@ -203,7 +203,7 @@ impl Format> for FormatInterpolatedElement<'_> { // # comment 27 // :test}" // ``` - if comments.has_trailing_own_line(expression) { + if comments.has_trailing(expression) { soft_line_break().fmt(f)?; } @@ -214,31 +214,6 @@ impl Format> for FormatInterpolatedElement<'_> { } } - // These trailing comments can only occur if the format specifier is - // present. For example, - // - // ```python - // f"{ - // x:.3f - // # comment - // }" - // ``` - - // This can also be triggered outside of a format spec, at - // least until https://github.com/astral-sh/ruff/issues/18632 is a syntax error - // TODO(https://github.com/astral-sh/ruff/issues/18632) Remove this - // and double check if it is still necessary for the triple quoted case - // once this is a syntax error. - // ```py - // f"{ - // foo - // :{x} - // # comment 28 - // } woah {x}" - // ``` - // Any other trailing comments are attached to the expression itself. - trailing_comments(comments.trailing(self.element)).fmt(f)?; - if conversion.is_none() && format_spec.is_none() { bracket_spacing.fmt(f)?; } @@ -258,15 +233,7 @@ impl Format> for FormatInterpolatedElement<'_> { let mut f = WithNodeLevel::new(NodeLevel::ParenthesizedExpression, f); if self.context.is_multiline() { - // TODO: The `or comments.has_trailing...` can be removed once newlines in format specs are a syntax error. - // This is to support the following case: - // ```py - // x = f"{x !s - // :>0 - // # comment 21 - // }" - // ``` - if format_spec.is_none() || comments.has_trailing_own_line(self.element) { + if format_spec.is_none() { group(&format_args![ open_parenthesis_comments, soft_block_indent(&item) @@ -276,6 +243,7 @@ impl Format> for FormatInterpolatedElement<'_> { // For strings ending with a format spec, don't add a newline between the end of the format spec // and closing curly brace because that is invalid syntax for single quoted strings and // the newline is preserved as part of the format spec for triple quoted strings. + group(&format_args![ open_parenthesis_comments, indent(&format_args![soft_line_break(), item]) diff --git a/crates/ruff_python_formatter/tests/fixtures.rs b/crates/ruff_python_formatter/tests/fixtures.rs index 7a42e93c52..49b207621f 100644 --- a/crates/ruff_python_formatter/tests/fixtures.rs +++ b/crates/ruff_python_formatter/tests/fixtures.rs @@ -324,7 +324,12 @@ fn format_file(source: &str, options: &PyFormatOptions, input_path: &Path) -> St (Cow::Owned(without_markers), content) } else { - let printed = format_module_source(source, options.clone()).expect("Formatting to succeed"); + let printed = format_module_source(source, options.clone()).unwrap_or_else(|err| { + panic!( + "Formatting `{input_path} was expected to succeed but it failed: {err}", + input_path = input_path.display() + ) + }); let formatted_code = printed.into_code(); ensure_stability_when_formatting_twice(&formatted_code, options, input_path); diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pep_701.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pep_701.py.snap index e31f4723b9..63d6544faa 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pep_701.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pep_701.py.snap @@ -81,8 +81,7 @@ x = f"a{2+2:=^{foo(x+y**2):something else}one more}b" f'{(abc:=10)}' f"This is a really long string, but just make sure that you reflow fstrings { - 2+2:d -}" + 2+2:d}" f"This is a really long string, but just make sure that you reflow fstrings correctly {2+2:d}" f"{2+2=}" diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring.py.snap index 1d5f69f234..c929f160df 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring.py.snap @@ -284,16 +284,7 @@ x = f"aaaaaaaaa { x = !r }" # Combine conversion flags with format specifiers x = f"{x = !s - :>0 - - }" -# This is interesting. There can be a comment after the format specifier but only if it's -# on it's own line. Refer to https://github.com/astral-sh/ruff/pull/7787 for more details. -# We'll format is as trailing comments. -x = f"{x !s - :>0 - # comment 21 - }" + :>0}" x = f"{ x!s:>{ @@ -301,6 +292,13 @@ x = f"{ # comment 21-2 }}" +f"{1 + # comment 21-3 +:}" + +f"{1 # comment 21-4 +:} a" + x = f""" { # comment 22 @@ -317,14 +315,14 @@ x = f"""{"foo " + # comment 24 """ # Mix of various features. -f"{ # comment 26 +f"""{ # comment 26 foo # after foo :>{ x # after x } # comment 27 # comment 28 -} woah {x}" +} woah {x}""" f"""{foo @@ -338,8 +336,7 @@ f"""{foo f"{ # comment 31 foo - :> -}" + :>}" # Assignment statement @@ -493,13 +490,11 @@ aaaaa[aaaaaaaaaaa] = ( # This is not a multiline f-string even though it has a newline after the format specifier. aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeee{ - a:.3f - }moreeeeeeeeeeeeeeeeeetest" # comment + a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment aaaaaaaaaaaaaaaaaa = ( f"testeeeeeeeeeeeeeeeeeeeeeeeee{ - a:.3f - }moreeeeeeeeeeeeeeeeeetest" # comment + a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment ) # The newline is only considered when it's a tripled-quoted f-string. @@ -1071,13 +1066,6 @@ x = f"aaaaaaaaa { x = !r}" # Combine conversion flags with format specifiers x = f"{x = !s:>0}" -# This is interesting. There can be a comment after the format specifier but only if it's -# on it's own line. Refer to https://github.com/astral-sh/ruff/pull/7787 for more details. -# We'll format is as trailing comments. -x = f"{ - x!s:>0 - # comment 21 -}" x = f"{ x!s:>{ @@ -1085,6 +1073,15 @@ x = f"{ # comment 21-2 }}" +f"{ + 1 + # comment 21-3 + :}" + +f"{ + 1 # comment 21-4 + :} a" + x = f""" { # comment 22 @@ -1102,13 +1099,14 @@ x = f"""{ """ # Mix of various features. -f"{ # comment 26 - foo:>{ # after foo +f"""{ # comment 26 + foo # after foo + :>{ x # after x } # comment 27 # comment 28 -} woah {x}" +} woah {x}""" f"""{ @@ -1895,13 +1893,6 @@ x = f"aaaaaaaaa { x = !r}" # Combine conversion flags with format specifiers x = f"{x = !s:>0}" -# This is interesting. There can be a comment after the format specifier but only if it's -# on it's own line. Refer to https://github.com/astral-sh/ruff/pull/7787 for more details. -# We'll format is as trailing comments. -x = f"{ - x!s:>0 - # comment 21 -}" x = f"{ x!s:>{ @@ -1909,6 +1900,15 @@ x = f"{ # comment 21-2 }}" +f"{ + 1 + # comment 21-3 + :}" + +f"{ + 1 # comment 21-4 + :} a" + x = f""" { # comment 22 @@ -1926,13 +1926,14 @@ x = f"""{ """ # Mix of various features. -f"{ # comment 26 - foo:>{ # after foo +f"""{ # comment 26 + foo # after foo + :>{ x # after x } # comment 27 # comment 28 -} woah {x}" +} woah {x}""" f"""{ diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__tstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__tstring.py.snap index fef750a2a9..3352f2e85f 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__tstring.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__tstring.py.snap @@ -280,16 +280,7 @@ x = t"aaaaaaaaa { x = !r }" # Combine conversion flags with format specifiers x = t"{x = !s - :>0 - - }" -# This is interesting. There can be a comment after the format specifier but only if it's -# on it's own line. Refer to https://github.com/astral-sh/ruff/pull/7787 for more details. -# We'll format is as trailing comments. -x = t"{x !s - :>0 - # comment 21 - }" + :>0}" x = f"{ x!s:>{ @@ -297,6 +288,13 @@ x = f"{ # comment 21-2 }}" +f"{1 + # comment 21-3 +:}" + +f"{1 # comment 21-4 +:} a" + x = t""" { # comment 22 x = :.0{y # comment 23 @@ -312,14 +310,14 @@ x = t"""{"foo " + # comment 24 """ # Mix of various features. -t"{ # comment 26 +t"""{ # comment 26 foo # after foo :>{ x # after x } # comment 27 # comment 28 -} woah {x}" +} woah {x}""" # Assignment statement @@ -473,13 +471,11 @@ aaaaa[aaaaaaaaaaa] = ( # This is not a multiline t-string even though it has a newline after the format specifier. aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeee{ - a:.3f - }moreeeeeeeeeeeeeeeeeetest" # comment + a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment aaaaaaaaaaaaaaaaaa = ( t"testeeeeeeeeeeeeeeeeeeeeeeeee{ - a:.3f - }moreeeeeeeeeeeeeeeeeetest" # comment + a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment ) # The newline is only considered when it's a tripled-quoted t-string. @@ -1045,13 +1041,6 @@ x = t"aaaaaaaaa { x = !r}" # Combine conversion flags with format specifiers x = t"{x = !s:>0}" -# This is interesting. There can be a comment after the format specifier but only if it's -# on it's own line. Refer to https://github.com/astral-sh/ruff/pull/7787 for more details. -# We'll format is as trailing comments. -x = t"{ - x!s:>0 - # comment 21 -}" x = f"{ x!s:>{ @@ -1059,6 +1048,15 @@ x = f"{ # comment 21-2 }}" +f"{ + 1 + # comment 21-3 + :}" + +f"{ + 1 # comment 21-4 + :} a" + x = t""" { # comment 22 x = :.0{y # comment 23 @@ -1075,13 +1073,14 @@ x = t"""{ """ # Mix of various features. -t"{ # comment 26 - foo:>{ # after foo +t"""{ # comment 26 + foo # after foo + :>{ x # after x } # comment 27 # comment 28 -} woah {x}" +} woah {x}""" # Assignment statement diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index ad00df0f76..03dd0132c2 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -65,28 +65,31 @@ pub enum InterpolatedStringErrorType { LambdaWithoutParentheses, /// Conversion flag does not immediately follow exclamation. ConversionFlagNotImmediatelyAfterExclamation, + /// Newline inside of a format spec for a single quoted f- or t-string. + NewlineInFormatSpec, } impl std::fmt::Display for InterpolatedStringErrorType { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - use InterpolatedStringErrorType::{ - ConversionFlagNotImmediatelyAfterExclamation, InvalidConversionFlag, - LambdaWithoutParentheses, SingleRbrace, UnclosedLbrace, UnterminatedString, - UnterminatedTripleQuotedString, - }; match self { - UnclosedLbrace => write!(f, "expecting '}}'"), - InvalidConversionFlag => write!(f, "invalid conversion character"), - SingleRbrace => write!(f, "single '}}' is not allowed"), - UnterminatedString => write!(f, "unterminated string"), - UnterminatedTripleQuotedString => write!(f, "unterminated triple-quoted string"), - LambdaWithoutParentheses => { + Self::UnclosedLbrace => write!(f, "expecting '}}'"), + Self::InvalidConversionFlag => write!(f, "invalid conversion character"), + Self::SingleRbrace => write!(f, "single '}}' is not allowed"), + Self::UnterminatedString => write!(f, "unterminated string"), + Self::UnterminatedTripleQuotedString => write!(f, "unterminated triple-quoted string"), + Self::LambdaWithoutParentheses => { write!(f, "lambda expressions are not allowed without parentheses") } - ConversionFlagNotImmediatelyAfterExclamation => write!( + Self::ConversionFlagNotImmediatelyAfterExclamation => write!( f, "conversion type must come right after the exclamation mark" ), + Self::NewlineInFormatSpec => { + write!( + f, + "newlines are not allowed in format specifiers when using single quotes" + ) + } } } } @@ -430,31 +433,31 @@ impl LexicalErrorType { impl std::fmt::Display for LexicalErrorType { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { - LexicalErrorType::StringError => write!(f, "Got unexpected string"), - LexicalErrorType::FStringError(error) => write!(f, "f-string: {error}"), - LexicalErrorType::TStringError(error) => write!(f, "t-string: {error}"), - LexicalErrorType::InvalidByteLiteral => { + Self::StringError => write!(f, "Got unexpected string"), + Self::FStringError(error) => write!(f, "f-string: {error}"), + Self::TStringError(error) => write!(f, "t-string: {error}"), + Self::InvalidByteLiteral => { write!(f, "bytes can only contain ASCII literal characters") } - LexicalErrorType::UnicodeError => write!(f, "Got unexpected unicode"), - LexicalErrorType::IndentationError => { + Self::UnicodeError => write!(f, "Got unexpected unicode"), + Self::IndentationError => { write!(f, "unindent does not match any outer indentation level") } - LexicalErrorType::UnrecognizedToken { tok } => { + Self::UnrecognizedToken { tok } => { write!(f, "Got unexpected token {tok}") } - LexicalErrorType::LineContinuationError => { + Self::LineContinuationError => { write!(f, "Expected a newline after line continuation character") } - LexicalErrorType::Eof => write!(f, "unexpected EOF while parsing"), - LexicalErrorType::OtherError(msg) => write!(f, "{msg}"), - LexicalErrorType::UnclosedStringError => { + Self::Eof => write!(f, "unexpected EOF while parsing"), + Self::OtherError(msg) => write!(f, "{msg}"), + Self::UnclosedStringError => { write!(f, "missing closing quote in string literal") } - LexicalErrorType::MissingUnicodeLbrace => { + Self::MissingUnicodeLbrace => { write!(f, "Missing `{{` in Unicode escape sequence") } - LexicalErrorType::MissingUnicodeRbrace => { + Self::MissingUnicodeRbrace => { write!(f, "Missing `}}` in Unicode escape sequence") } } diff --git a/crates/ruff_python_parser/src/lexer.rs b/crates/ruff_python_parser/src/lexer.rs index d04f377678..53cea048e7 100644 --- a/crates/ruff_python_parser/src/lexer.rs +++ b/crates/ruff_python_parser/src/lexer.rs @@ -826,19 +826,17 @@ impl<'src> Lexer<'src> { ))); } '\n' | '\r' if !interpolated_string.is_triple_quoted() => { - // If we encounter a newline while we're in a format spec, then - // we stop here and let the lexer emit the newline token. - // - // Relevant discussion: https://github.com/python/cpython/issues/110259 - if in_format_spec { - break; - } + // https://github.com/astral-sh/ruff/issues/18632 self.interpolated_strings.pop(); + + let error_type = if in_format_spec { + InterpolatedStringErrorType::NewlineInFormatSpec + } else { + InterpolatedStringErrorType::UnterminatedString + }; + return Some(self.push_error(LexicalError::new( - LexicalErrorType::from_interpolated_string_error( - InterpolatedStringErrorType::UnterminatedString, - string_kind, - ), + LexicalErrorType::from_interpolated_string_error(error_type, string_kind), self.token_range(), ))); } @@ -1768,6 +1766,7 @@ mod tests { } } + #[track_caller] fn lex_valid(source: &str, mode: Mode, start_offset: TextSize) -> LexerOutput { let output = lex(source, mode, start_offset); @@ -1783,6 +1782,7 @@ mod tests { output } + #[track_caller] fn lex_invalid(source: &str, mode: Mode) -> LexerOutput { let output = lex(source, mode, TextSize::default()); @@ -1794,14 +1794,17 @@ mod tests { output } + #[track_caller] fn lex_source(source: &str) -> LexerOutput { lex_valid(source, Mode::Module, TextSize::default()) } + #[track_caller] fn lex_source_with_offset(source: &str, start_offset: TextSize) -> LexerOutput { lex_valid(source, Mode::Module, start_offset) } + #[track_caller] fn lex_jupyter_source(source: &str) -> LexerOutput { lex_valid(source, Mode::Ipython, TextSize::default()) } @@ -2394,6 +2397,13 @@ f'''__{ b c }__''' +"; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_fstring_newline_format_spec() { + let source = r" f'__{ x:d }__' @@ -2402,7 +2412,7 @@ f'__{ b }__' "; - assert_snapshot!(lex_source(source)); + assert_snapshot!(lex_invalid(source, Mode::Module)); } #[test] @@ -2572,6 +2582,13 @@ t'''__{ b c }__''' +"; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_newline_format_spec() { + let source = r" t'__{ x:d }__' @@ -2580,7 +2597,7 @@ t'__{ b }__' "; - assert_snapshot!(lex_source(source)); + assert_snapshot!(lex_invalid(source, Mode::Module)); } #[test] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_newline_format_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_newline_format_spec.snap new file mode 100644 index 0000000000..868a705a31 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_newline_format_spec.snap @@ -0,0 +1,168 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: "lex_invalid(source, Mode::Module)" +--- +## Tokens +``` +[ + ( + NonLogicalNewline, + 0..1, + ), + ( + FStringStart, + 1..3, + TokenFlags( + F_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "__", + ), + 3..5, + TokenFlags( + F_STRING, + ), + ), + ( + Lbrace, + 5..6, + ), + ( + NonLogicalNewline, + 6..7, + ), + ( + Name( + Name("x"), + ), + 11..12, + ), + ( + Colon, + 12..13, + ), + ( + Unknown, + 13..14, + ), + ( + NonLogicalNewline, + 14..15, + ), + ( + Rbrace, + 15..16, + ), + ( + Name( + Name("__"), + ), + 16..18, + ), + ( + Unknown, + 18..19, + ), + ( + Newline, + 19..20, + ), + ( + FStringStart, + 20..22, + TokenFlags( + F_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "__", + ), + 22..24, + TokenFlags( + F_STRING, + ), + ), + ( + Lbrace, + 24..25, + ), + ( + NonLogicalNewline, + 25..26, + ), + ( + Name( + Name("x"), + ), + 30..31, + ), + ( + Colon, + 31..32, + ), + ( + Unknown, + 32..33, + ), + ( + NonLogicalNewline, + 33..34, + ), + ( + Name( + Name("b"), + ), + 42..43, + ), + ( + NonLogicalNewline, + 43..44, + ), + ( + Rbrace, + 44..45, + ), + ( + Name( + Name("__"), + ), + 45..47, + ), + ( + Unknown, + 47..48, + ), + ( + Newline, + 48..49, + ), +] +``` +## Errors +``` +[ + LexicalError { + error: FStringError( + NewlineInFormatSpec, + ), + location: 13..14, + }, + LexicalError { + error: UnclosedStringError, + location: 18..19, + }, + LexicalError { + error: FStringError( + NewlineInFormatSpec, + ), + location: 32..33, + }, + LexicalError { + error: UnclosedStringError, + location: 47..48, + }, +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_multiline_format_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_multiline_format_spec.snap index 6a0909bcdb..9a7be930a8 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_multiline_format_spec.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_multiline_format_spec.snap @@ -139,157 +139,5 @@ expression: lex_source(source) Newline, 67..68, ), - ( - FStringStart, - 68..70, - TokenFlags( - F_STRING, - ), - ), - ( - InterpolatedStringMiddle( - "__", - ), - 70..72, - TokenFlags( - F_STRING, - ), - ), - ( - Lbrace, - 72..73, - ), - ( - NonLogicalNewline, - 73..74, - ), - ( - Name( - Name("x"), - ), - 78..79, - ), - ( - Colon, - 79..80, - ), - ( - InterpolatedStringMiddle( - "d", - ), - 80..81, - TokenFlags( - F_STRING, - ), - ), - ( - NonLogicalNewline, - 81..82, - ), - ( - Rbrace, - 82..83, - ), - ( - InterpolatedStringMiddle( - "__", - ), - 83..85, - TokenFlags( - F_STRING, - ), - ), - ( - FStringEnd, - 85..86, - TokenFlags( - F_STRING, - ), - ), - ( - Newline, - 86..87, - ), - ( - FStringStart, - 87..89, - TokenFlags( - F_STRING, - ), - ), - ( - InterpolatedStringMiddle( - "__", - ), - 89..91, - TokenFlags( - F_STRING, - ), - ), - ( - Lbrace, - 91..92, - ), - ( - NonLogicalNewline, - 92..93, - ), - ( - Name( - Name("x"), - ), - 97..98, - ), - ( - Colon, - 98..99, - ), - ( - InterpolatedStringMiddle( - "a", - ), - 99..100, - TokenFlags( - F_STRING, - ), - ), - ( - NonLogicalNewline, - 100..101, - ), - ( - Name( - Name("b"), - ), - 109..110, - ), - ( - NonLogicalNewline, - 110..111, - ), - ( - Rbrace, - 111..112, - ), - ( - InterpolatedStringMiddle( - "__", - ), - 112..114, - TokenFlags( - F_STRING, - ), - ), - ( - FStringEnd, - 114..115, - TokenFlags( - F_STRING, - ), - ), - ( - Newline, - 115..116, - ), ] ``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_newline_format_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_newline_format_spec.snap new file mode 100644 index 0000000000..c4db37da4c --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_newline_format_spec.snap @@ -0,0 +1,168 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: "lex_invalid(source, Mode::Module)" +--- +## Tokens +``` +[ + ( + NonLogicalNewline, + 0..1, + ), + ( + TStringStart, + 1..3, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "__", + ), + 3..5, + TokenFlags( + T_STRING, + ), + ), + ( + Lbrace, + 5..6, + ), + ( + NonLogicalNewline, + 6..7, + ), + ( + Name( + Name("x"), + ), + 11..12, + ), + ( + Colon, + 12..13, + ), + ( + Unknown, + 13..14, + ), + ( + NonLogicalNewline, + 14..15, + ), + ( + Rbrace, + 15..16, + ), + ( + Name( + Name("__"), + ), + 16..18, + ), + ( + Unknown, + 18..19, + ), + ( + Newline, + 19..20, + ), + ( + TStringStart, + 20..22, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "__", + ), + 22..24, + TokenFlags( + T_STRING, + ), + ), + ( + Lbrace, + 24..25, + ), + ( + NonLogicalNewline, + 25..26, + ), + ( + Name( + Name("x"), + ), + 30..31, + ), + ( + Colon, + 31..32, + ), + ( + Unknown, + 32..33, + ), + ( + NonLogicalNewline, + 33..34, + ), + ( + Name( + Name("b"), + ), + 42..43, + ), + ( + NonLogicalNewline, + 43..44, + ), + ( + Rbrace, + 44..45, + ), + ( + Name( + Name("__"), + ), + 45..47, + ), + ( + Unknown, + 47..48, + ), + ( + Newline, + 48..49, + ), +] +``` +## Errors +``` +[ + LexicalError { + error: TStringError( + NewlineInFormatSpec, + ), + location: 13..14, + }, + LexicalError { + error: UnclosedStringError, + location: 18..19, + }, + LexicalError { + error: TStringError( + NewlineInFormatSpec, + ), + location: 32..33, + }, + LexicalError { + error: UnclosedStringError, + location: 47..48, + }, +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_multiline_format_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_multiline_format_spec.snap index 1525f2a0eb..a1e48290d1 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_multiline_format_spec.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_multiline_format_spec.snap @@ -139,157 +139,5 @@ expression: lex_source(source) Newline, 67..68, ), - ( - TStringStart, - 68..70, - TokenFlags( - T_STRING, - ), - ), - ( - InterpolatedStringMiddle( - "__", - ), - 70..72, - TokenFlags( - T_STRING, - ), - ), - ( - Lbrace, - 72..73, - ), - ( - NonLogicalNewline, - 73..74, - ), - ( - Name( - Name("x"), - ), - 78..79, - ), - ( - Colon, - 79..80, - ), - ( - InterpolatedStringMiddle( - "d", - ), - 80..81, - TokenFlags( - T_STRING, - ), - ), - ( - NonLogicalNewline, - 81..82, - ), - ( - Rbrace, - 82..83, - ), - ( - InterpolatedStringMiddle( - "__", - ), - 83..85, - TokenFlags( - T_STRING, - ), - ), - ( - TStringEnd, - 85..86, - TokenFlags( - T_STRING, - ), - ), - ( - Newline, - 86..87, - ), - ( - TStringStart, - 87..89, - TokenFlags( - T_STRING, - ), - ), - ( - InterpolatedStringMiddle( - "__", - ), - 89..91, - TokenFlags( - T_STRING, - ), - ), - ( - Lbrace, - 91..92, - ), - ( - NonLogicalNewline, - 92..93, - ), - ( - Name( - Name("x"), - ), - 97..98, - ), - ( - Colon, - 98..99, - ), - ( - InterpolatedStringMiddle( - "a", - ), - 99..100, - TokenFlags( - T_STRING, - ), - ), - ( - NonLogicalNewline, - 100..101, - ), - ( - Name( - Name("b"), - ), - 109..110, - ), - ( - NonLogicalNewline, - 110..111, - ), - ( - Rbrace, - 111..112, - ), - ( - InterpolatedStringMiddle( - "__", - ), - 112..114, - TokenFlags( - T_STRING, - ), - ), - ( - TStringEnd, - 114..115, - TokenFlags( - T_STRING, - ), - ), - ( - Newline, - 115..116, - ), ] ``` diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap index 708cad03c7..4cb5587c2a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap @@ -169,15 +169,7 @@ Module( InterpolatedStringFormatSpec { range: 226..228, node_index: AtomicNodeIndex(..), - elements: [ - Literal( - InterpolatedStringLiteralElement { - range: 226..228, - node_index: AtomicNodeIndex(..), - value: "\\", - }, - ), - ], + elements: [], }, ), }, @@ -385,11 +377,22 @@ Module( 6 | 'format spec'} 7 | 8 | f'middle {'string':\\ - | ^ Syntax Error: f-string: unterminated string + | ^^ Syntax Error: f-string: newlines are not allowed in format specifiers when using single quotes 9 | 'format spec'} | + | + 6 | 'format spec'} + 7 | + 8 | f'middle {'string':\\ + | ^ Syntax Error: f-string: expecting '}' + 9 | 'format spec'} +10 | +11 | f'middle {'string':\\\ + | + + | 8 | f'middle {'string':\\ 9 | 'format spec'} diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unterminated_fstring_newline_recovery.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unterminated_fstring_newline_recovery.py.snap index 7f914852c9..f02e2c45bd 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unterminated_fstring_newline_recovery.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unterminated_fstring_newline_recovery.py.snap @@ -384,7 +384,7 @@ Module( 3 | f"hello {x 4 | 2 + 2 5 | f"hello {x: - | ^ Syntax Error: f-string: unterminated string + | ^ Syntax Error: f-string: newlines are not allowed in format specifiers when using single quotes 6 | 3 + 3 7 | f"hello {x} |