fix(hir): honor scope-local class renames in instanceof#6444
Conversation
When two classes share a name in one compilation (e.g. a bundler flattens two module factories that each declare `class l`), the second is registered under a suffixed name (`l$0`) and `class_renames` maps the raw name to it. `extends`, `new`, and static reads already resolve through that map, but the `instanceof` right-hand side used the RAW identifier — so `x instanceof l` resolved to the OTHER factory's same-named class, mismatched the class_id, and returned `false` even though the prototype chain was correct. Resolve the `instanceof` class-name operand through `resolve_class_name`. Surfaced by Auth.js v5 bundled into one chunk: `AuthError` (renamed on a name collision) failed `error instanceof AuthError`, so a `CredentialsSignin` was mis-wrapped in a `CallbackRouteError` and the login redirected to `?error=Configuration` instead of `?error=CredentialsSignin`.
📝 WalkthroughWalkthrough
ChangesInstanceof class resolution
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/perry-hir/src/lower/lower_expr/arm_bin.rs (1)
55-63: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winEnforce consistent shadowing guard for built-in constructors.
The current check only verifies that the built-in is not shadowed by a non-class binding (
!ctx.shadows_unqualified_global). If a user definesclass WeakRef {}, it will bypass this check and incorrectly take the built-in fast path, causinginstanceofto return a boolean instead of evaluating normally.Based on learnings, you must check shadowing at both class binding sites (
ctx.classes_index) and non-class binding sites, and usebuiltin_constructor_inference_nameto safely determine if the name refers to a built-in.🐛 Proposed fix for the built-in shadowing guard
- if (class_name == "WeakRef" || class_name == "FinalizationRegistry") - && !ctx.shadows_unqualified_global(class_name) + let effective_name = crate::lower_types::builtin_constructor_inference_name(class_name); + if (effective_name == "WeakRef" || effective_name == "FinalizationRegistry") + && !ctx.shadows_unqualified_global(class_name) + && !ctx.classes_index.contains_key(class_name) { if let ast::Expr::Ident(left_ident) = bin.left.as_ref() { let local_name = left_ident.sym.to_string(); - let is_match = (class_name == "WeakRef" + let is_match = (effective_name == "WeakRef" && ctx.weakref_locals.contains(&local_name)) - || (class_name == "FinalizationRegistry" + || (effective_name == "FinalizationRegistry" && ctx.finreg_locals.contains(&local_name));🤖 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/lower_expr/arm_bin.rs` around lines 55 - 63, Update the built-in constructor guard in the arm_bin lowering logic to reject names shadowed by either class bindings in ctx.classes_index or non-class bindings via ctx.shadows_unqualified_global. Use builtin_constructor_inference_name to determine the canonical built-in name before applying the WeakRef and FinalizationRegistry local checks, preserving normal instanceof evaluation for user-defined classes.Source: Learnings
🤖 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.
Outside diff comments:
In `@crates/perry-hir/src/lower/lower_expr/arm_bin.rs`:
- Around line 55-63: Update the built-in constructor guard in the arm_bin
lowering logic to reject names shadowed by either class bindings in
ctx.classes_index or non-class bindings via ctx.shadows_unqualified_global. Use
builtin_constructor_inference_name to determine the canonical built-in name
before applying the WeakRef and FinalizationRegistry local checks, preserving
normal instanceof evaluation for user-defined classes.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: a777b70e-15e1-4117-8a4b-f239f8b06e30
📒 Files selected for processing (2)
crates/perry-hir/src/lower/lower_expr/arm_bin.rstest-files/test_gap_instanceof_flattened_class_rename.ts
Summary
When two classes share a name in one compilation — e.g. a bundler (webpack/turbopack) flattens two module factories that each declare
class l— the second is registered under a suffixed name (l$0), andclass_renamesmaps the raw name to it.extends,new, and static reads already resolve through that map, but theinstanceofright-hand side used the raw identifier. Sox instanceof linside the second factory resolved to the other factory's same-named class, mismatched the class_id, and returnedfalse— even though the runtime prototype chain was correct.Resolve the
instanceofclass-name operand throughresolve_class_name, exactly like the other class references.Why it matters
Auth.js v5 bundled into one chunk: its
AuthErrorcollided with another same-named minified class and was renamed.error instanceof AuthErrorthen returnedfalse, so aCredentialsSigninwas mis-wrapped in aCallbackRouteErrorand the login callback redirected to?error=Configurationinstead of?error=CredentialsSignin.Testing
test_gap_instanceof_flattened_class_rename.ts(byte-compared to Node): two "module factory" closures each declareclass L;x instanceof Linside the second must resolve to its ownL(a subclass instance isinstanceofits own base, not the other factory'sL).https://claude.ai/code/session_01NjymZygYh31wM9h3wLnUqv
Summary by CodeRabbit
Bug Fixes
instanceofchecks for same-named classes defined in different module scopes.Tests
instanceofbehavior across colliding class names.