Skip to content

Commit 107aea0

Browse files
stroxlermeta-codesync[bot]
authored andcommitted
Add contextual type for attribute assignments to __static__ primitives
Summary: Extend the contextual type post-processing to handle attribute assignment targets (e.g. `self.x = 42` or `self.x: int64 = 42` where `x` is typed as `int64`). Uses class field bindings to resolve the attribute's declared type since the trace map doesn't record types for assignment targets. - Add `lookup_attr_type` helper that resolves attribute types via KeyClassField - Handle Expr::Attribute targets in both AnnAssign and Assign visitors - Add tests for attribute assignment and annotated attribute assignment Reviewed By: alexmalyshev Differential Revision: D96508513 fbshipit-source-id: fe0828fd144febed077213f883f93c752f90edd7
1 parent f108cd5 commit 107aea0

File tree

2 files changed

+146
-19
lines changed

2 files changed

+146
-19
lines changed

pyrefly/lib/report/cinderx/collect.rs

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use pyrefly_types::types::Type;
2323
use pyrefly_util::visit::Visit;
2424
use ruff_python_ast::AtomicNodeIndex;
2525
use ruff_python_ast::Expr;
26+
use ruff_python_ast::ExprAttribute;
2627
use ruff_python_ast::ExprName;
2728
use ruff_python_ast::ExprNumberLiteral;
2829
use ruff_python_ast::Int;
@@ -34,9 +35,11 @@ use ruff_python_ast::statement_visitor::walk_stmt;
3435
use ruff_text_size::Ranged;
3536
use ruff_text_size::TextRange;
3637
use ruff_text_size::TextSize;
38+
use starlark_map::Hashed;
3739

3840
use crate::alt::answers::Answers;
3941
use crate::binding::binding::Key;
42+
use crate::binding::binding::KeyClassField;
4043
use crate::binding::bindings::Bindings;
4144
use crate::module::module_info::ModuleInfo;
4245
use crate::report::cinderx::convert::type_to_structured;
@@ -127,6 +130,25 @@ fn is_static_primitive(ty: &Type) -> bool {
127130
}
128131
}
129132

