Browse Source

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>
Olaf Monien 4 tháng trước cách đây
mục cha
commit
425bfceea5
6 tập tin đã thay đổi với 1347 bổ sung4 xóa
  1. 702 0
      __tests__/extraction.test.ts
  2. 21 3
      package-lock.json
  3. 2 1
      package.json
  4. 11 0
      src/extraction/grammars.ts
  5. 603 0
      src/extraction/tree-sitter.ts
  6. 8 0
      src/types.ts

+ 702 - 0
__tests__/extraction.test.ts

@@ -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;
 

+ 21 - 3
package-lock.json

@@ -15,7 +15,8 @@
         "commander": "^14.0.2",
         "figlet": "^1.8.0",
         "picomatch": "^4.0.3",
-        "tree-sitter": "0.22.4"
+        "tree-sitter": "0.22.4",
+        "tree-sitter-pascal": "github:Isopod/tree-sitter-pascal"
       },
       "bin": {
         "codegraph": "dist/bin/codegraph.js"
@@ -877,7 +878,6 @@
       "hasInstallScript": true,
       "license": "MIT",
       "optional": true,
-      "peer": true,
       "dependencies": {
         "node-addon-api": "^8.3.0",
         "node-gyp-build": "^4.8.4"
@@ -909,7 +909,6 @@
       "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
       "license": "MIT",
       "optional": true,
-      "peer": true,
       "engines": {
         "node": "^18 || ^20 || >= 21"
       }
@@ -2342,6 +2341,7 @@
       "integrity": "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==",
       "hasInstallScript": true,
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "node-addon-api": "^8.3.0",
         "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": {
       "version": "0.23.11",
       "resolved": "https://registry.npmjs.org/tree-sitter-php/-/tree-sitter-php-0.23.11.tgz",
@@ -2818,6 +2835,7 @@
       "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
       "dev": true,
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "esbuild": "^0.21.3",
         "postcss": "^8.4.43",

+ 2 - 1
package.json

@@ -38,7 +38,8 @@
     "commander": "^14.0.2",
     "figlet": "^1.8.0",
     "picomatch": "^4.0.3",
-    "tree-sitter": "0.22.4"
+    "tree-sitter": "0.22.4",
+    "tree-sitter-pascal": "github:Isopod/tree-sitter-pascal"
   },
   "devDependencies": {
     "@types/better-sqlite3": "^7.6.0",

+ 11 - 0
src/extraction/grammars.ts

@@ -80,6 +80,10 @@ const grammarLoaders: Record<GrammarLanguage, GrammarLoader> = {
     // eslint-disable-next-line @typescript-eslint/no-require-imports
     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+
   // Liquid extraction is handled separately via regex in tree-sitter.ts
 };
@@ -116,6 +120,12 @@ export const EXTENSION_MAP: Record<string, Language> = {
   '.dart': 'dart',
   '.liquid': 'liquid',
   '.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',
     svelte: 'Svelte',
     liquid: 'Liquid',
+    pascal: 'Pascal / Delphi',
     unknown: 'Unknown',
   };
   return names[language] || language;

+ 603 - 0
src/extraction/tree-sitter.ts

@@ -755,6 +755,64 @@ const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
       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
@@ -924,6 +982,12 @@ export class TreeSitterExtractor {
     const nodeType = node.type;
     let skipChildren = false;
 
+    // Pascal-specific AST handling
+    if (this.language === 'pascal') {
+      skipChildren = this.visitPascalNode(node);
+      if (skipChildren) return;
+    }
+
     // Check for function declarations
     // For Python/Ruby, function_definition inside a class should be treated as method
     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
  */
@@ -2545,6 +3139,15 @@ export function extractFromSource(
     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);
   return extractor.extract();
 }

+ 8 - 0
src/types.ts

@@ -74,6 +74,7 @@ export type Language =
   | 'dart'
   | 'svelte'
   | 'liquid'
+  | 'pascal'
   | 'unknown';
 
 // =============================================================================
@@ -519,6 +520,13 @@ export const DEFAULT_CONFIG: CodeGraphConfig = {
     '**/*.svelte',
     // Liquid (Shopify themes)
     '**/*.liquid',
+    // Pascal / Delphi
+    '**/*.pas',
+    '**/*.dpr',
+    '**/*.dpk',
+    '**/*.lpr',
+    '**/*.dfm',
+    '**/*.fmx',
   ],
   exclude: [
     // Version control