mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-26 04:19:33 +08:00
DFM/FMX component ranges stayed pinned to their declaration lines because closing blocks only popped IDs. Store nodes in the component stack and update their ending positions at the matching end. Cover persisted ranges and MCP source retrieval while preserving containment and event references. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c505d0de08
commit
f6e557c3ec
@@ -5822,6 +5822,89 @@ end`;
|
||||
expect(components.length).toBe(2);
|
||||
});
|
||||
|
||||
describe('component source ranges (#1350)', () => {
|
||||
let tempDir: string;
|
||||
let cg: CodeGraph | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = createTempDir();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cg?.close();
|
||||
cg = undefined;
|
||||
cleanupTempDir(tempDir);
|
||||
});
|
||||
|
||||
it.each(['dfm', 'fmx'])('persists complete nested %s bodies and retrieves event bindings', async (extension) => {
|
||||
const source = `inherited Form1: TForm1
|
||||
inline Frame1: TFrame
|
||||
object Button1: TButton
|
||||
Caption = 'Click'
|
||||
Items.Strings = (
|
||||
'First'
|
||||
'Second')
|
||||
Panels = <
|
||||
item
|
||||
Width = 100
|
||||
end
|
||||
item
|
||||
Width = 200
|
||||
end>
|
||||
OnClick = Button1Click
|
||||
end
|
||||
OnEnter = FrameEnter
|
||||
end
|
||||
object Label1: TLabel
|
||||
Caption = 'Sibling'
|
||||
end
|
||||
end`;
|
||||
const fileName = `Form1.${extension}`;
|
||||
fs.writeFileSync(path.join(tempDir, fileName), source);
|
||||
cg = CodeGraph.initSync(tempDir);
|
||||
expect((await cg.indexAll()).filesIndexed).toBe(1);
|
||||
|
||||
const nodes = cg.getNodesInFile(fileName);
|
||||
const components = nodes.filter((node) => node.kind === 'component');
|
||||
expect(components.map(({ name, startLine, endLine, endColumn }) => ({
|
||||
name, startLine, endLine, endColumn,
|
||||
}))).toEqual(expect.arrayContaining([
|
||||
{ name: 'Form1', startLine: 1, endLine: 22, endColumn: 3 },
|
||||
{ name: 'Frame1', startLine: 2, endLine: 18, endColumn: 5 },
|
||||
{ name: 'Button1', startLine: 3, endLine: 16, endColumn: 7 },
|
||||
{ name: 'Label1', startLine: 19, endLine: 21, endColumn: 5 },
|
||||
]));
|
||||
expect(components).toHaveLength(4);
|
||||
|
||||
const extracted = extractFromSource(fileName, source);
|
||||
const file = extracted.nodes.find((node) => node.kind === 'file')!;
|
||||
const form = components.find((node) => node.name === 'Form1')!;
|
||||
const frame = components.find((node) => node.name === 'Frame1')!;
|
||||
const button = components.find((node) => node.name === 'Button1')!;
|
||||
const label = components.find((node) => node.name === 'Label1')!;
|
||||
for (const [parent, child] of [[file, form], [form, frame], [frame, button], [form, label]]) {
|
||||
expect(extracted.edges).toContainEqual({ source: parent!.id, target: child!.id, kind: 'contains' });
|
||||
}
|
||||
expect(extracted.unresolvedReferences).toEqual([
|
||||
expect.objectContaining({ fromNodeId: button.id, referenceName: 'Button1Click' }),
|
||||
expect.objectContaining({ fromNodeId: frame.id, referenceName: 'FrameEnter' }),
|
||||
]);
|
||||
expect(file.endLine).toBe(22);
|
||||
|
||||
const { ToolHandler } = await import('../src/mcp/tools');
|
||||
const handler = new ToolHandler(cg);
|
||||
for (const [tool, args] of [
|
||||
['codegraph_node', { symbol: 'Button1', includeCode: true }],
|
||||
['codegraph_explore', { query: 'Button1' }],
|
||||
] as const) {
|
||||
const result = await handler.execute(tool, args);
|
||||
expect(result.isError).toBeUndefined();
|
||||
expect(result.content[0]!.text).toContain("Caption = 'Click'");
|
||||
expect(result.content[0]!.text).toContain('OnClick = Button1Click');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Full fixture: MainForm.dfm', () => {
|
||||
const code = `object frmMain: TfrmMain
|
||||
Left = 0
|
||||
|
||||
@@ -34,7 +34,7 @@ export class DfmExtractor {
|
||||
|
||||
try {
|
||||
const fileNode = this.createFileNode();
|
||||
this.parseComponents(fileNode.id);
|
||||
this.parseComponents(fileNode);
|
||||
} catch (error) {
|
||||
this.errors.push({
|
||||
message: `DFM extraction error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
@@ -76,9 +76,9 @@ export class DfmExtractor {
|
||||
}
|
||||
|
||||
/** Parse object/end blocks and extract components + event handlers */
|
||||
private parseComponents(fileNodeId: string): void {
|
||||
private parseComponents(fileNode: Node): void {
|
||||
const lines = this.source.split('\n');
|
||||
const stack: string[] = [fileNodeId];
|
||||
const stack: Node[] = [fileNode];
|
||||
|
||||
const objectPattern = /^\s*(object|inherited|inline)\s+(\w+)\s*:\s*(\w+)/;
|
||||
const eventPattern = /^\s*(On\w+)\s*=\s*(\w+)\s*$/;
|
||||
@@ -113,7 +113,7 @@ export class DfmExtractor {
|
||||
if (objMatch) {
|
||||
const [, , name, typeName] = objMatch;
|
||||
const nodeId = generateNodeId(this.filePath, 'component', name!, lineNum);
|
||||
this.nodes.push({
|
||||
const node: Node = {
|
||||
id: nodeId,
|
||||
kind: 'component',
|
||||
name: name!,
|
||||
@@ -126,13 +126,14 @@ export class DfmExtractor {
|
||||
endColumn: line.length,
|
||||
signature: typeName,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
};
|
||||
this.nodes.push(node);
|
||||
this.edges.push({
|
||||
source: stack[stack.length - 1]!,
|
||||
source: stack[stack.length - 1]!.id,
|
||||
target: nodeId,
|
||||
kind: 'contains',
|
||||
});
|
||||
stack.push(nodeId);
|
||||
stack.push(node);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -141,7 +142,7 @@ export class DfmExtractor {
|
||||
if (eventMatch) {
|
||||
const [, , methodName] = eventMatch;
|
||||
this.unresolvedReferences.push({
|
||||
fromNodeId: stack[stack.length - 1]!,
|
||||
fromNodeId: stack[stack.length - 1]!.id,
|
||||
referenceName: methodName!,
|
||||
referenceKind: 'references',
|
||||
line: lineNum,
|
||||
@@ -152,7 +153,11 @@ export class DfmExtractor {
|
||||
|
||||
// Block end
|
||||
if (endPattern.test(line)) {
|
||||
if (stack.length > 1) stack.pop();
|
||||
if (stack.length > 1) {
|
||||
const node = stack.pop()!;
|
||||
node.endLine = lineNum;
|
||||
node.endColumn = line.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user