Add syntax error when conversion flag does not immediately follow exclamation mark (#18706)

Closes #18671

Note that while this has, I believe, always been invalid syntax, it was
reported as a different syntax error until Python 3.12:

Python 3.11:

```pycon
>>> x = 1
>>> f"{x! s}"
  File "<stdin>", line 1
    f"{x! s}"
             ^
SyntaxError: f-string: invalid conversion character: expected 's', 'r', or 'a'
```

Python 3.12:

```pycon
>>> x = 1
>>> f"{x! s}"
  File "<stdin>", line 1
    f"{x! s}"
        ^^^
SyntaxError: f-string: conversion type must come right after the exclamanation mark
```
This commit is contained in:
Dylan 2025-06-16 11:44:42 -05:00 committed by GitHub
parent a842899862
commit c5b58187da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 219 additions and 38 deletions

View file

@ -267,17 +267,13 @@ aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {
# Conversion flags # Conversion flags
# #
# This is not a valid Python code because of the additional whitespace between the `!`
# and conversion type. But, our parser isn't strict about this. This should probably be
# removed once we have a strict parser.
x = f"aaaaaaaaa { x ! r }"
# Even in the case of debug expressions, we only need to preserve the whitespace within # Even in the case of debug expressions, we only need to preserve the whitespace within
# the expression part of the replacement field. # the expression part of the replacement field.
x = f"aaaaaaaaa { x = ! r }" x = f"aaaaaaaaa { x = !r }"
# Combine conversion flags with format specifiers # Combine conversion flags with format specifiers
x = f"{x = ! s x = f"{x = !s
:>0 :>0
}" }"

View file

@ -265,17 +265,13 @@ aaaaaaaaaaa = t"""asaaaaaaaaaaaaaaaa {
# Conversion flags # Conversion flags
# #
# This is not a valid Python code because of the additional whitespace between the `!`
# and conversion type. But, our parser isn't strict about this. This should probably be
# removed once we have a strict parser.
x = t"aaaaaaaaa { x ! r }"
# Even in the case of debug expressions, we only need to preserve the whitespace within # Even in the case of debug expressions, we only need to preserve the whitespace within
# the expression part of the replacement field. # the expression part of the replacement field.
x = t"aaaaaaaaa { x = ! r }" x = t"aaaaaaaaa { x = !r }"
# Combine conversion flags with format specifiers # Combine conversion flags with format specifiers
x = t"{x = ! s x = t"{x = !s
:>0 :>0
}" }"

View file

@ -273,17 +273,13 @@ aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {
# Conversion flags # Conversion flags
# #
# This is not a valid Python code because of the additional whitespace between the `!`
# and conversion type. But, our parser isn't strict about this. This should probably be
# removed once we have a strict parser.
x = f"aaaaaaaaa { x ! r }"
# Even in the case of debug expressions, we only need to preserve the whitespace within # Even in the case of debug expressions, we only need to preserve the whitespace within
# the expression part of the replacement field. # the expression part of the replacement field.
x = f"aaaaaaaaa { x = ! r }" x = f"aaaaaaaaa { x = !r }"
# Combine conversion flags with format specifiers # Combine conversion flags with format specifiers
x = f"{x = ! s x = f"{x = !s
:>0 :>0
}" }"
@ -1036,10 +1032,6 @@ aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {aaaaaaaaaaaa + bbbbbbbbbbbb + cccccccccccc
# Conversion flags # Conversion flags
# #
# This is not a valid Python code because of the additional whitespace between the `!`
# and conversion type. But, our parser isn't strict about this. This should probably be
# removed once we have a strict parser.
x = f"aaaaaaaaa {x!r}"
# Even in the case of debug expressions, we only need to preserve the whitespace within # Even in the case of debug expressions, we only need to preserve the whitespace within
# the expression part of the replacement field. # the expression part of the replacement field.
@ -1836,10 +1828,6 @@ aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {aaaaaaaaaaaa + bbbbbbbbbbbb + cccccccccccc
# Conversion flags # Conversion flags
# #
# This is not a valid Python code because of the additional whitespace between the `!`
# and conversion type. But, our parser isn't strict about this. This should probably be
# removed once we have a strict parser.
x = f"aaaaaaaaa {x!r}"
# Even in the case of debug expressions, we only need to preserve the whitespace within # Even in the case of debug expressions, we only need to preserve the whitespace within
# the expression part of the replacement field. # the expression part of the replacement field.

View file

@ -271,17 +271,13 @@ aaaaaaaaaaa = t"""asaaaaaaaaaaaaaaaa {
# Conversion flags # Conversion flags
# #
# This is not a valid Python code because of the additional whitespace between the `!`
# and conversion type. But, our parser isn't strict about this. This should probably be
# removed once we have a strict parser.
x = t"aaaaaaaaa { x ! r }"
# Even in the case of debug expressions, we only need to preserve the whitespace within # Even in the case of debug expressions, we only need to preserve the whitespace within
# the expression part of the replacement field. # the expression part of the replacement field.
x = t"aaaaaaaaa { x = ! r }" x = t"aaaaaaaaa { x = !r }"
# Combine conversion flags with format specifiers # Combine conversion flags with format specifiers
x = t"{x = ! s x = t"{x = !s
:>0 :>0
}" }"
@ -1032,10 +1028,6 @@ aaaaaaaaaaa = t"""asaaaaaaaaaaaaaaaa {aaaaaaaaaaaa + bbbbbbbbbbbb + cccccccccccc
# Conversion flags # Conversion flags
# #
# This is not a valid Python code because of the additional whitespace between the `!`
# and conversion type. But, our parser isn't strict about this. This should probably be
# removed once we have a strict parser.
x = t"aaaaaaaaa {x!r}"
# Even in the case of debug expressions, we only need to preserve the whitespace within # Even in the case of debug expressions, we only need to preserve the whitespace within
# the expression part of the replacement field. # the expression part of the replacement field.

View file

@ -0,0 +1,3 @@
f"{x! s}"
t"{x! s}"
f"{x! z}"

View file

@ -63,13 +63,16 @@ pub enum InterpolatedStringErrorType {
UnterminatedTripleQuotedString, UnterminatedTripleQuotedString,
/// A lambda expression without parentheses was encountered. /// A lambda expression without parentheses was encountered.
LambdaWithoutParentheses, LambdaWithoutParentheses,
/// Conversion flag does not immediately follow exclamation.
ConversionFlagNotImmediatelyAfterExclamation,
} }
impl std::fmt::Display for InterpolatedStringErrorType { impl std::fmt::Display for InterpolatedStringErrorType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
use InterpolatedStringErrorType::{ use InterpolatedStringErrorType::{
InvalidConversionFlag, LambdaWithoutParentheses, SingleRbrace, UnclosedLbrace, ConversionFlagNotImmediatelyAfterExclamation, InvalidConversionFlag,
UnterminatedString, UnterminatedTripleQuotedString, LambdaWithoutParentheses, SingleRbrace, UnclosedLbrace, UnterminatedString,
UnterminatedTripleQuotedString,
}; };
match self { match self {
UnclosedLbrace => write!(f, "expecting '}}'"), UnclosedLbrace => write!(f, "expecting '}}'"),
@ -80,6 +83,10 @@ impl std::fmt::Display for InterpolatedStringErrorType {
LambdaWithoutParentheses => { LambdaWithoutParentheses => {
write!(f, "lambda expressions are not allowed without parentheses") write!(f, "lambda expressions are not allowed without parentheses")
} }
ConversionFlagNotImmediatelyAfterExclamation => write!(
f,
"conversion type must come right after the exclamation mark"
),
} }
} }
} }

View file

@ -1771,6 +1771,19 @@ impl<'src> Parser<'src> {
let conversion = if self.eat(TokenKind::Exclamation) { let conversion = if self.eat(TokenKind::Exclamation) {
let conversion_flag_range = self.current_token_range(); let conversion_flag_range = self.current_token_range();
if self.at(TokenKind::Name) { if self.at(TokenKind::Name) {
// test_err f_string_conversion_follows_exclamation
// f"{x! s}"
// t"{x! s}"
// f"{x! z}"
if self.prev_token_end != conversion_flag_range.start() {
self.add_error(
ParseErrorType::from_interpolated_string_error(
InterpolatedStringErrorType::ConversionFlagNotImmediatelyAfterExclamation,
string_kind,
),
TextRange::new(self.prev_token_end, conversion_flag_range.start()),
);
}
let TokenValue::Name(name) = self.bump_value(TokenKind::Name) else { let TokenValue::Name(name) = self.bump_value(TokenKind::Name) else {
unreachable!(); unreachable!();
}; };

View file

@ -0,0 +1,186 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/f_string_conversion_follows_exclamation.py
---
## AST
```
Module(
ModModule {
node_index: AtomicNodeIndex(..),
range: 0..30,
body: [
Expr(
StmtExpr {
node_index: AtomicNodeIndex(..),
range: 0..9,
value: FString(
ExprFString {
node_index: AtomicNodeIndex(..),
range: 0..9,
value: FStringValue {
inner: Single(
FString(
FString {
range: 0..9,
node_index: AtomicNodeIndex(..),
elements: [
Interpolation(
InterpolatedElement {
range: 2..8,
node_index: AtomicNodeIndex(..),
expression: Name(
ExprName {
node_index: AtomicNodeIndex(..),
range: 3..4,
id: Name("x"),
ctx: Load,
},
),
debug_text: None,
conversion: Str,
format_spec: None,
},
),
],
flags: FStringFlags {
quote_style: Double,
prefix: Regular,
triple_quoted: false,
},
},
),
),
},
},
),
},
),
Expr(
StmtExpr {
node_index: AtomicNodeIndex(..),
range: 10..19,
value: TString(
ExprTString {
node_index: AtomicNodeIndex(..),
range: 10..19,
value: TStringValue {
inner: Single(
TString(
TString {
range: 10..19,
node_index: AtomicNodeIndex(..),
elements: [
Interpolation(
InterpolatedElement {
range: 12..18,
node_index: AtomicNodeIndex(..),
expression: Name(
ExprName {
node_index: AtomicNodeIndex(..),
range: 13..14,
id: Name("x"),
ctx: Load,
},
),
debug_text: None,
conversion: Str,
format_spec: None,
},
),
],
flags: TStringFlags {
quote_style: Double,
prefix: Regular,
triple_quoted: false,
},
},
),
),
},
},
),
},
),
Expr(
StmtExpr {
node_index: AtomicNodeIndex(..),
range: 20..29,
value: FString(
ExprFString {
node_index: AtomicNodeIndex(..),
range: 20..29,
value: FStringValue {
inner: Single(
FString(
FString {
range: 20..29,
node_index: AtomicNodeIndex(..),
elements: [
Interpolation(
InterpolatedElement {
range: 22..28,
node_index: AtomicNodeIndex(..),
expression: Name(
ExprName {
node_index: AtomicNodeIndex(..),
range: 23..24,
id: Name("x"),
ctx: Load,
},
),
debug_text: None,
conversion: None,
format_spec: None,
},
),
],
flags: FStringFlags {
quote_style: Double,
prefix: Regular,
triple_quoted: false,
},
},
),
),
},
},
),
},
),
],
},
)
```
## Errors
|
1 | f"{x! s}"
| ^ Syntax Error: f-string: conversion type must come right after the exclamation mark
2 | t"{x! s}"
3 | f"{x! z}"
|
|
1 | f"{x! s}"
2 | t"{x! s}"
| ^ Syntax Error: t-string: conversion type must come right after the exclamation mark
3 | f"{x! z}"
|
|
1 | f"{x! s}"
2 | t"{x! s}"
3 | f"{x! z}"
| ^ Syntax Error: f-string: conversion type must come right after the exclamation mark
|
|
1 | f"{x! s}"
2 | t"{x! s}"
3 | f"{x! z}"
| ^ Syntax Error: f-string: invalid conversion character
|