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 => { Some('\r' | '\n') if !triple_quoted => {
if let Some(fstring) = self.fstrings.current() { 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 // matches with f-strings quotes and if it is, then this must be a
// missing '}' token so raise the proper error. // missing '}' token so raise the proper error.
if fstring.quote_char() == quote && !fstring.is_triple_quoted() { if fstring.quote_char() == quote && !fstring.is_triple_quoted() {
@ -708,7 +708,7 @@ impl<'source> Lexer<'source> {
error: LexicalErrorType::FStringError( error: LexicalErrorType::FStringError(
FStringErrorType::UnclosedLbrace, FStringErrorType::UnclosedLbrace,
), ),
location: self.offset() - fstring.quote_size(), location: self.offset() - TextSize::new(1),
}); });
} }
} }
@ -732,7 +732,7 @@ impl<'source> Lexer<'source> {
Some(_) => {} Some(_) => {}
None => { None => {
if let Some(fstring) = self.fstrings.current() { 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 // matches with f-strings quotes and if it is, then this must be a
// missing '}' token so raise the proper error. // missing '}' token so raise the proper error.
if fstring.quote_char() == quote if fstring.quote_char() == quote
@ -742,7 +742,7 @@ impl<'source> Lexer<'source> {
error: LexicalErrorType::FStringError( error: LexicalErrorType::FStringError(
FStringErrorType::UnclosedLbrace, 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)); 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 { fn lex_fstring_error(source: &str) -> FStringErrorType {
match lex(source, Mode::Module).find_map(std::result::Result::err) { match lex_error(source).error {
Some(err) => match err.error { LexicalErrorType::FStringError(error) => error,
LexicalErrorType::FStringError(error) => error, err => panic!("Expected FStringError: {err:?}"),
_ => panic!("Expected FStringError: {err:?}"),
},
_ => panic!("Expected atleast one FStringError"),
} }
} }
@ -2246,4 +2250,25 @@ f"{(lambda x:{x})}"
UnterminatedTripleQuotedString 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 bitflags::bitflags;
use ruff_text_size::TextSize;
bitflags! { bitflags! {
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct FStringContextFlags: u8 { 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 /// Returns the triple quotes for the current f-string if it is a triple-quoted
/// f-string, `None` otherwise. /// f-string, `None` otherwise.
pub(crate) const fn triple_quotes(&self) -> Option<&'static str> { pub(crate) const fn triple_quotes(&self) -> Option<&'static str> {