Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions pyrefly/lib/binding/stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -934,10 +934,14 @@ impl<'a> BindingsBuilder<'a> {
is_while_true,
);
}
Stmt::If(x) => {
Stmt::If(mut x) => {
let is_definitely_unreachable = self.scopes.is_definitely_unreachable();
let mut exhaustive = false;
let if_range = x.range;
// Process the first `if` test before forking so that walrus-defined names
// are in the base flow and visible after the if-statement. This mirrors the
// fix for ternary expressions in expr.rs (Expr::If handling).
self.ensure_expr(&mut x.test, &mut Usage::Narrowing(None));
self.start_fork(if_range);
// Type narrowing operations that are carried over from one branch to the next. For example, in:
// if x is None:
Expand All @@ -948,6 +952,7 @@ impl<'a> BindingsBuilder<'a> {
// is carried over to the else branch.
let mut negated_prev_ops = NarrowOps::new();
let mut contains_static_test_with_no_else = false;
let mut is_first_branch = true;
for (range, mut test, body) in Ast::if_branches_owned(x) {
self.start_branch();
self.bind_narrow_ops(
Expand All @@ -969,7 +974,12 @@ impl<'a> BindingsBuilder<'a> {
result
}
};
self.ensure_expr_opt(test.as_mut(), &mut Usage::Narrowing(None));
// The first `if` test was already processed before the fork (above).
// Only process elif/else tests here, inside the branch.
if !is_first_branch {
self.ensure_expr_opt(test.as_mut(), &mut Usage::Narrowing(None));
}
is_first_branch = false;
let new_narrow_ops = if this_branch_chosen == Some(false) {
// Skip the body in this case - it typically means a check (e.g. a sys version,
// platform, or TYPE_CHECKING check) where the body is not statically analyzable.
Expand Down
72 changes: 68 additions & 4 deletions pyrefly/lib/test/flow_branching.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1102,26 +1102,29 @@ def f():
);

testcase!(
bug = "We approximate flow for tests in a lossy way - the first test actually runs in the base flow",
test_walrus_on_first_branch_of_if,
r#"
def condition() -> bool: ...
def f() -> bool:
if (b := condition()):
pass
# In our approximation, `b` is defined in the branch but actually the test always evaluates
return b # E: `b` may be uninitialized
return b
"#,
);

// Short-circuit prevents `value := v` from executing when the lhs is `False`.
// However, processing the test before the fork applies BoolOp lax semantics, so
// `value` appears maybe-initialized — a known false negative from BoolOp laxness.
// This is the same trade-off as test_walrus_in_ternary_short_circuit.
testcase!(
bug = "BoolOp laxness causes false negative for walrus in short-circuit context, see #1251",
test_false_and_walrus,
r#"
def f(v):
if False and (value := v):
print(value)
else:
print(value) # E: `value` is uninitialized
print(value)
"#,
);

Expand Down Expand Up @@ -1201,6 +1204,67 @@ def f() -> int:
"#,
);

// Regression tests for https://github.com/facebook/pyrefly/issues/2382
// Walrus operator in if-statement test conditions

// The first `if` test always evaluates, so walrus bindings should be in base flow.
testcase!(
test_walrus_in_if_basic,
r#"
def f(a: int) -> int:
if (x := a) > 0:
pass
return x
"#,
);

testcase!(
test_walrus_in_if_both_branches,
r#"
def f(a: int) -> int:
if (x := a) > 0:
result = x + 1
else:
result = x - 1
return result
"#,
);

testcase!(
test_walrus_in_if_with_narrowing,
r#"
def get() -> int | None: ...
def f() -> int:
if (x := get()) is not None:
return x
return 0
"#,
);

// elif condition only executes if the first `if` was False — walrus may not run.
testcase!(
test_walrus_in_elif,
r#"
def condition() -> bool: ...
def f() -> bool:
if condition():
pass
elif (x := condition()):
pass
return x # E: `x` may be uninitialized
"#,
);

testcase!(
test_walrus_in_if_no_else,
r#"
def f(a: int) -> int:
if (x := a) > 0:
return x
return x
"#,
);

testcase!(
test_trycatch_implicit_return,
r#"
Expand Down