Skip to content

fix(hir): #5912 — new URL()/TextEncoder()/etc. ignore lexical shadowing#5913

Open
proggeramlug wants to merge 1 commit into
PerryTS:mainfrom
proggeramlug:fix/url-new-lexical-shadowing
Open

fix(hir): #5912 — new URL()/TextEncoder()/etc. ignore lexical shadowing#5913
proggeramlug wants to merge 1 commit into
PerryTS:mainfrom
proggeramlug:fix/url-new-lexical-shadowing

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Fixes #5912.

Problem

new URL(...) (and URLSearchParams/URLPattern/TextEncoder/TextDecoder) were dispatched by bare identifier name in expr_new.rs with no check for whether the name actually resolves to the global constructor or is shadowed by a local function/class — unlike sibling well-known constructors (Function, Object) in the same match, which already guard on lookup_local/lookup_func/lookup_class.

Real packages ship their own tolerant polyfills under these names — found via a real-world source compile of @mixmark-io/domino's lib/URL.js, which defines function URL(url) { ... } and internally calls new URL() with zero args against its own constructor. Perry routed that straight into the native WHATWG URL constructor instead, which requires at least one argument:

function URL(url?: string) {
  return { url: url ?? "default" };
}
const t = new URL(); // Error: URL constructor requires at least 1 argument

Second site

The same unguarded name match also existed independently in static_receiver_class (expr_call/static_receiver.rs), which decides whether .toString() / .toJSON() / JSON.stringify(...) on a receiver should route through the native Date/URL fast paths. It classified any new URL(...) expression — or a local whose inferred type name is "URL" — as the native type purely by name, so even after fixing the construction site, printing/serializing a shadowed instance still silently substituted native URL serialization (UrlInstanceToJSON/UrlInstanceToString) for the user's own object.

Fix

Both sites now check lookup_local/lookup_func/lookup_class before applying the native lowering/classification, matching the existing shadowing-guard pattern already used for Function/Object in the same file.

Testing

  • Added test-files/test_issue_5912_url_new_local_shadow.ts (function-shadow + nested-function-shadow cases), output verified byte-for-byte against node --experimental-strip-types.
  • Manually verified a class URL { ... } shadow (construction + .toString() + JSON.stringify()) matches Node.
  • Manually verified normal (unshadowed) new URL(...) usage (hostname/pathname/toString/toJSON/JSON.stringify) is unaffected.
  • Confirmed a handful of pre-existing test-files/test_gap_url_*/test_compat_url_date_math.ts/shadowing-related gap tests either pass or fail identically with and without this change (those failures are pre-existing linker/runtime gaps unrelated to this fix, reproduced on unmodified main too).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of locally shadowed built-in constructor names so user-defined URL, TextEncoder, and related names are treated correctly.
    • Prevented ambiguous method selection when a local, function, or class shares a well-known receiver name.
    • Ensured new expressions fall back to generic behavior when a matching name is defined in user code.
  • Tests
    • Added a regression test covering shadowed constructor scenarios and their runtime behavior.

…shadowing

`new URL(...)` (and URLSearchParams/URLPattern/TextEncoder/TextDecoder)
were dispatched by bare identifier name with no check for whether the
name actually resolves to the global constructor or is shadowed by a
local function/class — unlike sibling well-known constructors
(Function, Object) in the same match, which already guard on
lookup_local/lookup_func/lookup_class.

Real packages ship their own tolerant polyfills under these names
(found via @mixmark-io/domino's lib/URL.js, which defines
`function URL(url) { ... }` and calls `new URL()` with zero args
against its own constructor) and hit perry's native WHATWG URL
constructor instead, which requires at least one argument.

The same unguarded name match also existed in a second, independent
spot: `static_receiver_class` (used to decide whether `.toString()` /
`.toJSON()` / `JSON.stringify()` on a receiver should route through
the native Date/URL fast paths) classified any `new URL(...)` — or a
local typed as `URL` — as the native type by name alone, so even after
fixing the construction site, printing/serializing a shadowed
instance still silently substituted native URL output.

Fixes both sites by checking `lookup_local`/`lookup_func`/
`lookup_class` before applying the native lowering, matching the
existing shadowing-guard pattern used elsewhere in expr_new.rs.
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR fixes Issue #5912 by adding shadowing checks in receiver classification and constructor lowering so user-defined locals, functions, or classes named URL, URLSearchParams, URLPattern, TextEncoder, or TextDecoder route to generic object dispatch instead of built-in fast paths. A regression test is included.

