diff --git a/pyrefly/lib/commands/coverage/collect.rs b/pyrefly/lib/commands/coverage/collect.rs index d958b2f83b..d41cf460b2 100644 --- a/pyrefly/lib/commands/coverage/collect.rs +++ b/pyrefly/lib/commands/coverage/collect.rs @@ -194,14 +194,7 @@ fn merge_overloads(functions: &mut Vec) { for &idx in &indices { let func = &functions[idx]; - let short_name = func.name.rsplit('.').next().unwrap_or(&func.name); - let has_annotation = func.return_annotation.is_some(); - let ret = if !has_annotation && is_implicit_dunder_return(short_name) { - SlotRank::Skip - } else { - SlotRank::classify(has_annotation, func.is_return_type_known) - }; - return_rank = return_rank.max(ret); + return_rank = return_rank.max(func.return_rank); for param in &func.parameters { if let Some(key) = ¶m.merge_key { @@ -772,23 +765,20 @@ fn parse_functions( .and_then(|t| t.property_metadata().map(|m| m.role.clone())); let is_property_deleter = matches!(property_role, Some(PropertyRole::DeleterDecorator)); - // Implicit dunder returns (e.g. __init__ → None) are always - // excluded from coverage, even when explicitly annotated. - // - // Property setters/deleters have a trivial `-> None` return that - // is not a meaningful typable, so skip it like implicit returns. + // Implicit dunder returns (e.g. __init__ → None) and property setter/deleter + // returns are trivial, so they are excluded even when explicitly annotated. let skip_return = is_property_deleter || matches!( property_role, Some(PropertyRole::Setter | PropertyRole::SetterDecorator) ) || (fun.class_key.is_some() && is_implicit_dunder_return(fun.def.name.as_str())); - let return_slot = if skip_return { - SlotCounts::default() + let return_rank = if skip_return { + SlotRank::Skip } else { - SlotRank::classify(return_annotation.is_some(), is_return_type_known).into() + SlotRank::classify(return_annotation.is_some(), is_return_type_known) }; - let mut func_slots = return_slot; + let mut func_slots = SlotCounts::from(return_rank); let mut n_params = 0usize; let mut non_self_index = 0usize; @@ -862,7 +852,7 @@ fn parse_functions( functions.push(Function { name: func_name, return_annotation, - is_return_type_known, + return_rank, parameters, is_type_known, property_role, @@ -901,7 +891,7 @@ fn parse_functions( location, range, return_annotation: target_func.return_annotation.clone(), - is_return_type_known: target_func.is_return_type_known, + return_rank: target_func.return_rank, parameters: target_func.parameters.clone(), is_type_known: target_func.is_type_known, property_role: target_func.property_role.clone(), @@ -2302,7 +2292,7 @@ mod tests { } else { None }, - is_return_type_known: has_return, + return_rank: SlotRank::classify(has_return, has_return), parameters: params .into_iter() .enumerate() diff --git a/pyrefly/lib/commands/coverage/types.rs b/pyrefly/lib/commands/coverage/types.rs index fcc88c2601..34567e2e7b 100644 --- a/pyrefly/lib/commands/coverage/types.rs +++ b/pyrefly/lib/commands/coverage/types.rs @@ -173,8 +173,8 @@ pub struct ReportSuppression { pub struct Function { pub name: String, pub return_annotation: Option, - /// Whether the return type contains no `Any`. - pub is_return_type_known: bool, + #[serde(skip)] + pub return_rank: SlotRank, pub parameters: Vec, pub is_type_known: bool, /// Property role if this function is a property accessor, `None` otherwise. diff --git a/pyrefly/lib/test/coverage/test_files/overloads.expected.json b/pyrefly/lib/test/coverage/test_files/overloads.expected.json index 41ab0afad6..8ce7013f12 100644 --- a/pyrefly/lib/test/coverage/test_files/overloads.expected.json +++ b/pyrefly/lib/test/coverage/test_files/overloads.expected.json @@ -4,9 +4,11 @@ "names": [ "test.f", "test.WithOverloads.method", + "test.OverloadedInit.__init__", + "test.OverloadedInit", "test.WithOverloads" ], - "line_count": 33, + "line_count": 41, "symbol_reports": [ { "kind": "function", @@ -32,6 +34,30 @@ "column": 5 } }, + { + "kind": "function", + "name": "test.OverloadedInit.__init__", + "n_typable": 1, + "n_typed": 1, + "n_any": 0, + "n_untyped": 0, + "location": { + "line": 36, + "column": 5 + } + }, + { + "kind": "class", + "name": "test.OverloadedInit", + "n_typable": 0, + "n_typed": 0, + "n_any": 0, + "n_untyped": 0, + "location": { + "line": 35, + "column": 1 + } + }, { "kind": "class", "name": "test.WithOverloads", @@ -46,17 +72,17 @@ } ], "type_ignores": [], - "n_typable": 4, - "n_typed": 4, + "n_typable": 5, + "n_typed": 5, "n_any": 0, "n_untyped": 0, "coverage": 100.0, "strict_coverage": 100.0, "n_functions": 1, - "n_methods": 1, + "n_methods": 2, "n_function_params": 1, - "n_method_params": 1, - "n_classes": 1, + "n_method_params": 2, + "n_classes": 2, "n_attrs": 0, "n_properties": 0, "n_type_ignores": 0 diff --git a/pyrefly/lib/test/coverage/test_files/overloads.py b/pyrefly/lib/test/coverage/test_files/overloads.py index c2b704a1b9..f1257bb152 100644 --- a/pyrefly/lib/test/coverage/test_files/overloads.py +++ b/pyrefly/lib/test/coverage/test_files/overloads.py @@ -30,3 +30,11 @@ def method(self, x: str) -> str: ... def method(self, x): return x + + +class OverloadedInit: + @overload + def __init__(self, x: int) -> None: ... + @overload + def __init__(self, x: str) -> None: ... + def __init__(self, x): ...