diff --git a/changelog.d/9931-class-expression-typeof-self.md b/changelog.d/9931-class-expression-typeof-self.md new file mode 100644 index 0000000000..1a805508fa --- /dev/null +++ b/changelog.d/9931-class-expression-typeof-self.md @@ -0,0 +1,7 @@ +### Fixed + +- Resolve a named class expression's inner name inside `typeof`, including + when its outer binding has a different name and the compiler registers the + class under a generated key. The unresolved-global shortcut now excludes the + active class inner name so lexical lowering can resolve the class binding; + an HIR regression covers that path. diff --git a/crates/perry-hir/src/lower/lower_expr/arm_unary.rs b/crates/perry-hir/src/lower/lower_expr/arm_unary.rs index 848592b200..49d98308eb 100644 --- a/crates/perry-hir/src/lower/lower_expr/arm_unary.rs +++ b/crates/perry-hir/src/lower/lower_expr/arm_unary.rs @@ -124,6 +124,15 @@ pub(crate) fn lower_unary_expr(ctx: &mut LoweringContext, unary: &ast::UnaryExpr && ctx.lookup_native_module(n).is_none() && ctx.lookup_imported_func(n).is_none() && ctx.lookup_class(n).is_none() + // A named class expression can use a synthetic registry key + // while its source-level inner binding keeps the written + // name. Let ordinary identifier lowering resolve that binding + // to the current class instead of treating it as an optional + // global. For example, `var B = class l { static f() { + // return typeof l } }` registers the class under a generated + // `l__class_expr_*` key, but `l` is still lexically bound in + // the class body. + && ctx.current_class_inner_name.as_deref() != Some(n) && !is_builtin_function(n) && !is_known_global_identifier_name(n) && !matches!(n, "undefined" | "null" | "NaN" | "Infinity") diff --git a/crates/perry-hir/src/lower/tests.rs b/crates/perry-hir/src/lower/tests.rs index fcf1c2943d..946f1264e9 100644 --- a/crates/perry-hir/src/lower/tests.rs +++ b/crates/perry-hir/src/lower/tests.rs @@ -1182,6 +1182,33 @@ fn named_class_expr_static_private_update_in_arrow_keeps_lexical_brand_owner() { ); } +/// A named class expression whose outer binding has a different name uses a +/// synthetic registry key. `typeof` must still resolve the source-level inner +/// name through the class body's lexical binding rather than an optional +/// global lookup. +#[test] +fn typeof_named_class_expr_inner_binding_uses_the_current_class() { + let source = r#" + var B = class l { + static selfType(): string { return typeof l; } + }; + "#; + let module = perry_parser::parse_typescript(source, "t.ts").expect("source parses"); + let hir = super::lower_module(&module, "t", "t.ts").expect("source lowers"); + let method = hir + .classes + .iter() + .flat_map(|class| &class.static_methods) + .find(|method| method.name == "selfType") + .expect("static selfType method is lowered"); + let body = format!("{:#?}", method.body); + + assert!( + body.contains("ClassRef") && !body.contains("js_global_get_optional"), + "the class's inner name must resolve to its synthetic ClassRef: {body}" + ); +} + /// A sibling class declaration is already a known lexical binding while an /// earlier class method is lowered, even though its registry entry is emitted /// later. The unresolved-constructor guard must preserve that forward binding.