Skip to content

Duplicate class declarations at different nesting depths alias onto ONE ClassId — the inner body is silently dropped (wrong code) #9466

Description

@proggeramlug

Wrong code, not metadata: the second and third same-name class at different depths share one ClassId, so the inner definition's body is silently replaced by the outer one's.

class M { v(){return "top"} }
function h(){ class M { v(){return "outer"} }
  function h2(){ class M { v(){return "inner"} } return new M().v() }
  return [new M().v(), h2()].join(",") }
console.log(new M().v(), h());   // node: top outer,inner   perry: top outer,outer

Sibling blocks at module top level hit it too:

{ class Blk { v(){return "b1"} } console.log(new Blk().v()); }  // b1 — correct
{ class Blk { v(){return "b2"} } console.log(new Blk().v()); }  // node b2, perry b1

Mechanism (located, not fixed)

maybe_rename_colliding_class (crates/perry-hir/src/lower/context.rs:476) returns early when class_renames already holds the name, so a third occurrence reuses the second's uniquified key instead of minting its own — and sibling module-top blocks appear not to run the Phase-1.5 rename scan at all. Every same-name class after the second aliases onto one ClassId; whichever body registered first wins, and the program runs the wrong methods with no diagnostic.

Why this is worse than the #9413 name leak

#9413 (PR #9465) fixed the reported nameMade$0Made — but the underlying identity collision produces wrong execution, and it is exactly the shape of the OpenCode #9133 collision recorded in the per-module class-id work: reflection and dispatch keyed on a class id that two source classes share. Any codebase with a common class name (Point, Node, Item) declared in more than two scopes is exposed.

Verification bar

A gap fixture demonstrated failing on unfixed origin/main, byte-compared to node, covering: three same-name classes at three depths (each body distinguishable by a method's return value), sibling blocks at module top level, sibling blocks inside a function, same-name classes in sibling functions, a shadowed class captured in a closure and called after the block exits, and instanceof across the shadowing boundary (an inner instance must NOT be instanceof the outer class). The instanceof arm is the one that catches an aliasing fix that merely re-splits names without re-splitting ids.

Found while fixing #9413 (PR #9465).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions