Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog.d/9931-class-expression-typeof-self.md
Original file line number Diff line number Diff line change
@@ -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.
9 changes: 9 additions & 0 deletions crates/perry-hir/src/lower/lower_expr/arm_unary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
27 changes: 27 additions & 0 deletions crates/perry-hir/src/lower/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading