Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion crates/perry-hir/src/lower/lower_expr/arm_bin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name>`
// 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() {
Expand Down
27 changes: 27 additions & 0 deletions test-files/test_gap_instanceof_flattened_class_rename.ts
Original file line number Diff line number Diff line change
@@ -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);
Loading