diff --git a/gitnexus/src/core/ingestion/tree-sitter-queries.ts b/gitnexus/src/core/ingestion/tree-sitter-queries.ts index 2375ab37f..51504853b 100644 --- a/gitnexus/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus/src/core/ingestion/tree-sitter-queries.ts @@ -342,6 +342,25 @@ export const TYPESCRIPT_QUERIES = ` (public_field_definition name: (private_property_identifier) @name) @definition.property +; Closure-valued class fields (#2693): \`handler = (x) => x\` is a CALLABLE +; member, so it emits Method like every other closure binding rather than a +; Property that CALLS edges would point at — a call target must be callable. +; Kotlin already models its class-body closure this way (Method + HAS_METHOD). +; +; Note this diverges from tsc's SymbolFlags and SCIP's descriptor, which both +; class an arrow-initialized field as a PROPERTY/term. That is deliberate: the +; label here means "is a call target", not "is a tsc symbol kind", and #2687 set +; that convention for closure bindings in every language. Anchored on +; public_field_definition — the same node the property rules use — so the +; parse-worker dedup collapses the pair (callable ranks highest). +(public_field_definition + name: (property_identifier) @name + value: (arrow_function)) @definition.method + +(public_field_definition + name: (property_identifier) @name + value: (function_expression)) @definition.method + ; Constructor parameter properties: constructor(public address: Address) (required_parameter (accessibility_modifier) @@ -667,6 +686,16 @@ export const JAVASCRIPT_QUERIES = ` (field_definition property: (property_identifier) @name) @definition.property +; Closure-valued class fields (#2693) — see the TypeScript block for why these +; are Method rather than Property. +(field_definition + property: (property_identifier) @name + value: (arrow_function)) @definition.method + +(field_definition + property: (property_identifier) @name + value: (function_expression)) @definition.method + ; Write access: obj.field = value (assignment_expression left: (member_expression diff --git a/gitnexus/test/integration/closure-binding-labels.test.ts b/gitnexus/test/integration/closure-binding-labels.test.ts index 4dca786d9..bbad6f8cc 100644 --- a/gitnexus/test/integration/closure-binding-labels.test.ts +++ b/gitnexus/test/integration/closure-binding-labels.test.ts @@ -116,6 +116,14 @@ describe('closure bindings emit a single Function node in every language', () => ]); }); + it('TypeScript: a NON-closure class field stays a Property', async () => { + // The closure rule must key on the initializer, not the field syntax — + // otherwise every class field would become a callable member. + expect( + await labelsFor('src/plain.ts', 'export class A {\n address = "x";\n}\n', 'address'), + ).toEqual(['Property']); + }); + it('Python: an annotated attribute stays a Property, not a Variable', async () => { // Regression guard. Python matches BOTH `@definition.property` (annotated) // and `@definition.variable` (bare assignment) on the same statement at the @@ -405,6 +413,32 @@ describeIfWorkerBuilt('closure bindings resolve in the remaining languages (#269 expect(targets).toEqual(['Function:b.php:handler']); }); + it('TypeScript: a class-field arrow is a callable member, like Kotlin', async () => { + // A CALLS edge must target a CALLABLE node. This field used to emit + // Property, so the edge pointed at a non-callable — the same defect class + // as the `var` case below. Kotlin already modelled its class-body closure + // as Method + HAS_METHOD. + // + // This diverges from tsc (PropertyDeclaration) and SCIP (a `.` term), both + // of which class an arrow-initialised field as a property. Deliberate: the + // label means "is a call target" here, not "is a tsc symbol kind". + const targets = await callTargetsFor( + 'Box.ts', + 'export class Box {\n handler = (x: number) => x;\n caller(): number { return this.handler(1); }\n}\n', + ); + + expect(targets).toEqual(['Method:Box.ts:Box.handler']); + }); + + it('JavaScript: a class-field arrow is a callable member', async () => { + const targets = await callTargetsFor( + 'C.js', + 'export class C {\n handler = (x) => x;\n caller() { return this.handler(1); }\n}\n', + ); + + expect(targets).toEqual(['Method:C.js:C.handler']); + }); + it('JavaScript: a `var` closure binding is a Function, like const/let', async () => { // `var` is a different grammar node than const/let, so it kept a Variable // label — and the CALLS edge that resolved through the declaration route