Skip to content

static this leaks the instance type: typeof this.instanceMethod is "function" in a static body (node: undefined) #9404

Description

@proggeramlug

Inside a static method, perry treats this as though it were an instance, so instance members appear to exist on the class object.

class P { static sf = 7; m() { return 1; } static probe() { return typeof this.m; } }
P.probe()   // node: "undefined"   perry: "function"

This class has no computed member. Confirmed on 757beace0 (current main, i.e. with #9386 merged), byte-compared against node --experimental-strip-types.

Why this is filed separately from #9369

#9386 fixed the computed-member case by gating the computed-member lowering routes on a new FnCtx::is_static_class_this. It deliberately left the deeper cause alone, and that cause is not computed-member-specific:

static_type_of(Expr::This) (crates/perry-hir/src/type_analysis/predicates.rs:689) returns Named(class_stack.last()) in a static body — the same answer it gives in an instance body — and refine_type_from_init writes that into local_types. receiver_class_name(Expr::This) (same file, ~366) has the identical shape.

So a static body's this is typed as an instance of the class everywhere that answer is consumed.

Two known consequences

  1. The example above: instance members resolve on the class object.
  2. The alias residual from fix(codegen): a static body's this is the class, not an instance — unblocks cc --help (#9369, #9341) #9386, still broken on current main:
const K = "dy" + "n";
class G { static sf = 9; [K](){return 4}
  static viaLocal(){ const t = this; return [typeof t.prototype, t.sf].join("|") } }
G.viaLocal()   // node: "object|9"   perry: "undefined|"

const t = this makes the receiver a LocalGet, so is_static_class_this no longer applies and it reaches the computed-member route through the metadata-only guarded_declared_class_get_candidate fallback — carrying the wrong type from local_types.

Note also that the computed-member gate is not under receiver_class_is_proven, contrary to the rule stated three lines above it in that file.

Scope, and why it wasn't folded into #9386

Making static_type_of / receiver_class_name honest about static this changes every consumer of that answer inside static bodies — static field reads, static-method dispatch, private brand checks. That is materially larger than a change whose job was to unblock the cc --help parity gate, which is why #9386 scoped it out deliberately rather than by oversight.

Not currently reachable from claude-code

A tokenizer scan of the cc bundle (brace-matching through strings/templates/regex/comments) found 412 static methods, 0 containing an alias-of-this that is then member-accessed. The scanner was validated by appending known instances to a copy of the bundle and finding 2/2. So the alias half is theoretical for cc today; the typeof this.instanceMethod half is a plain spec divergence reachable by any user.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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