133+
/// Look up the declared type of an attribute on a class via `KeyClassField`.
134+
///
135+
/// Given an `ExprAttribute` (e.g. `self.x`), looks up the type of the base
136+
/// expression from the trace, extracts the class, and resolves the attribute
137+
/// via `KeyClassField`. Returns `None` if any step fails (e.g. the base is
138+
/// not a class instance, or the attribute is not a known class field in this
139+
/// module's bindings).
140+
fn lookup_attr_type(attr: &ExprAttribute, answers: &Answers, bindings: &Bindings) -> Option<Type> {
141+
let base_ty = answers.get_type_trace(attr.value.range())?;
142+
let class_def_index = match &base_ty {
143+
Type::ClassType(ct) | Type::SelfType(ct) => ct.class_object().index(),
144+
_ => return None,
145+
};
146+
let key = KeyClassField(class_def_index, attr.attr.id.clone());
147+
let idx = bindings.key_to_idx_hashed_opt(Hashed::new(&key))?;
148+
let class_field = answers.get_idx(idx)?;
149+
Some(class_field.ty())
150+
}
151+
130152
/// Statement visitor that builds a map from RHS expression ranges to their
131153
/// contextual (annotation) types for `AnnAssign` and `Assign` statements
132154
/// targeting `__static__` primitive types.
@@ -140,36 +162,51 @@ impl<'a> StatementVisitor<'a> for ContextualTypeCollector<'a> {
140162
fn visit_stmt(&mut self, stmt: &'a Stmt) {
141163
if let Stmt::AnnAssign(ann) = stmt
142164
&& let Some(ref value) = ann.value
143-
&& let Expr::Name(name) = ann.target.as_ref()
144165
{
145-
let key = Key::Definition(ShortIdentifier::expr_name(name));
146-
if self.bindings.is_valid_key(&key)
147-
&& let Some(ty) = self.answers.get_type_at(self.bindings.key_to_idx(&key))
166+
let target_type = match ann.target.as_ref() {
167+
Expr::Name(name) => {
168+
let key = Key::Definition(ShortIdentifier::expr_name(name));
169+
if self.bindings.is_valid_key(&key) {
170+
self.answers.get_type_at(self.bindings.key_to_idx(&key))
171+
} else {
172+
None
173+
}
174+
}
175+
Expr::Attribute(attr) => lookup_attr_type(attr, self.answers, self.bindings),
176+
_ => None,
177+
};
178+
if let Some(ty) = target_type
148179
&& is_static_primitive(&ty)
149180
{
150181
self.contextual_types.insert(value.range(), ty);
151182
}
152183
}
153184
if let Stmt::Assign(assign) = stmt {
154185
for target in &assign.targets {
155-
if let Expr::Name(name) = target {
156-
let key = Key::BoundName(ShortIdentifier::expr_name(name));
157-
let valid_key = if self.bindings.is_valid_key(&key) {
158-
Some(key)
159-
} else {
160-
let key = Key::Definition(ShortIdentifier::expr_name(name));
161-
if self.bindings.is_valid_key(&key) {
186+
let target_type = match target {
187+
Expr::Name(name) => {
188+
let key = Key::BoundName(ShortIdentifier::expr_name(name));
189+
let valid_key = if self.bindings.is_valid_key(&key) {
162190
Some(key)
163191
} else {
164-
None
165-
}
166-
};
167-
if let Some(key) = valid_key
168-
&& let Some(ty) = self.answers.get_type_at(self.bindings.key_to_idx(&key))
169-
&& is_static_primitive(&ty)
170-
{
171-
self.contextual_types.insert(assign.value.range(), ty);
192+
let key = Key::Definition(ShortIdentifier::expr_name(name));
193+
if self.bindings.is_valid_key(&key) {
194+
Some(key)
195+
} else {
196+
None
197+
}
198+
};
199+
valid_key.and_then(|key| {
200+
self.answers.get_type_at(self.bindings.key_to_idx(&key))
201+
})
172202
}
203+
Expr::Attribute(attr) => lookup_attr_type(attr, self.answers, self.bindings),
204+
_ => None,
205+
};
206+
if let Some(ty) = target_type
207+
&& is_static_primitive(&ty)
208+
{
209+
self.contextual_types.insert(assign.value.range(), ty);
173210
}
174211
}
175212
}

pyrefly/lib/test/cinderx.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,6 +1027,96 @@ y: double = 3.14
10271027
);
10281028
}
10291029

1030+
/// When a literal int is assigned to an attribute annotated with `__static__.int64`
1031+
/// (e.g. `self.x = 42` where `x: int64`), the CinderX report should record
1032+
/// the contextual type `__static__.int64` for the literal expression.
1033+
#[test]
1034+
fn test_static_attr_assign() {
1035+
let state = create_state_with_static(
1036+
"test",
1037+
r#"
1038+
from __static__ import int64
1039+
1040+
class Foo:
1041+
x: int64
1042+
def __init__(self) -> None:
1043+
self.x = 42
1044+
"#,
1045+
);
1046+
let transaction = state.transaction();
1047+
let handle = get_handle("test", &transaction);
1048+
1049+
let data = collect_module_types(&transaction, &handle).expect("should collect types");
1050+
1051+
// The type table should contain `__static__.int64` as a class entry.
1052+
let int64_idx = data
1053+
.entries
1054+
.iter()
1055+
.position(|entry| {
1056+
matches!(
1057+
&entry.ty,
1058+
StructuredType::Class { qname, .. } if qname == "__static__.int64"
1059+
)
1060+
})
1061+
.expect("__static__.int64 should exist in the type table");
1062+
1063+
// Find a located type with contextual_type pointing to __static__.int64.
1064+
// The RHS `42` of `self.x = 42` should have it.
1065+
let loc_with_contextual = data
1066+
.locations
1067+
.iter()
1068+
.find(|loc| loc.contextual_type == Some(int64_idx));
1069+
assert!(
1070+
loc_with_contextual.is_some(),
1071+
"expected a located type for literal 42 (attr assign) with contextual_type pointing to __static__.int64, got locations: {:#?}",
1072+
data.locations,
1073+
);
1074+
}
1075+
1076+
/// When a class body has an annotated assignment like `x: int64 = 42`,
1077+
/// the CinderX report should record the contextual type `__static__.int64`
1078+
/// for the literal expression.
1079+
#[test]
1080+
fn test_static_attr_ann_assign() {
1081+
let state = create_state_with_static(
1082+
"test",
1083+
r#"
1084+
from __static__ import int64
1085+
1086+
class Bar:
1087+
x: int64 = 42
1088+
"#,
1089+
);
1090+
let transaction = state.transaction();
1091+
let handle = get_handle("test", &transaction);
1092+
1093+
let data = collect_module_types(&transaction, &handle).expect("should collect types");
1094+
1095+
// The type table should contain `__static__.int64` as a class entry.
1096+
let int64_idx = data
1097+
.entries
1098+
.iter()
1099+
.position(|entry| {
1100+
matches!(
1101+
&entry.ty,
1102+
StructuredType::Class { qname, .. } if qname == "__static__.int64"
1103+
)
1104+
})
1105+
.expect("__static__.int64 should exist in the type table");
1106+
1107+
// Find a located type with contextual_type pointing to __static__.int64.
1108+
// The RHS `42` of `x: int64 = 42` in the class body should have it.
1109+
let loc_with_contextual = data
1110+
.locations
1111+
.iter()
1112+
.find(|loc| loc.contextual_type == Some(int64_idx));
1113+
assert!(
1114+
loc_with_contextual.is_some(),
1115+
"expected a located type for literal 42 (class body ann assign) with contextual_type pointing to __static__.int64, got locations: {:#?}",
1116+
data.locations,
1117+
);
1118+
}
1119+
10301120
#[test]
10311121
fn test_literal_promoted_type() {
10321122
let state = create_state("test", "x = 42");

0 commit comments

Comments
 (0)