Ignore trailing quotes for unclosed l-brace errors (#9388)

## Summary

Given:

```python
F"{"ڤ
```

We try to locate the "unclosed left brace" error by subtracting the
quote size from the lexer offset -- so we subtract 1 from the end of the
source, which puts us in the middle of a Unicode character. I don't
think we should try to adjust the offset in this way, since there can be
content _after_ the quote. For example, with the advent of PEP 701, this
string could reasonably be fixed as:

```python
F"{"ڤ"}"
````

Closes https://github.com/astral-sh/ruff/issues/9379.
This commit is contained in:
Charlie Marsh 2024-01-04 01:00:55 -04:00 committed by GitHub
parent 9a14f403c8
commit f0d43dafcf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 35 additions and 21 deletions

View file

@ -700,7 +700,7 @@ impl<'source> Lexer<'source> {
}
Some('\r' | '\n') if !triple_quoted => {
if let Some(fstring) = self.fstrings.current() {
// When we are in an f-string, check whether does the initial quote
// When we are in an f-string, check whether the initial quote
// matches with f-strings quotes and if it is, then this must be a
// missing '}' token so raise the proper error.
if fstring.quote_char() == quote && !fstring.is_triple_quoted() {
@ -708,7 +708,7 @@ impl<'source> Lexer<'source> {
error: LexicalErrorType::FStringError(
FStringErrorType::UnclosedLbrace,
),
location: self.offset() - fstring.quote_size(),
location: self.offset() - TextSize::new(1),
});
}
}
@ -732,7 +732,7 @@ impl<'source> Lexer<'source> {
Some(_) => {}
None => {
if let Some(fstring) = self.fstrings.current() {
// When we are in an f-string, check whether does the initial quote
// When we are in an f-string, check whether the initial quote
// matches with f-strings quotes and if it is, then this must be a
// missing '}' token so raise the proper error.
if fstring.quote_char() == quote
@ -742,7 +742,7 @@ impl<'source> Lexer<'source> {
error: LexicalErrorType::FStringError(
FStringErrorType::UnclosedLbrace,
),
location: self.offset() - fstring.quote_size(),
location: self.offset(),
});
}
}
@ -2195,13 +2195,17 @@ f"{(lambda x:{x})}"
assert_debug_snapshot!(lex_jupyter_source(source));
}
fn lex_error(source: &str) -> LexicalError {
match lex(source, Mode::Module).find_map(Result::err) {
Some(err) => err,
_ => panic!("Expected at least one error"),
}
}
fn lex_fstring_error(source: &str) -> FStringErrorType {
match lex(source, Mode::Module).find_map(std::result::Result::err) {
Some(err) => match err.error {
LexicalErrorType::FStringError(error) => error,
_ => panic!("Expected FStringError: {err:?}"),
},
_ => panic!("Expected atleast one FStringError"),
match lex_error(source).error {
LexicalErrorType::FStringError(error) => error,
err => panic!("Expected FStringError: {err:?}"),
}
}
@ -2246,4 +2250,25 @@ f"{(lambda x:{x})}"
UnterminatedTripleQuotedString
);
}
#[test]
fn test_fstring_error_location() {
assert_debug_snapshot!(lex_error("f'{'"), @r###"
LexicalError {
error: FStringError(
UnclosedLbrace,
),
location: 4,
}
"###);
assert_debug_snapshot!(lex_error("f'{'α"), @r###"
LexicalError {
error: FStringError(
UnclosedLbrace,
),
location: 6,
}
"###);
}
}

View file

@ -1,7 +1,5 @@
use bitflags::bitflags;
use ruff_text_size::TextSize;
bitflags! {
#[derive(Debug)]
pub(crate) struct FStringContextFlags: u8 {
@ -58,15 +56,6 @@ impl FStringContext {
}
}
/// Returns the number of quotes for the current f-string.
pub(crate) const fn quote_size(&self) -> TextSize {
if self.is_triple_quoted() {
TextSize::new(3)
} else {
TextSize::new(1)
}
}
/// Returns the triple quotes for the current f-string if it is a triple-quoted
/// f-string, `None` otherwise.
pub(crate) const fn triple_quotes(&self) -> Option<&'static str> {