Skip to content

Commit 9b38ee1

Browse files
yangdanny97meta-codesync[bot]
authored andcommitted
preserve literal types for module-scoped final vars inside function scopes
Summary: fixes #3378 Reviewed By: stroxler Differential Revision: D105257293 fbshipit-source-id: d210327d091b05efd07aa2faf61c511027063ef7
1 parent 97df423 commit 9b38ee1

3 files changed

Lines changed: 62 additions & 6 deletions

File tree

pyrefly/lib/binding/expr.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,8 @@ impl<'a> BindingsBuilder<'a> {
373373

374374
let promote = self.scopes.in_function_scope()
375375
&& (is_module_scope || self.scopes.is_defined_at_module_scope(&name.id))
376-
&& !is_constant_name(&name.id);
376+
&& !is_constant_name(&name.id)
377+
&& !self.scopes.is_final_at_module_scope(&name.id);
377378
if promote {
378379
self.promote_ranges.insert(name.range);
379380
}

pyrefly/lib/binding/scope.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -404,8 +404,8 @@ impl Static {
404404
}
405405

406406
/// Populate static definitions from a list of statements.
407-
/// Returns the set of implicit captures (names read but not locally defined)
408-
/// and a map of Final variable string values.
407+
/// Returns the set of implicit captures (names read but not locally defined),
408+
/// the set of all Final names, and a map of Final variable string values.
409409
fn stmts(
410410
&mut self,
411411
x: &[Stmt],
@@ -415,7 +415,7 @@ impl Static {
415415
sys_info: SysInfo,
416416
get_annotation_idx: &mut impl FnMut(ShortIdentifier) -> Idx<KeyAnnotation>,
417417
scopes: Option<&Scopes>,
418-
) -> (SmallSet<Name>, SmallMap<Name, String>) {
418+
) -> (SmallSet<Name>, SmallSet<Name>, SmallMap<Name, String>) {
419419
let mut d = Definitions::new(
420420
x,
421421
module_info.name(),
@@ -464,12 +464,13 @@ impl Static {
464464
self.upsert(name.cloned(), range, StaticStyle::MergeableImport, range)
465465
}
466466
}
467+
let final_names = d.final_names.keys().cloned().collect();
467468
let final_string_values = d
468469
.final_names
469470
.into_iter()
470471
.filter_map(|(name, value)| value.map(|v| (name, v)))
471472
.collect();
472-
(implicit_captures, final_string_values)
473+
(implicit_captures, final_names, final_string_values)
473474
}
474475

475476
fn expr_lvalue(&mut self, x: &Expr) {
@@ -1206,6 +1207,9 @@ pub struct Scope {
12061207
/// from enclosing scopes. Populated during `init_current_static` from the
12071208
/// `Definitions` phase. Used to seed flow entries for captured variables.
12081209
implicit_captures: SmallSet<Name>,
1210+
/// All names marked `Final` in this scope. Used to prevent literal
1211+
/// promotion so that `Final` variables preserve their literal types.
1212+
final_names: SmallSet<Name>,
12091213
/// Names marked `Final` with string literal values, e.g. `X: Final = "x"`.
12101214
/// Used to resolve Final variable references in synthesized class field names.
12111215
final_string_values: SmallMap<Name, String>,
@@ -1227,6 +1231,7 @@ impl Scope {
12271231
finally_depth: 0,
12281232
with_depth: 0,
12291233
implicit_captures: SmallSet::new(),
1234+
final_names: SmallSet::new(),
12301235
final_string_values: SmallMap::new(),
12311236
}
12321237
}
@@ -1673,7 +1678,7 @@ impl Scopes {
16731678
get_annotation_idx: &mut impl FnMut(ShortIdentifier) -> Idx<KeyAnnotation>,
16741679
) {
16751680
let mut initialize = |scope: &mut Scope, myself: Option<&Self>| {
1676-
let (implicit_captures, final_string_values) = scope.stat.stmts(
1681+
let (implicit_captures, final_names, final_string_values) = scope.stat.stmts(
16771682
x,
16781683
module_info,
16791684
top_level,
@@ -1683,6 +1688,7 @@ impl Scopes {
16831688
myself,
16841689
);
16851690
scope.implicit_captures = implicit_captures;
1691+
scope.final_names = final_names;
16861692
scope.final_string_values = final_string_values;
16871693
// Presize the flow, as its likely to need as much space as static
16881694
scope.flow.info.reserve(scope.stat.0.capacity());
@@ -1701,6 +1707,11 @@ impl Scopes {
17011707
}
17021708
}
17031709

1710+
/// Check if a name is declared as `Final` at module scope.
1711+
pub fn is_final_at_module_scope(&self, name: &Name) -> bool {
1712+
self.scopes.first().scope.final_names.contains(name)
1713+
}
1714+
17041715
/// Look up a Final variable's string literal value in the current scope stack.
17051716
/// Searches from the innermost scope outward, stopping at the first scope
17061717
/// that binds the name, even if it's not Final.

pyrefly/lib/test/narrow.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,50 @@ def f2(x: E | int):
159159
"#,
160160
);
161161

162+
testcase!(
163+
test_is_not_final_enum,
164+
r#"
165+
from enum import Enum
166+
from typing import Final, Iterable, assert_type
167+
168+
class EmptyType(Enum):
169+
EMPTY = 0
170+
171+
Empty: Final = EmptyType.EMPTY
172+
173+
class Bar:
174+
baz: int | EmptyType = 4
175+
176+
def foo(bar: Bar) -> Iterable[int]:
177+
if bar.baz is not Empty:
178+
assert_type(bar.baz, int)
179+
return [bar.baz]
180+
return []
181+
"#,
182+
);
183+
184+
testcase!(
185+
test_is_not_final_enum_type_alias,
186+
r#"
187+
from enum import Enum
188+
from typing import Final, Iterable, Literal, TypeAlias
189+
190+
class _EmptyEnum(Enum):
191+
EMPTY = 0
192+
193+
EmptyType: TypeAlias = Literal[_EmptyEnum.EMPTY]
194+
Empty: Final = _EmptyEnum.EMPTY
195+
196+
class Bar:
197+
baz: int | EmptyType = 4
198+
199+
def foo(bar: Bar) -> Iterable[int]:
200+
if bar.baz is not Empty:
201+
return [bar.baz]
202+
return []
203+
"#,
204+
);
205+
162206
testcase!(
163207
test_ellipsis_is,
164208
r#"

0 commit comments

Comments
 (0)