mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-05 07:37:30 +00:00
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
* 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>
296 lines
12 KiB
TypeScript
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);
|
|
});
|
|
});
|