diff --git a/changelog.d/10417-cross-module-enum-inlining.md b/changelog.d/10417-cross-module-enum-inlining.md new file mode 100644 index 0000000000..a265c1ee3c --- /dev/null +++ b/changelog.d/10417-cross-module-enum-inlining.md @@ -0,0 +1,6 @@ +Cross-module inlining now keeps functions and methods that reference TypeScript +enum members in their source module. Previously, copying such a body into an +importer made code generation resolve the enum against the importer's enum +table, causing compilation to fail with `enum member X.Y not found in enums +table`. This unblocks packages such as cheerio that use small exported helpers +backed by module-local enums (#10417). diff --git a/crates/perry-transform/src/inline/cross_module.rs b/crates/perry-transform/src/inline/cross_module.rs index ebb9b4fef9..feedd1b168 100644 --- a/crates/perry-transform/src/inline/cross_module.rs +++ b/crates/perry-transform/src/inline/cross_module.rs @@ -20,6 +20,7 @@ pub fn is_cross_module_safe(body: &[Stmt]) -> bool { | Expr::ExternFuncRef { .. } | Expr::GlobalGet(_) | Expr::GlobalSet(_, _) + | Expr::EnumMember { .. } | Expr::NativeModuleRef(_) => false, // Closures are out of scope for cross-module inlining: the // closure body has its own LocalIds, captures lists, and may @@ -396,6 +397,7 @@ fn cross_function_expr_is_safe( } Expr::GlobalGet(_) | Expr::GlobalSet(_, _) + | Expr::EnumMember { .. } | Expr::NativeModuleRef(_) // These nodes carry source-module-relative path sets. Codegen resolves // those sets through the current module's dynamic-target map, so a @@ -1181,6 +1183,7 @@ pub fn is_cross_module_safe_with_externs(body: &[Stmt], extern_names: &mut Vec false, Expr::Closure { .. } => false, Expr::ExternFuncRef { name, .. } => { diff --git a/crates/perry-transform/src/inline/mod.rs b/crates/perry-transform/src/inline/mod.rs index 11a313a1bb..ca66b6a901 100644 --- a/crates/perry-transform/src/inline/mod.rs +++ b/crates/perry-transform/src/inline/mod.rs @@ -1486,6 +1486,42 @@ mod tests { assert!(gather_cross_module_functions(&source).is_empty()); } + #[test] + fn cross_module_enum_members_stay_in_the_source_module() { + let enum_member = Expr::EnumMember { + enum_name: "CharacterCode".to_string(), + member_name: "Lt".to_string(), + }; + + let mut source = Module::new("/src/utils.ts"); + let mut helper = function(1, vec![Stmt::Return(Some(enum_member.clone()))]); + helper.name = "isHtml".to_string(); + helper.is_exported = true; + source.functions.push(helper.clone()); + source + .exported_functions + .push(("isHtml".to_string(), helper.id)); + + assert!( + gather_cross_module_functions(&source).is_empty(), + "free functions that depend on a source enum must remain outlined" + ); + + let mut class = anon_class(2, "Probe"); + class.is_exported = true; + class.methods.push(helper); + source.classes.push(class); + + assert!( + gather_cross_module_methods(&source).is_empty(), + "strict method harvesting must reject source enum references" + ); + assert!( + gather_cross_module_methods_with_extern_imports(&source).is_empty(), + "extern-aware method harvesting must reject source enum references" + ); + } + #[test] fn cross_module_free_function_with_dynamic_require_is_rejected() { let mut source = Module::new("/src/package/index.ts"); diff --git a/crates/perry/tests/source_graph_export_regressions.rs b/crates/perry/tests/source_graph_export_regressions.rs index 58df217fed..b386de38fe 100644 --- a/crates/perry/tests/source_graph_export_regressions.rs +++ b/crates/perry/tests/source_graph_export_regressions.rs @@ -972,3 +972,5 @@ mod issue_10180; mod issue_10197; #[path = "source_graph_export_regressions/issue_10258.rs"] mod issue_10258; +#[path = "source_graph_export_regressions/issue_10417.rs"] +mod issue_10417; diff --git a/crates/perry/tests/source_graph_export_regressions/issue_10417.rs b/crates/perry/tests/source_graph_export_regressions/issue_10417.rs new file mode 100644 index 0000000000..77d78d9da2 --- /dev/null +++ b/crates/perry/tests/source_graph_export_regressions/issue_10417.rs @@ -0,0 +1,34 @@ +use super::{compile_and_run, write}; + +#[test] +fn enum_members_are_not_copied_into_importing_modules() { + let dir = tempfile::tempdir().expect("tempdir"); + write( + dir.path(), + "utils.ts", + "enum CharacterCode { LowerA = 97, LowerZ = 122, Lt = 60 }\n\ + export function isHtml(str: string): boolean {\n\ + const c = str.charCodeAt(1);\n\ + return str.charCodeAt(0) === CharacterCode.Lt &&\n\ + c >= CharacterCode.LowerA && c <= CharacterCode.LowerZ;\n\ + }\n\ + export class Probe {\n\ + isLt(str: string): boolean {\n\ + return str.charCodeAt(0) === CharacterCode.Lt;\n\ + }\n\ + }\n", + ); + write( + dir.path(), + "main.ts", + "import { isHtml, Probe } from './utils';\n\ + console.log(isHtml('
'), isHtml('hello'));\n\ + const probe = new Probe();\n\ + console.log(probe.isLt('