Skip to content

Commit 407ce40

Browse files
committed
fix property typable counts in pyrefly report #3099
1 parent 31708eb commit 407ce40

2 files changed

Lines changed: 60 additions & 72 deletions

File tree

pyrefly/lib/commands/report.rs

Lines changed: 53 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use pyrefly_python::module::Module;
2121
use pyrefly_python::module_name::ModuleName;
2222
use pyrefly_python::nesting_context::NestingContext;
2323
use pyrefly_python::short_identifier::ShortIdentifier;
24+
use pyrefly_types::callable::PropertyRole;
2425
use pyrefly_types::class::ClassDefIndex;
2526
use pyrefly_types::types::Type;
2627
use pyrefly_util::forgetter::Forgetter;
@@ -171,7 +172,9 @@ struct Function {
171172
is_return_type_known: bool,
172173
parameters: Vec<Parameter>,
173174
is_type_known: bool,
174-
is_property: bool,
175+
/// Property role if this function is a property accessor, `None` otherwise.
176+
#[serde(skip)]
177+
property_role: Option<PropertyRole>,
175178
/// Number of non-self/cls, non-implicit params (for entity counting).
176179
n_params: usize,
177180
slots: SlotCounts,
@@ -817,15 +820,29 @@ impl ReportArgs {
817820
let all_params = Self::extract_parameters(&fun.def.parameters);
818821
let mut all_params_type_known = true;
819822

823+
let property_role = answers
824+
.get_type_at(idx)
825+
.and_then(|t| t.property_metadata().map(|m| m.role.clone()));
826+
let is_property_deleter =
827+
matches!(property_role, Some(PropertyRole::DeleterDecorator));
828+
820829
// Compute slot counts: return + non-self/cls params.
821830
// Some dunder methods have implicit return types that don't need
822831
// annotation (__init__ → None, __bool__ → bool, __len__ → int, etc.).
823832
// Only treat as implicit when the annotation is ABSENT; explicit
824833
// annotations (e.g. `-> bool` on `__bool__`) are counted normally.
825-
let has_implicit_return = fun.class_key.is_some()
826-
&& return_annotation.is_none()
827-
&& Self::is_implicit_dunder_return(fun.def.name.as_str());
828-
let return_slot = if has_implicit_return {
834+
//
835+
// Property setters/deleters have a trivial `-> None` return that
836+
// is not a meaningful typable, so skip it like implicit returns.
837+
let skip_return = is_property_deleter
838+
|| matches!(
839+
property_role,
840+
Some(PropertyRole::Setter | PropertyRole::SetterDecorator)
841+
)
842+
|| (fun.class_key.is_some()
843+
&& return_annotation.is_none()
844+
&& Self::is_implicit_dunder_return(fun.def.name.as_str()));
845+
let return_slot = if skip_return {
829846
SlotCounts::default()
830847
} else {
831848
Self::classify_slot(return_annotation.is_some(), is_return_type_known)
@@ -873,7 +890,11 @@ impl ReportArgs {
873890
all_params_type_known = false;
874891
}
875892

876-
if !Self::is_self_or_cls(i, param_name) && !is_implicit_param {
893+
// Deleters have 0 typables; skip parameter slots entirely.
894+
if !Self::is_self_or_cls(i, param_name)
895+
&& !is_implicit_param
896+
&& !is_property_deleter
897+
{
877898
let param_slot =
878899
Self::classify_slot(param_annotation.is_some(), is_param_type_known);
879900
func_slots = func_slots.merge(param_slot);
@@ -895,17 +916,14 @@ impl ReportArgs {
895916
.all(|(i, p)| Self::is_self_or_cls(i, &p.name) || p.annotation.is_some());
896917
let is_type_known =
897918
is_fully_annotated && is_return_type_known && all_params_type_known;
898-
let is_property = answers
899-
.get_type_at(idx)
900-
.is_some_and(|t| t.property_metadata().is_some());
901919

902920
functions.push(Function {
903921
name: func_name,
904922
return_annotation,
905923
is_return_type_known,
906924
parameters,
907925
is_type_known,
908-
is_property,
926+
property_role,
909927
n_params,
910928
slots: func_slots,
911929
location,
@@ -944,7 +962,7 @@ impl ReportArgs {
944962
is_return_type_known: target_func.is_return_type_known,
945963
parameters: target_func.parameters.clone(),
946964
is_type_known: target_func.is_type_known,
947-
is_property: target_func.is_property,
965+
property_role: target_func.property_role.clone(),
948966
n_params: target_func.n_params,
949967
});
950968
}
@@ -1352,23 +1370,34 @@ impl ReportArgs {
13521370
});
13531371
}
13541372

1373+
// Merge same-name property accessors into a single report entry.
1374+
let mut property_map: Vec<(String, SlotCounts, Location)> = Vec::new();
13551375
for func in functions {
1356-
total_slots = total_slots.merge(func.slots);
1357-
names.push(func.name.clone());
1358-
if func.is_property {
1359-
symbol_reports.push(SymbolReport::Property {
1360-
name: func.name.clone(),
1361-
slots: func.slots,
1362-
location: func.location,
1363-
});
1376+
if func.property_role.is_some() {
1377+
if let Some(entry) = property_map.iter_mut().find(|(n, _, _)| *n == func.name) {
1378+
entry.1 = entry.1.merge(func.slots);
1379+
} else {
1380+
property_map.push((func.name.clone(), func.slots, func.location));
1381+
}
13641382
} else {
1383+
total_slots = total_slots.merge(func.slots);
1384+
names.push(func.name.clone());
13651385
symbol_reports.push(SymbolReport::Function {
13661386
name: func.name.clone(),
13671387
slots: func.slots,
13681388
location: func.location,
13691389
});
13701390
}
13711391
}
1392+
for (name, slots, location) in &property_map {
1393+
total_slots = total_slots.merge(*slots);
1394+
names.push(name.clone());
1395+
symbol_reports.push(SymbolReport::Property {
1396+
name: name.clone(),
1397+
slots: *slots,
1398+
location: *location,
1399+
});
1400+
}
13721401

13731402
for cls in classes {
13741403
names.push(cls.name.clone());
@@ -1390,9 +1419,9 @@ impl ReportArgs {
13901419
let mut n_classes = 0usize;
13911420
let mut n_attrs = 0usize;
13921421
let mut n_properties = 0usize;
1393-
// Count functions/methods using the Function list directly so we
1394-
// can use n_params (accurate even for implicit-return dunders).
1395-
for func in functions.iter().filter(|f| !f.is_property) {
1422+
// Count functions/methods using the `Function` list directly so we
1423+
// can use `n_params` (accurate even for implicit-return dunders).
1424+
for func in functions.iter().filter(|f| f.property_role.is_none()) {
13961425
if Self::is_method(&func.name, &module_prefix) {
13971426
n_methods += 1;
13981427
n_method_params += func.n_params;
@@ -2004,7 +2033,7 @@ mod tests {
20042033
})
20052034
.collect(),
20062035
is_type_known: false, // Not relevant for annotation-only tests
2007-
is_property: false,
2036+
property_role: None,
20082037
n_params: 0,
20092038
slots: SlotCounts::default(),
20102039
location: Location { line: 1, column: 1 },
@@ -2096,9 +2125,7 @@ mod tests {
20962125
// These tests capture pyrefly's CURRENT behaviour for scenarios from typestats.
20972126
// Snapshots may need updating when later diffs improve property/overload/schema handling.
20982127

2099-
/// @property getter/setter/deleter reporting.
2100-
/// Current: each accessor is a separate Function with is_property=true.
2101-
/// Typestats: single PropertyReport with fget/fset/fdel slot counts.
2128+
/// @property getter/setter/deleter merged into one report per property.
21022129
#[test]
21032130
fn test_report_property_basic() {
21042131
let report = build_module_report_for_test("property_basic.py");

pyrefly/lib/test/report/test_files/property_basic.expected.json

Lines changed: 7 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44
"test.Getter.x",
55
"test.GetterUntyped.x",
66
"test.GetterSetter.x",
7-
"test.GetterSetter.x",
8-
"test.GetterSetterDeleter.x",
9-
"test.GetterSetterDeleter.x",
107
"test.GetterSetterDeleter.x",
118
"test.MultipleProperties.a",
129
"test.MultipleProperties.b",
@@ -42,18 +39,6 @@
4239
"column": 5
4340
}
4441
},
45-
{
46-
"kind": "property",
47-
"name": "test.GetterSetter.x",
48-
"n_typable": 1,
49-
"n_typed": 1,
50-
"n_any": 0,
51-
"n_untyped": 0,
52-
"location": {
53-
"line": 23,
54-
"column": 5
55-
}
56-
},
5742
{
5843
"kind": "property",
5944
"name": "test.GetterSetter.x",
@@ -62,19 +47,7 @@
6247
"n_any": 0,
6348
"n_untyped": 0,
6449
"location": {
65-
"line": 27,
66-
"column": 5
67-
}
68-
},
69-
{
70-
"kind": "property",
71-
"name": "test.GetterSetterDeleter.x",
72-
"n_typable": 1,
73-
"n_typed": 1,
74-
"n_any": 0,
75-
"n_untyped": 0,
76-
"location": {
77-
"line": 33,
50+
"line": 23,
7851
"column": 5
7952
}
8053
},
@@ -86,19 +59,7 @@
8659
"n_any": 0,
8760
"n_untyped": 0,
8861
"location": {
89-
"line": 37,
90-
"column": 5
91-
}
92-
},
93-
{
94-
"kind": "property",
95-
"name": "test.GetterSetterDeleter.x",
96-
"n_typable": 1,
97-
"n_typed": 1,
98-
"n_any": 0,
99-
"n_untyped": 0,
100-
"location": {
101-
"line": 41,
62+
"line": 33,
10263
"column": 5
10364
}
10465
},
@@ -188,18 +149,18 @@
188149
}
189150
],
190151
"type_ignores": [],
191-
"n_typable": 11,
192-
"n_typed": 10,
152+
"n_typable": 8,
153+
"n_typed": 7,
193154
"n_any": 0,
194155
"n_untyped": 1,
195-
"coverage": 90.9090909090909,
196-
"strict_coverage": 90.9090909090909,
156+
"coverage": 87.5,
157+
"strict_coverage": 87.5,
197158
"n_functions": 0,
198159
"n_methods": 0,
199160
"n_function_params": 0,
200161
"n_method_params": 0,
201162
"n_classes": 5,
202163
"n_attrs": 0,
203-
"n_properties": 9,
164+
"n_properties": 6,
204165
"n_type_ignores": 0
205166
}

0 commit comments

Comments
 (0)