dfm-extractor.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import { Node, Edge, ExtractionResult, ExtractionError, UnresolvedReference } from '../types';
  2. import { generateNodeId } from './tree-sitter-helpers';
  3. /**
  4. * Custom extractor for Delphi DFM/FMX form files.
  5. *
  6. * DFM/FMX files describe the visual component hierarchy and event handler
  7. * bindings. They use a simple text format (object/end blocks) that we parse
  8. * with regex — no tree-sitter grammar exists for this format.
  9. *
  10. * Extracted information:
  11. * - Components as NodeKind `component`
  12. * - Nesting as EdgeKind `contains`
  13. * - Event handlers (OnClick = MethodName) as UnresolvedReference → EdgeKind `references`
  14. */
  15. export class DfmExtractor {
  16. private filePath: string;
  17. private source: string;
  18. private nodes: Node[] = [];
  19. private edges: Edge[] = [];
  20. private unresolvedReferences: UnresolvedReference[] = [];
  21. private errors: ExtractionError[] = [];
  22. constructor(filePath: string, source: string) {
  23. this.filePath = filePath;
  24. this.source = source;
  25. }
  26. /**
  27. * Extract components and event handler references from DFM/FMX source
  28. */
  29. extract(): ExtractionResult {
  30. const startTime = Date.now();
  31. try {
  32. const fileNode = this.createFileNode();
  33. this.parseComponents(fileNode.id);
  34. } catch (error) {
  35. this.errors.push({
  36. message: `DFM extraction error: ${error instanceof Error ? error.message : String(error)}`,
  37. severity: 'error',
  38. code: 'parse_error',
  39. });
  40. }
  41. return {
  42. nodes: this.nodes,
  43. edges: this.edges,
  44. unresolvedReferences: this.unresolvedReferences,
  45. errors: this.errors,
  46. durationMs: Date.now() - startTime,
  47. };
  48. }
  49. /** Create a file node for the DFM form file */
  50. private createFileNode(): Node {
  51. const lines = this.source.split('\n');
  52. const id = generateNodeId(this.filePath, 'file', this.filePath, 1);
  53. const fileNode: Node = {
  54. id,
  55. kind: 'file',
  56. name: this.filePath.split('/').pop() || this.filePath,
  57. qualifiedName: this.filePath,
  58. filePath: this.filePath,
  59. language: 'pascal',
  60. startLine: 1,
  61. endLine: lines.length,
  62. startColumn: 0,
  63. endColumn: lines[lines.length - 1]?.length || 0,
  64. updatedAt: Date.now(),
  65. };
  66. this.nodes.push(fileNode);
  67. return fileNode;
  68. }
  69. /** Parse object/end blocks and extract components + event handlers */
  70. private parseComponents(fileNodeId: string): void {
  71. const lines = this.source.split('\n');
  72. const stack: string[] = [fileNodeId];
  73. const objectPattern = /^\s*(object|inherited|inline)\s+(\w+)\s*:\s*(\w+)/;
  74. const eventPattern = /^\s*(On\w+)\s*=\s*(\w+)\s*$/;
  75. const endPattern = /^\s*end\s*$/;
  76. const multiLineStart = /=\s*\(\s*$/;
  77. const multiLineItemStart = /=\s*<\s*$/;
  78. let inMultiLine = false;
  79. let multiLineEndChar = ')';
  80. for (let i = 0; i < lines.length; i++) {
  81. const line = lines[i]!;
  82. const lineNum = i + 1;
  83. // Skip multi-line properties
  84. if (inMultiLine) {
  85. if (line.trimEnd().endsWith(multiLineEndChar)) inMultiLine = false;
  86. continue;
  87. }
  88. if (multiLineStart.test(line)) {
  89. inMultiLine = true;
  90. multiLineEndChar = ')';
  91. continue;
  92. }
  93. if (multiLineItemStart.test(line)) {
  94. inMultiLine = true;
  95. multiLineEndChar = '>';
  96. continue;
  97. }
  98. // Component declaration
  99. const objMatch = line.match(objectPattern);
  100. if (objMatch) {
  101. const [, , name, typeName] = objMatch;
  102. const nodeId = generateNodeId(this.filePath, 'component', name!, lineNum);
  103. this.nodes.push({
  104. id: nodeId,
  105. kind: 'component',
  106. name: name!,
  107. qualifiedName: `${this.filePath}#${name}`,
  108. filePath: this.filePath,
  109. language: 'pascal',
  110. startLine: lineNum,
  111. endLine: lineNum,
  112. startColumn: 0,
  113. endColumn: line.length,
  114. signature: typeName,
  115. updatedAt: Date.now(),
  116. });
  117. this.edges.push({
  118. source: stack[stack.length - 1]!,
  119. target: nodeId,
  120. kind: 'contains',
  121. });
  122. stack.push(nodeId);
  123. continue;
  124. }
  125. // Event handler
  126. const eventMatch = line.match(eventPattern);
  127. if (eventMatch) {
  128. const [, , methodName] = eventMatch;
  129. this.unresolvedReferences.push({
  130. fromNodeId: stack[stack.length - 1]!,
  131. referenceName: methodName!,
  132. referenceKind: 'references',
  133. line: lineNum,
  134. column: 0,
  135. });
  136. continue;
  137. }
  138. // Block end
  139. if (endPattern.test(line)) {
  140. if (stack.length > 1) stack.pop();
  141. }
  142. }
  143. }
  144. }