From 2d4e3b0a847b77e95fa205901a0db51b7914a57b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 16 Jul 2026 00:30:30 +0200 Subject: [PATCH] fix(hir): honor scope-local class renames in `instanceof` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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`. --- .../perry-hir/src/lower/lower_expr/arm_bin.rs | 14 +++++++++- ...t_gap_instanceof_flattened_class_rename.ts | 27 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 test-files/test_gap_instanceof_flattened_class_rename.ts diff --git a/crates/perry-hir/src/lower/lower_expr/arm_bin.rs b/crates/perry-hir/src/lower/lower_expr/arm_bin.rs index ab7629655d..7df8bfa130 100644 --- a/crates/perry-hir/src/lower/lower_expr/arm_bin.rs +++ b/crates/perry-hir/src/lower/lower_expr/arm_bin.rs @@ -68,7 +68,19 @@ pub(crate) fn lower_bin_expr(ctx: &mut LoweringContext, bin: &ast::BinExpr) -> R let expr = Box::new(lower_expr(ctx, &bin.left)?); // Right side can be an identifier (ClassName) or member expression (Module.ClassName) let ty = match bin.right.as_ref() { - ast::Expr::Ident(ident) => ident.sym.to_string(), + // Honor a scope-local class rename: when two same-named classes + // collide (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` already resolve through this map, but `instanceof ` + // used the RAW ident — so `x instanceof l` resolved to the OTHER + // factory's `l` (class_id mismatch) and returned false even though + // the prototype chain was correct. This broke Auth.js's + // `error instanceof AuthError` guard (AuthError was the renamed + // class), so a `CredentialsSignin` was mis-wrapped in a + // `CallbackRouteError` and the login redirect fell back to + // `?error=Configuration` instead of `?error=CredentialsSignin`. + ast::Expr::Ident(ident) => ctx.resolve_class_name(ident.sym.as_ref()), ast::Expr::Member(member) => { // Handle Module.ClassName - extract the full qualified name let obj_name = if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() { diff --git a/test-files/test_gap_instanceof_flattened_class_rename.ts b/test-files/test_gap_instanceof_flattened_class_rename.ts new file mode 100644 index 0000000000..d00e71ea37 --- /dev/null +++ b/test-files/test_gap_instanceof_flattened_class_rename.ts @@ -0,0 +1,27 @@ +// Two "module factories" (as a bundler emits them into one file) each declare a +// class named `L`. Perry flattens them and scope-renames the second `L`. An +// `x instanceof L` inside the second factory must still resolve to THAT +// factory's `L` (its own class), not the first factory's same-named class. +// Regression: the rename was applied to `class`/`extends`/`new` but NOT to the +// `instanceof` operand, so `instanceof L` matched the wrong class_id and +// returned false even though the prototype chain was correct. +const factories: Array<(r: any) => void> = [ + (r) => { + class L { + seal(): string { return "util"; } + } + r.Util = L; + }, + (r) => { + class L {} + class D extends L {} + class G extends D {} + const g = new G(); + r.iofL = g instanceof L; + r.iofD = g instanceof D; + }, +]; +const out: any = {}; +for (const f of factories) f(out); +console.log("iofL=" + out.iofL); +console.log("iofD=" + out.iofD);