@@ -21,6 +21,7 @@ use pyrefly_python::module::Module;
2121use pyrefly_python:: module_name:: ModuleName ;
2222use pyrefly_python:: nesting_context:: NestingContext ;
2323use pyrefly_python:: short_identifier:: ShortIdentifier ;
24+ use pyrefly_types:: callable:: PropertyRole ;
2425use pyrefly_types:: class:: ClassDefIndex ;
2526use pyrefly_types:: types:: Type ;
2627use 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" ) ;
0 commit comments