Changes

Shadow-aware dispatch for built-in constructors

Layer / File(s) Summary
Static receiver classification shadow guards
crates/perry-hir/src/lower/expr_call/static_receiver.rs
Adds checks so new-expression callee names and inferred identifier type names that match a local, function, or class are classified as "Object" instead of resolving via built-in class-name mapping.
lower_new shadowing guards for URL/encoding constructors
crates/perry-hir/src/lower/expr_new.rs
Requires no entry-time local/param shadowing and no same-named func/class before applying special-cased lowering for URL, URLSearchParams/URLPattern, TextEncoder, and TextDecoder.
Regression test for shadowed constructors
test-files/test_issue_5912_url_new_local_shadow.ts
Adds a test defining locally shadowed URL and TextEncoder constructors and exercising new dispatch, including no-argument calls.

Estimated code review effort: 2 (Simple) | ~15 minutes

Possibly related issues

Possibly related PRs

  • PerryTS/perry#5612: Both PRs modify lower_new in expr_new.rs to change how shadowed constructor identifiers are detected and resolved.
  • PerryTS/perry#5659: Both PRs fix dispatch issues caused by local/param bindings shadowing same-named classes, gating different call paths.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the PR’s main change: fixing URL/TextEncoder shadowing in HIR lowering.
Description check ✅ Passed The description covers the problem, fix, issue reference, and testing, though it doesn’t follow the template headings exactly.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
crates/perry-hir/src/lower/expr_call/static_receiver.rs (1)

52-80: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Skip the shadow check for new globalThis.URL()

globalThis.URL still flows through the same shadowing guard as bare URL, so a local URL binding forces new globalThis.URL(...) to classify as "Object" and miss the native URL fast path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-hir/src/lower/expr_call/static_receiver.rs` around lines 52 -
80, The shadowing guard in static receiver classification is too broad and
incorrectly treats new globalThis.URL() like a locally shadowed URL binding.
Update the logic in the static receiver lowering path around the class_name
extraction and lookup checks so that the shadow check only applies to bare
identifiers, not to Member expressions rooted at globalThis. Preserve the native
fast path for globalThis.URL while still returning "Object" for truly shadowed
local names.
crates/perry-hir/src/lower/expr_new.rs (2)

944-1036: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard these constructor fast paths with imported-binding shadowing. Imported names like URL or TextEncoder can still take the native path here; use shadows_unqualified_global (and the same gate for WeakRef / FinalizationRegistry) instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-hir/src/lower/expr_new.rs` around lines 944 - 1036, The
constructor fast paths in the lowering logic still ignore imported bindings, so
names like URL and TextEncoder can incorrectly take the native path even when
shadowed. Update the checks in the new-expression handling branch to use
shadows_unqualified_global alongside the existing
callee_local_at_entry/lookup_func/lookup_class guards, and apply the same
imported-binding shadowing gate to the WeakRef and FinalizationRegistry special
cases in expr_new::lower_expr.

