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
25 changes: 24 additions & 1 deletion pyrefly/lib/alt/solve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2315,6 +2315,28 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
fn binding_to_type_info(&self, binding: &Binding, errors: &ErrorCollector) -> TypeInfo {
match binding {
Binding::Forward(k) => self.get_idx(*k).arc_clone(),
Binding::InitCheck {
forward_key,
name,
range,
termination_keys,
} => {
// Check if all termination keys resolve to Never.
// If any termination key is None (no terminal statement) or doesn't resolve
// to Never, we have a potentially uninitialized variable.
let all_never = termination_keys
.iter()
.all(|term_key| term_key.is_some_and(|tk| self.get_idx(tk).ty().is_never()));
if !all_never {
self.error(
errors,
*range,
ErrorInfo::Kind(ErrorKind::UnboundName),
format!("`{name}` may be uninitialized"),
);
}
self.get_idx(*forward_key).arc_clone()
}
Binding::Narrow(k, op, range) => {
self.narrow(self.get_idx(*k).as_ref(), op, range.range(), errors)
}
Expand Down Expand Up @@ -2849,7 +2871,8 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
| Binding::Narrow(..)
| Binding::AssignToAttribute { .. }
| Binding::AssignToSubscript(..)
| Binding::PossibleLegacyTParam(..) => {
| Binding::PossibleLegacyTParam(..)
| Binding::InitCheck { .. } => {
// These forms require propagating attribute narrowing information, so they
// are handled in `binding_to_type_info`
self.binding_to_type_info(binding, errors).into_ty()
Expand Down
27 changes: 26 additions & 1 deletion pyrefly/lib/binding/binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1398,6 +1398,16 @@ pub enum Binding {
ClassDef(Idx<KeyClass>, Box<[Idx<KeyDecorator>]>),
/// A forward reference to another binding.
Forward(Idx<Key>),
/// A forward reference with a solve-time initialization check.
/// The termination keys are from branches that don't have a value for this variable.
/// At solve time, if all termination keys resolve to Never, no error is emitted.
/// Otherwise, an "uninitialized" error is emitted.
InitCheck {
forward_key: Idx<Key>,
name: Name,
range: TextRange,
termination_keys: Vec<Option<Idx<Key>>>,
},
/// A phi node, representing the union of several alternative keys.
/// Each BranchInfo contains the value key and optional termination key from one branch.
Phi(JoinStyle<Idx<Key>>, Vec<BranchInfo>),
Expand Down Expand Up @@ -1796,6 +1806,20 @@ impl DisplayWith<Bindings> for Binding {
}
write!(f, ")")
}
Self::InitCheck {
forward_key,
name,
range,
termination_keys,
} => {
write!(
f,
"InitCheck({}, {name}, {}, {:?})",
ctx.display(*forward_key),
m.display(range),
termination_keys
)
}
}
}
}
Expand Down Expand Up @@ -1868,7 +1892,8 @@ impl Binding {
| Binding::CompletedPartialType(..)
| Binding::PartialTypeWithUpstreamsCompleted(..)
| Binding::Delete(_)
| Binding::ClassBodyUnknownName(_, _, _) => None,
| Binding::ClassBodyUnknownName(_, _, _)
| Binding::InitCheck { .. } => None,
}
}
}
Expand Down
14 changes: 4 additions & 10 deletions pyrefly/lib/binding/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,19 +132,12 @@ impl NameLookupResult {
pub enum InitializedInFlow {
Yes,
Conditionally,
/// Possibly uninitialized, but has termination keys to check at solve time.
/// If all termination keys resolve to Never, the variable is actually initialized.
ConditionallyWithTermKeys(Vec<Option<Idx<Key>>>),
No,
}

impl InitializedInFlow {
pub fn as_error_message(&self, name: &Name) -> Option<String> {
match self {
InitializedInFlow::Yes => None,
InitializedInFlow::Conditionally => Some(format!("`{name}` may be uninitialized")),
InitializedInFlow::No => Some(format!("`{name}` is uninitialized")),
}
}
}

#[derive(Clone, Dupe, Debug)]
pub struct Bindings(Arc<BindingsInner>);

Expand Down Expand Up @@ -857,6 +850,7 @@ impl<'a> BindingsBuilder<'a> {
FlowStyle::Other
| FlowStyle::ClassField { .. }
| FlowStyle::PossiblyUninitialized
| FlowStyle::PossiblyUninitializedWithTermKeys(_)
| FlowStyle::Uninitialized => {
self.special_export_from_binding_idx(idx, visited_names, visited_keys)
}
Expand Down
41 changes: 32 additions & 9 deletions pyrefly/lib/binding/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ use crate::binding::binding::PrivateAttributeAccessCheck;
use crate::binding::binding::SuperStyle;
use crate::binding::bindings::AwaitContext;
use crate::binding::bindings::BindingsBuilder;
use crate::binding::bindings::InitializedInFlow;
use crate::binding::bindings::LegacyTParamCollector;
use crate::binding::bindings::LegacyTParamId;
use crate::binding::bindings::NameLookupResult;
Expand Down Expand Up @@ -337,15 +338,37 @@ impl<'a> BindingsBuilder<'a> {
} => {
// Uninitialized local errors are only reported when we are neither in a stub
// nor a static type context.
if !used_in_static_type
&& !self.module_info.path().is_interface()
&& let Some(error_message) = is_initialized.as_error_message(&name.id)
{
self.error(
name.range,
ErrorInfo::Kind(ErrorKind::UnboundName),
error_message,
);
if !used_in_static_type && !self.module_info.path().is_interface() {
match is_initialized {
InitializedInFlow::Yes => {}
InitializedInFlow::Conditionally => {
self.error(
name.range,
ErrorInfo::Kind(ErrorKind::UnboundName),
format!("`{}` may be uninitialized", name.id),
);
}
InitializedInFlow::ConditionallyWithTermKeys(termination_keys) => {
// Defer error check to solve time where we can check if
// termination keys are Never
return self.insert_binding(
key,
Binding::InitCheck {
forward_key: value,
name: name.id.clone(),
range: name.range,
termination_keys,
},
);
}
InitializedInFlow::No => {
self.error(
name.range,
ErrorInfo::Kind(ErrorKind::UnboundName),
format!("`{}` is uninitialized", name.id),
);
}
}
}
self.insert_binding(key, Binding::Forward(value))
}
Expand Down
Loading
Loading