From 18a48c7313118d9c7973527d7855ede94890c231 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Mon, 3 Aug 2026 13:01:41 +0000 Subject: [PATCH] fix(javascript): type a class field from its initializer so it can be a receiver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../ingestion/languages/javascript/query.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/gitnexus/src/core/ingestion/languages/javascript/query.ts b/gitnexus/src/core/ingestion/languages/javascript/query.ts index 7445929ba..ec2d6a3f2 100644 --- a/gitnexus/src/core/ingestion/languages/javascript/query.ts +++ b/gitnexus/src/core/ingestion/languages/javascript/query.ts @@ -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