From b22221666b5489c9f9454ffd5e1b362583510e3c Mon Sep 17 00:00:00 2001 From: Andrew DiZenzo Date: Wed, 3 Jun 2026 16:26:12 +0000 Subject: [PATCH] fix(compile): canonicalize re-exported class method prefixes --- crates/perry/src/commands/compile.rs | 83 ++++++++++++++++++- .../issue_1021_rxjs_reexport_methods/index.ts | 2 + .../internal/Observable.ts | 11 +++ .../internal/Subject.ts | 17 ++++ .../test_issue_1021_rxjs_reexport_methods.ts | 18 ++++ 5 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 test-files/fixtures/issue_1021_rxjs_reexport_methods/index.ts create mode 100644 test-files/fixtures/issue_1021_rxjs_reexport_methods/internal/Observable.ts create mode 100644 test-files/fixtures/issue_1021_rxjs_reexport_methods/internal/Subject.ts create mode 100644 test-files/test_issue_1021_rxjs_reexport_methods.ts diff --git a/crates/perry/src/commands/compile.rs b/crates/perry/src/commands/compile.rs index 1414a6e6a7..12e07fbe6b 100644 --- a/crates/perry/src/commands/compile.rs +++ b/crates/perry/src/commands/compile.rs @@ -103,6 +103,63 @@ use super::progress::{ProgressSnapshot, VerboseProgress}; mod types; pub use types::*; +fn canonical_class_source_prefix( + class: &perry_hir::Class, + class_canonical_path: &HashMap, + project_root: &Path, + fallback_prefix: &str, +) -> String { + class_canonical_path + .get(&class.id) + .map(|path| compute_module_prefix(path, project_root)) + .unwrap_or_else(|| fallback_prefix.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn canonical_class_source_prefix_prefers_defining_path() { + let class = perry_hir::Class { + id: 7, + name: "Observable".to_string(), + type_params: Vec::new(), + extends: None, + extends_name: None, + native_extends: None, + extends_expr: None, + fields: Vec::new(), + constructor: None, + methods: Vec::new(), + getters: Vec::new(), + setters: Vec::new(), + static_fields: Vec::new(), + static_methods: Vec::new(), + computed_members: Vec::new(), + decorators: Vec::new(), + is_exported: true, + aliases: Vec::new(), + }; + let project_root = PathBuf::from("/repo"); + let mut class_canonical_path = HashMap::new(); + class_canonical_path.insert( + class.id, + "/repo/node_modules/rxjs/src/internal/Observable.ts".to_string(), + ); + + assert_eq!( + canonical_class_source_prefix( + &class, + &class_canonical_path, + &project_root, + "node_modules_rxjs_src_index_ts", + ), + "node_modules_rxjs_src_internal_Observable_ts" + ); + } +} + // `inject_ios_deeplinks`, `inject_google_auth_info_plist`, and // `lookup_bundle_id_from_info_plist` moved to `apple_info_plist.rs`. // `rust_target_triple` moved to `app_metadata.rs`. @@ -2194,10 +2251,16 @@ pub fn run_with_parse_cache( imported_vars.insert(export_name.clone()); } if let Some(class) = exported_classes.get(&key) { + let class_prefix = canonical_class_source_prefix( + class, + &class_canonical_path, + &ctx.project_root, + &origin_prefix, + ); imported_classes.push(perry_codegen::ImportedClass { name: class.name.clone(), local_alias: None, - source_prefix: origin_prefix.clone(), + source_prefix: class_prefix, constructor_param_count: class .constructor .as_ref() @@ -2382,10 +2445,16 @@ pub fn run_with_parse_cache( imported_vars.insert(export_name.clone()); } if let Some(class) = exported_classes.get(&key) { + let class_prefix = canonical_class_source_prefix( + class, + &class_canonical_path, + &ctx.project_root, + &origin_prefix, + ); imported_classes.push(perry_codegen::ImportedClass { name: class.name.clone(), local_alias: None, - source_prefix: origin_prefix.clone(), + source_prefix: class_prefix, constructor_param_count: class .constructor .as_ref() @@ -2599,6 +2668,12 @@ pub fn run_with_parse_cache( // Imported classes if let Some(class) = exported_classes.get(&key) { + let class_prefix = canonical_class_source_prefix( + class, + &class_canonical_path, + &ctx.project_root, + &effective_prefix, + ); // Issue #665: when the user wrote `import X from "pkg"` // and `pkg`'s default export is a class, the importer // still registers `exported_name="default"` into @@ -2625,7 +2700,7 @@ pub fn run_with_parse_cache( imported_classes.push(perry_codegen::ImportedClass { name: class.name.clone(), local_alias: Some(exported_name.clone()), - source_prefix: effective_prefix.clone(), + source_prefix: class_prefix.clone(), constructor_param_count: class .constructor .as_ref() @@ -2689,7 +2764,7 @@ pub fn run_with_parse_cache( } else { None }, - source_prefix: effective_prefix.clone(), + source_prefix: class_prefix, constructor_param_count: class .constructor .as_ref() diff --git a/test-files/fixtures/issue_1021_rxjs_reexport_methods/index.ts b/test-files/fixtures/issue_1021_rxjs_reexport_methods/index.ts new file mode 100644 index 0000000000..2ef9d44730 --- /dev/null +++ b/test-files/fixtures/issue_1021_rxjs_reexport_methods/index.ts @@ -0,0 +1,2 @@ +export { Observable } from "./internal/Observable.js"; +export { Subject } from "./internal/Subject.js"; diff --git a/test-files/fixtures/issue_1021_rxjs_reexport_methods/internal/Observable.ts b/test-files/fixtures/issue_1021_rxjs_reexport_methods/internal/Observable.ts new file mode 100644 index 0000000000..5ffcf5bb74 --- /dev/null +++ b/test-files/fixtures/issue_1021_rxjs_reexport_methods/internal/Observable.ts @@ -0,0 +1,11 @@ +export class Observable { + private value: string; + + constructor(value: string) { + this.value = value; + } + + subscribe(): string { + return "subscribe:" + this.value; + } +} diff --git a/test-files/fixtures/issue_1021_rxjs_reexport_methods/internal/Subject.ts b/test-files/fixtures/issue_1021_rxjs_reexport_methods/internal/Subject.ts new file mode 100644 index 0000000000..c3980ca52c --- /dev/null +++ b/test-files/fixtures/issue_1021_rxjs_reexport_methods/internal/Subject.ts @@ -0,0 +1,17 @@ +import { Observable } from "../index.js"; + +export class Subject extends Observable { + private closed = false; + + error(message: string): string { + this.closed = true; + return "error:" + message; + } + + next(value: string): string { + if (this.closed) { + return "closed"; + } + return this.subscribe() + ":next:" + value; + } +} diff --git a/test-files/test_issue_1021_rxjs_reexport_methods.ts b/test-files/test_issue_1021_rxjs_reexport_methods.ts new file mode 100644 index 0000000000..e0cbc8c869 --- /dev/null +++ b/test-files/test_issue_1021_rxjs_reexport_methods.ts @@ -0,0 +1,18 @@ +// Issue #1021 follow-up: RxJS-style barrels re-export classes from +// `src/internal/*`, while consumers import from the package index. The +// cross-module method metadata must keep the defining file's prefix, not +// the barrel's prefix, or the link step references method symbols that no +// module emitted. + +import { Observable, Subject } from "./fixtures/issue_1021_rxjs_reexport_methods/index.js"; + +function read(obs: Observable): string { + return obs.subscribe(); +} + +const subject = new Subject("seed"); + +console.log(read(new Observable("plain"))); +console.log(subject.next("value")); +console.log(subject.error("boom")); +console.log(subject.next("after"));