GitNexus/gitnexus/test/unit/method-declaration-facts.test.ts
Gergő Magyar c417ccfc81
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
perf(ingestion): reduce indexing overhead and declaration memory (#3726)
* refactor(ingestion): share callable declaration facts

Reuse pure parameter and signature facts between structure and scope
extraction, with tree-owned caches and read-only parameter contracts.
Preserve provider-specific wrappers and consumer projections.

Add characterization tests, real-worker parity checks, and reproducible
performance measurements. Keep the overall indexing effect explicit.

Refs #3723

* fix(bench): create result files exclusively

Address CodeQL review feedback on #3726. Use a private default directory and an exclusive file handle so existing files and symlinks cannot be overwritten.

* perf: speed analyzer identity guard probes

* perf(ingestion): reuse callable type and overload facts

* ci: retain callable time and memory benchmarks

* chore: use established CI performance measurements

* fix(bench): check callable baseline in CI

* perf(ingestion): guard callable scaling and reuse export checks

* fix(ci): budget for callable scaling matrix

* refactor(ingestion): simplify callable handling

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
2026-10-26 15:13:48 +00:00

296 lines
12 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import Parser from 'tree-sitter';
import Java from 'tree-sitter-java';
import TypeScript from 'tree-sitter-typescript';
import { createMethodExtractor } from '../../src/core/ingestion/method-extractors/generic.js';
import {
javaMethodConfig,
kotlinMethodConfig,
} from '../../src/core/ingestion/method-extractors/configs/jvm.js';
import {
typescriptMethodConfig,
javascriptMethodConfig,
} from '../../src/core/ingestion/method-extractors/configs/typescript-javascript.js';
import { csharpMethodConfig } from '../../src/core/ingestion/method-extractors/configs/csharp.js';
import { pythonMethodConfig } from '../../src/core/ingestion/method-extractors/configs/python.js';
import { phpMethodConfig } from '../../src/core/ingestion/method-extractors/configs/php.js';
import { swiftMethodConfig } from '../../src/core/ingestion/method-extractors/configs/swift.js';
import { dartMethodConfig } from '../../src/core/ingestion/method-extractors/configs/dart.js';
import { computeJavaArityMetadata } from '../../src/core/ingestion/languages/java/arity-metadata.js';
import { computeTsArityMetadata } from '../../src/core/ingestion/languages/typescript/arity-metadata.js';
import { memoizeDeclarationFact } from '../../src/core/ingestion/method-extractors/declaration-facts.js';
import { computeKotlinArityMetadata } from '../../src/core/ingestion/languages/kotlin/arity-metadata.js';
import { computeCsharpArityMetadata } from '../../src/core/ingestion/languages/csharp/arity-metadata.js';
import { computePythonArityMetadata } from '../../src/core/ingestion/languages/python/arity-metadata.js';
import { computePhpArityMetadata } from '../../src/core/ingestion/languages/php/arity-metadata.js';
import { computeSwiftArityMetadata } from '../../src/core/ingestion/languages/swift/arity-metadata.js';
import { computeDartArityMetadata } from '../../src/core/ingestion/languages/dart/arity-metadata.js';
import {
getLanguageGrammar,
isLanguageAvailable,
} from '../../src/core/tree-sitter/parser-loader.js';
import { synthesizeSwiftSignatureBindings } from '../../src/core/ingestion/languages/swift/signature-bindings.js';
import { synthesizeDartSignatureBindings } from '../../src/core/ingestion/languages/dart/signature-bindings.js';
describe('shared callable declaration facts', () => {
for (const fixture of [
{
config: pythonMethodConfig,
code: 'class C:\n @staticmethod\n def f(x: int) -> int:\n return x\n',
owner: 'class_definition',
declaration: 'function_definition',
arity: computePythonArityMetadata,
},
{
config: dartMethodConfig,
code: 'class C { C(int x) {} }',
owner: 'class_definition',
declaration: 'constructor_signature',
arity: computeDartArityMetadata,
},
]) {
for (const scopeFirst of [true, false]) {
it.skipIf(!isLanguageAvailable(fixture.config.language))(
`${fixture.config.language}: shares facts across declaration wrappers, scopeFirst=${scopeFirst}`,
() => {
const parser = new Parser();
parser.setLanguage(getLanguageGrammar(fixture.config.language)!);
const tree = parser.parse(fixture.code);
expect(tree.rootNode.hasError).toBe(false);
const owner = tree.rootNode.descendantsOfType(fixture.owner)[0]!;
const declaration = owner.descendantsOfType(fixture.declaration)[0]!;
const spy = vi.spyOn(fixture.config, 'extractParameters');
try {
if (scopeFirst) fixture.arity(declaration);
const result = createMethodExtractor(fixture.config).extract(owner, {
language: fixture.config.language,
filePath: 'fixture',
})!;
if (!scopeFirst) fixture.arity(declaration);
expect(result.methods).toHaveLength(1);
expect(result.methods[0]!.parameters.map((p) => p.type)).toEqual(['int']);
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.results[0]!.value).toBe(spy.mock.results[1]!.value);
} finally {
spy.mockRestore();
}
},
);
}
}
for (const fixture of [
{
language: Java,
config: javaMethodConfig,
code: 'class C { int f(int x, String... rest) { return x; } }',
nodeType: 'method_declaration',
arity: computeJavaArityMetadata,
expectedTypes: ['int', 'String'],
},
{
language: TypeScript.typescript,
config: typescriptMethodConfig,
code: 'class C { f(x: number, ...rest: string[]): number { return x; } }',
nodeType: 'method_definition',
arity: computeTsArityMetadata,
expectedTypes: ['number', 'string[]'],
},
]) {
for (const scopeFirst of [true, false]) {
it(`${fixture.config.language}: traverses parameters once, scopeFirst=${scopeFirst}`, () => {
const parser = new Parser();
parser.setLanguage(fixture.language);
const tree = parser.parse(fixture.code);
const node = tree.rootNode.descendantsOfType(fixture.nodeType)[0]!;
const read = vi.spyOn(node, 'childForFieldName');
const extractor = createMethodExtractor(fixture.config);
const structure = () =>
extractor.extractFromNode!(node, {
filePath: 'fixture',
language: fixture.config.language,
});
if (scopeFirst) fixture.arity(node);
const info = structure()!;
if (!scopeFirst) fixture.arity(node);
expect(info.name).toBe('f');
expect(info.parameters.map((p) => p.rawType ?? p.type)).toEqual(fixture.expectedTypes);
expect(info.parameters.map((p) => p.isVariadic)).toEqual([false, true]);
expect(read.mock.calls.filter(([field]) => field === 'parameters')).toHaveLength(1);
read.mockRestore();
});
}
}
for (const fixture of [
{
config: kotlinMethodConfig,
code: 'class C {\n fun f(x: Int, y: String): Int { return x }\n}',
nodeType: 'function_declaration',
arity: computeKotlinArityMetadata,
types: ['Int', 'String'],
},
{
config: csharpMethodConfig,
code: 'class C { public int F(int x, string y = "") { return x; } }',
nodeType: 'method_declaration',
arity: computeCsharpArityMetadata,
types: ['int', 'string'],
},
{
config: pythonMethodConfig,
code: 'class C:\n def f(self, x: int, y: str = "") -> int:\n return x\n',
nodeType: 'function_definition',
arity: computePythonArityMetadata,
types: ['int', 'str'],
},
{
config: phpMethodConfig,
code: '<?php class C { function f(int $x, string ...$rest): int { return $x; } }',
nodeType: 'method_declaration',
arity: computePhpArityMetadata,
types: ['int', 'string'],
},
{
config: swiftMethodConfig,
code: 'class C { func f(_ x: Int, y: String) -> Int { return x } }',
nodeType: 'function_declaration',
arity: computeSwiftArityMetadata,
types: ['Int', 'String'],
},
{
config: dartMethodConfig,
code: "class C { int f(int x, [String y = '']) { return x; } }",
nodeType: 'method_signature',
arity: computeDartArityMetadata,
types: ['int', 'String'],
},
]) {
it.skipIf(!isLanguageAvailable(fixture.config.language))(
`${fixture.config.language}: scope and structure reuse parameter facts`,
() => {
const parser = new Parser();
parser.setLanguage(getLanguageGrammar(fixture.config.language)!);
const tree = parser.parse(fixture.code);
expect(tree.rootNode.hasError).toBe(false);
const node = tree.rootNode.descendantsOfType(fixture.nodeType)[0]!;
const spy = vi.spyOn(fixture.config, 'extractParameters');
try {
const scope = fixture.arity(node);
const info = createMethodExtractor(fixture.config).extractFromNode!(node, {
language: fixture.config.language,
filePath: 'fixture',
})!;
expect(info.parameters.map((p) => p.type)).toEqual(fixture.types);
expect(scope.parameterTypes?.slice(0, 2)).toEqual(fixture.types);
expect(spy).toHaveBeenCalledTimes(2);
expect(info.parameters).toBe(spy.mock.results[0]!.value);
} finally {
spy.mockRestore();
}
},
);
}
it('shares the parameter callback through the JavaScript config copy', () => {
const parser = new Parser();
parser.setLanguage(getLanguageGrammar(javascriptMethodConfig.language)!);
const node = parser
.parse('class C { f(x, ...rest) {} }')
.rootNode.descendantsOfType('method_definition')[0]!;
expect(javascriptMethodConfig.extractParameters).toBe(typescriptMethodConfig.extractParameters);
expect(javascriptMethodConfig.extractParameters(node)).toBe(
typescriptMethodConfig.extractParameters(node),
);
});
for (const fixture of [
{
config: swiftMethodConfig,
code: 'class C { func f(_ x: Int) -> Int { return x } }',
nodeType: 'function_declaration',
bindings: synthesizeSwiftSignatureBindings,
},
{
config: dartMethodConfig,
code: 'class C { int f(int x) { return x; } }',
nodeType: 'method_signature',
bindings: (node: Parser.SyntaxNode) =>
synthesizeDartSignatureBindings(node, node.nextNamedSibling),
},
]) {
it.skipIf(!isLanguageAvailable(fixture.config.language))(
`${fixture.config.language}: signature bindings share name and return facts`,
() => {
const parser = new Parser();
parser.setLanguage(getLanguageGrammar(fixture.config.language)!);
const node = parser.parse(fixture.code).rootNode.descendantsOfType(fixture.nodeType)[0]!;
let reads = 0;
const counted = new Proxy(node, {
get(target, property) {
if (property !== 'tree' && property !== 'id') reads++;
const value = Reflect.get(target, property, target);
return typeof value === 'function' ? value.bind(target) : value;
},
});
expect(fixture.bindings(counted).length).toBeGreaterThan(0);
const before = reads;
expect(before).toBeGreaterThan(0);
expect(fixture.config.extractName(counted)).toBe('f');
expect(fixture.config.extractReturnType(counted)).toMatch(/^[Ii]nt$/);
expect(reads).toBe(before);
},
);
}
it('separates same-line declarations and reparsed trees', () => {
const parser = new Parser();
parser.setLanguage(Java);
const first = parser.parse('class C { void f(int x) {} void f(String s) {} }');
const second = parser.parse('class C { void f(long x) {} }');
const [intNode, stringNode] = first.rootNode.descendantsOfType('method_declaration');
const longNode = second.rootNode.descendantsOfType('method_declaration')[0]!;
expect(javaMethodConfig.extractParameters(intNode!)[0]!.type).toBe('int');
expect(javaMethodConfig.extractParameters(stringNode!)[0]!.type).toBe('String');
expect(javaMethodConfig.extractParameters(longNode)[0]!.type).toBe('long');
});
it('reuses empty and absent facts across wrappers with the same native identity', () => {
const parser = new Parser();
parser.setLanguage(Java);
const tree = parser.parse('class C { void f() {} }');
const node = tree.rootNode.descendantsOfType('method_declaration')[0]!;
// A fresh traversal may already reuse a JS wrapper. This wrapper deliberately
// differs while preserving the actual native tree/node identity.
const wrapper = new Proxy(node, {});
expect(wrapper).not.toBe(node);
const absent = vi.fn(() => undefined);
const empty = vi.fn(() => []);
for (const extract of [absent, empty]) {
const shared = memoizeDeclarationFact(extract);
const first = shared(node);
expect(shared(wrapper)).toBe(first);
expect(extract).toHaveBeenCalledTimes(1);
}
});
it('retries failures and bypasses sharing when tree identity is unavailable', () => {
const parser = new Parser();
parser.setLanguage(Java);
const node = parser.parse('class C {}').rootNode;
const extract = vi
.fn<() => string>()
.mockImplementationOnce(() => {
throw new Error('incomplete');
})
.mockReturnValue('complete');
const shared = memoizeDeclarationFact(extract);
expect(() => shared(node)).toThrow('incomplete');
expect(shared(node)).toBe('complete');
expect(shared(node)).toBe('complete');
expect(extract).toHaveBeenCalledTimes(2);
const partial = {} as Parser.SyntaxNode;
shared(partial);
shared(partial);
expect(extract).toHaveBeenCalledTimes(4);
});
});