Skip to content
Draft
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
62 changes: 52 additions & 10 deletions pyrefly/lib/binding/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<UnusedParameter>,
}
Expand Down Expand Up @@ -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(),
};
Expand Down Expand Up @@ -639,6 +642,29 @@ impl<'a> BindingsBuilder<'a> {
}
}

pub fn with_error_suppression<R>(
&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());
Expand Down Expand Up @@ -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<String>) {
if self.errors_suppressed() {
return;
}
self.errors.add(range, info, msg);
}

Expand Down Expand Up @@ -939,19 +971,29 @@ impl<'a> BindingsBuilder<'a> {
idx: Idx<Key>,
style: FlowStyle,
) -> Option<Idx<KeyAnnotation>> {
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
}
Expand Down
16 changes: 16 additions & 0 deletions pyrefly/lib/binding/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Comment on lines +1549 to +1553
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a bit complicated to me. I wonder if a better alternative would be to just keep all defs in the very beginning when they are calculated, recording their unreachable status so downstream type checking logic would avoid emitting type errors for those instead.

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).
Expand Down
20 changes: 14 additions & 6 deletions pyrefly/lib/binding/stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
12 changes: 9 additions & 3 deletions pyrefly/lib/test/lsp/definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -1533,7 +1537,9 @@ if False:
# main.py
4 | print(x)
^
Definition Result: None
Definition Result:
2 | x = 5
^

"#
.trim(),
Expand Down
14 changes: 7 additions & 7 deletions pyrefly/lib/test/lsp/hover_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,35 +289,35 @@ 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():
^
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(),
Expand Down
Loading