Skip to content

new globalThis.X(...) ignores the qualifier and builds a same-named imported class instead of the intrinsic #10359

Description

@proggeramlug

Summary

new globalThis.X(...) ignores the globalThis. qualifier and resolves X by bare name. When a module legitimately imports a user class that shadows a global, the qualified form builds the user class instead of the intrinsic.

The aliased form works, which is what makes this two lowerings rather than one broken one:

new globalThis.Event("ping")                              // perry: the USER class
const E = globalThis.Event; new E("ping")                 // perry: the intrinsic — correct

Reproducer

ev.ts:

export class Event { readonly tag = "user-Event" }

main.ts:

// @ts-nocheck
import { Event } from "./ev.js"   // a legitimate, explicit import that shadows the bare name
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)

Measured (perry @ v0.5.1579 + #10356, vs bun 1.3.14)

cell bun perry
e.tag undefined user-Event
e.type ping undefined
e.constructor?.name Event Event ok (both classes are named Event — this cell cannot discriminate)
e instanceof Event false true ✗ decisive

The last cell is the proof: the value built through globalThis.Event is an instance of the imported class.

Wider matrix on the same fixture — only the direct qualified new is affected:

cell bun perry
new Event().tag (the local binding, legitimately the user class) user-Event user-Event ok
typeof globalThis.Event function function ok
globalThis.Event?.name Event Event ok
new globalThis.Event("ping").type ping undefined
const E = globalThis.Event; new E("ping").type ping ping ok
new globalThis.CustomEvent("x",{detail:7}).detail 7 7 ok

With no shadowing user class in scope, every globalThis.-qualified form is correct (Event, CustomEvent, URL, Headers all pass). The defect only appears once a same-named binding exists in the module.

Why it matters

globalThis.X is the idiom people reach for precisely to escape a local shadow — it is the documented workaround for this situation, so silently resolving it to the shadow defeats its only purpose.

OpenCode's graph has many such shadows: packages/sdk/js/src/v2/gen/sdk.gen.ts exports Event, File and Request; packages/core exports Error twice; Effect exports Request, WebSocket, FormData and Storage.

Relationship to other issues

Distinct from #10356. There the class was never imported and should not have been in scope at all. Here the import is legitimate and the bare name should be the user class — but the globalThis.-qualified read must still reach the intrinsic.

Same family as #10303 (global.x reads collapsing to the intrinsic surface while the write landed): a qualified access and its unqualified sibling take different lowerings, and only one honors the qualifier. The asymmetry between the direct and aliased forms above is the signature.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions