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:
co-authored by
Claude Opus 4.6
parent
8dfc99572f
commit
425bfceea5
@@ -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', () => {
|
describe('Full Indexing', () => {
|
||||||
let tempDir: string;
|
let tempDir: string;
|
||||||
|
|
||||||
|
|||||||
Generated
+21
-3
@@ -15,7 +15,8 @@
|
|||||||
"commander": "^14.0.2",
|
"commander": "^14.0.2",
|
||||||
"figlet": "^1.8.0",
|
"figlet": "^1.8.0",
|
||||||
"picomatch": "^4.0.3",
|
"picomatch": "^4.0.3",
|
||||||
"tree-sitter": "0.22.4"
|
"tree-sitter": "0.22.4",
|
||||||
|
"tree-sitter-pascal": "github:Isopod/tree-sitter-pascal"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"codegraph": "dist/bin/codegraph.js"
|
"codegraph": "dist/bin/codegraph.js"
|
||||||
@@ -877,7 +878,6 @@
|
|||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"node-addon-api": "^8.3.0",
|
"node-addon-api": "^8.3.0",
|
||||||
"node-gyp-build": "^4.8.4"
|
"node-gyp-build": "^4.8.4"
|
||||||
@@ -909,7 +909,6 @@
|
|||||||
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
|
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18 || ^20 || >= 21"
|
"node": "^18 || ^20 || >= 21"
|
||||||
}
|
}
|
||||||
@@ -2342,6 +2341,7 @@
|
|||||||
"integrity": "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==",
|
"integrity": "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"node-addon-api": "^8.3.0",
|
"node-addon-api": "^8.3.0",
|
||||||
"node-gyp-build": "^4.8.4"
|
"node-gyp-build": "^4.8.4"
|
||||||
@@ -2582,6 +2582,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tree-sitter-pascal": {
|
||||||
|
"version": "0.10.2",
|
||||||
|
"resolved": "git+ssh://git@github.com/Isopod/tree-sitter-pascal.git#042119eca2e18a60e56317fb06ee3ba5c32cb447",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-gyp-build": "^4.8.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"tree-sitter": "^0.22.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"tree_sitter": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tree-sitter-php": {
|
"node_modules/tree-sitter-php": {
|
||||||
"version": "0.23.11",
|
"version": "0.23.11",
|
||||||
"resolved": "https://registry.npmjs.org/tree-sitter-php/-/tree-sitter-php-0.23.11.tgz",
|
"resolved": "https://registry.npmjs.org/tree-sitter-php/-/tree-sitter-php-0.23.11.tgz",
|
||||||
@@ -2818,6 +2835,7 @@
|
|||||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.43",
|
"postcss": "^8.4.43",
|
||||||
|
|||||||
+2
-1
@@ -38,7 +38,8 @@
|
|||||||
"commander": "^14.0.2",
|
"commander": "^14.0.2",
|
||||||
"figlet": "^1.8.0",
|
"figlet": "^1.8.0",
|
||||||
"picomatch": "^4.0.3",
|
"picomatch": "^4.0.3",
|
||||||
"tree-sitter": "0.22.4"
|
"tree-sitter": "0.22.4",
|
||||||
|
"tree-sitter-pascal": "github:Isopod/tree-sitter-pascal"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/better-sqlite3": "^7.6.0",
|
"@types/better-sqlite3": "^7.6.0",
|
||||||
|
|||||||
@@ -80,6 +80,10 @@ const grammarLoaders: Record<GrammarLanguage, GrammarLoader> = {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
return require('@sengac/tree-sitter-dart');
|
return require('@sengac/tree-sitter-dart');
|
||||||
},
|
},
|
||||||
|
pascal: () => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
|
return require('tree-sitter-pascal');
|
||||||
|
},
|
||||||
// Note: tree-sitter-liquid has ABI compatibility issues with tree-sitter 0.22+
|
// Note: tree-sitter-liquid has ABI compatibility issues with tree-sitter 0.22+
|
||||||
// Liquid extraction is handled separately via regex in tree-sitter.ts
|
// Liquid extraction is handled separately via regex in tree-sitter.ts
|
||||||
};
|
};
|
||||||
@@ -116,6 +120,12 @@ export const EXTENSION_MAP: Record<string, Language> = {
|
|||||||
'.dart': 'dart',
|
'.dart': 'dart',
|
||||||
'.liquid': 'liquid',
|
'.liquid': 'liquid',
|
||||||
'.svelte': 'svelte',
|
'.svelte': 'svelte',
|
||||||
|
'.pas': 'pascal',
|
||||||
|
'.dpr': 'pascal',
|
||||||
|
'.dpk': 'pascal',
|
||||||
|
'.lpr': 'pascal',
|
||||||
|
'.dfm': 'pascal',
|
||||||
|
'.fmx': 'pascal',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -245,6 +255,7 @@ export function getLanguageDisplayName(language: Language): string {
|
|||||||
dart: 'Dart',
|
dart: 'Dart',
|
||||||
svelte: 'Svelte',
|
svelte: 'Svelte',
|
||||||
liquid: 'Liquid',
|
liquid: 'Liquid',
|
||||||
|
pascal: 'Pascal / Delphi',
|
||||||
unknown: 'Unknown',
|
unknown: 'Unknown',
|
||||||
};
|
};
|
||||||
return names[language] || language;
|
return names[language] || language;
|
||||||
|
|||||||
@@ -755,6 +755,64 @@ const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
pascal: {
|
||||||
|
functionTypes: ['declProc'],
|
||||||
|
classTypes: ['declClass'],
|
||||||
|
methodTypes: ['declProc'],
|
||||||
|
interfaceTypes: ['declIntf'],
|
||||||
|
structTypes: [],
|
||||||
|
enumTypes: ['declEnum'],
|
||||||
|
typeAliasTypes: ['declType'],
|
||||||
|
importTypes: ['declUses'],
|
||||||
|
callTypes: ['exprCall'],
|
||||||
|
variableTypes: ['declField', 'declConst'],
|
||||||
|
nameField: 'name',
|
||||||
|
bodyField: 'body',
|
||||||
|
paramsField: 'args',
|
||||||
|
returnField: 'type',
|
||||||
|
getSignature: (node, source) => {
|
||||||
|
const args = getChildByField(node, 'args');
|
||||||
|
const returnType = node.namedChildren.find(
|
||||||
|
(c: SyntaxNode) => c.type === 'typeref'
|
||||||
|
);
|
||||||
|
if (!args && !returnType) return undefined;
|
||||||
|
let sig = '';
|
||||||
|
if (args) sig = getNodeText(args, source);
|
||||||
|
if (returnType) {
|
||||||
|
sig += ': ' + getNodeText(returnType, source);
|
||||||
|
}
|
||||||
|
return sig || undefined;
|
||||||
|
},
|
||||||
|
getVisibility: (node) => {
|
||||||
|
let current = node.parent;
|
||||||
|
while (current) {
|
||||||
|
if (current.type === 'declSection') {
|
||||||
|
for (let i = 0; i < current.childCount; i++) {
|
||||||
|
const child = current.child(i);
|
||||||
|
if (child?.type === 'kPublic' || child?.type === 'kPublished')
|
||||||
|
return 'public';
|
||||||
|
if (child?.type === 'kPrivate') return 'private';
|
||||||
|
if (child?.type === 'kProtected') return 'protected';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current = current.parent;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
isExported: (_node, _source) => {
|
||||||
|
// In Pascal, symbols declared in the interface section are exported
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
isStatic: (node) => {
|
||||||
|
for (let i = 0; i < node.childCount; i++) {
|
||||||
|
if (node.child(i)?.type === 'kClass') return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
isConst: (node) => {
|
||||||
|
return node.type === 'declConst';
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// TSX and JSX use the same extractors as their base languages
|
// TSX and JSX use the same extractors as their base languages
|
||||||
@@ -924,6 +982,12 @@ export class TreeSitterExtractor {
|
|||||||
const nodeType = node.type;
|
const nodeType = node.type;
|
||||||
let skipChildren = false;
|
let skipChildren = false;
|
||||||
|
|
||||||
|
// Pascal-specific AST handling
|
||||||
|
if (this.language === 'pascal') {
|
||||||
|
skipChildren = this.visitPascalNode(node);
|
||||||
|
if (skipChildren) return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check for function declarations
|
// Check for function declarations
|
||||||
// For Python/Ruby, function_definition inside a class should be treated as method
|
// For Python/Ruby, function_definition inside a class should be treated as method
|
||||||
if (this.extractor.functionTypes.includes(nodeType)) {
|
if (this.extractor.functionTypes.includes(nodeType)) {
|
||||||
@@ -1979,6 +2043,379 @@ export class TreeSitterExtractor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Pascal-specific AST structures.
|
||||||
|
* Returns true if the node was fully handled and children should be skipped.
|
||||||
|
*/
|
||||||
|
private visitPascalNode(node: SyntaxNode): boolean {
|
||||||
|
const nodeType = node.type;
|
||||||
|
|
||||||
|
// Unit/Program/Library → module node
|
||||||
|
if (nodeType === 'unit' || nodeType === 'program' || nodeType === 'library') {
|
||||||
|
const moduleNameNode = node.namedChildren.find(
|
||||||
|
(c: SyntaxNode) => c.type === 'moduleName'
|
||||||
|
);
|
||||||
|
if (moduleNameNode) {
|
||||||
|
const name = getNodeText(moduleNameNode, this.source);
|
||||||
|
this.createNode('module', name, node);
|
||||||
|
}
|
||||||
|
// Continue visiting children (interface/implementation sections)
|
||||||
|
for (let i = 0; i < node.namedChildCount; i++) {
|
||||||
|
const child = node.namedChild(i);
|
||||||
|
if (child) this.visitNode(child);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// declType wraps declClass/declIntf/declEnum/type-alias
|
||||||
|
// The name lives on declType, the inner node determines the kind
|
||||||
|
if (nodeType === 'declType') {
|
||||||
|
this.extractPascalDeclType(node);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// declUses → import nodes for each unit name
|
||||||
|
if (nodeType === 'declUses') {
|
||||||
|
this.extractPascalUses(node);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// declConsts → container; visit children for individual declConst
|
||||||
|
if (nodeType === 'declConsts') {
|
||||||
|
for (let i = 0; i < node.namedChildCount; i++) {
|
||||||
|
const child = node.namedChild(i);
|
||||||
|
if (child?.type === 'declConst') {
|
||||||
|
this.extractPascalConst(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// declConst at top level (outside declConsts)
|
||||||
|
if (nodeType === 'declConst') {
|
||||||
|
this.extractPascalConst(node);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// declTypes → container for type declarations
|
||||||
|
if (nodeType === 'declTypes') {
|
||||||
|
for (let i = 0; i < node.namedChildCount; i++) {
|
||||||
|
const child = node.namedChild(i);
|
||||||
|
if (child) this.visitNode(child);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// declVars → container for variable declarations
|
||||||
|
if (nodeType === 'declVars') {
|
||||||
|
for (let i = 0; i < node.namedChildCount; i++) {
|
||||||
|
const child = node.namedChild(i);
|
||||||
|
if (child?.type === 'declVar') {
|
||||||
|
const nameNode = getChildByField(child, 'name');
|
||||||
|
if (nameNode) {
|
||||||
|
const name = getNodeText(nameNode, this.source);
|
||||||
|
this.createNode('variable', name, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// defProc in implementation section → extract calls but don't create duplicate nodes
|
||||||
|
if (nodeType === 'defProc') {
|
||||||
|
this.extractPascalDefProc(node);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// declProp → property node
|
||||||
|
if (nodeType === 'declProp') {
|
||||||
|
const nameNode = getChildByField(node, 'name');
|
||||||
|
if (nameNode) {
|
||||||
|
const name = getNodeText(nameNode, this.source);
|
||||||
|
const visibility = this.extractor!.getVisibility?.(node);
|
||||||
|
this.createNode('property', name, node, { visibility });
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// declField → field node
|
||||||
|
if (nodeType === 'declField') {
|
||||||
|
const nameNode = getChildByField(node, 'name');
|
||||||
|
if (nameNode) {
|
||||||
|
const name = getNodeText(nameNode, this.source);
|
||||||
|
const visibility = this.extractor!.getVisibility?.(node);
|
||||||
|
this.createNode('field', name, node, { visibility });
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// declSection → visit children (propagates visibility via getVisibility)
|
||||||
|
if (nodeType === 'declSection') {
|
||||||
|
for (let i = 0; i < node.namedChildCount; i++) {
|
||||||
|
const child = node.namedChild(i);
|
||||||
|
if (child) this.visitNode(child);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// exprCall → extract function call reference
|
||||||
|
if (nodeType === 'exprCall') {
|
||||||
|
this.extractPascalCall(node);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// interface/implementation sections → visit children
|
||||||
|
if (nodeType === 'interface' || nodeType === 'implementation') {
|
||||||
|
for (let i = 0; i < node.namedChildCount; i++) {
|
||||||
|
const child = node.namedChild(i);
|
||||||
|
if (child) this.visitNode(child);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// block (begin..end) → visit for calls
|
||||||
|
if (nodeType === 'block') {
|
||||||
|
this.visitPascalBlock(node);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a Pascal declType node (class, interface, enum, or type alias)
|
||||||
|
*/
|
||||||
|
private extractPascalDeclType(node: SyntaxNode): void {
|
||||||
|
const nameNode = getChildByField(node, 'name');
|
||||||
|
if (!nameNode) return;
|
||||||
|
const name = getNodeText(nameNode, this.source);
|
||||||
|
|
||||||
|
// Find the inner type declaration
|
||||||
|
const declClass = node.namedChildren.find(
|
||||||
|
(c: SyntaxNode) => c.type === 'declClass'
|
||||||
|
);
|
||||||
|
const declIntf = node.namedChildren.find(
|
||||||
|
(c: SyntaxNode) => c.type === 'declIntf'
|
||||||
|
);
|
||||||
|
const typeChild = node.namedChildren.find(
|
||||||
|
(c: SyntaxNode) => c.type === 'type'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (declClass) {
|
||||||
|
const classNode = this.createNode('class', name, node);
|
||||||
|
// Extract inheritance from typeref children of declClass
|
||||||
|
this.extractPascalInheritance(declClass, classNode.id);
|
||||||
|
// Visit class body
|
||||||
|
this.nodeStack.push(classNode.id);
|
||||||
|
for (let i = 0; i < declClass.namedChildCount; i++) {
|
||||||
|
const child = declClass.namedChild(i);
|
||||||
|
if (child) this.visitNode(child);
|
||||||
|
}
|
||||||
|
this.nodeStack.pop();
|
||||||
|
} else if (declIntf) {
|
||||||
|
const ifaceNode = this.createNode('interface', name, node);
|
||||||
|
// Visit interface members
|
||||||
|
this.nodeStack.push(ifaceNode.id);
|
||||||
|
for (let i = 0; i < declIntf.namedChildCount; i++) {
|
||||||
|
const child = declIntf.namedChild(i);
|
||||||
|
if (child) this.visitNode(child);
|
||||||
|
}
|
||||||
|
this.nodeStack.pop();
|
||||||
|
} else if (typeChild) {
|
||||||
|
// Check if it contains a declEnum
|
||||||
|
const declEnum = typeChild.namedChildren.find(
|
||||||
|
(c: SyntaxNode) => c.type === 'declEnum'
|
||||||
|
);
|
||||||
|
if (declEnum) {
|
||||||
|
const enumNode = this.createNode('enum', name, node);
|
||||||
|
// Extract enum members
|
||||||
|
this.nodeStack.push(enumNode.id);
|
||||||
|
for (let i = 0; i < declEnum.namedChildCount; i++) {
|
||||||
|
const child = declEnum.namedChild(i);
|
||||||
|
if (child?.type === 'declEnumValue') {
|
||||||
|
const memberName = getChildByField(child, 'name');
|
||||||
|
if (memberName) {
|
||||||
|
this.createNode('enum_member', getNodeText(memberName, this.source), child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.nodeStack.pop();
|
||||||
|
} else {
|
||||||
|
// Simple type alias: type TFoo = string / type TFoo = Integer
|
||||||
|
this.createNode('type_alias', name, node);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: could be a forward declaration or simple alias
|
||||||
|
this.createNode('type_alias', name, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract Pascal uses clause into individual import nodes
|
||||||
|
*/
|
||||||
|
private extractPascalUses(node: SyntaxNode): void {
|
||||||
|
const importText = getNodeText(node, this.source).trim();
|
||||||
|
for (let i = 0; i < node.namedChildCount; i++) {
|
||||||
|
const child = node.namedChild(i);
|
||||||
|
if (child?.type === 'moduleName') {
|
||||||
|
const unitName = getNodeText(child, this.source);
|
||||||
|
this.createNode('import', unitName, child, {
|
||||||
|
signature: importText,
|
||||||
|
});
|
||||||
|
// Create unresolved reference for resolution
|
||||||
|
if (this.nodeStack.length > 0) {
|
||||||
|
const parentId = this.nodeStack[this.nodeStack.length - 1];
|
||||||
|
if (parentId) {
|
||||||
|
this.unresolvedReferences.push({
|
||||||
|
fromNodeId: parentId,
|
||||||
|
referenceName: unitName,
|
||||||
|
referenceKind: 'imports',
|
||||||
|
line: child.startPosition.row + 1,
|
||||||
|
column: child.startPosition.column,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a Pascal constant declaration
|
||||||
|
*/
|
||||||
|
private extractPascalConst(node: SyntaxNode): void {
|
||||||
|
const nameNode = getChildByField(node, 'name');
|
||||||
|
if (!nameNode) return;
|
||||||
|
const name = getNodeText(nameNode, this.source);
|
||||||
|
const defaultValue = node.namedChildren.find(
|
||||||
|
(c: SyntaxNode) => c.type === 'defaultValue'
|
||||||
|
);
|
||||||
|
const sig = defaultValue ? getNodeText(defaultValue, this.source) : undefined;
|
||||||
|
this.createNode('constant', name, node, { signature: sig });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract Pascal inheritance (extends/implements) from declClass typeref children
|
||||||
|
*/
|
||||||
|
private extractPascalInheritance(declClass: SyntaxNode, classId: string): void {
|
||||||
|
const typerefs = declClass.namedChildren.filter(
|
||||||
|
(c: SyntaxNode) => c.type === 'typeref'
|
||||||
|
);
|
||||||
|
for (let i = 0; i < typerefs.length; i++) {
|
||||||
|
const ref = typerefs[i]!;
|
||||||
|
const name = getNodeText(ref, this.source);
|
||||||
|
this.unresolvedReferences.push({
|
||||||
|
fromNodeId: classId,
|
||||||
|
referenceName: name,
|
||||||
|
referenceKind: i === 0 ? 'extends' : 'implements',
|
||||||
|
line: ref.startPosition.row + 1,
|
||||||
|
column: ref.startPosition.column,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract calls and resolve method context from a Pascal defProc (implementation body).
|
||||||
|
* Does not create a new node — the declaration was already captured from the interface section.
|
||||||
|
*/
|
||||||
|
private extractPascalDefProc(node: SyntaxNode): void {
|
||||||
|
// Find the matching declaration node by name to use as call parent
|
||||||
|
const declProc = node.namedChildren.find(
|
||||||
|
(c: SyntaxNode) => c.type === 'declProc'
|
||||||
|
);
|
||||||
|
if (!declProc) return;
|
||||||
|
|
||||||
|
const nameNode = getChildByField(declProc, 'name');
|
||||||
|
if (!nameNode) return;
|
||||||
|
const fullName = getNodeText(nameNode, this.source);
|
||||||
|
// fullName is like "TAuthService.Create" — we want just the method name part
|
||||||
|
const shortName = fullName.includes('.') ? fullName.split('.').pop()! : fullName;
|
||||||
|
|
||||||
|
// Find matching node from earlier extraction
|
||||||
|
const existingNode = this.nodes.find(
|
||||||
|
(n) => n.name === shortName && (n.kind === 'method' || n.kind === 'function')
|
||||||
|
);
|
||||||
|
|
||||||
|
const parentId = existingNode?.id || this.nodeStack[this.nodeStack.length - 1];
|
||||||
|
if (!parentId) return;
|
||||||
|
|
||||||
|
// Visit the block for calls
|
||||||
|
const block = node.namedChildren.find(
|
||||||
|
(c: SyntaxNode) => c.type === 'block'
|
||||||
|
);
|
||||||
|
if (block) {
|
||||||
|
this.nodeStack.push(parentId);
|
||||||
|
this.visitPascalBlock(block);
|
||||||
|
this.nodeStack.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract function calls from a Pascal expression
|
||||||
|
*/
|
||||||
|
private extractPascalCall(node: SyntaxNode): void {
|
||||||
|
if (this.nodeStack.length === 0) return;
|
||||||
|
const callerId = this.nodeStack[this.nodeStack.length - 1];
|
||||||
|
if (!callerId) return;
|
||||||
|
|
||||||
|
// Get the callee name — first child is typically the identifier or exprDot
|
||||||
|
const firstChild = node.namedChild(0);
|
||||||
|
if (!firstChild) return;
|
||||||
|
|
||||||
|
let calleeName = '';
|
||||||
|
if (firstChild.type === 'exprDot') {
|
||||||
|
// Qualified call: Obj.Method(...)
|
||||||
|
const identifiers = firstChild.namedChildren.filter(
|
||||||
|
(c: SyntaxNode) => c.type === 'identifier'
|
||||||
|
);
|
||||||
|
if (identifiers.length > 0) {
|
||||||
|
calleeName = identifiers.map((id: SyntaxNode) => getNodeText(id, this.source)).join('.');
|
||||||
|
}
|
||||||
|
} else if (firstChild.type === 'identifier') {
|
||||||
|
calleeName = getNodeText(firstChild, this.source);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (calleeName) {
|
||||||
|
this.unresolvedReferences.push({
|
||||||
|
fromNodeId: callerId,
|
||||||
|
referenceName: calleeName,
|
||||||
|
referenceKind: 'calls',
|
||||||
|
line: node.startPosition.row + 1,
|
||||||
|
column: node.startPosition.column,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also visit arguments for nested calls
|
||||||
|
const args = node.namedChildren.find(
|
||||||
|
(c: SyntaxNode) => c.type === 'exprArgs'
|
||||||
|
);
|
||||||
|
if (args) {
|
||||||
|
this.visitPascalBlock(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively visit a Pascal block/statement tree for call expressions
|
||||||
|
*/
|
||||||
|
private visitPascalBlock(node: SyntaxNode): void {
|
||||||
|
for (let i = 0; i < node.namedChildCount; i++) {
|
||||||
|
const child = node.namedChild(i);
|
||||||
|
if (!child) continue;
|
||||||
|
if (child.type === 'exprCall') {
|
||||||
|
this.extractPascalCall(child);
|
||||||
|
} else if (child.type === 'exprDot') {
|
||||||
|
// Check if exprDot contains an exprCall
|
||||||
|
for (let j = 0; j < child.namedChildCount; j++) {
|
||||||
|
const grandchild = child.namedChild(j);
|
||||||
|
if (grandchild?.type === 'exprCall') {
|
||||||
|
this.extractPascalCall(grandchild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.visitPascalBlock(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2523,6 +2960,163 @@ export class SvelteExtractor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom extractor for Delphi DFM/FMX form files.
|
||||||
|
*
|
||||||
|
* DFM/FMX files describe the visual component hierarchy and event handler
|
||||||
|
* bindings. They use a simple text format (object/end blocks) that we parse
|
||||||
|
* with regex — no tree-sitter grammar exists for this format.
|
||||||
|
*
|
||||||
|
* Extracted information:
|
||||||
|
* - Components as NodeKind `component`
|
||||||
|
* - Nesting as EdgeKind `contains`
|
||||||
|
* - Event handlers (OnClick = MethodName) as UnresolvedReference → EdgeKind `references`
|
||||||
|
*/
|
||||||
|
export class DfmExtractor {
|
||||||
|
private filePath: string;
|
||||||
|
private source: string;
|
||||||
|
private nodes: Node[] = [];
|
||||||
|
private edges: Edge[] = [];
|
||||||
|
private unresolvedReferences: UnresolvedReference[] = [];
|
||||||
|
private errors: ExtractionError[] = [];
|
||||||
|
|
||||||
|
constructor(filePath: string, source: string) {
|
||||||
|
this.filePath = filePath;
|
||||||
|
this.source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract components and event handler references from DFM/FMX source
|
||||||
|
*/
|
||||||
|
extract(): ExtractionResult {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileNode = this.createFileNode();
|
||||||
|
this.parseComponents(fileNode.id);
|
||||||
|
} catch (error) {
|
||||||
|
captureException(error, { operation: 'dfm-extraction', filePath: this.filePath });
|
||||||
|
this.errors.push({
|
||||||
|
message: `DFM extraction error: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
severity: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
nodes: this.nodes,
|
||||||
|
edges: this.edges,
|
||||||
|
unresolvedReferences: this.unresolvedReferences,
|
||||||
|
errors: this.errors,
|
||||||
|
durationMs: Date.now() - startTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a file node for the DFM form file */
|
||||||
|
private createFileNode(): Node {
|
||||||
|
const lines = this.source.split('\n');
|
||||||
|
const id = generateNodeId(this.filePath, 'file', this.filePath, 1);
|
||||||
|
|
||||||
|
const fileNode: Node = {
|
||||||
|
id,
|
||||||
|
kind: 'file',
|
||||||
|
name: this.filePath.split('/').pop() || this.filePath,
|
||||||
|
qualifiedName: this.filePath,
|
||||||
|
filePath: this.filePath,
|
||||||
|
language: 'pascal',
|
||||||
|
startLine: 1,
|
||||||
|
endLine: lines.length,
|
||||||
|
startColumn: 0,
|
||||||
|
endColumn: lines[lines.length - 1]?.length || 0,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.nodes.push(fileNode);
|
||||||
|
return fileNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse object/end blocks and extract components + event handlers */
|
||||||
|
private parseComponents(fileNodeId: string): void {
|
||||||
|
const lines = this.source.split('\n');
|
||||||
|
const stack: string[] = [fileNodeId];
|
||||||
|
|
||||||
|
const objectPattern = /^\s*(object|inherited|inline)\s+(\w+)\s*:\s*(\w+)/;
|
||||||
|
const eventPattern = /^\s*(On\w+)\s*=\s*(\w+)\s*$/;
|
||||||
|
const endPattern = /^\s*end\s*$/;
|
||||||
|
const multiLineStart = /=\s*\(\s*$/;
|
||||||
|
const multiLineItemStart = /=\s*<\s*$/;
|
||||||
|
let inMultiLine = false;
|
||||||
|
let multiLineEndChar = ')';
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i]!;
|
||||||
|
const lineNum = i + 1;
|
||||||
|
|
||||||
|
// Skip multi-line properties
|
||||||
|
if (inMultiLine) {
|
||||||
|
if (line.trimEnd().endsWith(multiLineEndChar)) inMultiLine = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (multiLineStart.test(line)) {
|
||||||
|
inMultiLine = true;
|
||||||
|
multiLineEndChar = ')';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (multiLineItemStart.test(line)) {
|
||||||
|
inMultiLine = true;
|
||||||
|
multiLineEndChar = '>';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Component declaration
|
||||||
|
const objMatch = line.match(objectPattern);
|
||||||
|
if (objMatch) {
|
||||||
|
const [, , name, typeName] = objMatch;
|
||||||
|
const nodeId = generateNodeId(this.filePath, 'component', name!, lineNum);
|
||||||
|
this.nodes.push({
|
||||||
|
id: nodeId,
|
||||||
|
kind: 'component',
|
||||||
|
name: name!,
|
||||||
|
qualifiedName: `${this.filePath}#${name}`,
|
||||||
|
filePath: this.filePath,
|
||||||
|
language: 'pascal',
|
||||||
|
startLine: lineNum,
|
||||||
|
endLine: lineNum,
|
||||||
|
startColumn: 0,
|
||||||
|
endColumn: line.length,
|
||||||
|
signature: typeName,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
});
|
||||||
|
this.edges.push({
|
||||||
|
source: stack[stack.length - 1]!,
|
||||||
|
target: nodeId,
|
||||||
|
kind: 'contains',
|
||||||
|
});
|
||||||
|
stack.push(nodeId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event handler
|
||||||
|
const eventMatch = line.match(eventPattern);
|
||||||
|
if (eventMatch) {
|
||||||
|
const [, , methodName] = eventMatch;
|
||||||
|
this.unresolvedReferences.push({
|
||||||
|
fromNodeId: stack[stack.length - 1]!,
|
||||||
|
referenceName: methodName!,
|
||||||
|
referenceKind: 'references',
|
||||||
|
line: lineNum,
|
||||||
|
column: 0,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block end
|
||||||
|
if (endPattern.test(line)) {
|
||||||
|
if (stack.length > 1) stack.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract nodes and edges from source code
|
* Extract nodes and edges from source code
|
||||||
*/
|
*/
|
||||||
@@ -2545,6 +3139,15 @@ export function extractFromSource(
|
|||||||
return extractor.extract();
|
return extractor.extract();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use custom extractor for DFM/FMX form files
|
||||||
|
if (
|
||||||
|
detectedLanguage === 'pascal' &&
|
||||||
|
(filePath.endsWith('.dfm') || filePath.endsWith('.fmx'))
|
||||||
|
) {
|
||||||
|
const extractor = new DfmExtractor(filePath, source);
|
||||||
|
return extractor.extract();
|
||||||
|
}
|
||||||
|
|
||||||
const extractor = new TreeSitterExtractor(filePath, source, detectedLanguage);
|
const extractor = new TreeSitterExtractor(filePath, source, detectedLanguage);
|
||||||
return extractor.extract();
|
return extractor.extract();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export type Language =
|
|||||||
| 'dart'
|
| 'dart'
|
||||||
| 'svelte'
|
| 'svelte'
|
||||||
| 'liquid'
|
| 'liquid'
|
||||||
|
| 'pascal'
|
||||||
| 'unknown';
|
| 'unknown';
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -519,6 +520,13 @@ export const DEFAULT_CONFIG: CodeGraphConfig = {
|
|||||||
'**/*.svelte',
|
'**/*.svelte',
|
||||||
// Liquid (Shopify themes)
|
// Liquid (Shopify themes)
|
||||||
'**/*.liquid',
|
'**/*.liquid',
|
||||||
|
// Pascal / Delphi
|
||||||
|
'**/*.pas',
|
||||||
|
'**/*.dpr',
|
||||||
|
'**/*.dpk',
|
||||||
|
'**/*.lpr',
|
||||||
|
'**/*.dfm',
|
||||||
|
'**/*.fmx',
|
||||||
],
|
],
|
||||||
exclude: [
|
exclude: [
|
||||||
// Version control
|
// Version control
|
||||||
|
|||||||
Reference in New Issue
Block a user