- Neues Dokument 07-DFM-FMX-Support.md: Format-Referenz, Extraktions-Strategie, DfmExtractor-Konzept - Neues Fixture MainForm.dfm: Verschachtelte Komponenten, Event-Handler (FormCreate, btnLoginClick, etc.) - Integration Guide erweitert: DfmExtractor-Implementierung (analog LiquidExtractor), Coding-Style-Hinweise - NodeKind-Mapping erweitert: DFM-Komponenten als component, Event-Handler als references, Verschachtelung als contains - Checklist: DFM/FMX von 'Zukunft' auf konkreten Plan (Phase 1b) hochgestuft - README aktualisiert: DFM/FMX-Support in Ziel, Inhalt und Grammar-Abschnitt aufgenommen
358 lines
11 KiB
Markdown
358 lines
11 KiB
Markdown
# Integration Guide: Konkrete Code-Änderungen
|
|
|
|
Dieses Dokument zeigt die **exakten Änderungen** an den CodeGraph-Quelldateien, die für Pascal/Delphi-Support nötig sind.
|
|
|
|
## 1) Dependency installieren
|
|
|
|
```bash
|
|
npm install tree-sitter-pascal
|
|
```
|
|
|
|
## 2) `src/types.ts` — Language-Typ erweitern
|
|
|
|
```typescript
|
|
// Zum Language Union-Type hinzufügen (nach 'liquid'):
|
|
export type Language =
|
|
| 'typescript'
|
|
// ...bestehende Sprachen...
|
|
| 'liquid'
|
|
| 'pascal' // ← NEU
|
|
| 'unknown';
|
|
```
|
|
|
|
**DEFAULT_CONFIG.include** erweitern:
|
|
|
|
```typescript
|
|
include: [
|
|
// ...bestehende Patterns...
|
|
// Liquid (Shopify themes)
|
|
'**/*.liquid',
|
|
// Pascal / Delphi ← NEU
|
|
'**/*.pas',
|
|
'**/*.dpr',
|
|
'**/*.dpk',
|
|
'**/*.lpr',
|
|
],
|
|
```
|
|
|
|
## 3) `src/extraction/grammars.ts` — Grammar registrieren
|
|
|
|
### grammarLoaders Map:
|
|
|
|
```typescript
|
|
const grammarLoaders: Record<GrammarLanguage, GrammarLoader> = {
|
|
// ...bestehende Loader...
|
|
dart: () => {
|
|
return require('@sengac/tree-sitter-dart');
|
|
},
|
|
pascal: () => { // ← NEU
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
return require('tree-sitter-pascal');
|
|
},
|
|
};
|
|
```
|
|
|
|
### EXTENSION_MAP:
|
|
|
|
```typescript
|
|
export const EXTENSION_MAP: Record<string, Language> = {
|
|
// ...bestehende Mappings...
|
|
'.liquid': 'liquid',
|
|
'.svelte': 'svelte',
|
|
'.pas': 'pascal', // ← NEU
|
|
'.dpr': 'pascal', // ← NEU
|
|
'.dpk': 'pascal', // ← NEU
|
|
'.lpr': 'pascal', // ← NEU
|
|
};
|
|
```
|
|
|
|
### getLanguageDisplayName():
|
|
|
|
```typescript
|
|
const names: Record<Language, string> = {
|
|
// ...bestehende Namen...
|
|
liquid: 'Liquid',
|
|
pascal: 'Pascal / Delphi', // ← NEU
|
|
unknown: 'Unknown',
|
|
};
|
|
```
|
|
|
|
## 4) `src/extraction/tree-sitter.ts` — LanguageExtractor
|
|
|
|
```typescript
|
|
pascal: {
|
|
functionTypes: ['declProc'], // Top-Level Funktionen/Prozeduren
|
|
classTypes: ['declClass'], // Klassen, Records, Objects
|
|
methodTypes: ['declProc'], // Methoden (in Klasse), Konstruktoren, Destruktoren
|
|
interfaceTypes: ['declIntf'], // Interfaces
|
|
structTypes: [], // Records über declClass abgedeckt
|
|
enumTypes: ['declEnum'], // Aufzählungen
|
|
typeAliasTypes: ['declType'], // Type-Aliase
|
|
importTypes: ['declUses'], // uses-Klauseln
|
|
callTypes: ['exprCall'], // Funktionsaufrufe
|
|
variableTypes: ['declField', 'declConst'], // Felder und Konstanten
|
|
nameField: 'name',
|
|
bodyField: 'body', // nur bei defProc (Implementierung)
|
|
paramsField: 'args', // declArgs
|
|
returnField: 'type',
|
|
getSignature: (node, source) => {
|
|
const args = getChildByField(node, 'args');
|
|
const returnType = getChildByField(node, 'type');
|
|
if (!args) return undefined;
|
|
const argsText = getNodeText(args, source);
|
|
return returnType
|
|
? getNodeText(returnType, source) + ' ' + argsText
|
|
: argsText;
|
|
},
|
|
getVisibility: (node) => {
|
|
// Suche den nächsten declSection-Vorfahren
|
|
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;
|
|
},
|
|
isStatic: (node) => {
|
|
// class procedure / class function
|
|
for (let i = 0; i < node.childCount; i++) {
|
|
if (node.child(i)?.type === 'kClass') return true;
|
|
}
|
|
return false;
|
|
},
|
|
isConst: (node) => {
|
|
return node.type === 'declConst';
|
|
},
|
|
},
|
|
```
|
|
|
|
## Hinweise zur Pascal-Integration
|
|
|
|
- `declProc` wird sowohl für `functionTypes` als auch `methodTypes` verwendet. CodeGraph unterscheidet anhand des AST-Kontexts (Parent ist `declClass` → method, sonst → function).
|
|
- Die `getVisibility`-Funktion traversiert aufwärts zum `declSection`-Knoten.
|
|
- Records werden als `class` behandelt, da `declClass` mit `kRecord` Kind-Knoten beides abdeckt.
|
|
|
|
---
|
|
|
|
## 5) DFM/FMX-Support: Custom Extractor
|
|
|
|
DFM/FMX-Dateien verwenden ein einfaches zeilenbasiertes Textformat (kein tree-sitter Parser vorhanden). Die Integration erfolgt analog zu `LiquidExtractor` und `SvelteExtractor` als **Custom Extractor**.
|
|
|
|
### Extension-Mapping ergänzen (`src/extraction/grammars.ts`)
|
|
|
|
```typescript
|
|
export const EXTENSION_MAP: Record<string, Language> = {
|
|
// ...bestehende Mappings...
|
|
'.pas': 'pascal',
|
|
'.dpr': 'pascal',
|
|
'.dpk': 'pascal',
|
|
'.lpr': 'pascal',
|
|
'.dfm': 'pascal', // ← NEU: DFM-Formulare
|
|
'.fmx': 'pascal', // ← NEU: FMX-Formulare
|
|
};
|
|
```
|
|
|
|
> **Hinweis:** DFM/FMX werden als Sprache `pascal` registriert. Die Unterscheidung zwischen tree-sitter-Parsing (`.pas`) und Custom Extractor (`.dfm`/`.fmx`) erfolgt in `extractFromSource()` anhand der Dateiendung.
|
|
|
|
### DEFAULT_CONFIG.include ergänzen (`src/types.ts`)
|
|
|
|
```typescript
|
|
include: [
|
|
// ...bestehende Patterns...
|
|
'**/*.pas',
|
|
'**/*.dpr',
|
|
'**/*.dpk',
|
|
'**/*.lpr',
|
|
'**/*.dfm', // ← NEU
|
|
'**/*.fmx', // ← NEU
|
|
],
|
|
```
|
|
|
|
### Routing in `extractFromSource()` (`src/extraction/tree-sitter.ts`)
|
|
|
|
```typescript
|
|
export function extractFromSource(
|
|
filePath: string,
|
|
source: string,
|
|
language?: Language
|
|
): ExtractionResult {
|
|
const detectedLanguage = language || detectLanguage(filePath);
|
|
|
|
// Use custom extractor for Svelte
|
|
if (detectedLanguage === 'svelte') {
|
|
const extractor = new SvelteExtractor(filePath, source);
|
|
return extractor.extract();
|
|
}
|
|
|
|
// Use custom extractor for Liquid
|
|
if (detectedLanguage === 'liquid') {
|
|
const extractor = new LiquidExtractor(filePath, source);
|
|
return extractor.extract();
|
|
}
|
|
|
|
// Use custom extractor for DFM/FMX form files ← NEU
|
|
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();
|
|
}
|
|
```
|
|
|
|
### DfmExtractor Klasse (Coding Style analog LiquidExtractor)
|
|
|
|
```typescript
|
|
/**
|
|
* 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 { /* ... analog LiquidExtractor ... */ }
|
|
|
|
/** Parse object/end blocks and extract components + event handlers */
|
|
private parseComponents(fileNodeId: string): void {
|
|
const lines = this.source.split('\n');
|
|
const stack: string[] = [fileNodeId]; // Stack der Parent-Node-IDs
|
|
|
|
const objectPattern = /^\s*(object|inherited|inline)\s+(\w+)\s*:\s*(\w+)/;
|
|
const eventPattern = /^\s*(On\w+)\s*=\s*(\w+)/;
|
|
const endPattern = /^\s*end\s*$/;
|
|
const multiLineStart = /=\s*\(\s*$/;
|
|
let inMultiLine = false;
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
const lineNum = i + 1;
|
|
|
|
// Mehrzeilige Properties überspringen
|
|
if (inMultiLine) {
|
|
if (line.trimEnd().endsWith(')')) inMultiLine = false;
|
|
continue;
|
|
}
|
|
if (multiLineStart.test(line)) {
|
|
inMultiLine = true;
|
|
continue;
|
|
}
|
|
|
|
// Component-Deklaration
|
|
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,
|
|
qualifiedName: `${this.filePath}#${name}`,
|
|
filePath: this.filePath,
|
|
language: 'pascal',
|
|
startLine: lineNum,
|
|
endLine: lineNum, // wird beim zugehörigen 'end' aktualisiert
|
|
startColumn: 0,
|
|
endColumn: line.length,
|
|
metadata: { componentType: 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({
|
|
sourceNodeId: stack[stack.length - 1],
|
|
targetName: methodName,
|
|
kind: 'references',
|
|
filePath: this.filePath,
|
|
line: lineNum,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
// Block-Ende
|
|
if (endPattern.test(line)) {
|
|
if (stack.length > 1) stack.pop();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Coding-Style-Hinweise
|
|
|
|
Der `DfmExtractor` folgt dem bestehenden Coding Style der Custom Extractors:
|
|
|
|
- **JSDoc-Kommentare** (`/** */`) für jede öffentliche Methode und die Klasse selbst
|
|
- **Private Felder** für `nodes`, `edges`, `unresolvedReferences`, `errors`
|
|
- **`generateNodeId()`** für deterministische Node-IDs
|
|
- **`captureException()`** für Error-Tracking in catch-Blöcken
|
|
- **`ExtractionResult`** als Return-Type von `extract()`
|
|
- **`UnresolvedReference`** für Event-Handler (werden in der Resolution-Phase aufgelöst)
|
|
- **Kein tree-sitter** — rein Regex/zeilenbasiertes Parsing
|
|
|