feat: Add Pascal/Delphi language support with DFM/FMX extraction

Implements tree-sitter-based extraction for Pascal/Delphi source files
(.pas, .dpr, .dpk, .lpr) and a custom regex-based DfmExtractor for
form files (.dfm, .fmx). Covers classes, records, interfaces, methods,
properties, fields, enums, constants, type aliases, uses-imports,
visibility sections, inheritance, call extraction, and DFM component
hierarchies with event handler references.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Olaf Monien
2026-02-11 08:58:13 +01:00
co-authored by Claude Opus 4.6
parent 8dfc99572f
commit 425bfceea5
6 changed files with 1347 additions and 4 deletions
+702
View File
@@ -1810,6 +1810,708 @@ import 'package:flutter/material.dart';
});
});
// =============================================================================
// Pascal / Delphi Extraction
// =============================================================================
describe('Pascal / Delphi Extraction', () => {
describe('Language detection', () => {
it('should detect Pascal files', () => {
expect(detectLanguage('UAuth.pas')).toBe('pascal');
expect(detectLanguage('App.dpr')).toBe('pascal');
expect(detectLanguage('Package.dpk')).toBe('pascal');
expect(detectLanguage('App.lpr')).toBe('pascal');
expect(detectLanguage('MainForm.dfm')).toBe('pascal');
expect(detectLanguage('MainForm.fmx')).toBe('pascal');
});
it('should report Pascal as supported', () => {
expect(isLanguageSupported('pascal')).toBe(true);
expect(getSupportedLanguages()).toContain('pascal');
});
});
describe('Unit extraction', () => {
it('should extract unit as module', () => {
const code = `unit MyUnit;\ninterface\nimplementation\nend.`;
const result = extractFromSource('MyUnit.pas', code);
const moduleNode = result.nodes.find((n) => n.kind === 'module');
expect(moduleNode).toBeDefined();
expect(moduleNode?.name).toBe('MyUnit');
expect(moduleNode?.language).toBe('pascal');
});
it('should extract program as module', () => {
const code = `program MyApp;\nbegin\nend.`;
const result = extractFromSource('MyApp.dpr', code);
const moduleNode = result.nodes.find((n) => n.kind === 'module');
expect(moduleNode).toBeDefined();
expect(moduleNode?.name).toBe('MyApp');
});
});
describe('Uses clause (imports)', () => {
it('should extract uses as individual imports', () => {
const code = `unit Test;\ninterface\nuses\n System.SysUtils,\n System.Classes;\nimplementation\nend.`;
const result = extractFromSource('Test.pas', code);
const imports = result.nodes.filter((n) => n.kind === 'import');
expect(imports.length).toBe(2);
expect(imports.map((n) => n.name)).toContain('System.SysUtils');
expect(imports.map((n) => n.name)).toContain('System.Classes');
});
it('should create unresolved references for imports', () => {
const code = `unit Test;\ninterface\nuses\n UAuth;\nimplementation\nend.`;
const result = extractFromSource('Test.pas', code);
const importRef = result.unresolvedReferences.find(
(r) => r.referenceKind === 'imports'
);
expect(importRef).toBeDefined();
expect(importRef?.referenceName).toBe('UAuth');
});
});
describe('Class extraction', () => {
it('should extract class declarations', () => {
const code = `unit Test;\ninterface\ntype\n TMyClass = class\n public\n procedure DoSomething;\n end;\nimplementation\nend.`;
const result = extractFromSource('Test.pas', code);
const classNode = result.nodes.find((n) => n.kind === 'class');
expect(classNode).toBeDefined();
expect(classNode?.name).toBe('TMyClass');
});
it('should extract class with inheritance', () => {
const code = `unit Test;\ninterface\ntype\n TChild = class(TParent)\n end;\nimplementation\nend.`;
const result = extractFromSource('Test.pas', code);
const extendsRef = result.unresolvedReferences.find(
(r) => r.referenceKind === 'extends'
);
expect(extendsRef).toBeDefined();
expect(extendsRef?.referenceName).toBe('TParent');
});
it('should extract class with interface implementation', () => {
const code = `unit Test;\ninterface\ntype\n TService = class(TInterfacedObject, ILogger)\n end;\nimplementation\nend.`;
const result = extractFromSource('Test.pas', code);
const extendsRef = result.unresolvedReferences.find(
(r) => r.referenceKind === 'extends'
);
const implementsRef = result.unresolvedReferences.find(
(r) => r.referenceKind === 'implements'
);
expect(extendsRef?.referenceName).toBe('TInterfacedObject');
expect(implementsRef?.referenceName).toBe('ILogger');
});
});
describe('Record extraction', () => {
it('should extract records as class nodes', () => {
const code = `unit Test;\ninterface\ntype\n TPoint = record\n X: Double;\n Y: Double;\n end;\nimplementation\nend.`;
const result = extractFromSource('Test.pas', code);
const classNode = result.nodes.find((n) => n.kind === 'class');
expect(classNode).toBeDefined();
expect(classNode?.name).toBe('TPoint');
const fields = result.nodes.filter((n) => n.kind === 'field');
expect(fields.length).toBe(2);
expect(fields.map((f) => f.name)).toContain('X');
expect(fields.map((f) => f.name)).toContain('Y');
});
});
describe('Interface extraction', () => {
it('should extract interface declarations', () => {
const code = `unit Test;\ninterface\ntype\n ILogger = interface\n procedure Log(const AMsg: string);\n end;\nimplementation\nend.`;
const result = extractFromSource('Test.pas', code);
const ifaceNode = result.nodes.find((n) => n.kind === 'interface');
expect(ifaceNode).toBeDefined();
expect(ifaceNode?.name).toBe('ILogger');
});
});
describe('Method extraction', () => {
it('should extract methods with visibility', () => {
const code = `unit Test;\ninterface\ntype\n TMyClass = class\n private\n FValue: Integer;\n public\n constructor Create;\n function GetValue: Integer;\n end;\nimplementation\nend.`;
const result = extractFromSource('Test.pas', code);
const methods = result.nodes.filter((n) => n.kind === 'method');
expect(methods.length).toBe(2);
const createMethod = methods.find((m) => m.name === 'Create');
expect(createMethod?.visibility).toBe('public');
const getValue = methods.find((m) => m.name === 'GetValue');
expect(getValue?.visibility).toBe('public');
const fields = result.nodes.filter((n) => n.kind === 'field');
const fValue = fields.find((f) => f.name === 'FValue');
expect(fValue?.visibility).toBe('private');
});
it('should detect static methods (class methods)', () => {
const code = `unit Test;\ninterface\ntype\n THelper = class\n public\n class function Create: THelper; static;\n end;\nimplementation\nend.`;
const result = extractFromSource('Test.pas', code);
const methods = result.nodes.filter((n) => n.kind === 'method');
const staticMethod = methods.find((m) => m.name === 'Create');
expect(staticMethod?.isStatic).toBe(true);
});
});
describe('Enum extraction', () => {
it('should extract enums with members', () => {
const code = `unit Test;\ninterface\ntype\n TColor = (clRed, clGreen, clBlue);\nimplementation\nend.`;
const result = extractFromSource('Test.pas', code);
const enumNode = result.nodes.find((n) => n.kind === 'enum');
expect(enumNode).toBeDefined();
expect(enumNode?.name).toBe('TColor');
const members = result.nodes.filter((n) => n.kind === 'enum_member');
expect(members.length).toBe(3);
expect(members.map((m) => m.name)).toEqual(['clRed', 'clGreen', 'clBlue']);
});
});
describe('Property extraction', () => {
it('should extract properties', () => {
const code = `unit Test;\ninterface\ntype\n TObj = class\n public\n property Name: string read FName write FName;\n end;\nimplementation\nend.`;
const result = extractFromSource('Test.pas', code);
const propNode = result.nodes.find((n) => n.kind === 'property');
expect(propNode).toBeDefined();
expect(propNode?.name).toBe('Name');
expect(propNode?.visibility).toBe('public');
});
});
describe('Constant extraction', () => {
it('should extract constants', () => {
const code = `unit Test;\ninterface\nconst\n MAX_RETRIES = 3;\n APP_NAME = 'MyApp';\nimplementation\nend.`;
const result = extractFromSource('Test.pas', code);
const constants = result.nodes.filter((n) => n.kind === 'constant');
expect(constants.length).toBe(2);
expect(constants.map((c) => c.name)).toContain('MAX_RETRIES');
expect(constants.map((c) => c.name)).toContain('APP_NAME');
});
});
describe('Type alias extraction', () => {
it('should extract type aliases', () => {
const code = `unit Test;\ninterface\ntype\n TUserName = string;\nimplementation\nend.`;
const result = extractFromSource('Test.pas', code);
const aliasNode = result.nodes.find((n) => n.kind === 'type_alias');
expect(aliasNode).toBeDefined();
expect(aliasNode?.name).toBe('TUserName');
});
});
describe('Call extraction', () => {
it('should extract calls from implementation bodies', () => {
const code = `unit Test;\ninterface\ntype\n TObj = class\n public\n procedure DoWork;\n end;\nimplementation\nprocedure TObj.DoWork;\nbegin\n WriteLn('hello');\nend;\nend.`;
const result = extractFromSource('Test.pas', code);
const callRef = result.unresolvedReferences.find(
(r) => r.referenceKind === 'calls'
);
expect(callRef).toBeDefined();
expect(callRef?.referenceName).toBe('WriteLn');
});
});
describe('Containment edges', () => {
it('should create contains edges for class members', () => {
const code = `unit Test;\ninterface\ntype\n TObj = class\n public\n procedure Foo;\n end;\nimplementation\nend.`;
const result = extractFromSource('Test.pas', code);
const classNode = result.nodes.find((n) => n.kind === 'class');
const methodNode = result.nodes.find((n) => n.kind === 'method');
expect(classNode).toBeDefined();
expect(methodNode).toBeDefined();
const containsEdge = result.edges.find(
(e) => e.source === classNode?.id && e.target === methodNode?.id && e.kind === 'contains'
);
expect(containsEdge).toBeDefined();
});
});
describe('Full fixture: UAuth.pas', () => {
const code = `unit UAuth;
interface
uses
System.SysUtils,
System.Classes;
type
ITokenValidator = interface
['{11111111-1111-1111-1111-111111111111}']
function Validate(const AToken: string): Boolean;
end;
TAuthService = class(TInterfacedObject, ITokenValidator)
private
FToken: string;
FLoginCount: Integer;
procedure IncLoginCount;
protected
function GetToken: string;
public
constructor Create;
destructor Destroy; override;
function Validate(const AToken: string): Boolean;
function Login(const AUser, APass: string): string;
property Token: string read GetToken;
property LoginCount: Integer read FLoginCount;
end;
implementation
constructor TAuthService.Create;
begin
inherited Create;
FToken := '';
FLoginCount := 0;
end;
destructor TAuthService.Destroy;
begin
FToken := '';
inherited Destroy;
end;
procedure TAuthService.IncLoginCount;
begin
Inc(FLoginCount);
end;
function TAuthService.GetToken: string;
begin
Result := FToken;
end;
function TAuthService.Validate(const AToken: string): Boolean;
begin
Result := AToken <> '';
end;
function TAuthService.Login(const AUser, APass: string): string;
begin
IncLoginCount;
if Validate(AUser + ':' + APass) then
begin
FToken := AUser;
Result := 'ok';
end
else
Result := '';
end;
end.`;
it('should extract all expected nodes', () => {
const result = extractFromSource('UAuth.pas', code);
expect(result.errors).toHaveLength(0);
// Module
const moduleNode = result.nodes.find((n) => n.kind === 'module');
expect(moduleNode?.name).toBe('UAuth');
// Imports
const imports = result.nodes.filter((n) => n.kind === 'import');
expect(imports.length).toBe(2);
// Interface
const ifaceNode = result.nodes.find((n) => n.kind === 'interface');
expect(ifaceNode?.name).toBe('ITokenValidator');
// Class
const classNode = result.nodes.find((n) => n.kind === 'class');
expect(classNode?.name).toBe('TAuthService');
// Methods
const methods = result.nodes.filter((n) => n.kind === 'method');
expect(methods.length).toBeGreaterThanOrEqual(6);
expect(methods.map((m) => m.name)).toContain('Create');
expect(methods.map((m) => m.name)).toContain('Destroy');
expect(methods.map((m) => m.name)).toContain('Login');
// Fields
const fields = result.nodes.filter((n) => n.kind === 'field');
expect(fields.length).toBe(2);
expect(fields.every((f) => f.visibility === 'private')).toBe(true);
// Properties
const props = result.nodes.filter((n) => n.kind === 'property');
expect(props.length).toBe(2);
expect(props.map((p) => p.name)).toContain('Token');
expect(props.map((p) => p.name)).toContain('LoginCount');
});
it('should extract inheritance and interface implementation', () => {
const result = extractFromSource('UAuth.pas', code);
const extendsRef = result.unresolvedReferences.find(
(r) => r.referenceKind === 'extends'
);
expect(extendsRef?.referenceName).toBe('TInterfacedObject');
const implementsRef = result.unresolvedReferences.find(
(r) => r.referenceKind === 'implements'
);
expect(implementsRef?.referenceName).toBe('ITokenValidator');
});
it('should extract calls from implementation', () => {
const result = extractFromSource('UAuth.pas', code);
const callRefs = result.unresolvedReferences.filter(
(r) => r.referenceKind === 'calls'
);
expect(callRefs.map((r) => r.referenceName)).toContain('Inc');
expect(callRefs.map((r) => r.referenceName)).toContain('Validate');
});
});
describe('Full fixture: UTypes.pas', () => {
const code = `unit UTypes;
interface
uses
System.SysUtils;
const
C_MAX_RETRIES = 3;
C_DEFAULT_NAME = 'Guest';
type
TUserRole = (urAdmin, urEditor, urViewer);
TPoint2D = record
X: Double;
Y: Double;
end;
TUserName = string;
TUserInfo = class
public
type
TAddress = record
Street: string;
City: string;
Zip: string;
end;
private
FName: TUserName;
FRole: TUserRole;
FAddress: TAddress;
public
constructor Create(const AName: TUserName; ARole: TUserRole);
function GetDisplayName: string;
class function CreateAdmin(const AName: TUserName): TUserInfo; static;
property Name: TUserName read FName write FName;
property Role: TUserRole read FRole;
property Address: TAddress read FAddress write FAddress;
end;
implementation
constructor TUserInfo.Create(const AName: TUserName; ARole: TUserRole);
begin
FName := AName;
FRole := ARole;
end;
function TUserInfo.GetDisplayName: string;
begin
if FRole = urAdmin then
Result := '[Admin] ' + FName
else
Result := FName;
end;
class function TUserInfo.CreateAdmin(const AName: TUserName): TUserInfo;
begin
Result := TUserInfo.Create(AName, urAdmin);
end;
end.`;
it('should extract enums with members', () => {
const result = extractFromSource('UTypes.pas', code);
const enumNode = result.nodes.find((n) => n.kind === 'enum');
expect(enumNode?.name).toBe('TUserRole');
const members = result.nodes.filter((n) => n.kind === 'enum_member');
expect(members.length).toBe(3);
expect(members.map((m) => m.name)).toEqual(['urAdmin', 'urEditor', 'urViewer']);
});
it('should extract constants', () => {
const result = extractFromSource('UTypes.pas', code);
const constants = result.nodes.filter((n) => n.kind === 'constant');
expect(constants.length).toBe(2);
expect(constants.map((c) => c.name)).toContain('C_MAX_RETRIES');
expect(constants.map((c) => c.name)).toContain('C_DEFAULT_NAME');
});
it('should extract type aliases', () => {
const result = extractFromSource('UTypes.pas', code);
const aliases = result.nodes.filter((n) => n.kind === 'type_alias');
expect(aliases.map((a) => a.name)).toContain('TUserName');
});
it('should extract records as classes with fields', () => {
const result = extractFromSource('UTypes.pas', code);
const classes = result.nodes.filter((n) => n.kind === 'class');
expect(classes.map((c) => c.name)).toContain('TPoint2D');
// TPoint2D fields
const fields = result.nodes.filter((n) => n.kind === 'field');
expect(fields.map((f) => f.name)).toContain('X');
expect(fields.map((f) => f.name)).toContain('Y');
});
it('should extract static class methods', () => {
const result = extractFromSource('UTypes.pas', code);
const methods = result.nodes.filter((n) => n.kind === 'method');
const staticMethod = methods.find((m) => m.name === 'CreateAdmin');
expect(staticMethod).toBeDefined();
expect(staticMethod?.isStatic).toBe(true);
});
it('should extract nested types', () => {
const result = extractFromSource('UTypes.pas', code);
const classes = result.nodes.filter((n) => n.kind === 'class');
expect(classes.map((c) => c.name)).toContain('TAddress');
});
});
});
// =============================================================================
// DFM/FMX Extraction
// =============================================================================
describe('DFM/FMX Extraction', () => {
it('should extract components from DFM', () => {
const code = `object Form1: TForm1
Left = 0
Top = 0
Caption = 'My Form'
object Button1: TButton
Left = 10
Top = 10
Caption = 'Click Me'
end
end`;
const result = extractFromSource('Form1.dfm', code);
const components = result.nodes.filter((n) => n.kind === 'component');
expect(components.length).toBe(2);
expect(components.map((c) => c.name)).toContain('Form1');
expect(components.map((c) => c.name)).toContain('Button1');
const button = components.find((c) => c.name === 'Button1');
expect(button?.signature).toBe('TButton');
});
it('should extract nested component hierarchy', () => {
const code = `object Form1: TForm1
object Panel1: TPanel
object Label1: TLabel
Caption = 'Hello'
end
end
end`;
const result = extractFromSource('Form1.dfm', code);
const components = result.nodes.filter((n) => n.kind === 'component');
expect(components.length).toBe(3);
// Check nesting: Panel1 contains Label1
const panel = components.find((c) => c.name === 'Panel1');
const label = components.find((c) => c.name === 'Label1');
const containsEdge = result.edges.find(
(e) => e.source === panel?.id && e.target === label?.id && e.kind === 'contains'
);
expect(containsEdge).toBeDefined();
});
it('should extract event handler references', () => {
const code = `object Form1: TForm1
OnCreate = FormCreate
OnDestroy = FormDestroy
object Button1: TButton
OnClick = Button1Click
end
end`;
const result = extractFromSource('Form1.dfm', code);
const refs = result.unresolvedReferences;
expect(refs.length).toBe(3);
expect(refs.map((r) => r.referenceName)).toContain('FormCreate');
expect(refs.map((r) => r.referenceName)).toContain('FormDestroy');
expect(refs.map((r) => r.referenceName)).toContain('Button1Click');
expect(refs.every((r) => r.referenceKind === 'references')).toBe(true);
});
it('should handle multi-line properties', () => {
const code = `object Form1: TForm1
SQL.Strings = (
'SELECT * FROM users'
'WHERE active = 1')
object Button1: TButton
OnClick = Button1Click
end
end`;
const result = extractFromSource('Form1.dfm', code);
const components = result.nodes.filter((n) => n.kind === 'component');
expect(components.length).toBe(2);
const refs = result.unresolvedReferences;
expect(refs.length).toBe(1);
expect(refs[0]?.referenceName).toBe('Button1Click');
});
it('should handle inherited keyword', () => {
const code = `inherited Form1: TForm1
Caption = 'Inherited Form'
object Button1: TButton
OnClick = Button1Click
end
end`;
const result = extractFromSource('Form1.dfm', code);
const components = result.nodes.filter((n) => n.kind === 'component');
expect(components.length).toBe(2);
expect(components.map((c) => c.name)).toContain('Form1');
});
it('should handle item collection properties', () => {
const code = `object Form1: TForm1
object StatusBar1: TStatusBar
Panels = <
item
Width = 200
end
item
Width = 200
end>
end
end`;
const result = extractFromSource('Form1.dfm', code);
const components = result.nodes.filter((n) => n.kind === 'component');
expect(components.length).toBe(2);
});
describe('Full fixture: MainForm.dfm', () => {
const code = `object frmMain: TfrmMain
Left = 0
Top = 0
Caption = 'CodeGraph DFM Fixture'
ClientHeight = 480
ClientWidth = 640
OnCreate = FormCreate
OnDestroy = FormDestroy
object pnlTop: TPanel
Left = 0
Top = 0
Width = 640
Height = 50
object lblTitle: TLabel
Left = 16
Top = 16
Caption = 'Authentication Service'
end
object btnLogin: TButton
Left = 540
Top = 12
OnClick = btnLoginClick
end
end
object pnlContent: TPanel
Left = 0
Top = 50
object edtUsername: TEdit
Left = 16
Top = 16
OnChange = edtUsernameChange
end
object edtPassword: TEdit
Left = 16
Top = 48
OnKeyPress = edtPasswordKeyPress
end
object mmoLog: TMemo
Left = 16
Top = 88
end
end
object pnlStatus: TStatusBar
Left = 0
Top = 440
Panels = <
item
Width = 200
end
item
Width = 200
end>
end
end`;
it('should extract all components', () => {
const result = extractFromSource('MainForm.dfm', code);
const components = result.nodes.filter((n) => n.kind === 'component');
expect(components.length).toBe(9);
expect(components.map((c) => c.name)).toEqual(
expect.arrayContaining([
'frmMain', 'pnlTop', 'lblTitle', 'btnLogin',
'pnlContent', 'edtUsername', 'edtPassword', 'mmoLog', 'pnlStatus',
])
);
});
it('should extract all event handlers', () => {
const result = extractFromSource('MainForm.dfm', code);
const refs = result.unresolvedReferences;
expect(refs.length).toBe(5);
expect(refs.map((r) => r.referenceName)).toEqual(
expect.arrayContaining([
'FormCreate', 'FormDestroy', 'btnLoginClick',
'edtUsernameChange', 'edtPasswordKeyPress',
])
);
});
});
});
describe('Full Indexing', () => {
let tempDir: string;