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
21 changes: 17 additions & 4 deletions pyrefly/lib/binding/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2232,13 +2232,26 @@ impl Scopes {
x: &ExprAttribute,
value: ExprOrBinding,
annotation: Option<Idx<KeyAnnotation>>,
) -> bool {
self.record_self_attr_assign_named(&x.value, &x.attr.id, x.attr.range(), value, annotation)
}

/// Like `record_self_attr_assign`, but for initialization via `object.__setattr__`
/// / `setattr(self, "attr", value)` where the attribute name is a string literal.
pub fn record_self_attr_assign_named(
&mut self,
receiver: &Expr,
attr_name: &Name,
attr_range: TextRange,
value: ExprOrBinding,
annotation: Option<Idx<KeyAnnotation>>,
) -> bool {
for scope in self.iter_rev_mut() {
if let ScopeKind::Method(method_scope) = &mut scope.kind
&& let Some(self_name) = &method_scope.self_name
&& matches!(&*x.value, Expr::Name(name) if name.id == self_name.id)
&& matches!(receiver, Expr::Name(name) if name.id == self_name.id)
{
if let Some(attr) = method_scope.instance_attributes.get_mut(&x.attr.id) {
if let Some(attr) = method_scope.instance_attributes.get_mut(attr_name) {
// Accumulate subsequent assignments in the method.
attr.0.push(value);
// Keep the first type annotation encountered in the method.
Expand All @@ -2248,11 +2261,11 @@ impl Scopes {
} else {
// First time seeing this attribute in this method: record it.
method_scope.instance_attributes.insert(
x.attr.id.clone(),
attr_name.clone(),
InstanceAttribute(
vec![value],
annotation,
x.attr.range(), // Keep the range of the first assignment as the definition location.
attr_range, // Keep the range of the first assignment as the definition location.
method_scope.receiver_kind,
),
);
Expand Down
30 changes: 30 additions & 0 deletions pyrefly/lib/binding/stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2216,6 +2216,36 @@ impl<'a> BindingsBuilder<'a> {
);
}
}
// `object.__setattr__(self, "attr", value)` and `setattr(self, "attr", value)`
// initialize instance attributes the same way `self.attr = value` does.
// Frozen dataclasses commonly use this pattern in `__init__`.
if let Expr::Call(call) = x.value.as_ref() {
let is_object_setattr = matches!(
call.func.as_ref(),
Expr::Attribute(func)
if func.attr.id.as_str() == "__setattr__"
&& matches!(
func.value.as_ref(),
Expr::Name(base) if base.id.as_str() == "object"
)
);
let is_builtin_setattr = matches!(
call.func.as_ref(),
Expr::Name(name) if name.id.as_str() == "setattr"
);
if (is_object_setattr || is_builtin_setattr)
&& call.arguments.args.len() >= 3
&& let Expr::StringLiteral(attr_name) = &call.arguments.args[1]
{
self.scopes.record_self_attr_assign_named(
&call.arguments.args[0],
&Name::new(attr_name.value.to_str()),
attr_name.range,
ExprOrBinding::Expr(call.arguments.args[2].clone()),
None,
);
}
}
let special_export = if let Expr::Call(ExprCall { func, .. }) = &*x.value {
self.as_special_export(func)
} else {
Expand Down
25 changes: 25 additions & 0 deletions pyrefly/lib/test/uninitialized_instance_variable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,28 @@ class SetOnOtherObject:
return super().__new__(cls)
"#,
);

// https://github.com/facebook/pyrefly/issues/5326
testcase!(
test_uninitialized_instance_variable_frozen_dataclass_setattr,
TestEnv::new().enable_uninitialized_instance_variable_error(),
r#"
from dataclasses import dataclass
from pathlib import PurePosixPath
@dataclass(frozen=True, slots=True, init=False)
class File:
inner: tuple[PurePosixPath, int]
def __init__(self, path: str, /, *, size: int) -> None:
object.__setattr__(self, "inner", (PurePosixPath(path), size))
@dataclass(frozen=True, init=False)
class ViaBuiltin:
x: int
def __init__(self, x: int) -> None:
setattr(self, "x", x)
@dataclass(frozen=True, init=False)
class StillUninit:
y: int # E: Instance attribute `y` is declared but never initialized
def __init__(self) -> None:
pass
"#,
);
Loading