419-427: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Alias-based URL/encoding constructors still need the shadowing guard
resolve_class_alias() can remap a local alias like MyURL back to URL, and this early branch returns before the later callee_local_at_entry/lookup_func/lookup_class checks. That lets new MyURL(...) bind to the native constructor even when URL is shadowed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-hir/src/lower/expr_new.rs` around lines 419 - 427, The early
alias-resolution branch in expr_new’s constructor lowering bypasses the
shadowing check, so a local alias like MyURL can still resolve to the native
URL/encoding constructor even when URL is shadowed. Update the logic around
resolve_class_alias, is_url_encoding_constructor_name, and
lower_url_encoding_constructor so it still consults the callee_local_at_entry /
lookup_func / lookup_class shadowing guard before returning the lowered
expression, or otherwise fold the alias case into the same guarded path.
🧹 Nitpick comments (1)
crates/perry-hir/src/lower/expr_new.rs (1)

953-1036: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Repeated 3-condition guard across four branches.

The same callee_local_at_entry.is_none() && ctx.lookup_func(...).is_none() && ctx.lookup_class(...).is_none() triplet is duplicated for URL, URLSearchParams/URLPattern, TextEncoder, and TextDecoder. Consider a small helper (e.g. fn is_shadowed(ctx, callee_local_at_entry, name) -> bool) to reduce duplication and keep future edits (e.g. adding lookup_imported_func) in one place.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-hir/src/lower/expr_new.rs` around lines 953 - 1036, The same
shadowing check is duplicated in the constructor lowering branches for URL,
URLSearchParams/URLPattern, TextEncoder, and TextDecoder. Add a small helper in
expr_new.rs, such as one used from lower_new_expr/lower_url_encoding_constructor
call sites, to centralize the callee_local_at_entry plus
ctx.lookup_func/ctx.lookup_class checks. Replace the repeated guard with that
helper so future changes to shadowing rules only need to be made in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-hir/src/lower/expr_call/static_receiver.rs`:
- Around line 75-80: The static receiver guard in `StaticReceiver::lower` is
reimplementing shadowing logic manually and misses imported functions, so
replace the combined `lookup_local`/`lookup_func`/`lookup_class` checks with
`ctx.shadows_unqualified_global(class_name)` to match the existing semantics in
`Context`. This keeps the behavior consistent with `context.rs` and ensures
imported bindings are also treated as shadows when resolving the class name.

---

Outside diff comments:
In `@crates/perry-hir/src/lower/expr_call/static_receiver.rs`:
- Around line 52-80: The shadowing guard in static receiver classification is
too broad and incorrectly treats new globalThis.URL() like a locally shadowed
URL binding. Update the logic in the static receiver lowering path around the
class_name extraction and lookup checks so that the shadow check only applies to
bare identifiers, not to Member expressions rooted at globalThis. Preserve the
native fast path for globalThis.URL while still returning "Object" for truly
shadowed local names.

In `@crates/perry-hir/src/lower/expr_new.rs`:
- Around line 944-1036: The constructor fast paths in the lowering logic still
ignore imported bindings, so names like URL and TextEncoder can incorrectly take
the native path even when shadowed. Update the checks in the new-expression
handling branch to use shadows_unqualified_global alongside the existing
callee_local_at_entry/lookup_func/lookup_class guards, and apply the same
imported-binding shadowing gate to the WeakRef and FinalizationRegistry special
cases in expr_new::lower_expr.
- Around line 419-427: The early alias-resolution branch in expr_new’s
constructor lowering bypasses the shadowing check, so a local alias like MyURL
can still resolve to the native URL/encoding constructor even when URL is
shadowed. Update the logic around resolve_class_alias,
is_url_encoding_constructor_name, and lower_url_encoding_constructor so it still
consults the callee_local_at_entry / lookup_func / lookup_class shadowing guard
before returning the lowered expression, or otherwise fold the alias case into
the same guarded path.

---

Nitpick comments:
In `@crates/perry-hir/src/lower/expr_new.rs`:
- Around line 953-1036: The same shadowing check is duplicated in the
constructor lowering branches for URL, URLSearchParams/URLPattern, TextEncoder,
and TextDecoder. Add a small helper in expr_new.rs, such as one used from
lower_new_expr/lower_url_encoding_constructor call sites, to centralize the
callee_local_at_entry plus ctx.lookup_func/ctx.lookup_class checks. Replace the
repeated guard with that helper so future changes to shadowing rules only need
to be made in one place.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: df79d016-2d59-4b62-8123-8544468b2762

📥 Commits

Reviewing files that changed from the base of the PR and between 95f393c and 9308eee.

📒 Files selected for processing (3)
  • crates/perry-hir/src/lower/expr_call/static_receiver.rs
  • crates/perry-hir/src/lower/expr_new.rs
  • test-files/test_issue_5912_url_new_local_shadow.ts

Comment on lines +75 to +80
if ctx.lookup_local(class_name).is_some()
|| ctx.lookup_func(class_name).is_some()
|| ctx.lookup_class(class_name).is_some()
{
return Some("Object");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reuse ctx.shadows_unqualified_global instead of duplicating the check.

context.rs already defines shadows_unqualified_global combining lookup_local/lookup_func/lookup_imported_func/lookup_class. This new guard reimplements three of those four checks manually and omits lookup_imported_func — so a same-named binding imported from another module (import { URL } from './polyfill') would not be caught here, unlike the intended shadowing semantics.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-hir/src/lower/expr_call/static_receiver.rs` around lines 75 -
80, The static receiver guard in `StaticReceiver::lower` is reimplementing
shadowing logic manually and misses imported functions, so replace the combined
`lookup_local`/`lookup_func`/`lookup_class` checks with
`ctx.shadows_unqualified_global(class_name)` to match the existing semantics in
`Context`. This keeps the behavior consistent with `context.rs` and ensures
imported bindings are also treated as shadows when resolving the class name.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

new URL() ignores lexical scoping — hardcoded to native constructor even when shadowed by a local function/class

1 participant