diff --git a/changelog.d/9296-renamed-namespace-constructor.md b/changelog.d/9296-renamed-namespace-constructor.md new file mode 100644 index 0000000000..33b2654c39 --- /dev/null +++ b/changelog.d/9296-renamed-namespace-constructor.md @@ -0,0 +1,7 @@ +### Fixed + +- **Classes constructed through renamed namespace re-exports now receive their + constructor arguments (#9285).** `new ns.PublicChild(value)` resolves the + class metadata from its defining export while retaining the namespace-visible + alias, so constructor parameter properties and other initialization run + normally without sacrificing collision-safe namespace class identity. diff --git a/crates/perry-codegen/src/expr/new_dynamic.rs b/crates/perry-codegen/src/expr/new_dynamic.rs index 429fa7461b..39b221c8f2 100644 --- a/crates/perry-codegen/src/expr/new_dynamic.rs +++ b/crates/perry-codegen/src/expr/new_dynamic.rs @@ -232,7 +232,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // params from the decl-site snapshot. No-op for non-capturing // classes (no cap params to fill). if let Some(name) = try_static_class_name(callee.as_ref(), ctx) { - return crate::lower_call::lower_new_member_captured(ctx, name, args); + return crate::lower_call::lower_new_member_captured(ctx, name.as_ref(), args); } // date-fns `constructFrom(date, value)`: diff --git a/crates/perry-codegen/src/expr/v8_interop.rs b/crates/perry-codegen/src/expr/v8_interop.rs index c33a5efbf5..fca116e6d4 100644 --- a/crates/perry-codegen/src/expr/v8_interop.rs +++ b/crates/perry-codegen/src/expr/v8_interop.rs @@ -1,6 +1,8 @@ //! V8-fallback-module interop helpers + static-class-name resolution //! (extracted from `expr.rs`, issue #1098). Pure move — no logic changes. +use std::borrow::Cow; + use perry_hir::Expr; use super::FnCtx; @@ -272,10 +274,10 @@ pub(crate) fn emit_v8_member_method_call( /// `ns_id` is a namespace import local (`import * as ns from 'm'; /// new ns.Foo()`). The local id is mapped to its name via /// `ctx.local_id_to_name`, then checked against -/// `ctx.namespace_imports`. The property name is returned as the -/// class name; the rest of the lower_new path resolves it via the -/// usual `ctx.classes` lookup, which contains imported classes -/// under their original (un-namespaced) names. +/// `ctx.namespace_imports`. Namespace classes are returned under their +/// collision-free `(namespace, member)` registry key; the rest of the +/// lower_new path resolves that key through the usual `ctx.classes` +/// lookup. fn is_global_object_expr(expr: &Expr) -> bool { match expr { Expr::GlobalGet(_) => true, @@ -286,9 +288,9 @@ fn is_global_object_expr(expr: &Expr) -> bool { } } -pub(crate) fn try_static_class_name<'a>(callee: &'a Expr, ctx: &FnCtx<'_>) -> Option<&'a str> { +pub(crate) fn try_static_class_name<'a>(callee: &'a Expr, ctx: &FnCtx<'_>) -> Option> { match callee { - Expr::ClassRef(name) => Some(name.as_str()), + Expr::ClassRef(name) => Some(Cow::Borrowed(name.as_str())), // Refs #486: `new _X()` where `_X` is the inner self-binding name of // a class expression (e.g. `var X = class _X { ... new _X() ... }`) // lowers to `NewDynamic { callee: ExternFuncRef("_X") }` because the @@ -299,12 +301,14 @@ pub(crate) fn try_static_class_name<'a>(callee: &'a Expr, ctx: &FnCtx<'_>) -> Op // compile_module entry. Without this, the call falls through to the // empty-object placeholder path with class_id=0 and method dispatch // breaks on the resulting instance. - Expr::ExternFuncRef { name, .. } if ctx.class_ids.contains_key(name) => Some(name.as_str()), + Expr::ExternFuncRef { name, .. } if ctx.class_ids.contains_key(name) => { + Some(Cow::Borrowed(name.as_str())) + } Expr::PropertyGet { object, property, .. } => { if is_global_object_expr(object.as_ref()) { - return Some(property.as_str()); + return Some(Cow::Borrowed(property.as_str())); } // Namespace import: `import * as ns from 'm'; new ns.Foo()`. // The namespace object surfaces either as `LocalGet(id)` (mapped to @@ -325,16 +329,22 @@ pub(crate) fn try_static_class_name<'a>(callee: &'a Expr, ctx: &FnCtx<'_>) -> Op // member as a closure value and run it via // `js_new_function_construct` (which also intercepts the builtin // global thunks, so re-exported builtins keep working). - let object_is_namespace = match object.as_ref() { + let namespace = match object.as_ref() { Expr::LocalGet(id) => ctx .local_id_to_name .get(id) - .is_some_and(|name| ctx.namespace_imports.contains(name)), - Expr::ExternFuncRef { name, .. } => ctx.namespace_imports.contains(name), - _ => false, + .filter(|name| ctx.namespace_imports.contains(*name)) + .map(String::as_str), + Expr::ExternFuncRef { name, .. } if ctx.namespace_imports.contains(name) => { + Some(name.as_str()) + } + _ => None, }; - if object_is_namespace && ctx.classes.contains_key(property.as_str()) { - return Some(property.as_str()); + if let Some(namespace) = namespace { + let class_name = crate::namespace_member_class_key(namespace, property); + if ctx.classes.contains_key(&class_name) { + return Some(Cow::Owned(class_name)); + } } None } diff --git a/crates/perry-codegen/src/type_analysis/predicates.rs b/crates/perry-codegen/src/type_analysis/predicates.rs index 9c97a56504..79f9482947 100644 --- a/crates/perry-codegen/src/type_analysis/predicates.rs +++ b/crates/perry-codegen/src/type_analysis/predicates.rs @@ -578,7 +578,7 @@ pub(crate) fn static_type_of(ctx: &FnCtx<'_>, e: &Expr) -> Option { return Some(HirType::String); } if let Some(static_method_ty) = crate::expr::try_static_class_name(object, ctx) - .and_then(|class_name| ctx.classes.get(class_name)) + .and_then(|class_name| ctx.classes.get(class_name.as_ref())) .and_then(|class| { class .static_methods diff --git a/crates/perry/src/commands/compile/run_pipeline.rs b/crates/perry/src/commands/compile/run_pipeline.rs index f56a8295e4..2cd21a99d5 100644 --- a/crates/perry/src/commands/compile/run_pipeline.rs +++ b/crates/perry/src/commands/compile/run_pipeline.rs @@ -3826,7 +3826,20 @@ pub fn run_with_parse_cache( export_name, )); } - if let Some(class) = exported_classes.get(&key) { + // #9285: re-export renames keep the class + // metadata under its origin name (`Child`), not + // the namespace-visible alias (`PublicChild`). + // The var path above already consults this + // origin key; class registration must do the + // same or `new ns.PublicChild(arg)` falls + // through to a function wrapper and never runs + // the class constructor. + let imported_class = exported_classes.get(&key).or_else(|| { + origin_key_under_origin_name + .as_ref() + .and_then(|origin_key| exported_classes.get(origin_key)) + }); + if let Some(class) = imported_class { let class_prefix = canonical_class_source_prefix( class, &class_canonical_path, @@ -4310,7 +4323,19 @@ pub fn run_with_parse_cache( ), ); } - if let Some(class) = exported_classes.get(&key) { + // #9285: a renamed class re-export is + // indexed under its origin name, while this + // namespace exposes `export_name`. Resolve + // through the same origin key used for the + // var classification above so the scoped + // ImportedClass retains both identities. + let imported_class = + exported_classes.get(&key).or_else(|| { + origin_key_under_origin_name.as_ref().and_then( + |origin_key| exported_classes.get(origin_key), + ) + }); + if let Some(class) = imported_class { let class_prefix = canonical_class_source_prefix( class, &class_canonical_path,