fix(semantic): respect shadowed Infinity and NaN in enum evaluation (#25604)

## Summary

Fix enum evaluation to resolve identifiers before treating `Infinity` and `NaN` as unbound globals.
This commit is contained in:
camc314 committed 2026-08-14 02:36:24 +00:00
1 parent bb5a232424
commit 059784d96b
2 files changed
+76 -32

No files matched your search

+41 -32
View File
@@ -21,6 +21,12 @@ struct EnumEvalCtx<'s> {
scoping: &'s Scoping,
}
enum SiblingLookup {
Missing,
Unknown,
Value(ConstantValue),
}
/// Evaluate all enum member values in a `TSEnumDeclaration` and store them in `Scoping`.
///
/// Runs during semantic analysis so the transformer can look up pre-computed values
@@ -120,9 +126,9 @@ fn evaluate_expression(expr: &Expression<'_>, ctx: &EnumEvalCtx<'_>) -> Option<C
/// Resolve an identifier or member expression to a previously evaluated enum value.
///
/// Uses a three-level lookup strategy for bare identifiers:
/// 1. Resolved reference — `enum A { X = 1, Y = X }` (X is resolved in the same scope)
/// 2. Scope binding fallback — handles cases where references aren't yet resolved
/// 3. Sibling enum fallback — `enum A { X = 1 } enum A { Y = X }` (merged declarations)
/// 1. Sibling enum fallback — `enum A { X = 1 } enum A { Y = X }` (merged declarations)
/// 2. Lexical binding — `enum A { X = 1, Y = X }` (X is resolved in the same scope)
/// 3. Unbound globals — `Infinity` and `NaN`
///
/// Also handles cross-enum member access:
/// ```ts
@@ -133,34 +139,28 @@ fn evaluate_expression(expr: &Expression<'_>, ctx: &EnumEvalCtx<'_>) -> Option<C
fn evaluate_ref(expr: &Expression<'_>, ctx: &EnumEvalCtx<'_>) -> Option<ConstantValue> {
match expr {
Expression::Identifier(ident) => {
if ident.name == "Infinity" {
return Some(ConstantValue::Number(f64::INFINITY));
}
if ident.name == "NaN" {
return Some(ConstantValue::Number(f64::NAN));
}
if let Some(ref_id) = ident.reference_id.get()
&& let Some(symbol_id) = ctx.scoping.get_reference(ref_id).symbol_id()
{
return ctx.scoping.get_enum_member_value(symbol_id).cloned();
}
// Fallback: look up as a binding in the current enum body scope.
if let Some(symbol_id) =
ctx.scoping.get_binding(ctx.scope_id, ident.name.as_str().into())
&& let Some(value) = ctx.scoping.get_enum_member_value(symbol_id)
{
return Some(value.clone());
}
// Sibling enum fallback for merged enums.
find_in_sibling_enum_scopes(
match find_in_sibling_enum_scopes(
ident.name.as_str(),
ctx.scope_id,
ctx.enum_symbol_id,
ctx.scoping,
)
) {
SiblingLookup::Value(value) => return Some(value),
SiblingLookup::Unknown => return None,
SiblingLookup::Missing => {}
}
if let Some(symbol_id) = resolve_identifier_symbol(ident, ctx) {
return ctx.scoping.get_enum_member_value(symbol_id).cloned();
}
match ident.name.as_str() {
"Infinity" => return Some(ConstantValue::Number(f64::INFINITY)),
"NaN" => return Some(ConstantValue::Number(f64::NAN)),
_ => {}
}
None
}
Expression::StaticMemberExpression(member_expr) => {
let Expression::Identifier(obj_ident) = &member_expr.object else { return None };
@@ -312,20 +312,29 @@ fn find_in_enum_body_scopes(
/// enum Foo { A = 1 }
/// enum Foo { B = A + 1 } // `A` is in the first Foo's scope, not the current one
/// ```
///
/// Returns [`SiblingLookup::Unknown`] when the member exists but could not be evaluated, so
/// callers do not fall back to a global with the same name.
fn find_in_sibling_enum_scopes(
name: &str,
current_scope_id: ScopeId,
enum_symbol_id: Option<SymbolId>,
scoping: &Scoping,
) -> Option<ConstantValue> {
let body_scopes = scoping.get_enum_body_scopes(enum_symbol_id?)?;
) -> SiblingLookup {
let Some(enum_symbol_id) = enum_symbol_id else { return SiblingLookup::Missing };
let Some(body_scopes) = scoping.get_enum_body_scopes(enum_symbol_id) else {
return SiblingLookup::Missing;
};
for &body_scope in body_scopes {
if body_scope != current_scope_id
&& let Some(member_sym) = scoping.get_binding(body_scope, name.into())
&& let Some(value) = scoping.get_enum_member_value(member_sym)
{
return Some(value.clone());
return scoping
.get_enum_member_value(member_sym)
.cloned()
.map_or(SiblingLookup::Unknown, SiblingLookup::Value);
}
}
None
SiblingLookup::Missing
}
@@ -69,6 +69,13 @@ fn cross_member_reference() {
debug_assert_eq!(get_enum_member_value(source, "Y"), Some(ConstantValue::Number(2.0)));
}
#[test]
fn enum_member_shadows_outer_binding() {
let source = "var X = 4; enum A { X = 1, Y = X + 1 }";
assert_eq!(get_enum_member_value(source, "X"), Some(ConstantValue::Number(1.0)));
assert_eq!(get_enum_member_value(source, "Y"), Some(ConstantValue::Number(2.0)));
}
#[test]
fn infinity_nan() {
let source = "enum A { X = Infinity, Y = NaN }";
@@ -82,6 +89,34 @@ fn infinity_nan() {
}
}
#[test]
fn shadowed_infinity_nan_are_not_evaluated_as_globals() {
let source =
"function f() { const Infinity = 1; const NaN = 2; enum A { X = Infinity, Y = NaN } }";
assert_eq!(get_enum_member_value(source, "X"), None);
assert_eq!(get_enum_member_value(source, "Y"), None);
}
#[test]
fn merged_enum_members_named_infinity_nan_take_precedence_over_globals() {
let source = "enum A { Infinity = 1, NaN = 2 } enum A { X = Infinity, Y = NaN }";
assert_eq!(get_enum_member_value(source, "X"), Some(ConstantValue::Number(1.0)));
assert_eq!(get_enum_member_value(source, "Y"), Some(ConstantValue::Number(2.0)));
}
#[test]
fn unevaluated_merged_enum_member_shadows_global() {
let source = "enum A { Infinity = foo() } enum A { X = Infinity }";
assert_eq!(get_enum_member_value(source, "X"), None);
}
#[test]
fn merged_enum_member_shadows_outer_binding() {
let source =
"function f() { const Infinity = 3; enum A { Infinity = 1 } enum A { X = Infinity } }";
assert_eq!(get_enum_member_value(source, "X"), Some(ConstantValue::Number(1.0)));
}
#[test]
fn string_concat() {
let source = r#"enum A { X = "a" + "b" }"#;