Skip to content

Commit a2e7a9f

Browse files
committed
Merge branch 'dcreager/non-inferable-api' into dcreager/non-non-inferable
* dcreager/non-inferable-api: just the api parts [ty] Fix further issues in `super()` inference logic (#20843) [ty] Document when a rule was added (#20859) [ty] Treat `Callable` dunder members as bound method descriptors (#20860) [ty] Handle decorators which return unions of `Callable`s (#20858) Fix false negatives in `Truthiness::from_expr` for lambdas, generators, and f-strings (#20704) [ty] Rename Type unwrapping methods (#20857) Update `lint.flake8-type-checking.quoted-annotations` docs (#20765)
2 parents c836146 + 5a21bea commit a2e7a9f

41 files changed

Lines changed: 1193 additions & 559 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

crates/ruff_dev/src/generate_ty_rules.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,39 @@ fn generate_markdown() -> String {
9393
})
9494
.join("\n");
9595

96+
let status_text = match lint.status() {
97+
ty_python_semantic::lint::LintStatus::Stable { since } => {
98+
format!(
99+
r#"Added in <a href="https://github.com/astral-sh/ty/releases/tag/{since}">{since}</a>"#
100+
)
101+
}
102+
ty_python_semantic::lint::LintStatus::Preview { since } => {
103+
format!(
104+
r#"Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/{since}">{since}</a>)"#
105+
)
106+
}
107+
ty_python_semantic::lint::LintStatus::Deprecated { since, .. } => {
108+
format!(
109+
r#"Deprecated (since <a href="https://github.com/astral-sh/ty/releases/tag/{since}">{since}</a>)"#
110+
)
111+
}
112+
ty_python_semantic::lint::LintStatus::Removed { since, .. } => {
113+
format!(
114+
r#"Removed (since <a href="https://github.com/astral-sh/ty/releases/tag/{since}">{since}</a>)"#
115+
)
116+
}
117+
};
118+
96119
let _ = writeln!(
97120
&mut output,
98121
r#"<small>
99-
Default level: [`{level}`](../rules.md#rule-levels "This lint has a default level of '{level}'.") ·
100-
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20{encoded_name}) ·
101-
[View source](https://github.com/astral-sh/ruff/blob/main/{file}#L{line})
122+
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of '{level}'."><code>{level}</code></a> ·
123+
{status_text} ·
124+
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20{encoded_name}" target="_blank">Related issues</a> ·
125+
<a href="https://github.com/astral-sh/ruff/blob/main/{file}#L{line}" target="_blank">View source</a>
102126
</small>
103127
128+
104129
{documentation}
105130
"#,
106131
level = lint.default_level(),

crates/ruff_linter/resources/test/fixtures/flake8_bandit/S602.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,10 @@ def fetch_shell_config(self, username):
3333

3434
def run(self, username):
3535
Popen("true", shell={**self.shell_defaults, **self.fetch_shell_config(username)})
36+
37+
# Additional truthiness cases for generator, lambda, and f-strings
38+
Popen("true", shell=(i for i in ()))
39+
Popen("true", shell=lambda: 0)
40+
Popen("true", shell=f"{b''}")
41+
x = 1
42+
Popen("true", shell=f"{x=}")

crates/ruff_linter/resources/test/fixtures/flake8_bandit/S604.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,19 @@ def foo(shell):
66

77
foo(shell={**{}})
88
foo(shell={**{**{}}})
9+
10+
# Truthy non-bool values for `shell`
11+
foo(shell=(i for i in ()))
12+
foo(shell=lambda: 0)
13+
14+
# f-strings guaranteed non-empty
15+
foo(shell=f"{b''}")
16+
x = 1
17+
foo(shell=f"{x=}")
18+
19+
# Additional truthiness cases for generator, lambda, and f-strings
20+
foo(shell=(i for i in ()))
21+
foo(shell=lambda: 0)
22+
foo(shell=f"{b''}")
23+
x = 1
24+
foo(shell=f"{x=}")

crates/ruff_linter/resources/test/fixtures/flake8_bandit/S609.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,10 @@
99

1010
subprocess.Popen(["chmod", "+w", "*.py"], shell={**{}})
1111
subprocess.Popen(["chmod", "+w", "*.py"], shell={**{**{}}})
12+
13+
# Additional truthiness cases for generator, lambda, and f-strings
14+
subprocess.Popen("chmod +w foo*", shell=(i for i in ()))
15+
subprocess.Popen("chmod +w foo*", shell=lambda: 0)
16+
subprocess.Popen("chmod +w foo*", shell=f"{b''}")
17+
x = 1
18+
subprocess.Popen("chmod +w foo*", shell=f"{x=}")

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,10 @@ def secondToTime(s0: int) -> ((int, int, int) or str):
197197

198198
# https://github.com/astral-sh/ruff/issues/7127
199199
def f(a: "'b' or 'c'"): ...
200+
201+
# https://github.com/astral-sh/ruff/issues/20703
202+
print(f"{b''}" or "bar") # SIM222
203+
x = 1
204+
print(f"{x=}" or "bar") # SIM222
205+
(lambda: 1) or True # SIM222
206+
(i for i in range(1)) or "bar" # SIM222

crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S602_S602.py.snap

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,44 @@ S602 `subprocess` call with `shell=True` identified, security issue
127127
21 |
128128
22 | # Check dict display with only double-starred expressions can be falsey.
129129
|
130+
131+
S602 `subprocess` call with truthy `shell` seems safe, but may be changed in the future; consider rewriting without `shell`
132+
--> S602.py:38:1
133+
|
134+
37 | # Additional truthiness cases for generator, lambda, and f-strings
135+
38 | Popen("true", shell=(i for i in ()))
136+
| ^^^^^
137+
39 | Popen("true", shell=lambda: 0)
138+
40 | Popen("true", shell=f"{b''}")
139+
|
140+
141+
S602 `subprocess` call with truthy `shell` seems safe, but may be changed in the future; consider rewriting without `shell`
142+
--> S602.py:39:1
143+
|
144+
37 | # Additional truthiness cases for generator, lambda, and f-strings
145+
38 | Popen("true", shell=(i for i in ()))
146+
39 | Popen("true", shell=lambda: 0)
147+
| ^^^^^
148+
40 | Popen("true", shell=f"{b''}")
149+
41 | x = 1
150+
|
151+
152+
S602 `subprocess` call with truthy `shell` seems safe, but may be changed in the future; consider rewriting without `shell`
153+
--> S602.py:40:1
154+
|
155+
38 | Popen("true", shell=(i for i in ()))
156+
39 | Popen("true", shell=lambda: 0)
157+
40 | Popen("true", shell=f"{b''}")
158+
| ^^^^^
159+
41 | x = 1
160+
42 | Popen("true", shell=f"{x=}")
161+
|
162+
163+
S602 `subprocess` call with truthy `shell` seems safe, but may be changed in the future; consider rewriting without `shell`
164+
--> S602.py:42:1
165+
|
166+
40 | Popen("true", shell=f"{b''}")
167+
41 | x = 1
168+
42 | Popen("true", shell=f"{x=}")
169+
| ^^^^^
170+
|

crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S604_S604.py.snap

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,85 @@ S604 Function call with `shell=True` parameter identified, security issue
99
6 |
1010
7 | foo(shell={**{}})
1111
|
12+
13+
S604 Function call with truthy `shell` parameter identified, security issue
14+
--> S604.py:11:1
15+
|
16+
10 | # Truthy non-bool values for `shell`
17+
11 | foo(shell=(i for i in ()))
18+
| ^^^
19+
12 | foo(shell=lambda: 0)
20+
|
21+
22+
S604 Function call with truthy `shell` parameter identified, security issue
23+
--> S604.py:12:1
24+
|
25+
10 | # Truthy non-bool values for `shell`
26+
11 | foo(shell=(i for i in ()))
27+
12 | foo(shell=lambda: 0)
28+
| ^^^
29+
13 |
30+
14 | # f-strings guaranteed non-empty
31+
|
32+
33+
S604 Function call with truthy `shell` parameter identified, security issue
34+
--> S604.py:15:1
35+
|
36+
14 | # f-strings guaranteed non-empty
37+
15 | foo(shell=f"{b''}")
38+
| ^^^
39+
16 | x = 1
40+
17 | foo(shell=f"{x=}")
41+
|
42+
43+
S604 Function call with truthy `shell` parameter identified, security issue
44+
--> S604.py:17:1
45+
|
46+
15 | foo(shell=f"{b''}")
47+
16 | x = 1
48+
17 | foo(shell=f"{x=}")
49+
| ^^^
50+
18 |
51+
19 | # Additional truthiness cases for generator, lambda, and f-strings
52+
|
53+
54+
S604 Function call with truthy `shell` parameter identified, security issue
55+
--> S604.py:20:1
56+
|
57+
19 | # Additional truthiness cases for generator, lambda, and f-strings
58+
20 | foo(shell=(i for i in ()))
59+
| ^^^
60+
21 | foo(shell=lambda: 0)
61+
22 | foo(shell=f"{b''}")
62+
|
63+
64+
S604 Function call with truthy `shell` parameter identified, security issue
65+
--> S604.py:21:1
66+
|
67+
19 | # Additional truthiness cases for generator, lambda, and f-strings
68+
20 | foo(shell=(i for i in ()))
69+
21 | foo(shell=lambda: 0)
70+
| ^^^
71+
22 | foo(shell=f"{b''}")
72+
23 | x = 1
73+
|
74+
75+
S604 Function call with truthy `shell` parameter identified, security issue
76+
--> S604.py:22:1
77+
|
78+
20 | foo(shell=(i for i in ()))
79+
21 | foo(shell=lambda: 0)
80+
22 | foo(shell=f"{b''}")
81+
| ^^^
82+
23 | x = 1
83+
24 | foo(shell=f"{x=}")
84+
|
85+
86+
S604 Function call with truthy `shell` parameter identified, security issue
87+
--> S604.py:24:1
88+
|
89+
22 | foo(shell=f"{b''}")
90+
23 | x = 1
91+
24 | foo(shell=f"{x=}")
92+
| ^^^
93+
|

crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S609_S609.py.snap

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,44 @@ S609 Possible wildcard injection in call due to `*` usage
4343
9 |
4444
10 | subprocess.Popen(["chmod", "+w", "*.py"], shell={**{}})
4545
|
46+
47+
S609 Possible wildcard injection in call due to `*` usage
48+
--> S609.py:14:18
49+
|
50+
13 | # Additional truthiness cases for generator, lambda, and f-strings
51+
14 | subprocess.Popen("chmod +w foo*", shell=(i for i in ()))
52+
| ^^^^^^^^^^^^^^^
53+
15 | subprocess.Popen("chmod +w foo*", shell=lambda: 0)
54+
16 | subprocess.Popen("chmod +w foo*", shell=f"{b''}")
55+
|
56+
57+
S609 Possible wildcard injection in call due to `*` usage
58+
--> S609.py:15:18
59+
|
60+
13 | # Additional truthiness cases for generator, lambda, and f-strings
61+
14 | subprocess.Popen("chmod +w foo*", shell=(i for i in ()))
62+
15 | subprocess.Popen("chmod +w foo*", shell=lambda: 0)
63+
| ^^^^^^^^^^^^^^^
64+
16 | subprocess.Popen("chmod +w foo*", shell=f"{b''}")
65+
17 | x = 1
66+
|
67+
68+
S609 Possible wildcard injection in call due to `*` usage
69+
--> S609.py:16:18
70+
|
71+
14 | subprocess.Popen("chmod +w foo*", shell=(i for i in ()))
72+
15 | subprocess.Popen("chmod +w foo*", shell=lambda: 0)
73+
16 | subprocess.Popen("chmod +w foo*", shell=f"{b''}")
74+
| ^^^^^^^^^^^^^^^
75+
17 | x = 1
76+
18 | subprocess.Popen("chmod +w foo*", shell=f"{x=}")
77+
|
78+
79+
S609 Possible wildcard injection in call due to `*` usage
80+
--> S609.py:18:18
81+
|
82+
16 | subprocess.Popen("chmod +w foo*", shell=f"{b''}")
83+
17 | x = 1
84+
18 | subprocess.Popen("chmod +w foo*", shell=f"{x=}")
85+
| ^^^^^^^^^^^^^^^
86+
|

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

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,3 +1062,77 @@ help: Replace with `"bar"`
10621062
170 |
10631063
171 |
10641064
note: This is an unsafe fix and may change runtime behavior
1065+
1066+
SIM222 [*] Use `f"{b''}"` instead of `f"{b''}" or ...`
1067+
--> SIM222.py:202:7
1068+
|
1069+
201 | # https://github.com/astral-sh/ruff/issues/20703
1070+
202 | print(f"{b''}" or "bar") # SIM222
1071+
| ^^^^^^^^^^^^^^^^^
1072+
203 | x = 1
1073+
204 | print(f"{x=}" or "bar") # SIM222
1074+
|
1075+
help: Replace with `f"{b''}"`
1076+
199 | def f(a: "'b' or 'c'"): ...
1077+
200 |
1078+
201 | # https://github.com/astral-sh/ruff/issues/20703
1079+
- print(f"{b''}" or "bar") # SIM222
1080+
202 + print(f"{b''}") # SIM222
1081+
203 | x = 1
1082+
204 | print(f"{x=}" or "bar") # SIM222
1083+
205 | (lambda: 1) or True # SIM222
1084+
note: This is an unsafe fix and may change runtime behavior
1085+
1086+
SIM222 [*] Use `f"{x=}"` instead of `f"{x=}" or ...`
1087+
--> SIM222.py:204:7
1088+
|
1089+
202 | print(f"{b''}" or "bar") # SIM222
1090+
203 | x = 1
1091+
204 | print(f"{x=}" or "bar") # SIM222
1092+
| ^^^^^^^^^^^^^^^^
1093+
205 | (lambda: 1) or True # SIM222
1094+
206 | (i for i in range(1)) or "bar" # SIM222
1095+
|
1096+
help: Replace with `f"{x=}"`
1097+
201 | # https://github.com/astral-sh/ruff/issues/20703
1098+
202 | print(f"{b''}" or "bar") # SIM222
1099+
203 | x = 1
1100+
- print(f"{x=}" or "bar") # SIM222
1101+
204 + print(f"{x=}") # SIM222
1102+
205 | (lambda: 1) or True # SIM222
1103+
206 | (i for i in range(1)) or "bar" # SIM222
1104+
note: This is an unsafe fix and may change runtime behavior
1105+
1106+
SIM222 [*] Use `lambda: 1` instead of `lambda: 1 or ...`
1107+
--> SIM222.py:205:1
1108+
|
1109+
203 | x = 1
1110+
204 | print(f"{x=}" or "bar") # SIM222
1111+
205 | (lambda: 1) or True # SIM222
1112+
| ^^^^^^^^^^^^^^^^^^^
1113+
206 | (i for i in range(1)) or "bar" # SIM222
1114+
|
1115+
help: Replace with `lambda: 1`
1116+
202 | print(f"{b''}" or "bar") # SIM222
1117+
203 | x = 1
1118+
204 | print(f"{x=}" or "bar") # SIM222
1119+
- (lambda: 1) or True # SIM222
1120+
205 + lambda: 1 # SIM222
1121+
206 | (i for i in range(1)) or "bar" # SIM222
1122+
note: This is an unsafe fix and may change runtime behavior
1123+
1124+
SIM222 [*] Use `(i for i in range(1))` instead of `(i for i in range(1)) or ...`
1125+
--> SIM222.py:206:1
1126+
|
1127+
204 | print(f"{x=}" or "bar") # SIM222
1128+
205 | (lambda: 1) or True # SIM222
1129+
206 | (i for i in range(1)) or "bar" # SIM222
1130+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1131+
|
1132+
help: Replace with `(i for i in range(1))`
1133+
203 | x = 1
1134+
204 | print(f"{x=}" or "bar") # SIM222
1135+
205 | (lambda: 1) or True # SIM222
1136+
- (i for i in range(1)) or "bar" # SIM222
1137+
206 + (i for i in range(1)) # SIM222
1138+
note: This is an unsafe fix and may change runtime behavior

crates/ruff_python_ast/src/helpers.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use crate::parenthesize::parenthesized_range;
1212
use crate::statement_visitor::StatementVisitor;
1313
use crate::visitor::Visitor;
1414
use crate::{
15-
self as ast, Arguments, AtomicNodeIndex, CmpOp, DictItem, ExceptHandler, Expr,
15+
self as ast, Arguments, AtomicNodeIndex, CmpOp, DictItem, ExceptHandler, Expr, ExprNoneLiteral,
1616
InterpolatedStringElement, MatchCase, Operator, Pattern, Stmt, TypeParam,
1717
};
1818
use crate::{AnyNodeRef, ExprContext};
@@ -1219,6 +1219,8 @@ impl Truthiness {
12191219
F: Fn(&str) -> bool,
12201220
{
12211221
match expr {
1222+
Expr::Lambda(_) => Self::Truthy,
1223+
Expr::Generator(_) => Self::Truthy,
12221224
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
12231225
if value.is_empty() {
12241226
Self::Falsey
@@ -1388,7 +1390,9 @@ fn is_non_empty_f_string(expr: &ast::ExprFString) -> bool {
13881390
Expr::FString(f_string) => is_non_empty_f_string(f_string),
13891391
// These literals may or may not be empty.
13901392
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => !value.is_empty(),
1391-
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => !value.is_empty(),
1393+
// Confusingly, f"{b""}" renders as the string 'b""', which is non-empty.
1394+
// Therefore, any bytes interpolation is guaranteed non-empty when stringified.
1395+
Expr::BytesLiteral(_) => true,
13921396
}
13931397
}
13941398

@@ -1397,7 +1401,9 @@ fn is_non_empty_f_string(expr: &ast::ExprFString) -> bool {
13971401
ast::FStringPart::FString(f_string) => {
13981402
f_string.elements.iter().all(|element| match element {
13991403
InterpolatedStringElement::Literal(string_literal) => !string_literal.is_empty(),
1400-
InterpolatedStringElement::Interpolation(f_string) => inner(&f_string.expression),
1404+
InterpolatedStringElement::Interpolation(f_string) => {
1405+
f_string.debug_text.is_some() || inner(&f_string.expression)
1406+
}
14011407
})
14021408
}
14031409
})
@@ -1493,7 +1499,7 @@ pub fn pep_604_optional(expr: &Expr) -> Expr {
14931499
ast::ExprBinOp {
14941500
left: Box::new(expr.clone()),
14951501
op: Operator::BitOr,
1496-
right: Box::new(Expr::NoneLiteral(ast::ExprNoneLiteral::default())),
1502+
right: Box::new(Expr::NoneLiteral(ExprNoneLiteral::default())),
14971503
range: TextRange::default(),
14981504
node_index: AtomicNodeIndex::NONE,
14991505
}

0 commit comments

Comments
 (0)