Skip to content

Commit c086f27

Browse files
author
Ralph Küpper
committed
fix(hir): aliased native-class import new no longer throws ReferenceError
An aliased ESM named import of a Node built-in class (`import { BlockList as Wj4 } from "net"; new Wj4()`, `{ AsyncLocalStorage as J_z } from "async_hooks"`, `{ PassThrough as Lrz } from "stream"`) threw `ReferenceError: identifier is not defined` when constructed at module init, crashing the natively-compiled Claude Code cli.js 2.1.112 bundle on nearly every command. The alias-rewrite block in `lower_new` (#5472) already rewrites the callee `class_name` from the local import name (`Wj4`) to the class's EXPORT name (`BlockList`) so construction matches the un-aliased form. The unresolved-`new` guard added in #8688 then re-probed `lookup_native_module(&class_name)` under that rewritten export name, but the registry is keyed on the LOCAL import name, so the lookup missed; none of these classes are reified global builtins, so the guard fired and threw even though the binding is perfectly resolvable. The guard now also consults the registry under the original imported identifier, so aliased native-class imports resolve and construct exactly like their un-aliased form. Un-aliased imports and genuinely-undefined `new` targets are unchanged. Fixes #8730 Claude-Session: https://claude.ai/code/session_01TwxRkALrR9HKSF1zKLSTAF
1 parent d53f34b commit c086f27

3 files changed

Lines changed: 116 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix an aliased ESM named import of a Node built-in class (`import { BlockList as Wj4 } from "net"`, `{ AsyncLocalStorage as J_z } from "async_hooks"`, `{ PassThrough as Lrz } from "stream"`) throwing `ReferenceError: identifier is not defined` when constructed. The `new`-lowering already rewrites the alias to the class's export name so construction matches the un-aliased form, but the unresolved-`new` guard added in #8688 re-checked the native-module registry under that rewritten export name — which is keyed on the local import name — and, since these classes are not reified global builtins, fired the nameless throw at module init. The guard now also consults the registry under the original imported identifier, so aliased native-class imports resolve and construct exactly like their un-aliased form. This unblocked the natively compiled Claude Code cli.js 2.1.112 bundle, which crashed at module init on nearly every command.

crates/perry-hir/src/lower/expr_new.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1571,12 +1571,26 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R
15711571
// evaluating the constructor reference. That is a ReferenceError
15721572
// (`new Missing()`), distinct from the TypeError produced when a
15731573
// present binding's value is non-constructable.
1574+
//
1575+
// Consult the native-module registry under BOTH the (possibly
1576+
// rewritten) `class_name` AND the original `source_class_name`.
1577+
// The alias-rewrite block just above replaces `class_name` with a
1578+
// native class's EXPORT name (`Wj4` → `BlockList`) so the
1579+
// construction path below matches the un-aliased form, but the
1580+
// registry is keyed on the LOCAL import name (`Wj4`), so
1581+
// `lookup_native_module(&class_name)` misses under the export name.
1582+
// Checking `source_class_name` recognizes the aliased native import
1583+
// as resolved; without it, an aliased `import { BlockList as Wj4 }`
1584+
// / `{ AsyncLocalStorage as J_z }` / `{ PassThrough as Lrz }` (none
1585+
// of which are reified global builtins) fell through to this throw
1586+
// at module init even though the binding is perfectly resolvable.
15741587
if ctx.lookup_class(&class_name).is_none()
15751588
&& ctx.resolve_class_alias(&class_name).is_none()
15761589
&& ctx.lookup_local(&class_name).is_none()
15771590
&& ctx.lookup_func(&class_name).is_none()
15781591
&& ctx.lookup_imported_func(&class_name).is_none()
15791592
&& ctx.lookup_native_module(&class_name).is_none()
1593+
&& ctx.lookup_native_module(source_class_name).is_none()
15801594
&& !ctx.forward_class_names.contains(source_class_name)
15811595
&& !is_reified_global_builtin_constructor(&class_name)
15821596
{
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//! Regression test for #8730: an ALIASED ESM named import of a Node built-in
2+
//! class (`import { BlockList as Wj4 } from "net"; new Wj4()`) must not lower
3+
//! `new <alias>()` to the nameless `js_throw_reference_error_unresolved_get`
4+
//! throw.
5+
//!
6+
//! Root cause: the alias-rewrite block in `lower_new` replaces the callee's
7+
//! `class_name` with the native class's EXPORT name (`Wj4` -> `BlockList`) so
8+
//! the construction path matches the un-aliased form, but the freshly-added
9+
//! (#8688) unresolved-`new` guard then consulted `lookup_native_module` under
10+
//! that rewritten export name — which is not in the registry (it is keyed on
11+
//! the LOCAL import name). None of these classes are reified global builtins,
12+
//! so the guard fired and every command threw `ReferenceError: identifier is
13+
//! not defined` at module init.
14+
15+
use perry_diagnostics::SourceCache;
16+
use perry_hir::lower_module;
17+
use perry_parser::parse_typescript_with_cache;
18+
19+
const THROW_HELPER: &str = "js_throw_reference_error_unresolved_get";
20+
21+
fn lower_debug(src: &str) -> String {
22+
let src = src.to_string();
23+
std::thread::Builder::new()
24+
.stack_size(32 * 1024 * 1024)
25+
.spawn(move || {
26+
let mut cache = SourceCache::new();
27+
let parsed =
28+
parse_typescript_with_cache(&src, "aliased_native_new_resolution.ts", &mut cache)
29+
.expect("parse should succeed");
30+
let module = lower_module(&parsed.module, "test", "aliased_native_new_resolution.ts")
31+
.expect("lowering should succeed");
32+
format!("{module:#?}")
33+
})
34+
.expect("spawn lower thread")
35+
.join()
36+
.expect("lower thread panicked")
37+
}
38+
39+
#[test]
40+
fn aliased_native_class_import_does_not_lower_to_nameless_throw() {
41+
// Each mirrors a real cli.js 2.1.112 shape from #8730 (BlockList/Wj4 built
42+
// and `.addSubnet`-ed at module init; AsyncLocalStorage/J_z; PassThrough).
43+
let cases = [
44+
(
45+
"BlockList",
46+
r#"import { BlockList as Wj4 } from "net";
47+
const b = new Wj4();
48+
b.addSubnet("10.0.0.0", 8);
49+
console.log(b.check("10.1.2.3"));"#,
50+
),
51+
(
52+
"AsyncLocalStorage",
53+
r#"import { AsyncLocalStorage as J_z } from "async_hooks";
54+
const s = new J_z();
55+
console.log(typeof s.run);"#,
56+
),
57+
(
58+
"PassThrough",
59+
r#"import { PassThrough as Lrz } from "stream";
60+
const p = new Lrz();
61+
console.log(typeof p.pipe);"#,
62+
),
63+
];
64+
65+
for (label, src) in cases {
66+
let debug = lower_debug(src);
67+
assert!(
68+
!debug.contains(THROW_HELPER),
69+
"aliased native import `{label}` must construct, not throw the nameless \
70+
ReferenceError at module init:\n{debug}"
71+
);
72+
}
73+
}
74+
75+
#[test]
76+
fn unaliased_native_class_import_still_constructs() {
77+
// Control: the un-aliased form was never broken; keep it green so the fix
78+
// is symmetric across aliased/un-aliased native imports.
79+
let debug = lower_debug(
80+
r#"import { BlockList } from "net";
81+
const b = new BlockList();
82+
b.addSubnet("10.0.0.0", 8);
83+
console.log(b.check("10.1.2.3"));"#,
84+
);
85+
assert!(
86+
!debug.contains(THROW_HELPER),
87+
"un-aliased native import must construct, not throw:\n{debug}"
88+
);
89+
}
90+
91+
#[test]
92+
fn genuinely_unresolved_new_still_throws() {
93+
// Positive control: the guard must still fire for a `new` on an identifier
94+
// that resolves to no binding at all — the fix must not blanket-suppress it.
95+
let debug = lower_debug(r#"const x = new Totally_Undefined_Constructor_Xyz();"#);
96+
assert!(
97+
debug.contains(THROW_HELPER),
98+
"a genuinely unresolved `new` must still lower to the nameless \
99+
ReferenceError throw:\n{debug}"
100+
);
101+
}

0 commit comments

Comments
 (0)