diff --git a/gitnexus/bench/scope-capture/baselines.json b/gitnexus/bench/scope-capture/baselines.json index 17e5d168a..113e42b0d 100644 --- a/gitnexus/bench/scope-capture/baselines.json +++ b/gitnexus/bench/scope-capture/baselines.json @@ -51,9 +51,10 @@ "_rebaselined": "#1956 synth-widening: + java-iface-extends fixture; synthesizeJavaInheritanceReferences now ALSO walks interface_declaration extends_interfaces (interface IA extends IB, IC), matching the #1940 legacy leg. (Earlier U2+review: java-qualified-base fixture covers 2- AND 3-segment qualified bases guarding the legacy end-anchor; synth tail-resolves scoped bases.) Linear (~1.03). (Earliest: java added to bench, exposed+fixed the O(n^2) findNodeAtRange root-walk; 3.09 -> ~0.99.)" }, "typescript": { - "fingerprint": "7087f62dbab5fff0d8a9c39f7bc305842ee73a7ba20d7b44677f6511c92e5b92", + "fingerprint": "3f44a4a6892698df2d145c8ff2812c3b318807648983c88aca28fbd694f172f9", "scaling_budget": 1.5, - "_rebaselined": "#1956 tri-review U2: + typescript-qualified-base fixture AND terminalTsTypeNameNode now treats a member_expression tail (property_identifier) as a leaf name, so qualified `extends ns.Base` synthesizes an edge (was dropped). Linear (~1.03)." + "_rebaselined": "#1962: F44 (class scope@), F85 (enum member declarations), F87 (optional_parameter type annotations) add new captures — fingerprint drift expected.", + "_note": "#1968: F44, F85, F87 — fingerprint drift expected." }, "javascript": { "fingerprint": "a8ddfb15620ae55e50651fc21ab14c4a1f874d9b19e208cc6cbf0a8daac8ec5b", diff --git a/gitnexus/src/core/ingestion/languages/javascript/query.ts b/gitnexus/src/core/ingestion/languages/javascript/query.ts index d07371782..f7c662b5f 100644 --- a/gitnexus/src/core/ingestion/languages/javascript/query.ts +++ b/gitnexus/src/core/ingestion/languages/javascript/query.ts @@ -446,7 +446,8 @@ const JAVASCRIPT_SCOPE_QUERY = ` constructor: (identifier) @reference.name) @reference.call.constructor (new_expression - constructor: (member_expression) @reference.call.constructor.qualified) @reference.call.constructor + constructor: (member_expression + property: (property_identifier) @reference.name) @reference.call.constructor.qualified) @reference.call.constructor ;; Write access: obj.field = value (assignment_expression diff --git a/gitnexus/src/core/ingestion/languages/typescript/query.ts b/gitnexus/src/core/ingestion/languages/typescript/query.ts index 3d54e839e..3c3986143 100644 --- a/gitnexus/src/core/ingestion/languages/typescript/query.ts +++ b/gitnexus/src/core/ingestion/languages/typescript/query.ts @@ -33,9 +33,8 @@ * same identifier also binds as a parameter in the constructor scope * via the normal `required_parameter` → `@type-binding.parameter` path. * - **Enum** — dual type+value. Emits `@scope.class` (enum body contains - * member declarations) + `@declaration.enum`. Members are captured as - * `@declaration.property` via the generic property_identifier pattern - * inside enum_body. + * member declarations) + `@declaration.enum`. Enum member names + * are captured via `enum_assignment` (see below). * * Node types pinned via `scripts/_probe_typescript_grammar.ts`: * internal_module, namespace_export, namespace_import, import_specifier, @@ -89,6 +88,11 @@ const TYPESCRIPT_SCOPE_QUERY = ` (abstract_class_declaration) @scope.class (interface_declaration) @scope.class (enum_declaration) @scope.class +;; Class expressions: const Foo = class { ... } / const Foo = class Named { ... }. +;; tree-sitter-typescript uses (class) (NOT class_expression) -- same node +;; name as tree-sitter-javascript. The name field is optional (anonymous +;; class expressions omit it); ScopeExtractor tolerates missing names. +(class) @scope.class (function_declaration) @scope.function (generator_function_declaration) @scope.function @@ -393,6 +397,9 @@ const TYPESCRIPT_SCOPE_QUERY = ` ${ARRAY_METHOD_NOT_ANY_OF_PREDICATE}) ;; Method definitions — regular + private (#field) methods. +;; These match inside both class_declaration and class (class expression) +;; bodies. The @scope.class for class expressions (see (class) above) +;; ensures methods inside class expressions get the correct Class scope parent. (method_definition name: (property_identifier) @declaration.name) @declaration.method @@ -414,6 +421,15 @@ const TYPESCRIPT_SCOPE_QUERY = ` (public_field_definition name: (private_property_identifier) @declaration.name) @declaration.property +;; Enum members: enum Color { Red, Green = 1, Blue }. +;; Bare members (no value): Red, Blue — property_identifier inside enum_body. +;; Members with value: Green = 1 — enum_assignment with name field. +(enum_body + (property_identifier) @declaration.name) @declaration.property + +(enum_assignment + name: (_) @declaration.name) @declaration.property + ;; Declarations — parameter properties: \`constructor(public name: string)\`. ;; The accessibility_modifier presence distinguishes these from regular ;; parameters. The identifier is also bound as a parameter in the @@ -535,6 +551,26 @@ const TYPESCRIPT_SCOPE_QUERY = ` type: (type_annotation (generic_type) @type-binding.type)) @type-binding.parameter +(optional_parameter + pattern: (identifier) @type-binding.name + type: (type_annotation + (predefined_type) @type-binding.type)) @type-binding.parameter + +(optional_parameter + pattern: (identifier) @type-binding.name + type: (type_annotation + (union_type) @type-binding.type)) @type-binding.parameter + +(optional_parameter + pattern: (identifier) @type-binding.name + type: (type_annotation + (array_type) @type-binding.type)) @type-binding.parameter + +(optional_parameter + pattern: (identifier) @type-binding.name + type: (type_annotation + (readonly_type) @type-binding.type)) @type-binding.parameter + ;; Type bindings — variable annotations: \`let u: User = ...\` / \`const u: User\`. (variable_declarator name: (identifier) @type-binding.name @@ -934,7 +970,8 @@ const TYPESCRIPT_SCOPE_QUERY = ` constructor: (identifier) @reference.name) @reference.call.constructor (new_expression - constructor: (member_expression) @reference.call.constructor.qualified) @reference.call.constructor + constructor: (member_expression + property: (property_identifier) @reference.name) @reference.call.constructor.qualified) @reference.call.constructor ;; References — write access: \`obj.field = value\`. (assignment_expression diff --git a/gitnexus/test/integration/resolvers/js-parsing-coverage.test.ts b/gitnexus/test/integration/resolvers/js-parsing-coverage.test.ts new file mode 100644 index 000000000..eb3db8238 --- /dev/null +++ b/gitnexus/test/integration/resolvers/js-parsing-coverage.test.ts @@ -0,0 +1,140 @@ +/** + * Regression tests for JS/TS scope-resolution coverage gaps (issue #1929). + * + * Each fixture FAILS on main and PASSES on the fix branch. + */ +import { describe, it, expect } from 'vitest'; +import { emitTsScopeCaptures } from '../../../src/core/ingestion/languages/typescript/captures.js'; + +function countTags(src: string, predicate: (tags: string[]) => boolean): number { + const matches = emitTsScopeCaptures(src, 'test.ts'); + return matches.filter((m) => predicate(Object.keys(m))).length; +} + +/** + * F44: Class expression scope. + * On main: (class) is NOT matched → zero @scope.class for class expressions. + * On fix: (class) @scope.class matches → exactly one Class scope. + */ +describe('F44 — class expression @scope.class', () => { + it('anonymous class expression emits @scope.class', () => { + const src = ` + export const instance = class { + greet(): string { return 'hi'; } + }; + `; + const count = countTags(src, (t) => t.includes('@scope.class')); + expect(count).toBe(1); + }); + + it('named class expression emits @scope.class with the Class scope', () => { + const src = ` + const X = class Named { + greet(): string { return 'hi'; } + }; + `; + const count = countTags(src, (t) => t.includes('@scope.class')); + // 1 for class expression + expect(count).toBe(1); + }); +}); + +/** + * F86: Class expression method_definition scope parent (blocked on F44). + * On main: class expression has no @scope.class → method falls through to + * enclosing scope, losing Class ownership. + * On fix: (class) @scope.class (F44) gives the method a proper Class parent. + */ +describe('F86 — class expression method ownership', () => { + it('class expression method has @declaration.method and @declaration.name', () => { + const src = ` + export const instance = class { + greet(): string { return 'hi'; } + }; + `; + const matches = emitTsScopeCaptures(src, 'test.ts'); + const methodDecls = matches.filter((m) => Object.keys(m).includes('@declaration.method')); + expect(methodDecls.length).toBe(1); + expect(methodDecls[0]['@declaration.name']?.text).toBe('greet'); + }); +}); + +/** + * F83: Qualified new_expression name capture. + * On main: new ns.Foo() has no @reference.name on the property. + * On fix: the member_expression's property is captured as @reference.name. + */ +describe('F83 — qualified new_expression @reference.name', () => { + it('new ns.Foo() captures Foo as @reference.name', () => { + const src = ` + namespace ns { + export class Foo {} + } + const x = new ns.Foo(); + `; + const matches = emitTsScopeCaptures(src, 'test.ts'); + const nameTags = matches + .filter((m) => Object.keys(m).includes('@reference.name')) + .map((m) => m['@reference.name']?.text); + expect(nameTags).toContain('Foo'); + }); +}); + +/** + * F85: Enum member @declaration.property. + * On main: enum members are NOT captured as @declaration.property. + * On fix: enum_assignment.name is captured as @declaration.property. + */ +describe('F85 — enum member @declaration.property', () => { + it('enum member names are captured as @declaration.property', () => { + const src = ` + enum Color { + Red, + Green = 1, + Blue + } + `; + const propCount = countTags(src, (t) => t.includes('@declaration.property')); + // Red, Green, Blue → 3 enum members + expect(propCount).toBe(3); + }); +}); + +/** + * F87: Optional parameter type annotations. + * On main: optional_parameter only matches type_identifier and generic_type. + * On fix: matches predefined_type, union_type, array_type, readonly_type too. + */ +describe('F87 — optional_parameter type annotations', () => { + it('optional_parameter with predefined_type (string) captures type binding', () => { + const src = ` + function f(x?: string): void {} + `; + const typeCount = countTags(src, (t) => t.includes('@type-binding.parameter')); + expect(typeCount).toBe(1); + }); + + it('optional_parameter with union_type captures type binding', () => { + const src = ` + function f(x?: string | null): void {} + `; + const typeCount = countTags(src, (t) => t.includes('@type-binding.parameter')); + expect(typeCount).toBe(1); + }); + + it('optional_parameter with array_type captures type binding', () => { + const src = ` + function f(x?: string[]): void {} + `; + const typeCount = countTags(src, (t) => t.includes('@type-binding.parameter')); + expect(typeCount).toBe(1); + }); + + it('optional_parameter with readonly_type captures type binding', () => { + const src = ` + function f(x?: readonly string[]): void {} + `; + const typeCount = countTags(src, (t) => t.includes('@type-binding.parameter')); + expect(typeCount).toBe(1); + }); +});