mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-13 22:06:47 +00:00
[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:
parent
4ab529803f
commit
dcf31c9348
11 changed files with 2223 additions and 3 deletions
|
@ -11,13 +11,15 @@ use ruff_python_ast::{
|
|||
};
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
|
||||
use crate::error::{StarTupleKind, UnparenthesizedNamedExprKind};
|
||||
use crate::error::{FStringKind, StarTupleKind, UnparenthesizedNamedExprKind};
|
||||
use crate::parser::progress::ParserProgress;
|
||||
use crate::parser::{helpers, FunctionKind, Parser};
|
||||
use crate::string::{parse_fstring_literal_element, parse_string_literal, StringType};
|
||||
use crate::token::{TokenKind, TokenValue};
|
||||
use crate::token_set::TokenSet;
|
||||
use crate::{FStringErrorType, Mode, ParseErrorType, UnsupportedSyntaxErrorKind};
|
||||
use crate::{
|
||||
FStringErrorType, Mode, ParseErrorType, UnsupportedSyntaxError, UnsupportedSyntaxErrorKind,
|
||||
};
|
||||
|
||||
use super::{FStringElementsKind, Parenthesized, RecoveryContextKind};
|
||||
|
||||
|
@ -1393,13 +1395,89 @@ impl<'src> Parser<'src> {
|
|||
|
||||
self.expect(TokenKind::FStringEnd);
|
||||
|
||||
// test_ok pep701_f_string_py312
|
||||
// # parse_options: {"target-version": "3.12"}
|
||||
// f'Magic wand: { bag['wand'] }' # nested quotes
|
||||
// f"{'\n'.join(a)}" # escape sequence
|
||||
// f'''A complex trick: {
|
||||
// bag['bag'] # comment
|
||||
// }'''
|
||||
// f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting
|
||||
// f"{f'''{"nested"} inner'''} outer" # nested (triple) quotes
|
||||
// f"test {a \
|
||||
// } more" # line continuation
|
||||
|
||||
// test_ok pep701_f_string_py311
|
||||
// # parse_options: {"target-version": "3.11"}
|
||||
// f"outer {'# not a comment'}"
|
||||
// f'outer {x:{"# not a comment"} }'
|
||||
// f"""{f'''{f'{"# not a comment"}'}'''}"""
|
||||
// f"""{f'''# before expression {f'# aro{f"#{1+1}#"}und #'}'''} # after expression"""
|
||||
// f"escape outside of \t {expr}\n"
|
||||
// f"test\"abcd"
|
||||
|
||||
// test_err pep701_f_string_py311
|
||||
// # parse_options: {"target-version": "3.11"}
|
||||
// f'Magic wand: { bag['wand'] }' # nested quotes
|
||||
// f"{'\n'.join(a)}" # escape sequence
|
||||
// f'''A complex trick: {
|
||||
// bag['bag'] # comment
|
||||
// }'''
|
||||
// f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting
|
||||
// f"{f'''{"nested"} inner'''} outer" # nested (triple) quotes
|
||||
// f"test {a \
|
||||
// } more" # line continuation
|
||||
// f"""{f"""{x}"""}""" # mark the whole triple quote
|
||||
// f"{'\n'.join(['\t', '\v', '\r'])}" # multiple escape sequences, multiple errors
|
||||
|
||||
let range = self.node_range(start);
|
||||
|
||||
if !self.options.target_version.supports_pep_701() {
|
||||
let quote_bytes = flags.quote_str().as_bytes();
|
||||
let quote_len = flags.quote_len();
|
||||
for expr in elements.expressions() {
|
||||
for slash_position in memchr::memchr_iter(b'\\', self.source[expr.range].as_bytes())
|
||||
{
|
||||
let slash_position = TextSize::try_from(slash_position).unwrap();
|
||||
self.add_unsupported_syntax_error(
|
||||
UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::Backslash),
|
||||
TextRange::at(expr.range.start() + slash_position, '\\'.text_len()),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(quote_position) =
|
||||
memchr::memmem::find(self.source[expr.range].as_bytes(), quote_bytes)
|
||||
{
|
||||
let quote_position = TextSize::try_from(quote_position).unwrap();
|
||||
self.add_unsupported_syntax_error(
|
||||
UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::NestedQuote),
|
||||
TextRange::at(expr.range.start() + quote_position, quote_len),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
self.check_fstring_comments(range);
|
||||
}
|
||||
|
||||
ast::FString {
|
||||
elements,
|
||||
range: self.node_range(start),
|
||||
range,
|
||||
flags: ast::FStringFlags::from(flags),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check `range` for comment tokens and report an `UnsupportedSyntaxError` for each one found.
|
||||
fn check_fstring_comments(&mut self, range: TextRange) {
|
||||
self.unsupported_syntax_errors
|
||||
.extend(self.tokens.in_range(range).iter().filter_map(|token| {
|
||||
token.kind().is_comment().then_some(UnsupportedSyntaxError {
|
||||
kind: UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::Comment),
|
||||
range: token.range(),
|
||||
target_version: self.options.target_version,
|
||||
})
|
||||
}));
|
||||
}
|
||||
|
||||
/// Parses a list of f-string elements.
|
||||
///
|
||||
/// # Panics
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue