mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 21:35:58 +00:00
[red-knot] - Flow-control for boolean operations (#13940)
## Summary As python uses short-circuiting boolean operations in runtime, we should mimic that logic in redknot as well. For example, we should detect that in the following code `x` might be undefined inside the block: ```py if flag or (x := 1): print(x) ``` ## Test Plan Added mdtest suit for boolean expressions. --------- Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
parent
b6ffa51c16
commit
66c3aaa307
3 changed files with 125 additions and 1 deletions
|
@ -0,0 +1,78 @@
|
||||||
|
# Short-Circuit Evaluation
|
||||||
|
|
||||||
|
## Not all boolean expressions must be evaluated
|
||||||
|
|
||||||
|
In `or` expressions, if the left-hand side is truthy, the right-hand side is not evaluated.
|
||||||
|
Similarly, in `and` expressions, if the left-hand side is falsy, the right-hand side is not
|
||||||
|
evaluated.
|
||||||
|
|
||||||
|
```py
|
||||||
|
def bool_instance() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if bool_instance() or (x := 1):
|
||||||
|
# error: [possibly-unresolved-reference]
|
||||||
|
reveal_type(x) # revealed: Unbound | Literal[1]
|
||||||
|
|
||||||
|
if bool_instance() and (x := 1):
|
||||||
|
# error: [possibly-unresolved-reference]
|
||||||
|
reveal_type(x) # revealed: Unbound | Literal[1]
|
||||||
|
```
|
||||||
|
|
||||||
|
## First expression is always evaluated
|
||||||
|
|
||||||
|
```py
|
||||||
|
def bool_instance() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if (x := 1) or bool_instance():
|
||||||
|
reveal_type(x) # revealed: Literal[1]
|
||||||
|
|
||||||
|
if (x := 1) and bool_instance():
|
||||||
|
reveal_type(x) # revealed: Literal[1]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Statically known truthiness
|
||||||
|
|
||||||
|
```py
|
||||||
|
if True or (x := 1):
|
||||||
|
# TODO: infer that the second arm is never executed so type should be just "Unbound".
|
||||||
|
# error: [possibly-unresolved-reference]
|
||||||
|
reveal_type(x) # revealed: Unbound | Literal[1]
|
||||||
|
|
||||||
|
if True and (x := 1):
|
||||||
|
# TODO: infer that the second arm is always executed so type should be just "Literal[1]".
|
||||||
|
# error: [possibly-unresolved-reference]
|
||||||
|
reveal_type(x) # revealed: Unbound | Literal[1]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Later expressions can always use variables from earlier expressions
|
||||||
|
|
||||||
|
```py
|
||||||
|
def bool_instance() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
bool_instance() or (x := 1) or reveal_type(x) # revealed: Literal[1]
|
||||||
|
|
||||||
|
# error: [unresolved-reference]
|
||||||
|
bool_instance() or reveal_type(y) or (y := 1) # revealed: Unbound
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nested expressions
|
||||||
|
|
||||||
|
```py
|
||||||
|
def bool_instance() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if bool_instance() or ((x := 1) and bool_instance()):
|
||||||
|
# error: "Name `x` used when possibly not defined"
|
||||||
|
reveal_type(x) # revealed: Unbound | Literal[1]
|
||||||
|
|
||||||
|
if ((y := 1) and bool_instance()) or bool_instance():
|
||||||
|
reveal_type(y) # revealed: Literal[1]
|
||||||
|
|
||||||
|
# error: [possibly-unresolved-reference]
|
||||||
|
if (bool_instance() and (z := 1)) or reveal_type(z): # revealed: Unbound | Literal[1]
|
||||||
|
# error: [possibly-unresolved-reference]
|
||||||
|
reveal_type(z) # revealed: Unbound | Literal[1]
|
||||||
|
```
|
|
@ -1106,6 +1106,27 @@ where
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
ast::Expr::BoolOp(ast::ExprBoolOp {
|
||||||
|
values,
|
||||||
|
range: _,
|
||||||
|
op: _,
|
||||||
|
}) => {
|
||||||
|
// TODO detect statically known truthy or falsy values (via type inference, not naive
|
||||||
|
// AST inspection, so we can't simplify here, need to record test expression for
|
||||||
|
// later checking)
|
||||||
|
let mut snapshots = vec![];
|
||||||
|
|
||||||
|
for (index, value) in values.iter().enumerate() {
|
||||||
|
// The first item of BoolOp is always evaluated
|
||||||
|
if index > 0 {
|
||||||
|
snapshots.push(self.flow_snapshot());
|
||||||
|
}
|
||||||
|
self.visit_expr(value);
|
||||||
|
}
|
||||||
|
for snapshot in snapshots {
|
||||||
|
self.flow_merge(snapshot);
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
walk_expr(self, expr);
|
walk_expr(self, expr);
|
||||||
}
|
}
|
||||||
|
|
|
@ -148,7 +148,7 @@ static HEADER_RE: LazyLock<Regex> =
|
||||||
/// Matches a code block fenced by triple backticks, possibly with language and `key=val`
|
/// Matches a code block fenced by triple backticks, possibly with language and `key=val`
|
||||||
/// configuration items following the opening backticks (in the "tag string" of the code block).
|
/// configuration items following the opening backticks (in the "tag string" of the code block).
|
||||||
static CODE_RE: LazyLock<Regex> = LazyLock::new(|| {
|
static CODE_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||||
Regex::new(r"^```(?<lang>(?-u:\w)+)?(?<config>(?: +\S+)*)\s*\n(?<code>(?:.|\n)*?)\n?```\s*\n")
|
Regex::new(r"^```(?<lang>(?-u:\w)+)?(?<config>(?: +\S+)*)\s*\n(?<code>(?:.|\n)*?)\n?```\s*\n?")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -421,6 +421,31 @@ mod tests {
|
||||||
assert_eq!(file.code, "x = 1");
|
assert_eq!(file.code, "x = 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_new_line_at_eof() {
|
||||||
|
let source = dedent(
|
||||||
|
"
|
||||||
|
```py
|
||||||
|
x = 1
|
||||||
|
```",
|
||||||
|
);
|
||||||
|
let mf = super::parse("file.md", &source).unwrap();
|
||||||
|
|
||||||
|
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
|
||||||
|
panic!("expected one test");
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(test.name(), "file.md");
|
||||||
|
|
||||||
|
let [file] = test.files().collect::<Vec<_>>()[..] else {
|
||||||
|
panic!("expected one file");
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(file.path, "test.py");
|
||||||
|
assert_eq!(file.lang, "py");
|
||||||
|
assert_eq!(file.code, "x = 1");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn multiple_tests() {
|
fn multiple_tests() {
|
||||||
let source = dedent(
|
let source = dedent(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue