diff --git a/changelog.d/10375-new-globalthis-shadowed-binding.md b/changelog.d/10375-new-globalthis-shadowed-binding.md new file mode 100644 index 0000000000..d1c9494326 --- /dev/null +++ b/changelog.d/10375-new-globalthis-shadowed-binding.md @@ -0,0 +1,7 @@ +- **`new globalThis.X(...)` constructs the global even when a module binding shadows `X` (#10359).** `globalThis.X` names the global object's property, never a module binding, but every arm that lowered the qualified construct *by name* resolved it against the module's bindings. With `import { Event } from "./ev"` in scope, `new globalThis.Event("ping")` built the imported class (`e instanceof Event === true`, `e.type === undefined`), while the aliased `const E = globalThis.Event; new E("ping")` was correct. That defeats `globalThis.X`'s only purpose, escaping a local shadow; OpenCode's graph exports `Event`, `File`, `Request`, `Error`, `WebSocket`, `FormData` and `Storage`. Four by-name paths now back off when an import, class (at any depth), function, local or class alias shares the name, and construct the global property's value instead (`NewDynamic` over `globalThis.X`): + - the #6726 re-dispatch through the bare-identifier arm (`crates/perry-hir/src/lower/expr_new.rs`) ignored the shadow only for the dedicated intrinsic nodes (`SetNew`, `ErrorNew`, `UrlNew`, …). Names with none (`Event`, `Request`, `Headers`, `MessageChannel`, the three-argument typed-array form) reached the by-name tail (`New { class_name }` / `FuncRef` / `LocalGet`, plus the proxy-local and dynamic-function-subclass arms); + - `lower_new_member_native`'s `globalThis` fetch-constructor and `MessageChannel`/`BroadcastChannel` arms (`expr_new/member.rs`); + - `lower_new_non_ident`'s global-object fetch arm, reached through a `globalThis` alias (`const g = globalThis; new g.Headers()`); + - codegen's `try_static_class_name` (`crates/perry-codegen/src/expr/v8_interop.rs`) folded a `globalThis.X` callee onto a same-named module class, class alias or import. `class Widget {}` plus `globalThis.Widget = class {…}` built the module class, and with no such global `new globalThis.Gadget()` quietly built `class Gadget` instead of throwing a `TypeError`. Now `NewDynamic` (`expr/new_dynamic.rs`) builds the declined callee through the builtin table (`lower_global_intrinsic_new` → `lower_builtin_new`, skipping module classes), the construct the unshadowed form reaches. Only a name no builtin arm owns reads the property at runtime. Without that step, streams came back method-less and `WebSocket` had no `readyState`. + + Unshadowed names keep their by-name intrinsic construct, and a shadowed name with a dedicated intrinsic node (`new globalThis.Map()` under `import { Map }`) keeps that node. Tests: `lower::tests::global_this_new_shadowed` (the shadowed test fails against the pre-fix lowering) and `test-files/test_gap_new_globalthis_shadowed_10359.ts` (byte-identical to Node 26.5.1. Before the fix it diverged from line 1 and crashed at `mc.port1.close()`). diff --git a/crates/perry-codegen/src/expr/new_dynamic.rs b/crates/perry-codegen/src/expr/new_dynamic.rs index 39b221c8f2..5636b8f1c4 100644 --- a/crates/perry-codegen/src/expr/new_dynamic.rs +++ b/crates/perry-codegen/src/expr/new_dynamic.rs @@ -235,6 +235,23 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { return crate::lower_call::lower_new_member_captured(ctx, name.as_ref(), args); } + // #10359: a global-object callee gets here only when the fold above + // declined it because a module binding shares the name. Build the + // intrinsic the unshadowed fold reaches, never the binding. A name no + // builtin arm owns falls through and reads the property at runtime. + if let Expr::PropertyGet { + object, property, .. + } = callee.as_ref() + { + if super::v8_interop::is_global_object_expr(object) { + if let Some(value) = + crate::lower_call::lower_global_intrinsic_new(ctx, property, args)? + { + return Ok(value); + } + } + } + // date-fns `constructFrom(date, value)`: // return new date.constructor(value); // The callee is `PropertyGet { LocalGet(date), "constructor" }` diff --git a/crates/perry-codegen/src/expr/v8_interop.rs b/crates/perry-codegen/src/expr/v8_interop.rs index fca116e6d4..5e6fae9455 100644 --- a/crates/perry-codegen/src/expr/v8_interop.rs +++ b/crates/perry-codegen/src/expr/v8_interop.rs @@ -278,7 +278,7 @@ pub(crate) fn emit_v8_member_method_call( /// collision-free `(namespace, member)` registry key; the rest of the /// lower_new path resolves that key through the usual `ctx.classes` /// lookup. -fn is_global_object_expr(expr: &Expr) -> bool { +pub(crate) fn is_global_object_expr(expr: &Expr) -> bool { match expr { Expr::GlobalGet(_) => true, Expr::PropertyGet { @@ -308,6 +308,18 @@ pub(crate) fn try_static_class_name<'a>(callee: &'a Expr, ctx: &FnCtx<'_>) -> Op object, property, .. } => { if is_global_object_expr(object.as_ref()) { + // #10359: `lower_new` resolves the name against the module's + // classes, class aliases and imports before (or instead of) + // the builtin, but a module binding is never a property of + // the global object. With `import { Event } from "./ev"` in + // scope, folding `new globalThis.Event()` built the imported + // class; read the property and construct it at runtime. + if ctx.classes.contains_key(property) + || ctx.local_class_aliases.contains_key(property) + || ctx.import_function_prefixes.contains_key(property) + { + return None; + } return Some(Cow::Borrowed(property.as_str())); } // Namespace import: `import * as ns from 'm'; new ns.Foo()`. diff --git a/crates/perry-codegen/src/lower_call/builtin.rs b/crates/perry-codegen/src/lower_call/builtin.rs index 236b982c67..d976f3e675 100644 --- a/crates/perry-codegen/src/lower_call/builtin.rs +++ b/crates/perry-codegen/src/lower_call/builtin.rs @@ -1843,6 +1843,23 @@ pub(super) fn lower_builtin_new<'a>( } } +/// #10359: construct the global intrinsic `class_name` through the builtin +/// table alone, for a `new globalThis.()` whose name a module class, +/// class alias or import shadows. `lower_new` resolves those bindings first, +/// so it would construct the binding. `None` means no builtin arm owns the name +/// and no argument was lowered; the caller then constructs the global +/// property's runtime value. +pub(crate) fn lower_global_intrinsic_new( + ctx: &mut FnCtx<'_>, + class_name: &str, + args: &[Expr], +) -> Result> { + let mut group = rooting::open_rooted_group(args.len() + 1); + let result = lower_builtin_new(ctx, class_name, args, &mut group); + group.release(ctx); + result +} + /// Map a typed-array constructor name to its runtime `KIND_*` integer (mirrors /// `perry_runtime::typedarray::KIND_*`). Used by the `#4103` view-constructor /// arm to tell `js_typed_array_view` which element type to build. diff --git a/crates/perry-codegen/src/lower_call/mod.rs b/crates/perry-codegen/src/lower_call/mod.rs index 4874372745..87cdf9dcd1 100644 --- a/crates/perry-codegen/src/lower_call/mod.rs +++ b/crates/perry-codegen/src/lower_call/mod.rs @@ -157,6 +157,7 @@ pub(crate) use field_init::{ apply_field_initializers_recursive, defer_dynamic_derived_fields, FieldInitMode, }; pub(crate) use new::{emit_class_capture_writeback, lower_new, lower_new_member_captured}; +pub(crate) use builtin::lower_global_intrinsic_new; pub(crate) use new_ctor_args::{ bind_inline_constructor_params, restore_inline_constructor_scope, CaptureFill, }; diff --git a/crates/perry-hir/src/lower/expr_new.rs b/crates/perry-hir/src/lower/expr_new.rs index 89658745c3..8eeac99c36 100644 --- a/crates/perry-hir/src/lower/expr_new.rs +++ b/crates/perry-hir/src/lower/expr_new.rs @@ -24,8 +24,9 @@ mod member; mod non_ident; pub(crate) use helpers::{ - callee_is_generic_construct_shape, is_depd_wrapfunction_shape, is_fetch_constructor_name, - is_global_object_expr, is_url_encoding_constructor_name, is_worker_messaging_constructor_name, + callee_is_generic_construct_shape, global_name_has_user_binding, global_property_new_dynamic, + is_depd_wrapfunction_shape, is_fetch_constructor_name, is_global_object_expr, + is_url_encoding_constructor_name, is_worker_messaging_constructor_name, is_worker_threads_module_name, lower_new_spread_args, lower_optional_args, lower_text_decoder_new, lower_url_encoding_constructor, lower_worker_messaging_new, lower_worker_new, nonconstructable_builtin_throw_expr, peel_new_callee, @@ -211,6 +212,11 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R // shadows the bare name, so the re-dispatch sets // `global_intrinsic_new_once` to tell the recursive call to ignore that // shadowing (consumed at the top of `lower_new`, above). + // - #10359: ignoring the shadow only helps the arms that build a dedicated + // HIR node (`SetNew`, `ErrorNew`, …). A name with no such arm (`Event`, + // `Request`, `MessageChannel`, a multi-argument typed array) reaches the + // by-name tail, which a same-named user binding still captured; the + // recursive call builds `global_property_new_dynamic` there instead. if let ast::Expr::Member(member) = callee_expr { if let (ast::Expr::Ident(obj_ident), ast::MemberProp::Ident(prop_ident)) = (peel_new_callee(member.obj.as_ref()), &member.prop) @@ -248,7 +254,8 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R // must use the same kind-aware path (`new GeneratorFunction()`, // `new AsyncFunction(...)`, and async generators) instead of the // generic object-construction fallback. - if ctx.local_decl_scope_depth(ident.sym.as_ref()) == Some(0) { + if !force_global_intrinsic && ctx.local_decl_scope_depth(ident.sym.as_ref()) == Some(0) + { if let Some(super::fn_ctor_env::FnCtorShape::DynCtor(kind)) = ctx.fn_ctor_env.entries.get(ident.sym.as_str()).cloned() { @@ -287,7 +294,11 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R == Some(ident.sym.as_str()) && ctx.current_class.is_some() && !nearest_local_is_inside_class_binding; - let mut class_name = if is_current_class_self { + // #10359: a re-dispatched `globalThis.` is the global's own + // name — never a collision-renamed or enclosing user class key. + let mut class_name = if force_global_intrinsic { + source_class_name.to_string() + } else if is_current_class_self { ctx.current_class.clone().unwrap() } else { ctx.resolve_class_name(source_class_name) @@ -383,6 +394,12 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R || ctx.lookup_func(&class_name).is_some() || ctx.lookup_imported_func(&class_name).is_some() || ctx.forward_class_names.contains(source_class_name)); + // #10359: the re-dispatched counterpart. Here the shadowing binding + // must NOT win, so every arm that would construct by name backs + // off and the tail builds `global_property_new_dynamic`. Snapshotted + // with the flags above, for the same scope-stack reason. + let global_intrinsic_shadowed = + force_global_intrinsic && global_name_has_user_binding(ctx, source_class_name); if matches!( ctx.lookup_native_module(&class_name), Some(("url", Some("Url"))) @@ -448,7 +465,10 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R // runtime globals delegate to the registered worker_threads // factories when the stdlib is present, so ports stay fully // functional in graphs that have it. - if is_worker_messaging_constructor_name(&class_name) && !shadowed_by_user_binding { + if is_worker_messaging_constructor_name(&class_name) + && !shadowed_by_user_binding + && !global_intrinsic_shadowed + { return Ok(Expr::New { class_name: class_name.to_string(), args: lower_optional_args(ctx, new_expr.args.as_deref())?, @@ -717,8 +737,11 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R // the same kind-aware fold as a direct dynamic-function-constructor // call. The trivial explicit constructor supplies no arguments; // the implicit constructor forwards the new-site arguments. - if let Some((kind, forward_args)) = - ctx.dynamic_function_subclasses.get(&class_name).copied() + if let Some((kind, forward_args)) = ctx + .dynamic_function_subclasses + .get(&class_name) + .copied() + .filter(|_| !global_intrinsic_shadowed) { let empty_args: &[ast::ExprOrSpread] = &[]; let args_slice = if forward_args { @@ -1074,7 +1097,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R arg_present, }); } - if ctx.is_proxy_local(&class_name) { + if !global_intrinsic_shadowed && ctx.is_proxy_local(&class_name) { let args = new_expr .args .as_ref() @@ -1388,6 +1411,20 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R } } + // #10359: no dedicated intrinsic arm matched a re-dispatched + // `new globalThis.()` whose name a user binding shares. Every + // arm below resolves by name (`Expr::New { class_name }`, `FuncRef`, + // `LocalGet`), so it would construct that binding — construct the + // global property's value instead. + if global_intrinsic_shadowed { + let args = lower_optional_args(ctx, new_expr.args.as_deref())?; + return Ok(global_property_new_dynamic( + source_class_name, + args, + new_byte_offset, + )); + } + let mut args = new_expr .args .as_ref() diff --git a/crates/perry-hir/src/lower/expr_new/helpers.rs b/crates/perry-hir/src/lower/expr_new/helpers.rs index f09e5e454d..e465935861 100644 --- a/crates/perry-hir/src/lower/expr_new/helpers.rs +++ b/crates/perry-hir/src/lower/expr_new/helpers.rs @@ -375,3 +375,44 @@ pub(crate) fn is_global_object_expr(ctx: &LoweringContext, expr: &Expr) -> bool _ => false, } } + +/// #10359: does a binding in this module share a global constructor's name? +/// +/// A `globalThis.` member callee names the global object's property, +/// never a module binding — a class declaration, function, import, or local +/// does not create a property on the global object. But the construct arms +/// that lower such a callee by NAME (`Expr::New { class_name }`, and the +/// bare-identifier arm's `FuncRef` / `LocalGet` reroutes) are resolved against +/// the module's bindings, so a same-named binding captures them: with +/// `import { Event } from "./ev"` in scope, `new globalThis.Event("ping")` +/// built the imported class. Those arms consult this and, when it holds, build +/// [`global_property_new_dynamic`] instead. +/// +/// Covers every table a by-name construct resolves through: locals, functions, +/// imports and classes in scope (`shadows_unqualified_global`), `let`/`const` +/// class aliases, a sibling class declared later in the body, and a class +/// declared at any depth (codegen's class table is module-wide). +pub(crate) fn global_name_has_user_binding(ctx: &LoweringContext, name: &str) -> bool { + ctx.shadows_unqualified_global(name) + || ctx.resolve_class_alias(name).is_some() + || ctx.forward_class_names.contains(name) + || ctx.class_decl_names_any_depth.contains(name) +} + +/// #10359: `new globalThis.(args)` constructing the global, not a +/// same-named binding. Codegen's `try_static_class_name` declines to fold this +/// callee onto a module class, class alias or import of that name, and builds +/// the intrinsic through its builtin table (`lower_global_intrinsic_new`) — +/// the construct the unshadowed form reaches. A name the table does not own +/// reads the property and constructs its runtime value. +pub(crate) fn global_property_new_dynamic(name: &str, args: Vec, byte_offset: u32) -> Expr { + Expr::NewDynamic { + callee: Box::new(Expr::PropertyGet { + byte_offset: 0, + object: Box::new(Expr::GlobalGet(0)), + property: name.to_string(), + }), + args, + byte_offset, + } +} diff --git a/crates/perry-hir/src/lower/expr_new/member.rs b/crates/perry-hir/src/lower/expr_new/member.rs index d05b85eda4..08afc10181 100644 --- a/crates/perry-hir/src/lower/expr_new/member.rs +++ b/crates/perry-hir/src/lower/expr_new/member.rs @@ -65,7 +65,11 @@ pub(crate) fn lower_new_member_native( // that never import `node:worker_threads`. The runtime global // delegates to the full worker_threads factory whenever the // stdlib has registered it, so no behavior is lost. - if is_worker_messaging_constructor_name(class_name) { + // #10359: `Expr::New` resolves by name, so a same-named user + // binding would capture it — fall through to the re-dispatch. + if is_worker_messaging_constructor_name(class_name) + && !(obj_name == "globalThis" && global_name_has_user_binding(ctx, class_name)) + { return Ok(Some(Expr::New { class_name: class_name.to_string(), args: lower_optional_args(ctx, new_expr.args.as_deref())?, @@ -83,6 +87,7 @@ pub(crate) fn lower_new_member_native( if obj_name == "globalThis" && ctx.lookup_local("globalThis").is_none() && is_fetch_constructor_name(prop_ident.sym.as_ref()) + && !global_name_has_user_binding(ctx, prop_ident.sym.as_ref()) { ctx.uses_fetch = true; return Ok(Some(Expr::New { diff --git a/crates/perry-hir/src/lower/expr_new/non_ident.rs b/crates/perry-hir/src/lower/expr_new/non_ident.rs index 16305e3cfb..01604f126a 100644 --- a/crates/perry-hir/src/lower/expr_new/non_ident.rs +++ b/crates/perry-hir/src/lower/expr_new/non_ident.rs @@ -258,6 +258,11 @@ pub(crate) fn lower_new_non_ident( if is_fetch_constructor_name(property) { ctx.uses_fetch = true; } + // #10359: a same-named user binding would capture the by-name + // `Expr::New`; construct the global property's value instead. + if global_name_has_user_binding(ctx, property) { + return Ok(global_property_new_dynamic(property, args, new_byte_offset)); + } return Ok(Expr::New { class_name: property.clone(), args, diff --git a/crates/perry-hir/src/lower/tests.rs b/crates/perry-hir/src/lower/tests.rs index fc6adf5041..ec3fca2bdd 100644 --- a/crates/perry-hir/src/lower/tests.rs +++ b/crates/perry-hir/src/lower/tests.rs @@ -1984,6 +1984,8 @@ fn hoisted_class_constructs_sibling_declared_inside_a_later_closure() { mod unresolved_new_global; +mod global_this_new_shadowed; + mod capture_stash; mod mixin_parent_chain; mod native_module_sync; diff --git a/crates/perry-hir/src/lower/tests/global_this_new_shadowed.rs b/crates/perry-hir/src/lower/tests/global_this_new_shadowed.rs new file mode 100644 index 0000000000..7f7a653339 --- /dev/null +++ b/crates/perry-hir/src/lower/tests/global_this_new_shadowed.rs @@ -0,0 +1,88 @@ +//! #10359: `new globalThis.(…)` constructs the global property even when +//! a module binding shares the name. Split from `tests.rs` for the 2000-line +//! cap. + +fn lowered_function_debug(source: &str, name: &str) -> String { + 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 function = hir + .functions + .iter() + .find(|function| function.name == name) + .unwrap_or_else(|| panic!("{name} is lowered")); + format!("{function:?}") +} + +fn global_property_construct(name: &str) -> String { + format!( + r#"NewDynamic {{ callee: PropertyGet {{ object: GlobalGet(0), property: "{name}", byte_offset: 0 }}"# + ) +} + +/// The issue's shape: an import, a class, a function and a local each shadow a +/// global constructor with no dedicated intrinsic HIR node. Each used to lower +/// to a by-name construct (`New { class_name }` / `FuncRef` / `LocalGet`) that +/// bound to the shadowing binding; each must construct the global property, +/// exactly like the aliased `const E = globalThis.Event; new E()` form. +#[test] +fn shadowed_global_constructor_reads_the_global_property() { + let source = r#" + import { Event } from "./ev"; + class Headers { tag = 1 } + function Request(this: any) { this.tag = 2; } + export function viaImport(): any { return new globalThis.Event("ping"); } + export function viaClass(): any { return new globalThis.Headers({ a: "1" }); } + export function viaFunction(): any { return new globalThis.Request("http://x.test/"); } + export function viaLocal(): any { + const MessageChannel = function () {}; + return new globalThis.MessageChannel(); + } + export function viaGlobalAlias(): any { + const g = globalThis; + return new g.Headers(); + } + "#; + for (function, name) in [ + ("viaImport", "Event"), + ("viaClass", "Headers"), + ("viaFunction", "Request"), + ("viaLocal", "MessageChannel"), + ("viaGlobalAlias", "Headers"), + ] { + let debug = lowered_function_debug(source, function); + assert!( + debug.contains(&global_property_construct(name)), + "{function}: `new globalThis.{name}()` must construct the global property:\n{debug}" + ); + assert!( + !debug.contains(&format!(r#"New {{ class_name: "{name}""#)), + "{function}: a by-name construct binds to the shadowing `{name}`:\n{debug}" + ); + } +} + +/// Guards the other side: an unshadowed name keeps its by-name intrinsic +/// construct, and a shadowed name WITH a dedicated intrinsic node (#6726's +/// `class Set {}` case) keeps that node rather than going dynamic. +#[test] +fn unshadowed_and_dedicated_intrinsics_keep_their_lowering() { + let source = r#" + import { Map } from "./m"; + export function unshadowed(): any { return new globalThis.Event("ping"); } + export function dedicated(): any { return new globalThis.Map([[1, 2]]); } + "#; + let unshadowed = lowered_function_debug(source, "unshadowed"); + assert!( + unshadowed.contains(r#"New { class_name: "Event""#), + "an unshadowed global keeps the by-name intrinsic construct:\n{unshadowed}" + ); + let dedicated = lowered_function_debug(source, "dedicated"); + assert!( + dedicated.contains("MapNewFromArray"), + "a shadowed global with a dedicated intrinsic node keeps it:\n{dedicated}" + ); + assert!( + !dedicated.contains(&global_property_construct("Map")), + "the dedicated intrinsic node must not be replaced by a dynamic construct:\n{dedicated}" + ); +} diff --git a/test-files/_helpers/new_globalthis_shadowed_10359.ts b/test-files/_helpers/new_globalthis_shadowed_10359.ts new file mode 100644 index 0000000000..3479c1d8c5 --- /dev/null +++ b/test-files/_helpers/new_globalthis_shadowed_10359.ts @@ -0,0 +1,21 @@ +// Helper for test_gap_new_globalthis_shadowed_10359.ts — user classes that +// deliberately share their names with global constructors, so importing them +// shadows the bare names in the test module. +export class Event { + readonly tag = "user-Event"; +} +export class Request { + readonly tag = "user-Request"; +} +export class MessageChannel { + readonly tag = "user-MessageChannel"; +} +export class Map { + readonly tag = "user-Map"; +} +export class Int32Array { + readonly tag = "user-Int32Array"; +} +export class ReadableStream { + readonly tag = "user-ReadableStream"; +} diff --git a/test-files/test_gap_new_globalthis_shadowed_10359.ts b/test-files/test_gap_new_globalthis_shadowed_10359.ts new file mode 100644 index 0000000000..6db91ef202 --- /dev/null +++ b/test-files/test_gap_new_globalthis_shadowed_10359.ts @@ -0,0 +1,104 @@ +// #10359 — `new globalThis.X(...)` must construct the GLOBAL `X` even when a +// module binding shadows the bare name. `globalThis.X` is the idiom for +// escaping exactly that shadow, but the qualified `new` lowered to a by-name +// construct that bound to the shadowing import/class/function/local — so it +// built the user class, while the aliased `const E = globalThis.Event; +// new E()` form was correct. +// @ts-nocheck +import { + Event, + Request, + MessageChannel, + Map, + Int32Array, + ReadableStream, +} from "./_helpers/new_globalthis_shadowed_10359.ts"; + +// The exact reproduction from the issue: an explicit import shadows `Event`. +const e: any = new globalThis.Event("ping"); +console.log("tag:", e.tag); +console.log("type:", e.type); +console.log("ctor-name:", e.constructor?.name); +console.log("is-user-class:", e instanceof Event); + +// The bare name is still legitimately the imported class, and the aliased +// form (always correct) stays correct. +console.log("bare:", (new Event() as any).tag); +const E = globalThis.Event; +console.log("aliased:", new E("pong").type); + +// Fetch constructors took a separate by-name arm. +const r: any = new globalThis.Request("http://example.test/a"); +console.log("request:", r.tag, r.url, r instanceof Request); + +// So did MessageChannel. +const mc: any = new globalThis.MessageChannel(); +console.log("channel:", mc.tag, typeof mc.port1, mc instanceof MessageChannel); +mc.port1.close(); +mc.port2.close(); + +// A stream's methods come from codegen's builtin table, not from the +// runtime value of `globalThis.ReadableStream`. +const rs: any = new globalThis.ReadableStream(); +console.log("stream:", rs.tag, typeof rs.getReader, rs instanceof ReadableStream); + +// A constructor with a dedicated intrinsic node keeps constructing it. +const m: any = new globalThis.Map([[1, 2]]); +console.log("map:", m.tag, m.get(1), m.size); + +// The multi-argument typed-array form falls past its dedicated node. +const ia: any = new globalThis.Int32Array(new ArrayBuffer(16), 4, 2); +console.log("int32:", ia.tag, ia.length, ia.byteOffset); + +// A module-scope class and a function declaration shadow just like an import. +class Headers { + tag = "user-Headers"; +} +const h: any = new globalThis.Headers({ a: "1" }); +console.log("headers:", h.tag, h.get("a"), h instanceof Headers); + +function URLSearchParams(this: any) { + this.tag = "user-URLSearchParams"; +} +const usp: any = new globalThis.URLSearchParams("a=1&b=2"); +console.log("usp:", usp.tag, usp.get("b")); + +// A function-local binding shadows too. +function local() { + const CustomEvent = function (this: any) { + this.tag = "local-CustomEvent"; + }; + const ce: any = new globalThis.CustomEvent("x", { detail: 7 }); + console.log("local:", ce.tag, ce.detail, ce.type); +} +local(); + +// The same through a `globalThis` alias. +const g = globalThis; +const h2: any = new g.Headers({ b: "2" }); +console.log("alias-headers:", h2.tag, h2.get("b"), h2 instanceof Headers); + +// A global the program installs itself, shadowed by a module class of the same +// name: codegen folded the `globalThis.Widget` callee back onto the class. +class Widget { + kind = "module-class"; +} +globalThis.Widget = class { + kind = "global-property"; +}; +console.log("widget:", new globalThis.Widget().kind, new Widget().kind); + +// …and with no such global, the qualified construct must throw rather than +// quietly build the module class. +class Gadget { + kind = "module-class"; +} +try { + const gadget: any = new globalThis.Gadget(); + console.log("gadget: constructed", gadget.kind); +} catch (err) { + console.log("gadget: threw", err instanceof TypeError); +} + +// Unshadowed forms are unaffected. +console.log("unshadowed:", new globalThis.CustomEvent("y", { detail: 9 }).detail);