diff --git a/changelog.d/8466-create-require-not-a-shadow.md b/changelog.d/8466-create-require-not-a-shadow.md new file mode 100644 index 0000000000..15819d35f7 --- /dev/null +++ b/changelog.d/8466-create-require-not-a-shadow.md @@ -0,0 +1 @@ +fix(hir): a local `require` created by node:module's `createRequire` no longer counts as shadowing the require intrinsic (#8465). #8343's shadow guard regressed `const require = createRequire(import.meta.url); const net = require("net")` — the mysql2/turbopack bundling idiom — off the static native-namespace fold and onto a runtime dispatch surface whose `net.connect` arm is a null function pointer in default-feature builds, silently returning undefined. The binding is now marked createRequire-backed at declaration and exempted; a real `function require(...)` body (the CJS wrap's synthetic require, #8343) still shadows. diff --git a/crates/perry-hir/src/destructuring/var_decl/native_fetch.rs b/crates/perry-hir/src/destructuring/var_decl/native_fetch.rs index cbdd2b3777..d357c1412b 100644 --- a/crates/perry-hir/src/destructuring/var_decl/native_fetch.rs +++ b/crates/perry-hir/src/destructuring/var_decl/native_fetch.rs @@ -31,6 +31,28 @@ pub(crate) fn register_native_fetch_and_streams( // fs/path/crypto-only `is_require_builtin_module` path. Non-literal // / unresolvable specifiers fall through to the legacy compile-time // refusal in `expr_call::intrinsics::try_require_literal`. + // #8465: `const require = createRequire(import.meta.url)` — recognize the + // binding as the REAL module-scoped require (see the context field's doc) + // before the shadow check below consults it. Accepts a renamed import + // (`import { createRequire as cr } from "node:module"`) by resolving the + // callee through the imported-function table. + if name == "require" { + if let Some(init_expr) = &decl.init { + if let ast::Expr::Call(call) = init_expr.as_ref() { + if let ast::Callee::Expr(callee) = &call.callee { + if let ast::Expr::Ident(callee_ident) = callee.as_ref() { + let callee_name = callee_ident.sym.as_ref(); + let is_create_require = callee_name == "createRequire" + || ctx.lookup_imported_func(callee_name) == Some("createRequire"); + if is_create_require { + ctx.require_local_is_create_require = true; + } + } + } + } + } + } + if let Some(init_expr) = &decl.init { // #8342: inside a CJS-wrapped module the wrap's synthetic // `function require(...)` (with a `createRequire`-backed built-in arm) diff --git a/crates/perry-hir/src/destructuring/var_decl_sources.rs b/crates/perry-hir/src/destructuring/var_decl_sources.rs index 132254ee44..73aa53b0f2 100644 --- a/crates/perry-hir/src/destructuring/var_decl_sources.rs +++ b/crates/perry-hir/src/destructuring/var_decl_sources.rs @@ -70,7 +70,15 @@ pub(crate) fn require_resolvable_native_specifier(init: &ast::Expr) -> Option bool { - ctx.lookup_local("require").is_some() + // #8465: a local `require` created by node:module's `createRequire` IS the + // module-scoped require — for builtin specifiers it returns exactly the + // native namespace, so it must not suppress the static namespace fast + // path (pre-#8343 behavior for this idiom; regressed net.connect reached + // as a bound value to a runtime dispatch arm that is null in + // default-feature builds). + let local_shadow = + ctx.lookup_local("require").is_some() && !ctx.require_local_is_create_require; + local_shadow || ctx.lookup_func("require").is_some() || ctx.lookup_imported_func("require").is_some() } diff --git a/crates/perry-hir/src/lower/context.rs b/crates/perry-hir/src/lower/context.rs index 69b4fd5904..1aa0471358 100644 --- a/crates/perry-hir/src/lower/context.rs +++ b/crates/perry-hir/src/lower/context.rs @@ -223,6 +223,7 @@ impl LoweringContext { strict_mode_stack: Vec::new(), is_external_module: false, optional_require_try_depth: 0, + require_local_is_create_require: false, fn_ctor_env: super::fn_ctor_env::FnCtorEnv::default(), expr_lower_depth: 0, prelowered_member_receiver: None, diff --git a/crates/perry-hir/src/lower/lowering_context.rs b/crates/perry-hir/src/lower/lowering_context.rs index 0c22af08d4..7dda4662f0 100644 --- a/crates/perry-hir/src/lower/lowering_context.rs +++ b/crates/perry-hir/src/lower/lowering_context.rs @@ -982,6 +982,17 @@ pub struct LoweringContext { /// observe the failure instead of rejecting the whole user source at /// compile time. Outside try blocks, `require(literal)` still hard-errors. pub(crate) optional_require_try_depth: u32, + /// #8465: `const require = createRequire(import.meta.url)` (node:module's + /// own ESM idiom, including an aliased import of `createRequire`) binds + /// the REAL module-scoped CommonJS require — for builtin specifiers it + /// returns exactly the native namespace. Set when that declaration is + /// seen so `require_is_shadowed_by_local` does not treat the binding as + /// shadowing the require intrinsic; the CJS wrap's synthetic + /// `function require(...)` (a real function with a body) still shadows + /// via `lookup_func`. Module-wide and scope-blind like `proxy_locals` — + /// strictly narrower than the pre-#8343 behavior, which ignored ALL + /// local `require` bindings on this path. + pub(crate) require_local_is_create_require: bool, /// Pre-scanned constant environment for `new Function` / `Function(...)` /// argument resolution (single-assignment module vars, `toString`-bearing /// object literals, counters). Built once per module in diff --git a/crates/perry-hir/src/lower/tests.rs b/crates/perry-hir/src/lower/tests.rs index 4aa02682ed..1174f1deb3 100644 --- a/crates/perry-hir/src/lower/tests.rs +++ b/crates/perry-hir/src/lower/tests.rs @@ -990,7 +990,6 @@ fn method_local_shadowing_the_class_name_still_wins_in_new() { "a method-scope local named after the class must still win for `new`: {body}" ); } - /// #8447: the ambient-typing idiom `declare function require(name: string): any` /// names the global require intrinsic — it must NOT be registered as an /// external FFI function. That registration made every require-shadowing guard @@ -1042,3 +1041,56 @@ fn test_user_require_function_with_body_still_shadows_the_intrinsic() { call must not be rewritten into a native-module namespace: {dump}" ); } + +/// #8465: `const require = createRequire(import.meta.url)` binds the REAL +/// module-scoped require — `const net = require("net")` must still take the +/// static native-namespace fast path (as it did before #8343's shadow guard), +/// not flow to the runtime createRequire surface, where `net.connect` reached +/// as a bound value dispatches through a null-by-default function pointer and +/// silently returns undefined. +#[test] +fn test_create_require_local_keeps_the_native_namespace_fast_path() { + let source = r#" + import { createRequire } from "node:module"; + const require = createRequire(import.meta.url); + const net = require("net"); + console.log(typeof net.connect); + "#; + 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 dump = format!("{hir:?}"); + assert!( + dump.contains("NativeModuleRef(\"net\")"), + "require(\"net\") under a createRequire-backed local must fold to the \ + static native namespace: {dump}" + ); + assert!( + !dump.contains("name: \"net\""), + "the namespace binding must not leave a runtime `net` local behind: {dump}" + ); +} + +/// The #8465 counterpart, complementary to +/// `test_user_require_function_with_body_still_shadows_the_intrinsic` above: +/// that one pins that a real `function require` body suppresses the fold; this +/// one additionally pins that the bound name survives as a runtime local, which +/// is what the CJS wrap's synthetic require depends on. +#[test] +fn test_function_require_with_body_still_shadows_the_namespace_fast_path() { + let source = r#" + function require(name: string): any { return { connect: 1 }; } + const net = require("net"); + console.log(typeof net.connect); + "#; + 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 dump = format!("{hir:?}"); + assert!( + !dump.contains("NativeModuleRef(\"net\")"), + "a real `function require` body must keep shadowing: {dump}" + ); + assert!( + dump.contains("name: \"net\""), + "the `net` binding must stay a runtime local under a shadowing require: {dump}" + ); +}