Skip to content

Commit a018786

Browse files
committed
count bare Final as typed in pyrefly report #3172
1 parent 69bb021 commit a018786

2 files changed

Lines changed: 75 additions & 25 deletions

File tree

pyrefly/lib/commands/report.rs

Lines changed: 54 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -411,13 +411,13 @@ impl ReportArgs {
411411
}
412412

413413
/// Classify a single annotation slot: is it typed, any, or untyped?
414-
fn classify_slot(has_annotation: bool, is_type_known: bool) -> SlotCounts {
415-
if !has_annotation {
416-
SlotCounts::untyped()
417-
} else if is_type_known {
418-
SlotCounts::typed()
419-
} else {
420-
SlotCounts::any()
414+
/// Bare qualifiers (e.g. `Final`) resolve to `None` but are treated as typed (i.e. the type is
415+
/// inferred from the value).
416+
fn classify_slot(is_type_known: Option<bool>) -> SlotCounts {
417+
match is_type_known {
418+
Some(true) => SlotCounts::typed(),
419+
Some(false) => SlotCounts::any(),
420+
None => SlotCounts::untyped(),
421421
}
422422
}
423423

@@ -519,12 +519,12 @@ impl ReportArgs {
519519
}
520520
_ => None,
521521
};
522-
let is_type_known = annotation_text.is_some()
523-
&& answers
524-
.get_idx(*annot_idx)
525-
.and_then(|awt| awt.annotation.ty.as_ref().map(Self::is_type_known))
526-
.unwrap_or(false);
527-
let slots = Self::classify_slot(annotation_text.is_some(), is_type_known);
522+
let resolved_ty = answers
523+
.get_idx(*annot_idx)
524+
.and_then(|awt| awt.annotation.ty.as_ref().map(Self::is_type_known));
525+
let slots = Self::classify_slot(
526+
resolved_ty.or(annotation_text.is_some().then_some(true)),
527+
);
528528
variables.push(Variable {
529529
name: qualified_name,
530530
annotation: annotation_text,
@@ -711,15 +711,13 @@ impl ReportArgs {
711711
}
712712
_ => None,
713713
});
714-
let is_type_known = annotation_text.is_some()
715-
&& annotation_idx
716-
.and_then(|idx| {
717-
answers
718-
.get_idx(idx)
719-
.and_then(|awt| awt.annotation.ty.as_ref().map(Self::is_type_known))
720-
})
721-
.unwrap_or(false);
722-
let slots = Self::classify_slot(annotation_text.is_some(), is_type_known);
714+
let resolved_ty = annotation_idx.and_then(|idx| {
715+
answers
716+
.get_idx(idx)
717+
.and_then(|awt| awt.annotation.ty.as_ref().map(Self::is_type_known))
718+
});
719+
let slots =
720+
Self::classify_slot(resolved_ty.or(annotation_text.is_some().then_some(true)));
723721

724722
attrs.push(Variable {
725723
name: qualified_name,
@@ -841,7 +839,7 @@ impl ReportArgs {
841839
let return_slot = if has_implicit_return {
842840
SlotCounts::default()
843841
} else {
844-
Self::classify_slot(return_annotation.is_some(), is_return_type_known)
842+
Self::classify_slot(return_annotation.is_some().then_some(is_return_type_known))
845843
};
846844
let mut func_slots = return_slot;
847845
let mut n_params = 0usize;
@@ -882,8 +880,9 @@ impl ReportArgs {
882880
}
883881

884882
if !is_self && !is_implicit_param {
885-
let param_slot =
886-
Self::classify_slot(param_annotation.is_some(), is_param_type_known);
883+
let param_slot = Self::classify_slot(
884+
param_annotation.is_some().then_some(is_param_type_known),
885+
);
887886
func_slots = func_slots.merge(param_slot);
888887
n_params += 1;
889888
}
@@ -2255,6 +2254,36 @@ mod tests {
22552254
compare_snapshot("partial_any.expected.json", &report);
22562255
}
22572256

2257+
/// Bare `Final` should be typed, not `Any`
2258+
#[test]
2259+
fn test_report_bare_final() {
2260+
let report = build_module_report_for_test("bare_final.py");
2261+
let attr_slots = |name: &str| {
2262+
report
2263+
.symbol_reports
2264+
.iter()
2265+
.find_map(|s| match s {
2266+
SymbolReport::Attr { name: n, slots, .. } if n == name => Some(*slots),
2267+
_ => None,
2268+
})
2269+
.unwrap_or_else(|| panic!("no attr symbol named {name}"))
2270+
};
2271+
2272+
for name in [
2273+
"test.golden",
2274+
"test.golden_ratio",
2275+
"test.pi",
2276+
"test.name",
2277+
"test.Constants.rate",
2278+
"test.Constants.count",
2279+
] {
2280+
let slots = attr_slots(name);
2281+
assert_eq!(slots.n_typable, 1, "{name} should have 1 typable slot");
2282+
assert_eq!(slots.n_typed, 1, "{name} should be typed");
2283+
assert_eq!(slots.n_any, 0, "{name} should not be any");
2284+
}
2285+
}
2286+
22582287
#[test]
22592288
fn test_full_report_has_schema_version() {
22602289
let report = build_module_report_for_test("functions.py");
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
#
3+
# This source code is licensed under the MIT license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
# Bare `Final` should be typed: https://github.com/facebook/pyrefly/issues/3172
7+
8+
from typing import Final
9+
10+
golden: Final = 1.618033988749895
11+
golden_ratio: Final = golden
12+
13+
pi: Final[float] = 3.14159
14+
15+
name: Final = "hello"
16+
17+
18+
class Constants:
19+
def __init__(self):
20+
self.rate: Final = 0.05
21+
self.count: Final[int] = 10

0 commit comments

Comments
 (0)