esbuild icon indicating copy to clipboard operation
esbuild copied to clipboard

Class binding references in static field initializers should be resolved to the decorated class

Open JLHwung opened this issue 1 year ago • 3 comments

Input (playground):

class D {}
@(() => D)
class C {
  static p = C
}
console.log(C.p === D)

Option:

{ target: "es2022" }

Expected: It should print true, as per spec, static field initializers (step 40) should run after the class binding was initialized (step 38).

Actual: It prints false.

JLHwung avatar May 27 '24 07:05 JLHwung

I think it's unclear what should happen here until https://github.com/tc39/proposal-decorators/issues/529 is resolved, as the specification may be changed. Supposedly that may be resolved after the upcoming TC39 meeting (from June 11th to June 13th?).

evanw avatar Jun 07 '24 23:06 evanw

If I read correctly, the discussion result in https://github.com/tc39/proposal-decorators/issues/529 is that the homeObject of the static accessor might be reverted back to this. But the decision that class decorators should run before static fields was made long ago (c.f. https://github.com/tc39/proposal-decorators/issues/329#issuecomment-695365599). So the outcome of https://github.com/tc39/proposal-decorators/issues/529 should not affect the behaviour mentioned in this issue.

JLHwung avatar Jun 08 '24 05:06 JLHwung

In that case, consider the following test case:

let old
let block
class Bar {}

@(cls => (old = cls, Bar)) class Foo {
  static { block = Foo }

  method() { return Foo }
  static method() { return Foo }

  field = Foo
  static field = Foo

  get getter() { return Foo }
  static get getter() { return Foo }

  set setter(x) { x.foo = Foo }
  static set setter(x) { x.foo = Foo }

  accessor accessor = Foo
  static accessor accessor = Foo
}

let foo = new old
let obj

console.log(
  Foo !== old,
  Foo === Bar,
  block === Bar,

  Foo.field === Bar,
  old.getter === Bar,
  (obj = { foo: null }, old.setter = obj, obj.foo) === Bar,

  foo.field === Bar,
  foo.getter === Bar,
  (obj = { foo: null }, foo.setter = obj, obj.foo) === Bar,

  // These are determined by the outcome of https://github.com/tc39/proposal-decorators/issues/529
  // Foo.accessor === Bar,
  // old.accessor === Bar,
)

I think you're saying they should all return true here. That's currently only the case in Babel, not in esbuild or in TypeScript. I need to fix esbuild's handling of this.

I believe how accessors behave in this scenario is still determined by the outcome of https://github.com/tc39/proposal-decorators/issues/529. Currently old.accessor is specified to throw and Foo.accessor is specified to be undefined, although that may change so it's not really something we can write a test for at the moment. But I think what you're saying is whatever the behavior of accessors ends up being, there will be some way to get the value (whether it's old.accessor or Object.getOwnPropertyDescriptor(old, 'accessor').get.call(Foo)) and that value should be the right one.

evanw avatar Jun 09 '24 18:06 evanw