diff --git a/crates/perry/src/commands/compile/run_pipeline.rs b/crates/perry/src/commands/compile/run_pipeline.rs index b43c777e2..5bc18cda3 100644 --- a/crates/perry/src/commands/compile/run_pipeline.rs +++ b/crates/perry/src/commands/compile/run_pipeline.rs @@ -17,6 +17,78 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use crate::OutputFormat; +/// Builds the complete codegen view of a foreign HIR class. +/// +/// Callers choose only the import binding (when the route introduces one) and +/// the already-resolved defining-module prefix. All other metadata must +/// describe the class definition itself, so keeping it here prevents import +/// routes from drifting as `ImportedClass` gains fields. +fn imported_class_from_hir( + class: &perry_hir::Class, + source_prefix: String, + local_alias: Option, +) -> perry_codegen::ImportedClass { + perry_codegen::ImportedClass { + name: class.name.clone(), + local_alias, + source_prefix, + constructor_param_count: class + .constructor + .as_ref() + .map(|ctor| ctor.params.len()) + .unwrap_or(0), + has_own_constructor: class.constructor.is_some(), + constructor_has_rest: class + .constructor + .as_ref() + .map(|ctor| ctor.params.iter().any(|param| param.is_rest)) + .unwrap_or(false), + has_instance_fields: !class.fields.is_empty(), + method_names: class + .methods + .iter() + .map(|method| method.name.clone()) + .collect(), + method_param_counts: class + .methods + .iter() + .map(|method| method.params.len()) + .collect(), + method_has_rest: class + .methods + .iter() + .map(|method| method.params.iter().any(|param| param.is_rest)) + .collect(), + static_field_names: class + .static_fields + .iter() + .filter(|field| field.key_expr.is_none()) + .map(|field| field.name.clone()) + .collect(), + static_method_names: class + .static_methods + .iter() + .map(|method| method.name.clone()) + .collect(), + getter_names: class.getters.iter().map(|(name, _)| name.clone()).collect(), + setter_names: class.setters.iter().map(|(name, _)| name.clone()).collect(), + parent_name: class.extends_name.clone(), + field_names: class + .fields + .iter() + .filter(|field| field.key_expr.is_none()) + .map(|field| field.name.clone()) + .collect(), + field_types: class + .fields + .iter() + .filter(|field| field.key_expr.is_none()) + .map(|field| field.ty.clone()) + .collect(), + source_class_id: Some(class.id), + } +} + /// Same as [`run`] but accepts an optional in-memory [`ParseCache`] that /// `perry dev` uses to reuse parsed ASTs across rebuilds in a single session. /// Pass `None` for the batch-compile path. @@ -2421,73 +2493,16 @@ pub fn run_with_parse_cache( &ctx.project_root, &origin_prefix, ); - imported_classes.push(perry_codegen::ImportedClass { - name: class.name.clone(), - local_alias: None, - source_prefix: class_prefix, - constructor_param_count: class - .constructor - .as_ref() - .map(|c| c.params.len()) - .unwrap_or(0), - has_own_constructor: class.constructor.is_some(), - constructor_has_rest: class - .constructor - .as_ref() - .map(|c| c.params.iter().any(|p| p.is_rest)) - .unwrap_or(false), - has_instance_fields: !class.fields.is_empty(), - method_names: class - .methods - .iter() - .map(|m| m.name.clone()) - .collect(), - method_param_counts: class - .methods - .iter() - .map(|m| m.params.len()) - .collect(), - method_has_rest: class - .methods - .iter() - .map(|m| m.params.iter().any(|p| p.is_rest)) - .collect(), - static_method_names: class - .static_methods - .iter() - .map(|m| m.name.clone()) - .collect(), - static_field_names: class - .static_fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.name.clone()) - .collect(), - getter_names: class - .getters - .iter() - .map(|(n, _)| n.clone()) - .collect(), - setter_names: class - .setters - .iter() - .map(|(n, _)| n.clone()) - .collect(), - parent_name: class.extends_name.clone(), - field_names: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.name.clone()) - .collect(), - field_types: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.ty.clone()) - .collect(), - source_class_id: Some(class.id), - }); + let local_alias = if export_name == &class.name { + None + } else { + Some(export_name.clone()) + }; + imported_classes.push(imported_class_from_hir( + class, + class_prefix, + local_alias, + )); } if let Some(members) = exported_enums.get(&key) { imported_enums.push((export_name.clone(), members.clone())); @@ -2730,73 +2745,16 @@ pub fn run_with_parse_cache( &ctx.project_root, &origin_prefix, ); - imported_classes.push(perry_codegen::ImportedClass { - name: class.name.clone(), - local_alias: None, - source_prefix: class_prefix, - constructor_param_count: class - .constructor - .as_ref() - .map(|c| c.params.len()) - .unwrap_or(0), - has_own_constructor: class.constructor.is_some(), - constructor_has_rest: class - .constructor - .as_ref() - .map(|c| c.params.iter().any(|p| p.is_rest)) - .unwrap_or(false), - has_instance_fields: !class.fields.is_empty(), - method_names: class - .methods - .iter() - .map(|m| m.name.clone()) - .collect(), - method_param_counts: class - .methods - .iter() - .map(|m| m.params.len()) - .collect(), - method_has_rest: class - .methods - .iter() - .map(|m| m.params.iter().any(|p| p.is_rest)) - .collect(), - static_method_names: class - .static_methods - .iter() - .map(|m| m.name.clone()) - .collect(), - static_field_names: class - .static_fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.name.clone()) - .collect(), - getter_names: class - .getters - .iter() - .map(|(n, _)| n.clone()) - .collect(), - setter_names: class - .setters - .iter() - .map(|(n, _)| n.clone()) - .collect(), - parent_name: class.extends_name.clone(), - field_names: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.name.clone()) - .collect(), - field_types: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.ty.clone()) - .collect(), - source_class_id: Some(class.id), - }); + let local_alias = if export_name == &class.name { + None + } else { + Some(export_name.clone()) + }; + imported_classes.push(imported_class_from_hir( + class, + class_prefix, + local_alias, + )); } if let Some(members) = exported_enums.get(&key) { imported_enums.push((export_name.clone(), members.clone())); @@ -3005,133 +2963,21 @@ pub fn run_with_parse_cache( // letting consumer-side `Expr::ExternFuncRef { name: // exported_name }` resolve to the class-id NaN-box. if local_name != exported_name { - imported_classes.push(perry_codegen::ImportedClass { - name: class.name.clone(), - local_alias: Some(exported_name.clone()), - source_prefix: class_prefix.clone(), - constructor_param_count: class - .constructor - .as_ref() - .map(|c| c.params.len()) - .unwrap_or(0), - has_own_constructor: class.constructor.is_some(), - constructor_has_rest: class - .constructor - .as_ref() - .map(|c| c.params.iter().any(|p| p.is_rest)) - .unwrap_or(false), - has_instance_fields: !class.fields.is_empty(), - method_names: class - .methods - .iter() - .map(|m| m.name.clone()) - .collect(), - method_param_counts: class - .methods - .iter() - .map(|m| m.params.len()) - .collect(), - method_has_rest: class - .methods - .iter() - .map(|m| m.params.iter().any(|p| p.is_rest)) - .collect(), - static_method_names: class - .static_methods - .iter() - .map(|m| m.name.clone()) - .collect(), - static_field_names: class - .static_fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.name.clone()) - .collect(), - getter_names: class - .getters - .iter() - .map(|(n, _)| n.clone()) - .collect(), - setter_names: class - .setters - .iter() - .map(|(n, _)| n.clone()) - .collect(), - parent_name: class.extends_name.clone(), - field_names: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.name.clone()) - .collect(), - field_types: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.ty.clone()) - .collect(), - source_class_id: Some(class.id), - }); + imported_classes.push(imported_class_from_hir( + class, + class_prefix.clone(), + Some(exported_name.clone()), + )); } - imported_classes.push(perry_codegen::ImportedClass { - name: class.name.clone(), - local_alias: if local_name != class.name { + imported_classes.push(imported_class_from_hir( + class, + class_prefix, + if local_name != class.name { Some(local_name.clone()) } else { None }, - source_prefix: class_prefix, - constructor_param_count: class - .constructor - .as_ref() - .map(|c| c.params.len()) - .unwrap_or(0), - has_own_constructor: class.constructor.is_some(), - constructor_has_rest: class - .constructor - .as_ref() - .map(|c| c.params.iter().any(|p| p.is_rest)) - .unwrap_or(false), - has_instance_fields: !class.fields.is_empty(), - method_names: class.methods.iter().map(|m| m.name.clone()).collect(), - method_param_counts: class - .methods - .iter() - .map(|m| m.params.len()) - .collect(), - method_has_rest: class - .methods - .iter() - .map(|m| m.params.iter().any(|p| p.is_rest)) - .collect(), - static_method_names: class - .static_methods - .iter() - .map(|m| m.name.clone()) - .collect(), - static_field_names: class - .static_fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.name.clone()) - .collect(), - getter_names: class.getters.iter().map(|(n, _)| n.clone()).collect(), - setter_names: class.setters.iter().map(|(n, _)| n.clone()).collect(), - parent_name: class.extends_name.clone(), - field_names: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.name.clone()) - .collect(), - field_types: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.ty.clone()) - .collect(), - source_class_id: Some(class.id), - }); + )); } // Imported param counts @@ -3236,61 +3082,7 @@ pub fn run_with_parse_cache( continue; } let class_prefix = compute_module_prefix(&src_path, &ctx.project_root); - imported_classes.push(perry_codegen::ImportedClass { - name: class.name.clone(), - local_alias: None, - source_prefix: class_prefix, - constructor_param_count: class - .constructor - .as_ref() - .map(|c| c.params.len()) - .unwrap_or(0), - has_own_constructor: class.constructor.is_some(), - constructor_has_rest: class - .constructor - .as_ref() - .map(|c| c.params.iter().any(|p| p.is_rest)) - .unwrap_or(false), - has_instance_fields: !class.fields.is_empty(), - method_names: class.methods.iter().map(|m| m.name.clone()).collect(), - method_param_counts: class - .methods - .iter() - .map(|m| m.params.len()) - .collect(), - method_has_rest: class - .methods - .iter() - .map(|m| m.params.iter().any(|p| p.is_rest)) - .collect(), - static_method_names: class - .static_methods - .iter() - .map(|m| m.name.clone()) - .collect(), - static_field_names: class - .static_fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.name.clone()) - .collect(), - getter_names: class.getters.iter().map(|(n, _)| n.clone()).collect(), - setter_names: class.setters.iter().map(|(n, _)| n.clone()).collect(), - parent_name: class.extends_name.clone(), - field_names: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.name.clone()) - .collect(), - field_types: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.ty.clone()) - .collect(), - source_class_id: Some(class.id), - }); + imported_classes.push(imported_class_from_hir(class, class_prefix, None)); } } } @@ -3710,61 +3502,7 @@ pub fn run_with_parse_cache( continue; } let class_prefix = compute_module_prefix(&src_path, &ctx.project_root); - imported_classes.push(perry_codegen::ImportedClass { - name: class.name.clone(), - local_alias: None, - source_prefix: class_prefix, - constructor_param_count: class - .constructor - .as_ref() - .map(|c| c.params.len()) - .unwrap_or(0), - has_own_constructor: class.constructor.is_some(), - constructor_has_rest: class - .constructor - .as_ref() - .map(|c| c.params.iter().any(|p| p.is_rest)) - .unwrap_or(false), - has_instance_fields: !class.fields.is_empty(), - method_names: class.methods.iter().map(|m| m.name.clone()).collect(), - method_param_counts: class - .methods - .iter() - .map(|m| m.params.len()) - .collect(), - method_has_rest: class - .methods - .iter() - .map(|m| m.params.iter().any(|p| p.is_rest)) - .collect(), - static_method_names: class - .static_methods - .iter() - .map(|m| m.name.clone()) - .collect(), - static_field_names: class - .static_fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.name.clone()) - .collect(), - getter_names: class.getters.iter().map(|(n, _)| n.clone()).collect(), - setter_names: class.setters.iter().map(|(n, _)| n.clone()).collect(), - parent_name: class.extends_name.clone(), - field_names: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.name.clone()) - .collect(), - field_types: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.ty.clone()) - .collect(), - source_class_id: Some(class.id), - }); + imported_classes.push(imported_class_from_hir(class, class_prefix, None)); } } } @@ -3916,61 +3654,7 @@ pub fn run_with_parse_cache( } else { None }; - imported_classes.push(perry_codegen::ImportedClass { - name: class.name.clone(), - local_alias: alias, - source_prefix: class_prefix, - constructor_param_count: class - .constructor - .as_ref() - .map(|c| c.params.len()) - .unwrap_or(0), - has_own_constructor: class.constructor.is_some(), - constructor_has_rest: class - .constructor - .as_ref() - .map(|c| c.params.iter().any(|p| p.is_rest)) - .unwrap_or(false), - has_instance_fields: !class.fields.is_empty(), - method_names: class.methods.iter().map(|m| m.name.clone()).collect(), - method_param_counts: class - .methods - .iter() - .map(|m| m.params.len()) - .collect(), - method_has_rest: class - .methods - .iter() - .map(|m| m.params.iter().any(|p| p.is_rest)) - .collect(), - static_method_names: class - .static_methods - .iter() - .map(|m| m.name.clone()) - .collect(), - static_field_names: class - .static_fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.name.clone()) - .collect(), - getter_names: class.getters.iter().map(|(n, _)| n.clone()).collect(), - setter_names: class.setters.iter().map(|(n, _)| n.clone()).collect(), - parent_name: class.extends_name.clone(), - field_names: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.name.clone()) - .collect(), - field_types: class - .fields - .iter() - .filter(|f| f.key_expr.is_none()) - .map(|f| f.ty.clone()) - .collect(), - source_class_id: Some(class.id), - }); + imported_classes.push(imported_class_from_hir(class, class_prefix, alias)); visited_imports.insert(ref_name.clone()); // Process the entry we just pushed (by index, so a // same-named distinct-module class isn't skipped). Refs #26. diff --git a/crates/perry/tests/functional_batch2_regressions.rs b/crates/perry/tests/functional_batch2_regressions.rs index bcee1e9cb..b0ea0e407 100644 --- a/crates/perry/tests/functional_batch2_regressions.rs +++ b/crates/perry/tests/functional_batch2_regressions.rs @@ -198,3 +198,83 @@ console.log("neg:", ({} as any) instanceof ns.Thing);"#, ); assert_eq!(stdout, "member: true\nlocal: true\nneg: false\n"); } + +/// Characterizes the complete metadata surface assembled for an imported +/// class. The class travels through a renamed re-export and named alias, so its +/// source class id must remain the defining module's id rather than a fresh +/// importer-local id. +#[test] +fn imported_class_metadata_survives_named_alias_reexport() { + let dir = tempfile::tempdir().expect("tempdir"); + let stdout = compile_and_run( + dir.path(), + &[ + ("base.ts", r#"export class Parent { base = "parent"; }"#), + ( + "model.ts", + r#"import { Parent } from "./base.ts"; +const computedName = "not-a-static-global"; +export class Child extends Parent { + next: Parent = new Parent(); + private saved = ""; + static plain = "static"; + static [computedName] = "computed"; + constructor(...parts: string[]) { super(); this.saved = parts.join("|"); } + get value() { return this.saved; } + set value(value: string) { this.saved = "set:" + value; } + describe(first: string, ...tail: string[]) { return this.value + ":" + first + ":" + tail.length; } +}"#, + ), + ( + "barrel.ts", + r#"export { Child as PublicChild } from "./model.ts";"#, + ), + ( + "main.ts", + r#"import { PublicChild as LocalChild } from "./barrel.ts"; +const value: any = new LocalChild("one", "two"); +value.value = "changed"; +console.log("method:", value.describe("head", "tail-a", "tail-b")); +console.log("parent:", value.next.base); +console.log("static:", LocalChild.plain); +console.log("computed:", LocalChild["not-a-static-global"]); +console.log("named:", value instanceof LocalChild);"#, + ), + ], + "main.ts", + ); + assert_eq!( + stdout, + "method: set:changed:head:2\nparent: parent\nstatic: static\ncomputed: computed\nnamed: true\n" + ); +} + +/// A renamed re-export must retain its visible export name when consumed only +/// through a namespace. Keeping this entry point separate prevents named-import +/// metadata from masking a missing namespace registration. +#[test] +fn imported_class_namespace_reexport_uses_visible_alias() { + let dir = tempfile::tempdir().expect("tempdir"); + let stdout = compile_and_run( + dir.path(), + &[ + ( + "model.ts", + r#"export class Child { constructor(public value: string) {} }"#, + ), + ( + "barrel.ts", + r#"export { Child as PublicChild } from "./model.ts";"#, + ), + ( + "main.ts", + r#"import * as barrel from "./barrel.ts"; +const value: any = new barrel.PublicChild("namespace"); +console.log("value:", value.value); +console.log("instanceof:", value instanceof barrel.PublicChild);"#, + ), + ], + "main.ts", + ); + assert_eq!(stdout, "value: namespace\ninstanceof: true\n"); +}