Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions changelog.d/10621-asyncresource-heritage-shapes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
### Fixed

- **`class X extends AsyncResource` threw at `super()` unless the heritage
was a bare `import { AsyncResource } from "node:async_hooks"` binding**
(#10453). A local alias (`const Alias = AsyncResource`), a namespace
member (`ah.AsyncResource`), and a CJS destructured
`require('node:async_hooks')` — the exact shape `undici`'s API handlers
use everywhere (`lib/api/api-request.js` etc.) — all threw `Class
constructor AsyncResource cannot be invoked without 'new'`. Only the bare
import shape was recognized statically at HIR-lowering time
(`canonical_native_parent_name`, `crates/perry-hir/src/lower_decl/class_decl.rs`),
routing to the dedicated `js_async_resource_subclass_init` codegen; every
other shape fell through `js_fetch_or_value_super`
(`crates/perry-runtime/src/object/global_this/fetch_globals.rs`) to a
plain CALL of the bound `async_hooks` export, which throws by design
without `new`. `js_fetch_or_value_super` already resolves ANY heritage
value to its bound native module/method via
`bound_native_callable_module_and_method` for the WASI case, regardless
of how the value was reached — this fix adds the same recognition for
`async_hooks`'s `AsyncResource`, so every aliasing shape now runs the
same native-backing init the canonical import already used.
`AsyncLocalStorage` likely has the same gap but isn't fixed here — its
subclass-init helper lives in `perry-stdlib`, which `perry-runtime`
cannot depend on.
57 changes: 52 additions & 5 deletions crates/perry-runtime/src/object/global_this/fetch_globals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,13 @@ pub unsafe extern "C" fn js_fetch_or_value_super(
b"Super constructor null is not a constructor",
);
}
let wasi_parent = super::super::native_module::bound_native_callable_module_and_method(
// Resolve the parent to a bound native-module export VALUE, independent
// of how the heritage expression reached it: a bare import, a local
// alias, a namespace member, and a CJS destructured `require()` all
// produce the identical bound-closure representation (see
// `bound_native_callable_module_and_method`), even though only the bare
// import shape is recognized statically at HIR-lowering time.
let bound_native_parent = super::super::native_module::bound_native_callable_module_and_method(
parent_val,
)
.or_else(|| {
Expand All @@ -665,10 +671,13 @@ pub unsafe extern "C" fn js_fetch_or_value_super(
crate::object::class_registry::js_get_dynamic_parent_value(cid),
)
});
if wasi_parent.is_some_and(|(module, method)| {
super::super::native_module::normalize_native_module_alias(&module) == "wasi"
&& method == "WASI"
}) {
if bound_native_parent
.as_ref()
.is_some_and(|(module, method)| {
super::super::native_module::normalize_native_module_alias(module.as_str()) == "wasi"
&& method.as_str() == "WASI"
})
{
let arg0 = if args_len >= 1 && !args_ptr.is_null() {
*args_ptr
} else {
Expand All @@ -677,6 +686,44 @@ pub unsafe extern "C" fn js_fetch_or_value_super(
crate::wasi::js_wasi_init_subclass(this_box, arg0);
return undef;
}
// #10453: `class X extends AsyncResource` threw "Class constructor
// AsyncResource cannot be invoked without 'new'" for every heritage
// shape EXCEPT a bare `import { AsyncResource } from "node:async_hooks"`
// — the only shape `canonical_native_parent_name` recognizes statically
// (`crates/perry-hir/src/lower_decl/class_decl.rs`), which routes to the
// dedicated `js_async_resource_subclass_init` codegen
// (`crates/perry-codegen/src/expr/this_super_call.rs`). A local alias
// (`const Alias = AsyncResource`), a namespace member
// (`ah.AsyncResource`), and a CJS destructured
// `require('node:async_hooks')` all resolve `parent_val` to the exact
// same bound-native-export value the canonical import does, but HIR
// lowering can't see that statically for those shapes, so `super()` fell
// through to the ordinary value-super dispatch below — a plain CALL of
// the bound export, which `AsyncResource` throws on by design when
// invoked without `new` (`nm_dispatch_async_hooks`). Recognize the value
// here instead, exactly as the WASI arm above does, and run the same
// native-backing init the canonical path uses.
if bound_native_parent
.as_ref()
.is_some_and(|(module, method)| {
super::super::native_module::normalize_native_module_alias(module.as_str())
== "async_hooks"
&& method.as_str() == "AsyncResource"
})
{
let type_value = if args_len >= 1 && !args_ptr.is_null() {
*args_ptr
} else {
undef
};
let options = if args_len >= 2 && !args_ptr.is_null() {
*args_ptr.add(1)
} else {
undef
};
crate::async_hooks::js_async_resource_subclass_init(this_box, type_value, options);
return undef;
}
// `class X extends Temporal.<Type>` (non-spread `super(a, b)`): a Temporal
// constructor returns a fresh NaN-boxed cell and does NOT mutate the
// implicit `this`, so the ordinary dispatch below would drop that cell and
Expand Down
31 changes: 31 additions & 0 deletions test-files/gap_10453_asyncresource_heritage_helper.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use strict';
// CommonJS half of test_gap_10453_asyncresource_heritage.ts: the exact shape
// undici's API handlers use (`lib/api/api-request.js` etc.).
const { AsyncResource } = require('node:async_hooks');
const asyncHooks = require('node:async_hooks');

class Plain extends AsyncResource {
constructor(type) {
super(type);
}
}

class InTry extends AsyncResource {
constructor(type) {
try {
super(type);
} catch (err) {
throw err;
}
}
}

// `require('node:async_hooks').AsyncResource` reached via a namespace member
// on a plain `require()` result (not destructured).
class ViaMemberExport extends asyncHooks.AsyncResource {
constructor(type) {
super(type);
}
}

module.exports = { Plain, InTry, ViaMemberExport };
56 changes: 56 additions & 0 deletions test-files/test_gap_10453_asyncresource_heritage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// #10453: `class X extends AsyncResource` threw "Class constructor
// AsyncResource cannot be invoked without 'new'" at `super()` for every
// heritage shape EXCEPT a bare `import { AsyncResource } from
// "node:async_hooks"` binding. A local alias (`const Alias = AsyncResource`),
// a namespace member (`ah.AsyncResource`), and a CJS destructured
// `require('node:async_hooks')` all reach the same bound native export
// VALUE the bare import does, but HIR lowering only recognized the bare
// import shape statically, so `super()` for the other shapes fell through to
// a plain CALL of the native export — and `AsyncResource` throws by design
// when invoked without `new`.
//
// `undici` (`lib/api/api-request.js` etc.) uses exactly the CJS destructured
// shape: `const { AsyncResource } = require('node:async_hooks'); class …
// extends AsyncResource`.
import { AsyncResource } from "node:async_hooks";
import * as ah from "node:async_hooks";
import { Plain, InTry, ViaMemberExport } from "./gap_10453_asyncresource_heritage_helper.cjs";

const Alias = AsyncResource;

class ViaImport extends AsyncResource {
constructor() {
super("X");
}
}
class ViaAlias extends Alias {
constructor() {
super("X");
}
}
class ViaNamespace extends ah.AsyncResource {
constructor() {
super("X");
}
}
function t(name: string, C: any, ...args: unknown[]) {
try {
const r = new C(...args);
console.log(
name,
"ok",
typeof r.runInAsyncScope,
typeof r.triggerAsyncId(),
r instanceof AsyncResource,
);
} catch (e: any) {
console.log(name, "threw:", e.message);
}
}

t("TS extends AsyncResource (import) ", ViaImport, "X");
t("TS extends Alias ", ViaAlias, "X");
t("TS extends ah.AsyncResource ", ViaNamespace, "X");
t("CJS destructured require, super() ", Plain, "X");
t("CJS destructured require, try{super}", InTry, "X");
t("CJS namespace member export ", ViaMemberExport, "X");
Loading