Skip to content

Commit a31a314

Browse files
Account for possibly-empty f-string values in truthiness logic (#9484)
Closes #9479.
1 parent f9dd7bb commit a31a314

File tree

5 files changed

+155
-17
lines changed

5 files changed

+155
-17
lines changed

crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM222.py

+7
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,10 @@ def secondToTime(s0: int) -> (int, int, int) or str:
160160

161161
def secondToTime(s0: int) -> ((int, int, int) or str):
162162
m, s = divmod(s0, 60)
163+
164+
165+
# Regression test for: https://github.com/astral-sh/ruff/issues/9479
166+
print(f"{a}{b}" or "bar")
167+
print(f"{a}{''}" or "bar")
168+
print(f"{''}{''}" or "bar")
169+
print(f"{1}{''}" or "bar")

crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM223.py

+6
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,9 @@
147147

148148
if f(a and [] and False and []): # SIM223
149149
pass
150+
151+
# Regression test for: https://github.com/astral-sh/ruff/issues/9479
152+
print(f"{a}{b}" and "bar")
153+
print(f"{a}{''}" and "bar")
154+
print(f"{''}{''}" and "bar")
155+
print(f"{1}{''}" and "bar")

crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM222_SIM222.py.snap

+20
Original file line numberDiff line numberDiff line change
@@ -1040,5 +1040,25 @@ SIM222.py:161:31: SIM222 [*] Use `(int, int, int)` instead of `(int, int, int) o
10401040
161 |-def secondToTime(s0: int) -> ((int, int, int) or str):
10411041
161 |+def secondToTime(s0: int) -> ((int, int, int)):
10421042
162 162 | m, s = divmod(s0, 60)
1043+
163 163 |
1044+
164 164 |
1045+
1046+
SIM222.py:168:7: SIM222 [*] Use `"bar"` instead of `... or "bar"`
1047+
|
1048+
166 | print(f"{a}{b}" or "bar")
1049+
167 | print(f"{a}{''}" or "bar")
1050+
168 | print(f"{''}{''}" or "bar")
1051+
| ^^^^^^^^^^^^^^^^^^^^ SIM222
1052+
169 | print(f"{1}{''}" or "bar")
1053+
|
1054+
= help: Replace with `"bar"`
1055+
1056+
Unsafe fix
1057+
165 165 | # Regression test for: https://github.com/astral-sh/ruff/issues/9479
1058+
166 166 | print(f"{a}{b}" or "bar")
1059+
167 167 | print(f"{a}{''}" or "bar")
1060+
168 |-print(f"{''}{''}" or "bar")
1061+
168 |+print("bar")
1062+
169 169 | print(f"{1}{''}" or "bar")
10431063

10441064

crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM223_SIM223.py.snap

+20
Original file line numberDiff line numberDiff line change
@@ -1003,5 +1003,25 @@ SIM223.py:148:12: SIM223 [*] Use `[]` instead of `[] and ...`
10031003
148 |-if f(a and [] and False and []): # SIM223
10041004
148 |+if f(a and []): # SIM223
10051005
149 149 | pass
1006+
150 150 |
1007+
151 151 | # Regression test for: https://github.com/astral-sh/ruff/issues/9479
1008+
1009+
SIM223.py:154:7: SIM223 [*] Use `f"{''}{''}"` instead of `f"{''}{''}" and ...`
1010+
|
1011+
152 | print(f"{a}{b}" and "bar")
1012+
153 | print(f"{a}{''}" and "bar")
1013+
154 | print(f"{''}{''}" and "bar")
1014+
| ^^^^^^^^^^^^^^^^^^^^^ SIM223
1015+
155 | print(f"{1}{''}" and "bar")
1016+
|
1017+
= help: Replace with `f"{''}{''}"`
1018+
1019+
Unsafe fix
1020+
151 151 | # Regression test for: https://github.com/astral-sh/ruff/issues/9479
1021+
152 152 | print(f"{a}{b}" and "bar")
1022+
153 153 | print(f"{a}{''}" and "bar")
1023+
154 |-print(f"{''}{''}" and "bar")
1024+
154 |+print(f"{''}{''}")
1025+
155 155 | print(f"{1}{''}" and "bar")
10061026

10071027

crates/ruff_python_ast/src/helpers.rs

+102-17
Original file line numberDiff line numberDiff line change
@@ -308,10 +308,13 @@ pub fn any_over_pattern(pattern: &Pattern, func: &dyn Fn(&Expr) -> bool) -> bool
308308
}
309309
}
310310

311-
pub fn any_over_f_string_element(element: &FStringElement, func: &dyn Fn(&Expr) -> bool) -> bool {
311+
pub fn any_over_f_string_element(
312+
element: &ast::FStringElement,
313+
func: &dyn Fn(&Expr) -> bool,
314+
) -> bool {
312315
match element {
313-
FStringElement::Literal(_) => false,
314-
FStringElement::Expression(ast::FStringExpressionElement {
316+
ast::FStringElement::Literal(_) => false,
317+
ast::FStringElement::Expression(ast::FStringExpressionElement {
315318
expression,
316319
format_spec,
317320
..
@@ -1171,21 +1174,10 @@ impl Truthiness {
11711174
}
11721175
Expr::NoneLiteral(_) => Self::Falsey,
11731176
Expr::EllipsisLiteral(_) => Self::Truthy,
1174-
Expr::FString(ast::ExprFString { value, .. }) => {
1175-
if value.iter().all(|part| match part {
1176-
ast::FStringPart::Literal(string_literal) => string_literal.is_empty(),
1177-
ast::FStringPart::FString(f_string) => f_string.elements.is_empty(),
1178-
}) {
1177+
Expr::FString(f_string) => {
1178+
if is_empty_f_string(f_string) {
11791179
Self::Falsey
1180-
} else if value
1181-
.elements()
1182-
.any(|f_string_element| match f_string_element {
1183-
ast::FStringElement::Literal(ast::FStringLiteralElement {
1184-
value, ..
1185-
}) => !value.is_empty(),
1186-
ast::FStringElement::Expression(_) => true,
1187-
})
1188-
{
1180+
} else if is_non_empty_f_string(f_string) {
11891181
Self::Truthy
11901182
} else {
11911183
Self::Unknown
@@ -1243,6 +1235,99 @@ impl Truthiness {
12431235
}
12441236
}
12451237

1238+
/// Returns `true` if the expression definitely resolves to a non-empty string, when used as an
1239+
/// f-string expression, or `false` if the expression may resolve to an empty string.
1240+
fn is_non_empty_f_string(expr: &ast::ExprFString) -> bool {
1241+
fn inner(expr: &Expr) -> bool {
1242+
match expr {
1243+
// When stringified, these expressions are always non-empty.
1244+
Expr::Lambda(_) => true,
1245+
Expr::Dict(_) => true,
1246+
Expr::Set(_) => true,
1247+
Expr::ListComp(_) => true,
1248+
Expr::SetComp(_) => true,
1249+
Expr::DictComp(_) => true,
1250+
Expr::Compare(_) => true,
1251+
Expr::NumberLiteral(_) => true,
1252+
Expr::BooleanLiteral(_) => true,
1253+
Expr::NoneLiteral(_) => true,
1254+
Expr::EllipsisLiteral(_) => true,
1255+
Expr::List(_) => true,
1256+
Expr::Tuple(_) => true,
1257+
1258+
// These expressions must resolve to the inner expression.
1259+
Expr::IfExp(ast::ExprIfExp { body, orelse, .. }) => inner(body) && inner(orelse),
1260+
Expr::NamedExpr(ast::ExprNamedExpr { value, .. }) => inner(value),
1261+
1262+
// These expressions are complex. We can't determine whether they're empty or not.
1263+
Expr::BoolOp(ast::ExprBoolOp { .. }) => false,
1264+
Expr::BinOp(ast::ExprBinOp { .. }) => false,
1265+
Expr::UnaryOp(ast::ExprUnaryOp { .. }) => false,
1266+
Expr::GeneratorExp(_) => false,
1267+
Expr::Await(_) => false,
1268+
Expr::Yield(_) => false,
1269+
Expr::YieldFrom(_) => false,
1270+
Expr::Call(_) => false,
1271+
Expr::Attribute(_) => false,
1272+
Expr::Subscript(_) => false,
1273+
Expr::Starred(_) => false,
1274+
Expr::Name(_) => false,
1275+
Expr::Slice(_) => false,
1276+
Expr::IpyEscapeCommand(_) => false,
1277+
1278+
// These literals may or may not be empty.
1279+
Expr::FString(f_string) => is_non_empty_f_string(f_string),
1280+
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => !value.is_empty(),
1281+
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => !value.is_empty(),
1282+
}
1283+
}
1284+
1285+
expr.value.iter().any(|part| match part {
1286+
ast::FStringPart::Literal(string_literal) => !string_literal.is_empty(),
1287+
ast::FStringPart::FString(f_string) => {
1288+
f_string.elements.iter().all(|element| match element {
1289+
FStringElement::Literal(string_literal) => !string_literal.is_empty(),
1290+
FStringElement::Expression(f_string) => inner(&f_string.expression),
1291+
})
1292+
}
1293+
})
1294+
}
1295+
1296+
/// Returns `true` if the expression definitely resolves to the empty string, when used as an f-string
1297+
/// expression.
1298+
fn is_empty_f_string(expr: &ast::ExprFString) -> bool {
1299+
fn inner(expr: &Expr) -> bool {
1300+
match expr {
1301+
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.is_empty(),
1302+
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => value.is_empty(),
1303+
Expr::FString(ast::ExprFString { value, .. }) => {
1304+
value
1305+
.elements()
1306+
.all(|f_string_element| match f_string_element {
1307+
FStringElement::Literal(ast::FStringLiteralElement { value, .. }) => {
1308+
value.is_empty()
1309+
}
1310+
FStringElement::Expression(ast::FStringExpressionElement {
1311+
expression,
1312+
..
1313+
}) => inner(expression),
1314+
})
1315+
}
1316+
_ => false,
1317+
}
1318+
}
1319+
1320+
expr.value.iter().all(|part| match part {
1321+
ast::FStringPart::Literal(string_literal) => string_literal.is_empty(),
1322+
ast::FStringPart::FString(f_string) => {
1323+
f_string.elements.iter().all(|element| match element {
1324+
FStringElement::Literal(string_literal) => string_literal.is_empty(),
1325+
FStringElement::Expression(f_string) => inner(&f_string.expression),
1326+
})
1327+
}
1328+
})
1329+
}
1330+
12461331
pub fn generate_comparison(
12471332
left: &Expr,
12481333
ops: &[CmpOp],

0 commit comments

Comments
 (0)