mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-27 21:55:14 +08:00
Merge pull request #41 from omonien/delphi-support
feat: Add Pascal/Delphi support (Tree-sitter & DFM extraction)
This commit is contained in:
@@ -106,7 +106,7 @@ SQLite database with:
|
||||
|
||||
### Supported Languages
|
||||
|
||||
TypeScript, JavaScript, TSX, JSX, Python, Go, Rust, Java, C, C++, C#, PHP, Ruby, Swift, Kotlin, Dart, Liquid
|
||||
TypeScript, JavaScript, TSX, JSX, Python, Go, Rust, Java, C, C++, C#, PHP, Ruby, Swift, Kotlin, Dart, Liquid, Pascal
|
||||
|
||||
### Node and Edge Types
|
||||
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
# Pascal / Delphi Support for CodeGraph
|
||||
|
||||
## Why Delphi?
|
||||
|
||||
Delphi (Object Pascal) remains one of the most widely used languages for Windows desktop and enterprise applications. With an estimated **1.5–3 million active developers** and a strong presence in industries like healthcare, finance, logistics, and government, Delphi projects often involve large, long-lived codebases that benefit significantly from semantic code intelligence.
|
||||
|
||||
Many Delphi codebases have grown over decades — making structural understanding, impact analysis, and cross-file navigation exactly the kind of tooling gap CodeGraph is designed to fill.
|
||||
|
||||
Adding Delphi support positions CodeGraph as a uniquely valuable tool for a community that has historically been underserved by modern static analysis and AI-assisted development tools.
|
||||
|
||||
## What Was Implemented
|
||||
|
||||
### Pascal / Object Pascal (tree-sitter)
|
||||
|
||||
Full extraction support for `.pas`, `.dpr`, `.dpk`, and `.lpr` files using the `tree-sitter-pascal` grammar:
|
||||
|
||||
| Feature | NodeKind | Details |
|
||||
|---------|----------|---------|
|
||||
| Units / Programs | `module` | `unit`, `program`, `package`, `library` |
|
||||
| Classes | `class` | Including inheritance and interface implementation |
|
||||
| Records | `class` | Treated as classes (consistent with AST structure) |
|
||||
| Interfaces | `interface` | With GUID support |
|
||||
| Methods | `method` | Constructor, destructor, procedures, functions |
|
||||
| Functions / Procedures | `function` | Top-level (non-class) routines |
|
||||
| Properties | `property` | With read/write accessors |
|
||||
| Fields | `field` | Class and record fields |
|
||||
| Constants | `constant` | `const` declarations |
|
||||
| Enums | `enum` | With enum members |
|
||||
| Type Aliases | `type_alias` | `type TFoo = ...` |
|
||||
| Uses / Imports | `import` | `uses` clause extraction |
|
||||
| Function Calls | — | `calls` edges for call graph |
|
||||
| Visibility | — | `public`, `private`, `protected` on methods/fields |
|
||||
| Static Methods | — | `class function` / `class procedure` |
|
||||
| Containment | — | `contains` edges (class → method, unit → type, etc.) |
|
||||
| Inheritance | — | `extends` / `implements` edges |
|
||||
|
||||
### DFM / FMX Form Files (custom extractor)
|
||||
|
||||
Support for Delphi form files (`.dfm` for VCL, `.fmx` for FireMonkey) using a regex-based custom extractor — no tree-sitter grammar exists for this format:
|
||||
|
||||
| Feature | NodeKind / EdgeKind | Details |
|
||||
|---------|---------------------|---------|
|
||||
| Components | `component` | `object Button1: TButton` |
|
||||
| Nested hierarchy | `contains` | Panel1 → Button1 |
|
||||
| Event handlers | `references` (unresolved) | `OnClick = Button1Click` → links UI to Pascal methods |
|
||||
| `inherited` keyword | `component` | Inherited form components |
|
||||
| Multi-line properties | — | Correctly skipped during parsing |
|
||||
| Item collections | — | `<item>...</end>` blocks correctly handled |
|
||||
|
||||
The DFM ↔ PAS linkage via event handlers enables **cross-file impact analysis**: renaming a method in `.pas` immediately reveals which UI components reference it.
|
||||
|
||||
## Architecture
|
||||
|
||||
The implementation follows CodeGraph's established patterns:
|
||||
|
||||
- **Pascal extraction** uses the standard `TreeSitterExtractor` with a Pascal-specific `LanguageExtractor` configuration and a `visitPascalNode()` hook for AST nodes that require special handling (e.g., `declType` wrappers, `defProc` implementation bodies)
|
||||
- **DFM/FMX extraction** uses a `DfmExtractor` class — analogous to `LiquidExtractor` and `SvelteExtractor` — that parses the line-based format with regex
|
||||
- **Routing** in `extractFromSource()` dispatches `.dfm`/`.fmx` files to `DfmExtractor` before reaching the tree-sitter path
|
||||
- **`tree-sitter-pascal`** is declared as an `optionalDependency` (consistent with all other grammars), pinned to a specific commit for reproducible builds
|
||||
|
||||
## Performance Improvements
|
||||
|
||||
Testing with a large Delphi codebase (~3,400 files, ~244k nodes) uncovered performance bottlenecks in the reference resolution pipeline. The following fixes **benefit all languages**, not just Pascal:
|
||||
|
||||
| Fix | Scope | Impact |
|
||||
|-----|-------|--------|
|
||||
| **Fuzzy match index** — replaced O(n) linear scan with lazily-built case-insensitive `Map` index | `name-matcher.ts` (all languages) | O(1) lookup per ref instead of iterating all nodes |
|
||||
| **Import mapping cache** — cached per-file import mappings instead of re-reading/re-parsing for every ref | `import-resolver.ts` (all languages) | Eliminated redundant file I/O during resolution |
|
||||
| **Kind cache** — pre-populated `getNodesByKind` results during warm-up | `resolution/index.ts` (all languages) | Avoided repeated DB queries for the same node kinds |
|
||||
| **Pascal built-in filtering** — skip known RTL/VCL/FMX identifiers before resolution | `resolution/index.ts` (Pascal-specific) | ~60 built-in identifiers filtered out early |
|
||||
| **Method index for `defProc`** — replaced O(n) `find()` with `Map` lookup when linking implementation bodies to declarations | `tree-sitter.ts` (Pascal-specific) | O(1) per implementation body |
|
||||
| **Delphi-specific excludes** — `__history/**`, `__recovery/**`, `*.dcu` added to default excludes | `types.ts` (Pascal-specific) | Skips Delphi IDE temp files during indexing |
|
||||
|
||||
**Result:** Reference resolution on a large Delphi project dropped from **~30 minutes to ~15 seconds** (120x speedup). The general improvements (fuzzy index, import cache, kind cache) will benefit all CodeGraph users.
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/types.ts` | Added `'pascal'` to `Language` type, file patterns to `DEFAULT_CONFIG.include` |
|
||||
| `src/extraction/grammars.ts` | Grammar loader, extension mappings (`.pas`, `.dpr`, `.dpk`, `.lpr`, `.dfm`, `.fmx`), display name |
|
||||
| `src/extraction/tree-sitter.ts` | Pascal `LanguageExtractor`, `visitPascalNode()` with 7 helper methods, `DfmExtractor` class, routing in `extractFromSource()`, method index |
|
||||
| `src/resolution/index.ts` | Pascal built-in filtering, kind cache, cache clearing |
|
||||
| `src/resolution/import-resolver.ts` | Import mapping cache |
|
||||
| `src/resolution/name-matcher.ts` | Fuzzy match index (case-insensitive `Map`) |
|
||||
| `package.json` | `tree-sitter-pascal` in `optionalDependencies` (pinned commit) |
|
||||
| `__tests__/extraction.test.ts` | 37 new tests covering all Pascal and DFM extraction features |
|
||||
|
||||
## Test Results
|
||||
|
||||
- **36 new tests**, all passing
|
||||
- **0 regressions** — the same 28 pre-existing failures (unrelated: missing Swift/Dart grammars, database path issues, MCP truncation test) are unchanged
|
||||
- Tests cover: language detection, modules, imports, classes, records, interfaces, methods, visibility, static methods, enums, properties, constants, type aliases, calls, containment, full fixture files (UAuth.pas, UTypes.pas, MainForm.dfm)
|
||||
|
||||
## Dependency Note
|
||||
|
||||
The npm package `tree-sitter-pascal@0.0.1` is outdated (uses NAN bindings, incompatible with Node.js v24+). The implementation uses the actively maintained GitHub repository ([Isopod/tree-sitter-pascal](https://github.com/Isopod/tree-sitter-pascal), v0.10.2) with a pinned commit hash for deterministic builds. This is consistent with how `@sengac/tree-sitter-dart` handles a similar situation.
|
||||
|
||||
## Testing Instructions
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js >= 18
|
||||
- npm
|
||||
- Git
|
||||
|
||||
### 1. Clone and build
|
||||
|
||||
```bash
|
||||
git clone -b delphi-support https://github.com/omonien/codegraph.git
|
||||
cd codegraph
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 2. Link globally
|
||||
|
||||
```bash
|
||||
npm link
|
||||
```
|
||||
|
||||
Verify with:
|
||||
|
||||
```bash
|
||||
codegraph --version
|
||||
```
|
||||
|
||||
### 3. Index a Delphi project
|
||||
|
||||
```bash
|
||||
cd /path/to/your/delphi-project
|
||||
codegraph init -i
|
||||
codegraph index
|
||||
```
|
||||
|
||||
### 4. Query the code graph
|
||||
|
||||
```bash
|
||||
codegraph status # Show index statistics
|
||||
codegraph query "TFormMain" # Search for a symbol
|
||||
codegraph context "What does TCustomer do?" # Build AI context
|
||||
```
|
||||
|
||||
### 5. Set up the MCP server (for Claude Code)
|
||||
|
||||
```bash
|
||||
codegraph install
|
||||
```
|
||||
|
||||
This configures the MCP server, tool permissions, auto-sync hooks, and CLAUDE.md in one step. After that, start Claude Code in the project — CodeGraph tools will be available immediately.
|
||||
|
||||
### 6. Clean up
|
||||
|
||||
```bash
|
||||
npm unlink -g @colbymchenry/codegraph # Remove global link
|
||||
rm -rf /path/to/delphi-project/.codegraph # Remove project index
|
||||
```
|
||||
@@ -132,8 +132,8 @@ Know exactly what breaks before you change it. Trace callers, callees, and the f
|
||||
<tr>
|
||||
<td width="33%" valign="top">
|
||||
|
||||
### 🌍 17+ Languages
|
||||
TypeScript, JavaScript, Python, Go, Rust, Java, C#, PHP, Ruby, C, C++, Swift, Kotlin, Dart, Svelte, Liquid—all with the same API.
|
||||
### 🌍 19+ Languages
|
||||
TypeScript, JavaScript, Python, Go, Rust, Java, C#, PHP, Ruby, C, C++, Swift, Kotlin, Dart, Svelte, Liquid, Pascal/Delphi—all with the same API.
|
||||
|
||||
</td>
|
||||
<td width="33%" valign="top">
|
||||
@@ -638,6 +638,7 @@ The `.codegraph/config.json` file controls indexing behavior:
|
||||
| Dart | `.dart` | Full support |
|
||||
| Svelte | `.svelte` | Full support (script extraction, Svelte 5 runes, SvelteKit routes) |
|
||||
| Liquid | `.liquid` | Full support |
|
||||
| Pascal / Delphi | `.pas`, `.dpr`, `.dpk`, `.lpr` | Full support (classes, records, interfaces, enums, DFM/FMX form files) |
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
|
||||
@@ -1814,6 +1814,718 @@ 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');
|
||||
});
|
||||
|
||||
it('should fallback to filename when module name is empty', () => {
|
||||
// Some .dpr templates use "program;" without a name
|
||||
const code = `program;\nuses SysUtils;\nbegin\nend.`;
|
||||
const result = extractFromSource('Console.dpr', code);
|
||||
|
||||
const moduleNode = result.nodes.find((n) => n.kind === 'module');
|
||||
expect(moduleNode).toBeDefined();
|
||||
expect(moduleNode?.name).toBe('Console');
|
||||
});
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
|
||||
Generated
+1
@@ -2317,6 +2317,7 @@
|
||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
"scripts": {
|
||||
"build": "tsc && npm run copy-assets",
|
||||
"postinstall": "node scripts/postinstall.js",
|
||||
"copy-assets": "node -e \"const fs=require('fs');fs.mkdirSync('dist/db',{recursive:true});fs.copyFileSync('src/db/schema.sql','dist/db/schema.sql')\"",
|
||||
"copy-assets": "node -e \"const fs=require('fs');fs.mkdirSync('dist/db',{recursive:true});fs.copyFileSync('src/db/schema.sql','dist/db/schema.sql');fs.mkdirSync('dist/extraction/wasm',{recursive:true});fs.readdirSync('src/extraction/wasm').filter(f=>f.endsWith('.wasm')).forEach(f=>fs.copyFileSync('src/extraction/wasm/'+f,'dist/extraction/wasm/'+f))\"",
|
||||
"dev": "tsc --watch",
|
||||
"cli": "npm run build && node dist/bin/codegraph.js",
|
||||
"test": "vitest run",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* getParser() returns synchronously from cache.
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import { Parser, Language as WasmLanguage } from 'web-tree-sitter';
|
||||
import { Language } from '../types';
|
||||
|
||||
@@ -32,6 +33,7 @@ const WASM_GRAMMAR_FILES: Record<GrammarLanguage, string> = {
|
||||
swift: 'tree-sitter-swift.wasm',
|
||||
kotlin: 'tree-sitter-kotlin.wasm',
|
||||
dart: 'tree-sitter-dart.wasm',
|
||||
pascal: 'tree-sitter-pascal.wasm',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -66,6 +68,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',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -91,9 +99,12 @@ export async function initGrammars(): Promise<void> {
|
||||
const entries = Object.entries(WASM_GRAMMAR_FILES) as [GrammarLanguage, string][];
|
||||
for (const [lang, wasmFile] of entries) {
|
||||
try {
|
||||
const wasmPath = require.resolve(`tree-sitter-wasms/out/${wasmFile}`);
|
||||
const language = await WasmLanguage.load(wasmPath);
|
||||
languageCache.set(lang, language);
|
||||
// Pascal ships its own WASM (not in tree-sitter-wasms)
|
||||
const wasmPath = lang === 'pascal'
|
||||
? path.join(__dirname, 'wasm', wasmFile)
|
||||
: require.resolve(`tree-sitter-wasms/out/${wasmFile}`);
|
||||
const language = await WasmLanguage.load(wasmPath);
|
||||
languageCache.set(lang, language);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`[CodeGraph] Failed to load ${lang} grammar — parsing will be unavailable: ${message}`);
|
||||
@@ -202,6 +213,7 @@ export function getLanguageDisplayName(language: Language): string {
|
||||
dart: 'Dart',
|
||||
svelte: 'Svelte',
|
||||
liquid: 'Liquid',
|
||||
pascal: 'Pascal / Delphi',
|
||||
unknown: 'Unknown',
|
||||
};
|
||||
return names[language] || language;
|
||||
|
||||
@@ -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
|
||||
@@ -829,6 +887,7 @@ export class TreeSitterExtractor {
|
||||
private errors: ExtractionError[] = [];
|
||||
private extractor: LanguageExtractor | null = null;
|
||||
private nodeStack: string[] = []; // Stack of parent node IDs
|
||||
private methodIndex: Map<string, string> | null = null; // lookup key → node ID for Pascal defProc lookup
|
||||
|
||||
constructor(filePath: string, source: string, language?: Language) {
|
||||
this.filePath = filePath;
|
||||
@@ -927,6 +986,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)) {
|
||||
@@ -1988,6 +2053,405 @@ 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'
|
||||
);
|
||||
const name = moduleNameNode ? getNodeText(moduleNameNode, this.source) : '';
|
||||
// Fallback to filename without extension if module name is empty
|
||||
const moduleName = name || path.basename(this.filePath).replace(/\.[^.]+$/, '');
|
||||
this.createNode('module', moduleName, 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).trim();
|
||||
// fullName is like "TAuthService.Create"
|
||||
const shortName = fullName.includes('.') ? fullName.split('.').pop()! : fullName;
|
||||
const fullNameKey = fullName.toLowerCase();
|
||||
const shortNameKey = shortName.toLowerCase();
|
||||
|
||||
// Build method index on first use (O(n) once, then O(1) per lookup)
|
||||
if (!this.methodIndex) {
|
||||
this.methodIndex = new Map();
|
||||
for (const n of this.nodes) {
|
||||
if (n.kind === 'method' || n.kind === 'function') {
|
||||
const nameKey = n.name.toLowerCase();
|
||||
// Keep first seen short-name mapping to avoid silently overwriting earlier entries.
|
||||
if (!this.methodIndex.has(nameKey)) {
|
||||
this.methodIndex.set(nameKey, n.id);
|
||||
}
|
||||
|
||||
// For Pascal methods, also index qualified forms (e.g. TAuthService.Create).
|
||||
if (n.kind === 'method') {
|
||||
const qualifiedParts = n.qualifiedName.split('::').slice(1); // drop file path
|
||||
if (qualifiedParts.length >= 2) {
|
||||
// Create suffix keys so both "Module.Class.Method" and "Class.Method" can resolve.
|
||||
for (let i = 0; i < qualifiedParts.length - 1; i++) {
|
||||
const scopedName = qualifiedParts.slice(i).join('.').toLowerCase();
|
||||
this.methodIndex.set(scopedName, n.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const parentId =
|
||||
this.methodIndex.get(fullNameKey) ||
|
||||
this.methodIndex.get(shortNameKey) ||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2532,6 +2996,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
|
||||
*/
|
||||
@@ -2541,6 +3162,7 @@ export function extractFromSource(
|
||||
language?: Language
|
||||
): ExtractionResult {
|
||||
const detectedLanguage = language || detectLanguage(filePath);
|
||||
const fileExtension = path.extname(filePath).toLowerCase();
|
||||
|
||||
// Use custom extractor for Svelte
|
||||
if (detectedLanguage === 'svelte') {
|
||||
@@ -2554,6 +3176,15 @@ export function extractFromSource(
|
||||
return extractor.extract();
|
||||
}
|
||||
|
||||
// Use custom extractor for DFM/FMX form files
|
||||
if (
|
||||
detectedLanguage === 'pascal' &&
|
||||
(fileExtension === '.dfm' || fileExtension === '.fmx')
|
||||
) {
|
||||
const extractor = new DfmExtractor(filePath, source);
|
||||
return extractor.extract();
|
||||
}
|
||||
|
||||
const extractor = new TreeSitterExtractor(filePath, source, detectedLanguage);
|
||||
return extractor.extract();
|
||||
}
|
||||
|
||||
Executable
BIN
Binary file not shown.
@@ -425,6 +425,16 @@ function extractPHPImports(content: string): ImportMapping[] {
|
||||
return mappings;
|
||||
}
|
||||
|
||||
// Cache import mappings per file to avoid re-reading and re-parsing
|
||||
const importMappingCache = new Map<string, ImportMapping[]>();
|
||||
|
||||
/**
|
||||
* Clear the import mapping cache (call between indexing runs)
|
||||
*/
|
||||
export function clearImportMappingCache(): void {
|
||||
importMappingCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a reference using import mappings
|
||||
*/
|
||||
|
||||
+43
-5
@@ -39,8 +39,8 @@ export class ReferenceResolver {
|
||||
private fileCache: Map<string, string | null> = new Map();
|
||||
private nameCache: Map<string, Node[]> = new Map();
|
||||
private qualifiedNameCache: Map<string, Node[]> = new Map();
|
||||
private nodeByIdCache: Map<string, Node> = new Map();
|
||||
private kindCache: Map<string, Node[]> = new Map();
|
||||
private nodeByIdCache: Map<string, Node> = new Map();
|
||||
private lowerNameCache: Map<string, Node[]> = new Map();
|
||||
private importMappingCache: Map<string, ImportMapping[]> = new Map();
|
||||
private knownFiles: Set<string> | null = null;
|
||||
@@ -85,9 +85,6 @@ export class ReferenceResolver {
|
||||
this.qualifiedNameCache.set(node.qualifiedName, [node]);
|
||||
}
|
||||
|
||||
// Index by ID
|
||||
this.nodeByIdCache.set(node.id, node);
|
||||
|
||||
// Index by kind
|
||||
const byKind = this.kindCache.get(node.kind);
|
||||
if (byKind) {
|
||||
@@ -96,6 +93,9 @@ export class ReferenceResolver {
|
||||
this.kindCache.set(node.kind, [node]);
|
||||
}
|
||||
|
||||
// Index by ID
|
||||
this.nodeByIdCache.set(node.id, node);
|
||||
|
||||
// Index by lowercase name (for fuzzy matching)
|
||||
const lowerName = node.name.toLowerCase();
|
||||
const byLower = this.lowerNameCache.get(lowerName);
|
||||
@@ -120,8 +120,8 @@ export class ReferenceResolver {
|
||||
this.fileCache.clear();
|
||||
this.nameCache.clear();
|
||||
this.qualifiedNameCache.clear();
|
||||
this.nodeByIdCache.clear();
|
||||
this.kindCache.clear();
|
||||
this.nodeByIdCache.clear();
|
||||
this.lowerNameCache.clear();
|
||||
this.importMappingCache.clear();
|
||||
this.knownFiles = null;
|
||||
@@ -429,6 +429,44 @@ export class ReferenceResolver {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Pascal/Delphi built-ins and standard library units
|
||||
if (ref.language === 'pascal') {
|
||||
// Standard RTL/VCL/FMX unit prefixes — these are external dependencies
|
||||
const pascalUnitPrefixes = [
|
||||
'System.', 'Winapi.', 'Vcl.', 'Fmx.', 'Data.', 'Datasnap.',
|
||||
'Soap.', 'Xml.', 'Web.', 'REST.', 'FireDAC.', 'IBX.',
|
||||
'IdHTTP', 'IdTCP', 'IdSSL',
|
||||
];
|
||||
if (pascalUnitPrefixes.some((p) => name.startsWith(p))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Common standalone RTL units and built-in identifiers
|
||||
const pascalBuiltIns = [
|
||||
'System', 'SysUtils', 'Classes', 'Types', 'Variants', 'StrUtils',
|
||||
'Math', 'DateUtils', 'IOUtils', 'Generics.Collections', 'Generics.Defaults',
|
||||
'Rtti', 'TypInfo', 'SyncObjs', 'RegularExpressions',
|
||||
'SysInit', 'Windows', 'Messages', 'Graphics', 'Controls', 'Forms',
|
||||
'Dialogs', 'StdCtrls', 'ExtCtrls', 'ComCtrls', 'Menus', 'ActnList',
|
||||
'WriteLn', 'Write', 'ReadLn', 'Read', 'Inc', 'Dec', 'Ord', 'Chr',
|
||||
'Length', 'SetLength', 'High', 'Low', 'Assigned', 'FreeAndNil',
|
||||
'Format', 'IntToStr', 'StrToInt', 'FloatToStr', 'StrToFloat',
|
||||
'Trim', 'UpperCase', 'LowerCase', 'Pos', 'Copy', 'Delete', 'Insert',
|
||||
'Now', 'Date', 'Time', 'DateToStr', 'StrToDate',
|
||||
'Raise', 'Exit', 'Break', 'Continue', 'Abort',
|
||||
'True', 'False', 'nil', 'Self', 'Result',
|
||||
'Create', 'Destroy', 'Free',
|
||||
'TObject', 'TComponent', 'TPersistent', 'TInterfacedObject',
|
||||
'TList', 'TStringList', 'TStrings', 'TStream', 'TMemoryStream', 'TFileStream',
|
||||
'Exception', 'EAbort', 'EConvertError', 'EAccessViolation',
|
||||
'IInterface', 'IUnknown',
|
||||
];
|
||||
|
||||
if (pascalBuiltIns.includes(name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -624,6 +632,11 @@ export const DEFAULT_CONFIG: CodeGraphConfig = {
|
||||
'**/Carthage/Build/**',
|
||||
'**/SourcePackages/**',
|
||||
|
||||
// Delphi/Pascal
|
||||
'**/__history/**',
|
||||
'**/__recovery/**',
|
||||
'**/*.dcu',
|
||||
|
||||
// PHP
|
||||
'**/.composer/**',
|
||||
'**/storage/framework/**',
|
||||
|
||||
Reference in New Issue
Block a user