Skip to content

Commit 097e703

Browse files
authored
Consider unterminated f-strings in FStringRanges (#8154)
## Summary This PR removes the `debug_assertion` in the `Indexer` to allow unterminated f-strings. This is mainly a fix in the development build which now matches the release build. The fix is simple: remove the `debug_assertion` which means that the there could be `FStringStart` and possibly `FStringMiddle` tokens without a corresponding f-string range in the `Indexer`. This means that the code requesting for the f-string index need to account for the `None` case, making the code safer. This also updates the code which queries the `FStringRanges` to account for the `None` case. This will happen when the `FStringStart` / `FStringMiddle` tokens are present but the `FStringEnd` token isn't which means that the `Indexer` won't contain the range for that f-string. ## Test Plan `cargo test` Taking the following code as an example: ```python f"{123} ``` This only emits a `FStringStart` token, but no `FStringMiddle` or `FStringEnd` tokens. And, ```python f"\.png${ ``` This emits a `FStringStart` and `FStringMiddle` token, but no `FStringEnd` token. fixes: #8065
1 parent cd8e1ba commit 097e703

File tree

3 files changed

+32
-29
lines changed

3 files changed

+32
-29
lines changed

crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/implicit.rs

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -110,18 +110,27 @@ pub(crate) fn implicit(
110110
{
111111
let (a_range, b_range) = match (a_tok, b_tok) {
112112
(Tok::String { .. }, Tok::String { .. }) => (*a_range, *b_range),
113-
(Tok::String { .. }, Tok::FStringStart) => (
114-
*a_range,
115-
indexer.fstring_ranges().innermost(b_range.start()).unwrap(),
116-
),
117-
(Tok::FStringEnd, Tok::String { .. }) => (
118-
indexer.fstring_ranges().innermost(a_range.start()).unwrap(),
119-
*b_range,
120-
),
121-
(Tok::FStringEnd, Tok::FStringStart) => (
122-
indexer.fstring_ranges().innermost(a_range.start()).unwrap(),
123-
indexer.fstring_ranges().innermost(b_range.start()).unwrap(),
124-
),
113+
(Tok::String { .. }, Tok::FStringStart) => {
114+
match indexer.fstring_ranges().innermost(b_range.start()) {
115+
Some(b_range) => (*a_range, b_range),
116+
None => continue,
117+
}
118+
}
119+
(Tok::FStringEnd, Tok::String { .. }) => {
120+
match indexer.fstring_ranges().innermost(a_range.start()) {
121+
Some(a_range) => (a_range, *b_range),
122+
None => continue,
123+
}
124+
}
125+
(Tok::FStringEnd, Tok::FStringStart) => {
126+
match (
127+
indexer.fstring_ranges().innermost(a_range.start()),
128+
indexer.fstring_ranges().innermost(b_range.start()),
129+
) {
130+
(Some(a_range), Some(b_range)) => (a_range, b_range),
131+
_ => continue,
132+
}
133+
}
125134
_ => continue,
126135
};
127136

crates/ruff_linter/src/rules/pycodestyle/rules/invalid_escape_sequence.rs

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -78,18 +78,21 @@ pub(crate) fn invalid_escape_sequence(
7878
token: &Tok,
7979
token_range: TextRange,
8080
) {
81-
let token_source_code = match token {
81+
let (token_source_code, string_start_location) = match token {
8282
Tok::FStringMiddle { value, is_raw } => {
8383
if *is_raw {
8484
return;
8585
}
86-
value.as_str()
86+
let Some(range) = indexer.fstring_ranges().innermost(token_range.start()) else {
87+
return;
88+
};
89+
(value.as_str(), range.start())
8790
}
8891
Tok::String { kind, .. } => {
8992
if kind.is_raw() {
9093
return;
9194
}
92-
locator.slice(token_range)
95+
(locator.slice(token_range), token_range.start())
9396
}
9497
_ => return,
9598
};
@@ -206,17 +209,6 @@ pub(crate) fn invalid_escape_sequence(
206209
invalid_escape_sequence.push(diagnostic);
207210
}
208211
} else {
209-
let tok_start = if token.is_f_string_middle() {
210-
// SAFETY: If this is a `FStringMiddle` token, then the indexer
211-
// must have the f-string range.
212-
indexer
213-
.fstring_ranges()
214-
.innermost(token_range.start())
215-
.unwrap()
216-
.start()
217-
} else {
218-
token_range.start()
219-
};
220212
// Turn into raw string.
221213
for invalid_escape_char in &invalid_escape_chars {
222214
let diagnostic = Diagnostic::new(
@@ -231,8 +223,8 @@ pub(crate) fn invalid_escape_sequence(
231223
// `assert`, etc.) and the string. For example, `return"foo"` is valid, but
232224
// `returnr"foo"` is not.
233225
Fix::safe_edit(Edit::insertion(
234-
pad_start("r".to_string(), tok_start, locator),
235-
tok_start,
226+
pad_start("r".to_string(), string_start_location, locator),
227+
string_start_location,
236228
)),
237229
);
238230
invalid_escape_sequence.push(diagnostic);

crates/ruff_python_index/src/fstring_ranges.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ use ruff_text_size::{TextRange, TextSize};
55

66
/// Stores the ranges of all f-strings in a file sorted by [`TextRange::start`].
77
/// There can be multiple overlapping ranges for nested f-strings.
8+
///
9+
/// Note that the ranges for all unterminated f-strings are not stored.
810
#[derive(Debug)]
911
pub struct FStringRanges {
12+
// Mapping from the f-string start location to its range.
1013
raw: BTreeMap<TextSize, TextRange>,
1114
}
1215

@@ -89,7 +92,6 @@ impl FStringRangesBuilder {
8992
}
9093

9194
pub(crate) fn finish(self) -> FStringRanges {
92-
debug_assert!(self.start_locations.is_empty());
9395
FStringRanges { raw: self.raw }
9496
}
9597
}

0 commit comments

Comments
 (0)