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);