diff --git a/pyrefly/lib/binding/bindings.rs b/pyrefly/lib/binding/bindings.rs index b6542b165e..f24782eddc 100644 --- a/pyrefly/lib/binding/bindings.rs +++ b/pyrefly/lib/binding/bindings.rs @@ -13,6 +13,7 @@ use std::sync::Arc; use dupe::Dupe; use pyrefly_python::ast::Ast; use pyrefly_python::module_name::ModuleName; +use pyrefly_python::module_path::ModulePathDetails; use pyrefly_python::nesting_context::NestingContext; use pyrefly_python::short_identifier::ShortIdentifier; use pyrefly_python::sys_info::SysInfo; @@ -188,6 +189,7 @@ pub struct BindingsBuilder<'a> { pub has_docstring: bool, pub scopes: Scopes, table: BindingTable, + error_suppression_depth: usize, pub untyped_def_behavior: UntypedDefBehavior, unused_parameters: Vec, } @@ -366,6 +368,7 @@ impl Bindings { has_docstring: Ast::has_docstring(&x), scopes: Scopes::module(x.range, enable_trace), table: Default::default(), + error_suppression_depth: 0, untyped_def_behavior, unused_parameters: Vec::new(), }; @@ -639,6 +642,29 @@ impl<'a> BindingsBuilder<'a> { } } + pub fn with_error_suppression( + &mut self, + f: impl FnOnce(&mut BindingsBuilder<'a>) -> R, + ) -> R { + self.error_suppression_depth += 1; + let result = f(self); + self.error_suppression_depth -= 1; + result + } + + #[inline] + fn errors_suppressed(&self) -> bool { + self.error_suppression_depth > 0 + } + + pub(crate) fn should_bind_unreachable_branches(&self) -> bool { + matches!( + self.module_info.path().details(), + ModulePathDetails::FileSystem(_) | ModulePathDetails::Memory(_) + ) && self.module_info.name() != ModuleName::builtins() + && self.module_info.name() != ModuleName::extra_builtins() + } + fn inject_globals(&mut self) { for global in ImplicitGlobal::implicit_globals(self.has_docstring) { let key = Key::ImplicitGlobal(global.name().clone()); @@ -760,10 +786,16 @@ impl<'a> BindingsBuilder<'a> { } pub fn error(&self, range: TextRange, info: ErrorInfo, msg: String) { + if self.errors_suppressed() { + return; + } self.errors.add(range, info, vec1![msg]); } pub fn error_multiline(&self, range: TextRange, info: ErrorInfo, msg: Vec1) { + if self.errors_suppressed() { + return; + } self.errors.add(range, info, msg); } @@ -939,19 +971,29 @@ impl<'a> BindingsBuilder<'a> { idx: Idx, style: FlowStyle, ) -> Option> { - let name = Hashed::new(name); - let write_info = self + let mut hashed_name = Hashed::new(name); + let mut write_info = self .scopes - .define_in_current_flow(name, idx, style) - .unwrap_or_else(|| { - panic!( - "Name `{name}` not found in static scope of module `{}`.", - self.module_info.name(), - ) - }); + .define_in_current_flow(hashed_name, idx, style.clone()); + if write_info.is_none() + && self.errors_suppressed() + && self.should_bind_unreachable_branches() + { + let key_range = self.table.types.0.idx_to_key(idx).range(); + self.scopes.add_synthetic_definition(name, key_range); + // Recreate the hash since it borrows `name` by reference and we just mutated state + hashed_name = Hashed::new(name); + write_info = self.scopes.define_in_current_flow(hashed_name, idx, style); + } + let write_info = write_info.unwrap_or_else(|| { + panic!( + "Name `{name}` not found in static scope of module `{}`.", + self.module_info.name(), + ) + }); if let Some(range) = write_info.anywhere_range { self.table - .record_bind_in_anywhere(name.into_key().clone(), range, idx); + .record_bind_in_anywhere(hashed_name.into_key().clone(), range, idx); } write_info.annotation } diff --git a/pyrefly/lib/binding/scope.rs b/pyrefly/lib/binding/scope.rs index ed37b462d7..60e1954c28 100644 --- a/pyrefly/lib/binding/scope.rs +++ b/pyrefly/lib/binding/scope.rs @@ -1546,6 +1546,22 @@ impl Scopes { self.current_mut().stat.expr_lvalue(x); } + /// Synthesize a static definition entry for `name` in the current scope if it + /// is missing. This is used when we deliberately analyze unreachable code for + /// IDE metadata; those code paths may not have been included in the up-front + /// static scan, so we add a lightweight placeholder on demand. + pub fn add_synthetic_definition(&mut self, name: &Name, range: TextRange) { + let hashed_ref = Hashed::new(name); + if self.current().stat.0.get_hashed(hashed_ref).is_some() { + return; + } + self.current_mut().stat.upsert( + Hashed::new(name.clone()), + range, + StaticStyle::SingleDef(None), + ); + } + /// Add a loop exit point to the current innermost loop with the current flow. /// /// Return a bool indicating whether we were in a loop (if we weren't, we do nothing). diff --git a/pyrefly/lib/binding/stmt.rs b/pyrefly/lib/binding/stmt.rs index 1f0bf58d19..e5506ce1d1 100644 --- a/pyrefly/lib/binding/stmt.rs +++ b/pyrefly/lib/binding/stmt.rs @@ -701,14 +701,22 @@ impl<'a> BindingsBuilder<'a> { Some(x) => self.sys_info.evaluate_bool(x), }; self.ensure_expr_opt(test.as_mut(), &mut Usage::Narrowing(None)); - 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. + let new_narrow_ops = NarrowOps::from_expr(self, test.as_ref()); + if this_branch_chosen == Some(false) { + // Skip contributing to flow merges, but still bind names so IDE features work. + if self.should_bind_unreachable_branches() { + self.bind_narrow_ops( + &new_narrow_ops, + NarrowUseLocation::Span(range), + &Usage::Narrowing(None), + ); + self.with_error_suppression(|builder| { + builder.stmts(body, parent); + }); + } self.abandon_branch(); continue; - } else { - NarrowOps::from_expr(self, test.as_ref()) - }; + } if let Some(test_expr) = test { // Typecheck the test condition during solving. self.insert_binding( diff --git a/pyrefly/lib/test/lsp/definition.rs b/pyrefly/lib/test/lsp/definition.rs index b3f2a1ac54..5c04b562c9 100644 --- a/pyrefly/lib/test/lsp/definition.rs +++ b/pyrefly/lib/test/lsp/definition.rs @@ -96,11 +96,15 @@ if not TYPE_CHECKING: # main.py 5 | x = 1 ^ -Definition Result: None +Definition Result: +5 | x = 1 + ^ 7 | y = x ^ -Definition Result: None +Definition Result: +5 | x = 1 + ^ "# .trim(), report.trim(), @@ -1533,7 +1537,9 @@ if False: # main.py 4 | print(x) ^ -Definition Result: None +Definition Result: +2 | x = 5 + ^ "# .trim(), diff --git a/pyrefly/lib/test/lsp/hover_type.rs b/pyrefly/lib/test/lsp/hover_type.rs index b2ceb095b7..e0191ab82a 100644 --- a/pyrefly/lib/test/lsp/hover_type.rs +++ b/pyrefly/lib/test/lsp/hover_type.rs @@ -289,19 +289,19 @@ if False: # main.py 3 | def f(): ^ -Hover Result: None +Hover Result: `() -> None` 7 | x = 3 ^ -Hover Result: None +Hover Result: `Literal[3]` 9 | x ^ -Hover Result: None +Hover Result: `Literal[3]` 11 | f ^ -Hover Result: None +Hover Result: `() -> None` 14 | def f(): ^ @@ -309,15 +309,15 @@ Hover Result: None 18 | x = 3 ^ -Hover Result: None +Hover Result: `Literal[3]` 20 | x ^ -Hover Result: None +Hover Result: `Literal[3]` 22 | f ^ -Hover Result: None +Hover Result: `() -> None` "# .trim(), report.trim(),