[syntax-errors] PEP 701 f-strings before Python 3.12 (#16543)

## Summary

This PR detects the use of PEP 701 f-strings before 3.12. This one
sounded difficult and ended up being pretty easy, so I think there's a
good chance I've over-simplified things. However, from experimenting in
the Python REPL and checking with [pyright], I think this is correct.
pyright actually doesn't even flag the comment case, but Python does.

I also checked pyright's implementation for
[quotes](98dc4469cc/packages/pyright-internal/src/analyzer/checker.ts (L1379-L1398))
and
[escapes](98dc4469cc/packages/pyright-internal/src/analyzer/checker.ts (L1365-L1377))
and think I've approximated how they do it.

Python's error messages also point to the simple approach of these
characters simply not being allowed:

```pycon
Python 3.11.11 (main, Feb 12 2025, 14:51:05) [Clang 19.1.6 ] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> f'''multiline {
... expression # comment
... }'''
  File "<stdin>", line 3
    }'''
        ^
SyntaxError: f-string expression part cannot include '#'
>>> f'''{not a line \
... continuation}'''
  File "<stdin>", line 2
    continuation}'''
                    ^
SyntaxError: f-string expression part cannot include a backslash
>>> f'hello {'world'}'
  File "<stdin>", line 1
    f'hello {'world'}'
              ^^^^^
SyntaxError: f-string: expecting '}'
```

And since escapes aren't allowed, I don't think there are any tricky
cases where nested quotes or comments can sneak in.

It's also slightly annoying that the error is repeated for every nested
quote character, but that also mirrors pyright, although they highlight
the whole nested string, which is a little nicer. However, their check
is in the analysis phase, so I don't think we have such easy access to
the quoted range, at least without adding another mini visitor.

## Test Plan

New inline tests

[pyright]:
https://pyright-play.net/?pythonVersion=3.11&strict=true&code=EYQw5gBAvBAmCWBjALgCgO4gHaygRgEoAoEaCAIgBpyiiBiCLAUwGdknYIBHAVwHt2LIgDMA5AFlwSCJhwAuCAG8IoMAG1Rs2KIC6EAL6iIxosbPmLlq5foRWiEAAcmERAAsQAJxAomnltY2wuSKogA6WKIAdABWfPBYqCAE%2BuSBVqbpWVm2iHwAtvlMWMgB2ekiolUAgq4FjgA2TAAeEMieSADWCsoV5qoaqrrGDJ5MiDz%2B8ABuLqosAIREhlXlaybrmyYMXsDw7V4AnoysyAmQ5SIhwYo3d9cheADUeKlv5O%2BpQA
This commit is contained in:
Brent Westbrook 2025-03-18 11:12:15 -04:00 committed by GitHub
parent 4ab529803f
commit dcf31c9348
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 2223 additions and 3 deletions

View file

@ -452,6 +452,14 @@ pub enum StarTupleKind {
Yield,
}
/// The type of PEP 701 f-string error for [`UnsupportedSyntaxErrorKind::Pep701FString`].
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum FStringKind {
Backslash,
Comment,
NestedQuote,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum UnparenthesizedNamedExprKind {
SequenceIndex,
@ -661,6 +669,34 @@ pub enum UnsupportedSyntaxErrorKind {
TypeAliasStatement,
TypeParamDefault,
/// Represents the use of a [PEP 701] f-string before Python 3.12.
///
/// ## Examples
///
/// As described in the PEP, each of these cases were invalid before Python 3.12:
///
/// ```python
/// # nested quotes
/// f'Magic wand: { bag['wand'] }'
///
/// # escape characters
/// f"{'\n'.join(a)}"
///
/// # comments
/// f'''A complex trick: {
/// bag['bag'] # recursive bags!
/// }'''
///
/// # arbitrary nesting
/// f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}"
/// ```
///
/// These restrictions were lifted in Python 3.12, meaning that all of these examples are now
/// valid.
///
/// [PEP 701]: https://peps.python.org/pep-0701/
Pep701FString(FStringKind),
/// Represents the use of a parenthesized `with` item before Python 3.9.
///
/// ## Examples
@ -838,6 +874,15 @@ impl Display for UnsupportedSyntaxError {
UnsupportedSyntaxErrorKind::TypeParamDefault => {
"Cannot set default type for a type parameter"
}
UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::Backslash) => {
"Cannot use an escape sequence (backslash) in f-strings"
}
UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::Comment) => {
"Cannot use comments in f-strings"
}
UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::NestedQuote) => {
"Cannot reuse outer quote character in f-strings"
}
UnsupportedSyntaxErrorKind::ParenthesizedContextManager => {
"Cannot use parentheses within a `with` statement"
}
@ -904,6 +949,7 @@ impl UnsupportedSyntaxErrorKind {
UnsupportedSyntaxErrorKind::TypeParameterList => Change::Added(PythonVersion::PY312),
UnsupportedSyntaxErrorKind::TypeAliasStatement => Change::Added(PythonVersion::PY312),
UnsupportedSyntaxErrorKind::TypeParamDefault => Change::Added(PythonVersion::PY313),
UnsupportedSyntaxErrorKind::Pep701FString(_) => Change::Added(PythonVersion::PY312),
UnsupportedSyntaxErrorKind::ParenthesizedContextManager => {
Change::Added(PythonVersion::PY39)
}