mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-15 04:21:31 +08:00
codegraph_node's multi-definition answer rendered a name's definitions in index order (getNodesByName: file_path, start_line) until its caps ran out: 16 bodies within 12,000 characters, then 20 list lines, then a bare "+N more". Forty definitions under an early-sorting folder hid the only one in another language or a late folder: on the triage fixture (40 TypeScript `num` in aaa_client/, one Python `num` in zzz_server/), zzz_server/format.py appeared nowhere. Without includeCode, a long list was cut by the output cap at "... (output truncated)". The bodies still fill in index order, so every overload that rendered before still renders. On top of them, each language with no body gets its first hand-written definition rendered, while the whole answer stays under the output cap. The list of the rest fills its 20 slots in a new definitionCapOrder (the first definition of each language, then of each top-level folder within a language, then the rest, each tier in index order) and is shown in index order; the overflow line names what it leaves out by language and folder, like "+365 more: 365 typescript in aaa_client/". The list-only answer is unchanged unless it is longer than the output cap; then it fills in the same order and names the rest the same way. Generated stubs stay behind hand-written definitions. Promoting each folder's first definition into the bodies, as first tried, cost Alamofire's `validate` three Validation.swift overloads to a Tests/ helper, and `adapt`, `urlSession`, `encode` and `cancel` one or two Source/ bodies each, so folders get early list slots only. Validation: new node-overload-spread tests (6): 5 fail on main's source (the fits-every-cap guard passes on both) and all pass with the fix. A/B of every multi-definition name, with and without includeCode, main vs fix: gin 171 names, 170 identical (`_` only names its overflow); Alamofire 706 names, 702 identical. No body that rendered before is lost: `cancel` adds the jQuery `cancel` from docs/ as a 13th body, and `request`, `urlSession` and `encode` swap one Source/ list line for the first Tests/ one and name their overflow. Alamofire's `validate`, `task` and `didCompleteTask` and gin's `reset`, `Bind` and `Render` are byte-identical. Reimplements PR #1657 by @ferrine. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
158 lines
7.5 KiB
TypeScript
158 lines
7.5 KiB
TypeScript
/**
|
|
* codegraph_node on a name with more definitions than its caps hold (#1657).
|
|
*
|
|
* The index lists a name's definitions by path, and the multi-definition
|
|
* answer rendered and listed them in that order until its caps ran out: 16
|
|
* bodies, 20 list lines, then a bare "+N more". So hundreds of definitions
|
|
* under an early-sorting folder hid the only one in another language or a
|
|
* late folder, and nothing in the answer said another existed. Now a language
|
|
* with no body gets one on top of the overloads that fit, each language and
|
|
* top-level folder gets an early list line, and the overflow names what it
|
|
* leaves out. The overloads that rendered before still render.
|
|
*/
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import CodeGraph from '../src/index';
|
|
import { ToolHandler, definitionCapOrder, describeOmittedDefinitions } from '../src/mcp/tools';
|
|
import type { Node } from '../src/types';
|
|
|
|
const MAX_OUTPUT_LENGTH = 15000;
|
|
|
|
function write(root: string, rel: string, body: string): void {
|
|
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
|
fs.writeFileSync(path.join(root, rel), body);
|
|
}
|
|
|
|
async function indexed(root: string): Promise<{ cg: CodeGraph; handler: ToolHandler }> {
|
|
const cg = CodeGraph.initSync(root);
|
|
await cg.indexAll();
|
|
return { cg, handler: new ToolHandler(cg) };
|
|
}
|
|
|
|
async function nodeText(handler: ToolHandler, args: Record<string, unknown>): Promise<string> {
|
|
const res = await handler.execute('codegraph_node', args);
|
|
expect(res.isError).toBeFalsy();
|
|
return res.content?.[0]?.text ?? '';
|
|
}
|
|
|
|
describe('codegraph_node: definitions past the caps (#1657)', () => {
|
|
let root: string;
|
|
let cg: CodeGraph;
|
|
let handler: ToolHandler;
|
|
|
|
beforeAll(async () => {
|
|
// 400 TypeScript `num` methods under an early-sorting folder, one more in
|
|
// another top-level folder, and the only Python `num` in a late one.
|
|
root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-node-spread-'));
|
|
for (let f = 0; f < 40; f++) {
|
|
const classes = Array.from({ length: 10 }, (_, c) =>
|
|
`export class C${f}_${c} {\n num(v: number): string {\n return v.toFixed(${c});\n }\n}\n`).join('\n');
|
|
write(root, `aaa_client/m${String(f).padStart(2, '0')}.ts`, classes);
|
|
}
|
|
write(root, 'bbb_lib/helper.ts', 'export function num(v: number): number {\n return Math.round(v);\n}\n');
|
|
write(root, 'zzz_server/format.py', 'def num(value):\n return round(value, 2)\n');
|
|
({ cg, handler } = await indexed(root));
|
|
}, 120_000);
|
|
|
|
afterAll(() => {
|
|
handler?.closeAll();
|
|
cg?.destroy();
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
it('renders the only Python definition beside the TypeScript overloads that fit', async () => {
|
|
const text = await nodeText(handler, { symbol: 'num', includeCode: true });
|
|
expect(text).toContain('**402 definitions named "num"**');
|
|
expect(text).toContain('Returning 17 in full; 385 more listed below');
|
|
// main: the Python body, and the file, appeared nowhere.
|
|
expect(text).toContain('**Location:** zzz_server/format.py:1');
|
|
expect(text).toContain('def num(value):');
|
|
// The same sixteen TypeScript bodies as before still render, in index order.
|
|
const tsBodies = [...text.matchAll(/^\*\*Location:\*\* (aaa_client\/\S+)/gm)].map((m) => m[1]);
|
|
expect(tsBodies).toEqual([
|
|
...Array.from({ length: 10 }, (_, c) => `aaa_client/m00.ts:${2 + 6 * c}`),
|
|
...Array.from({ length: 6 }, (_, c) => `aaa_client/m01.ts:${2 + 6 * c}`),
|
|
]);
|
|
expect(text.length).toBeLessThanOrEqual(MAX_OUTPUT_LENGTH);
|
|
});
|
|
|
|
it('gives another top-level folder an early list line and names the overflow', async () => {
|
|
const text = await nodeText(handler, { symbol: 'num', includeCode: true });
|
|
const listed = [...text.slice(text.indexOf('**Other definitions**')).matchAll(/^- `num` \(\w+\) — (\S+)$/gm)].map((m) => m[1]);
|
|
expect(listed).toHaveLength(20);
|
|
// main: bbb_lib/helper.ts was behind a bare "- … +365 more".
|
|
expect(listed).toContain('bbb_lib/helper.ts:1');
|
|
expect(text).toContain('- … +365 more: 365 typescript in aaa_client/');
|
|
expect(text).not.toMatch(/^- … \+\d+ more$/m);
|
|
});
|
|
|
|
it('keeps every language and folder in a list too long for the output cap', async () => {
|
|
const text = await nodeText(handler, { symbol: 'num' });
|
|
// main: the output cap cut the list at "... (output truncated)", past
|
|
// which both late folders fell.
|
|
expect(text).not.toContain('(output truncated)');
|
|
expect(text.length).toBeLessThanOrEqual(MAX_OUTPUT_LENGTH);
|
|
expect(text).toContain('- `num` (function) — zzz_server/format.py:1');
|
|
expect(text).toContain('- `num` (function) — bbb_lib/helper.ts:1');
|
|
expect(text).toMatch(/^- … \+\d+ more: \d+ typescript in aaa_client\/$/m);
|
|
});
|
|
});
|
|
|
|
describe('codegraph_node: a set that fits every cap reads as before (#1657)', () => {
|
|
let root: string;
|
|
let cg: CodeGraph;
|
|
let handler: ToolHandler;
|
|
|
|
beforeAll(async () => {
|
|
root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-node-fits-'));
|
|
write(root, 'a/x.ts', 'export function num(v: number): string {\n return String(v);\n}\n');
|
|
write(root, 'b/y.py', 'def num(value):\n return str(value)\n');
|
|
write(root, 'c/z.ts', 'export function num(v: number): number {\n return v;\n}\n');
|
|
({ cg, handler } = await indexed(root));
|
|
}, 60_000);
|
|
|
|
afterAll(() => {
|
|
handler?.closeAll();
|
|
cg?.destroy();
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
it('renders every definition in index order with nothing listed', async () => {
|
|
const text = await nodeText(handler, { symbol: 'num', includeCode: true });
|
|
expect(text).toContain('Returning 3 in full — pick the one you need');
|
|
expect([...text.matchAll(/^\*\*Location:\*\* (\S+)/gm)].map((m) => m[1])).toEqual(['a/x.ts:1', 'b/y.py:1', 'c/z.ts:1']);
|
|
expect(text).not.toContain('**Other definitions**');
|
|
});
|
|
});
|
|
|
|
describe('definitionCapOrder / describeOmittedDefinitions (#1657)', () => {
|
|
const def = (filePath: string, language: string, startLine = 1): Node =>
|
|
({ id: `${filePath}:${startLine}`, name: 'num', kind: 'function', filePath, language, startLine } as unknown as Node);
|
|
|
|
it('puts each language first, then each top-level folder, then the rest in order', () => {
|
|
const nodes = [
|
|
def('a/1.ts', 'typescript'), def('a/2.ts', 'typescript'), def('b/1.ts', 'typescript'),
|
|
def('b/2.ts', 'typescript'), def('root.py', 'python'), def('z/1.py', 'python'),
|
|
];
|
|
expect(definitionCapOrder(nodes).map((n) => n.filePath)).toEqual([
|
|
'a/1.ts', 'root.py', // the first in each language
|
|
'b/1.ts', 'z/1.py', // the first in each other folder within a language
|
|
'a/2.ts', 'b/2.ts', // the rest
|
|
]);
|
|
const oneFolder = [def('a/1.ts', 'typescript'), def('a/2.ts', 'typescript', 9), def('a/3.ts', 'typescript')];
|
|
expect(definitionCapOrder(oneFolder)).toEqual(oneFolder);
|
|
});
|
|
|
|
it('names omitted definitions by language and folder, largest first', () => {
|
|
expect(describeOmittedDefinitions([
|
|
def('z/1.py', 'python'), def('a/1.ts', 'typescript'), def('a/2.ts', 'typescript'), def('main.go', 'go'),
|
|
])).toBe('2 typescript in a/, 1 python in z/, 1 go at the project root');
|
|
const many = Array.from({ length: 8 }, (_, i) => def(`d${i}/x.ts`, 'typescript'));
|
|
expect(describeOmittedDefinitions(many)).toBe(
|
|
'1 typescript in d0/, 1 typescript in d1/, 1 typescript in d2/, 1 typescript in d3/, 1 typescript in d4/, 1 typescript in d5/, 2 elsewhere',
|
|
);
|
|
});
|
|
});
|