From acc963f920487125f2c1a0dade30cda21813dd17 Mon Sep 17 00:00:00 2001 From: MarcoGorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Tue, 26 May 2026 13:17:32 +0100 Subject: [PATCH] enh: suppress false-positive unknown-name errors after short-circuit boolean ops --- pyrefly/lib/binding/expr.rs | 14 +++++++++ pyrefly/lib/test/sys_info.rs | 58 ++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/pyrefly/lib/binding/expr.rs b/pyrefly/lib/binding/expr.rs index e1fd75b37a..5feb2469ac 100644 --- a/pyrefly/lib/binding/expr.rs +++ b/pyrefly/lib/binding/expr.rs @@ -399,6 +399,9 @@ impl<'a> BindingsBuilder<'a> { self.defer_bound_name(key, lookup_result_idx, usage, promote) } NameLookupResult::NotFound => { + if self.scopes.is_definitely_unreachable() { + return self.insert_binding(key, Binding::Any(AnyStyle::Implicit)); + } let suggestion = self .scopes .suggest_similar_name(&name.id, name.range.start()); @@ -764,6 +767,14 @@ impl<'a> BindingsBuilder<'a> { self.ensure_expr(value, &mut Usage::narrowing_from(usage)); self.start_fork_and_branch(*range); let mut narrow_ops = get_narrow_ops(self, value, *op); + + let short_circuit_trigger: Option = match op { + BoolOp::And => Some(false), + BoolOp::Or => Some(true), + }; + if self.sys_info.evaluate_bool(value) == short_circuit_trigger { + self.scopes.set_definitely_unreachable(true); + } for value in values { self.bind_narrow_ops( &narrow_ops, @@ -773,6 +784,9 @@ impl<'a> BindingsBuilder<'a> { self.ensure_expr(value, &mut Usage::narrowing_from(usage)); let new_narrow_ops = get_narrow_ops(self, value, *op); narrow_ops.and_all(new_narrow_ops); + if self.sys_info.evaluate_bool(value) == short_circuit_trigger { + self.scopes.set_definitely_unreachable(true); + } } // Negate the narrow ops in the base flow and merge. // TODO(stroxler): We eventually want to drop all narrows but merge values. diff --git a/pyrefly/lib/test/sys_info.rs b/pyrefly/lib/test/sys_info.rs index 63659c957c..0e011fb158 100644 --- a/pyrefly/lib/test/sys_info.rs +++ b/pyrefly/lib/test/sys_info.rs @@ -323,6 +323,64 @@ def f(**kwargs: Unpack[Kwargs]) -> None: "#, ); +testcase!( + test_version_guard_and_short_circuit, + r#" +if False: + A = 3 + +if False: + B = 3 + +if False: + C = 3 + + +def test_and_basic() -> None: + # `and` short-circuits on False: A in the condition and body are unreachable. + if False and A: + _ = A + +def test_and_longer_chain() -> None: + # Both B and C are gated: the guard short-circuits before reaching them. + if False and B and C: + _ = B + _ = C + +def test_and_three_guards() -> None: + # Three consecutive False guards in a single and-chain. + if False and A and B and C: + _ = A + _ = B + _ = C + +def test_or_basic() -> None: + # `or` short-circuits when the left side is *true*. False on the left + # means the right side IS evaluated — the name is unknown. + if False or A: # E: Could not find name `A` + pass + +def test_or_always_true_guard() -> None: + # True → right side is unreachable. + if True or A: + pass + +def test_or_longer_chain() -> None: + # Chain: first element is always-true, so B and C are never evaluated. + if True or B or C: + pass + +def test_and_body_unreachable() -> None: + # An `if False` body is also unreachable; accessing A inside it is fine. + if False: + _ = A + +def test_no_guard_produces_error() -> None: + # Baseline: without any guard, accessing A must produce an error. + A # E: Could not find name `A` +"#, +); + testcase!( test_typechecking_unpack_alias, r#"