fix(javascript): type a class field from its initializer so it can be a receiver

JavaScript has no field annotations at all, so a class field's type can only
ever come from its initializer — which made this the strictly worse half of
#2807: `class C { p = new Outer(); }` gave `this.p` no type, and
`this.p.inner()` emitted nothing.

`synthesizeConstructorFieldBindings` in captures.ts already covered the sibling
shape, `this.p = new Outer()`, which is why THAT row resolved — but it only
walks `constructor` bodies, so a field initialized at its declaration matched
no pattern anywhere.

Adds the `field_definition` + `value: (new_expression)` patterns (the JS grammar
names the field `property:`, not `name:`), anchored so the binding lands in the
class body scope where `typeOfMemberOnClass` reads it. No hook change needed:
`jsBindingScopeFor` already delegates to `tsBindingScopeFor`, so it inherits the
`@type-binding.this-field` branch too.

Measured: `InferredField.run` now emits `Outer.inner`, exact parity with both
the local-const control and the constructor-assigned row. The second chain link
(`Inner.compute`) stays absent in ALL THREE rows — that is JavaScript's separate
return-type-inference gap, not this one.

Refs #2807

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-08-03 13:01:41 +00:00
parent 6869504408
commit 18a48c7313

View file

@ -430,6 +430,34 @@ export const JAVASCRIPT_SCOPE_QUERY = `
value: (new_expression
constructor: (member_expression) @type-binding.type)) @type-binding.constructor
;; Class field initializer: \`class C { p = new Outer(); }\` (#2807).
;; JavaScript has no field annotations at all, so a class field's type can only
;; ever come from its initializer — without this pattern \`this.p.inner()\` had
;; nothing to type the receiver with and the receiver fold declined the whole
;; chain. \`synthesizeConstructorFieldBindings\` in captures.ts already covers the
;; sibling shape (\`this.p = new Outer()\`), but only inside a \`constructor\`
;; body, so a field initialized at its declaration matched nothing.
;;
;; Anchored on \`field_definition\` so the binding lands in the class body scope,
;; where \`typeOfMemberOnClass\` reads it — the same anchoring TypeScript uses for
;; \`public_field_definition\`. Note the JS grammar names the field \`property:\`,
;; not \`name:\`.
(field_definition
property: (property_identifier) @type-binding.name
value: (new_expression
constructor: (identifier) @type-binding.type)) @type-binding.constructor
(field_definition
property: (property_identifier) @type-binding.name
value: (new_expression
constructor: (member_expression) @type-binding.type)) @type-binding.constructor
;; Private-name field: \`#p = new Outer()\`.
(field_definition
property: (private_property_identifier) @type-binding.name
value: (new_expression
constructor: (identifier) @type-binding.type)) @type-binding.constructor
;; Call-result alias: const u = getUser()
(variable_declarator
name: (identifier) @type-binding.name