Skip to content

Commit 00e7d1f

Browse files
authored
[pycodestyle] Handle brace escapes for t-strings in logical lines (#19358)
Tracks both f and t-strings in the logical line rules for `pycodestyle`. Progress towards #15506
1 parent f4d0273 commit 00e7d1f

File tree

8 files changed

+145
-14
lines changed

8 files changed

+145
-14
lines changed

crates/ruff_linter/resources/test/fixtures/pycodestyle/E20.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,3 +189,18 @@
189189
#: Okay: https://github.com/astral-sh/ruff/issues/12023
190190
f"{x = :.2f}"
191191
f"{(x) = :.2f}"
192+
193+
# t-strings
194+
t"{ {'a': 1} }"
195+
t"{[ { {'a': 1} } ]}"
196+
t"normal { {t"{ { [1, 2] } }" } } normal"
197+
198+
t"{x = :.2f}"
199+
t"{(x) = :.2f}"
200+
201+
#: Okay
202+
t"{ham[lower +1 :, "columnname"]}"
203+
204+
#: E203:1:13
205+
t"{ham[lower + 1 :, "columnname"]}"
206+

crates/ruff_linter/resources/test/fixtures/pycodestyle/E23.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,20 @@ class PEP696GoodWithEmptyBases[A: object="foo"[::-1], B: object =[[["foo", "bar"
142142

143143
class PEP696GoodWithNonEmptyBases[A: object="foo"[::-1], B: object =[[["foo", "bar"]]], C: object= bytes](object, something_dynamic[x::-1]):
144144
pass
145+
146+
# E231
147+
t"{(a,b)}"
148+
149+
# Okay because it's hard to differentiate between the usages of a colon in a t-string
150+
t"{a:=1}"
151+
t"{ {'a':1} }"
152+
t"{a:.3f}"
153+
t"{(a:=1)}"
154+
t"{(lambda x:x)}"
155+
t"normal{t"{a:.3f}"}normal"
156+
157+
#: Okay
158+
snapshot.file_uri[len(t's3://{self.s3_bucket_name}/'):]
159+
160+
#: E231
161+
{len(t's3://{self.s3_bucket_name}/'):1}

crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/extraneous_whitespace.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,16 +126,18 @@ impl AlwaysFixableViolation for WhitespaceBeforePunctuation {
126126

127127
/// E201, E202, E203
128128
pub(crate) fn extraneous_whitespace(line: &LogicalLine, context: &LintContext) {
129-
let mut fstrings = 0u32;
129+
let mut interpolated_strings = 0u32;
130130
let mut brackets = vec![];
131131
let mut prev_token = None;
132132
let mut iter = line.tokens().iter().peekable();
133133

134134
while let Some(token) = iter.next() {
135135
let kind = token.kind();
136136
match kind {
137-
TokenKind::FStringStart => fstrings += 1,
138-
TokenKind::FStringEnd => fstrings = fstrings.saturating_sub(1),
137+
TokenKind::FStringStart | TokenKind::TStringStart => interpolated_strings += 1,
138+
TokenKind::FStringEnd | TokenKind::TStringEnd => {
139+
interpolated_strings = interpolated_strings.saturating_sub(1);
140+
}
139141
TokenKind::Lsqb => {
140142
brackets.push(kind);
141143
}
@@ -161,7 +163,9 @@ pub(crate) fn extraneous_whitespace(line: &LogicalLine, context: &LintContext) {
161163
// Here, `{{` / `}} would be interpreted as a single raw `{` / `}`
162164
// character.
163165
match symbol {
164-
BracketOrPunctuation::OpenBracket(symbol) if symbol != '{' || fstrings == 0 => {
166+
BracketOrPunctuation::OpenBracket(symbol)
167+
if symbol != '{' || interpolated_strings == 0 =>
168+
{
165169
let (trailing, trailing_len) = line.trailing_whitespace(token);
166170
if !matches!(trailing, Whitespace::None) {
167171
if let Some(mut diagnostic) = context.report_diagnostic_if_enabled(
@@ -173,7 +177,9 @@ pub(crate) fn extraneous_whitespace(line: &LogicalLine, context: &LintContext) {
173177
}
174178
}
175179
}
176-
BracketOrPunctuation::CloseBracket(symbol) if symbol != '}' || fstrings == 0 => {
180+
BracketOrPunctuation::CloseBracket(symbol)
181+
if symbol != '}' || interpolated_strings == 0 =>
182+
{
177183
if !matches!(prev_token, Some(TokenKind::Comma)) {
178184
if let (Whitespace::Single | Whitespace::Many | Whitespace::Tab, offset) =
179185
line.leading_whitespace(token)
@@ -286,7 +292,7 @@ pub(crate) fn extraneous_whitespace(line: &LogicalLine, context: &LintContext) {
286292
}
287293
}
288294
} else {
289-
if fstrings > 0
295+
if interpolated_strings > 0
290296
&& symbol == ':'
291297
&& matches!(prev_token, Some(TokenKind::Equal))
292298
{

crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace.rs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ impl AlwaysFixableViolation for MissingWhitespace {
4141

4242
/// E231
4343
pub(crate) fn missing_whitespace(line: &LogicalLine, context: &LintContext) {
44-
let mut fstrings = 0u32;
44+
let mut interpolated_strings = 0u32;
4545
let mut definition_state = DefinitionState::from_tokens(line.tokens());
4646
let mut brackets = Vec::new();
4747
let mut iter = line.tokens().iter().peekable();
@@ -50,21 +50,23 @@ pub(crate) fn missing_whitespace(line: &LogicalLine, context: &LintContext) {
5050
let kind = token.kind();
5151
definition_state.visit_token_kind(kind);
5252
match kind {
53-
TokenKind::FStringStart => fstrings += 1,
54-
TokenKind::FStringEnd => fstrings = fstrings.saturating_sub(1),
55-
TokenKind::Lsqb if fstrings == 0 => {
53+
TokenKind::FStringStart | TokenKind::TStringStart => interpolated_strings += 1,
54+
TokenKind::FStringEnd | TokenKind::TStringEnd => {
55+
interpolated_strings = interpolated_strings.saturating_sub(1);
56+
}
57+
TokenKind::Lsqb if interpolated_strings == 0 => {
5658
brackets.push(kind);
5759
}
58-
TokenKind::Rsqb if fstrings == 0 => {
60+
TokenKind::Rsqb if interpolated_strings == 0 => {
5961
brackets.pop();
6062
}
61-
TokenKind::Lbrace if fstrings == 0 => {
63+
TokenKind::Lbrace if interpolated_strings == 0 => {
6264
brackets.push(kind);
6365
}
64-
TokenKind::Rbrace if fstrings == 0 => {
66+
TokenKind::Rbrace if interpolated_strings == 0 => {
6567
brackets.pop();
6668
}
67-
TokenKind::Colon if fstrings > 0 => {
69+
TokenKind::Colon if interpolated_strings > 0 => {
6870
// Colon in f-string, no space required. This will yield false
6971
// negatives for cases like the following as it's hard to
7072
// differentiate between the usage of a colon in a f-string.

crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E201_E20.py.snap

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,23 @@ E20.py:145:5: E201 [*] Whitespace after '['
183183
146 146 |
184184
147 147 | #: Okay
185185
148 148 | ham[lower + offset :: upper + offset]
186+
187+
E20.py:195:5: E201 [*] Whitespace after '['
188+
|
189+
193 | # t-strings
190+
194 | t"{ {'a': 1} }"
191+
195 | t"{[ { {'a': 1} } ]}"
192+
| ^ E201
193+
196 | t"normal { {t"{ { [1, 2] } }" } } normal"
194+
|
195+
= help: Remove whitespace before '['
196+
197+
Safe fix
198+
192 192 |
199+
193 193 | # t-strings
200+
194 194 | t"{ {'a': 1} }"
201+
195 |-t"{[ { {'a': 1} } ]}"
202+
195 |+t"{[{ {'a': 1} } ]}"
203+
196 196 | t"normal { {t"{ { [1, 2] } }" } } normal"
204+
197 197 |
205+
198 198 | t"{x = :.2f}"

crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E202_E20.py.snap

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,23 @@ E20.py:172:12: E202 [*] Whitespace before ']'
165165
173 173 |
166166
174 174 | #: E203:1:10
167167
175 175 | ham[upper :]
168+
169+
E20.py:195:18: E202 [*] Whitespace before ']'
170+
|
171+
193 | # t-strings
172+
194 | t"{ {'a': 1} }"
173+
195 | t"{[ { {'a': 1} } ]}"
174+
| ^ E202
175+
196 | t"normal { {t"{ { [1, 2] } }" } } normal"
176+
|
177+
= help: Remove whitespace before ']'
178+
179+
Safe fix
180+
192 192 |
181+
193 193 | # t-strings
182+
194 194 | t"{ {'a': 1} }"
183+
195 |-t"{[ { {'a': 1} } ]}"
184+
195 |+t"{[ { {'a': 1} }]}"
185+
196 196 | t"normal { {t"{ { [1, 2] } }" } } normal"
186+
197 197 |
187+
198 198 | t"{x = :.2f}"

crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E203_E20.py.snap

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,3 +345,19 @@ E20.py:187:17: E203 [*] Whitespace before ':'
345345
188 188 |
346346
189 189 | #: Okay: https://github.com/astral-sh/ruff/issues/12023
347347
190 190 | f"{x = :.2f}"
348+
349+
E20.py:205:17: E203 [*] Whitespace before ':'
350+
|
351+
204 | #: E203:1:13
352+
205 | t"{ham[lower + 1 :, "columnname"]}"
353+
| ^^ E203
354+
|
355+
= help: Remove whitespace before ':'
356+
357+
Safe fix
358+
202 202 | t"{ham[lower +1 :, "columnname"]}"
359+
203 203 |
360+
204 204 | #: E203:1:13
361+
205 |-t"{ham[lower + 1 :, "columnname"]}"
362+
205 |+t"{ham[lower + 1:, "columnname"]}"
363+
206 206 |

crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E231_E23.py.snap

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,3 +905,38 @@ E23.py:126:99: E231 [*] Missing whitespace after ':'
905905
127 127 | pass
906906
128 128 |
907907
129 129 | # Should be no E231 errors on any of these:
908+
909+
E23.py:147:6: E231 [*] Missing whitespace after ','
910+
|
911+
146 | # E231
912+
147 | t"{(a,b)}"
913+
| ^ E231
914+
148 |
915+
149 | # Okay because it's hard to differentiate between the usages of a colon in a t-string
916+
|
917+
= help: Add missing whitespace
918+
919+
Safe fix
920+
144 144 | pass
921+
145 145 |
922+
146 146 | # E231
923+
147 |-t"{(a,b)}"
924+
147 |+t"{(a, b)}"
925+
148 148 |
926+
149 149 | # Okay because it's hard to differentiate between the usages of a colon in a t-string
927+
150 150 | t"{a:=1}"
928+
929+
E23.py:161:37: E231 [*] Missing whitespace after ':'
930+
|
931+
160 | #: E231
932+
161 | {len(t's3://{self.s3_bucket_name}/'):1}
933+
| ^ E231
934+
|
935+
= help: Add missing whitespace
936+
937+
Safe fix
938+
158 158 | snapshot.file_uri[len(t's3://{self.s3_bucket_name}/'):]
939+
159 159 |
940+
160 160 | #: E231
941+
161 |-{len(t's3://{self.s3_bucket_name}/'):1}
942+
161 |+{len(t's3://{self.s3_bucket_name}/'): 1}

0 commit comments

Comments
 (0)