fix: JS/TS scope-resolution coverage gaps — F44, F83, F85, F86, F87 (#1929) (#1968)

* fix: JS/TS scope-resolution coverage gaps — F44, F83, F85, F86, F87 (#1929)

F44: Add (class) @scope.class for class expressions in TS query.
F83: Fix qualified new_expression (new ns.Foo()) to capture @reference.name.
F85: Add enum member declaration patterns (bare + valued) as @declaration.property.
F86: Unblocked by F44 — class expression methods get correct Class scope.
F87: Add 4 missing optional_parameter type annotation patterns (predefined_type,
     union_type, array_type, readonly_type) matching required_parameter.

Grammar verification via node-types.json confirms all node types exist.
9 new tests proving each fix fails on main and passes on the branch.

* chore(bench): update TypeScript scope-capture baseline after F44/F85/F87

---------

Co-authored-by: Sparsh <sparshprajapati2002@gmail.com>
This commit is contained in:
Gergő Magyar 2026-06-02 06:57:28 +01:00 committed by GitHub
parent bfe8a87831
commit 7691abf76a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 186 additions and 7 deletions

View file

@ -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<T>), 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",

View file

@ -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

View file

@ -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

View file

@ -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);
});
});