chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
"""Pascal_forms extractor. Moved verbatim from graphify/extract.py."""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from graphify.extractors.base import _file_stem, _make_id
|
||||
|
||||
|
||||
def extract_lazarus_form(path: Path) -> dict:
|
||||
"""Extract component hierarchy from Lazarus .lfm form files.
|
||||
|
||||
.lfm is a text-based declarative format for UI component trees, structured as:
|
||||
object ComponentName: TClassName
|
||||
PropertyName = Value
|
||||
OnEvent = HandlerName
|
||||
object ChildName: TChildClass
|
||||
...
|
||||
end
|
||||
end
|
||||
|
||||
Produces nodes for:
|
||||
- The form file itself
|
||||
- Each component class encountered (TForm1, TButton, TPanel, ...)
|
||||
- Event handler names referenced by OnXxx properties
|
||||
|
||||
Produces edges for:
|
||||
- file --contains--> root form class
|
||||
- parent component --contains--> child component class
|
||||
- component --references--> event handler (context: "event")
|
||||
"""
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
except Exception as e:
|
||||
return {"nodes": [], "edges": [], "error": str(e)}
|
||||
|
||||
import re
|
||||
str_path = str(path)
|
||||
stem = _file_stem(path)
|
||||
nodes: list[dict] = []
|
||||
edges: list[dict] = []
|
||||
seen_ids: set[str] = set()
|
||||
seen_edge_pairs: set[tuple[str, str, str]] = set()
|
||||
|
||||
def add_node(nid: str, label: str, line: int) -> None:
|
||||
if nid not in seen_ids:
|
||||
seen_ids.add(nid)
|
||||
nodes.append({
|
||||
"id": nid, "label": label, "file_type": "code",
|
||||
"source_file": str_path, "source_location": f"L{line}",
|
||||
})
|
||||
|
||||
def add_edge(
|
||||
src: str, tgt: str, relation: str, line: int,
|
||||
context: str | None = None,
|
||||
) -> None:
|
||||
key = (src, tgt, relation)
|
||||
if key in seen_edge_pairs:
|
||||
return
|
||||
seen_edge_pairs.add(key)
|
||||
edge: dict[str, Any] = {
|
||||
"source": src, "target": tgt, "relation": relation,
|
||||
"confidence": "EXTRACTED", "source_file": str_path,
|
||||
"source_location": f"L{line}", "weight": 1.0,
|
||||
}
|
||||
if context:
|
||||
edge["context"] = context
|
||||
edges.append(edge)
|
||||
|
||||
file_nid = _make_id(str(path))
|
||||
add_node(file_nid, path.name, 1)
|
||||
|
||||
obj_re = re.compile(r"^\s*object\s+\w+\s*:\s*(\w+)", re.IGNORECASE)
|
||||
event_re = re.compile(r"^\s*On\w+\s*=\s*(\w+)", re.IGNORECASE)
|
||||
end_re = re.compile(r"^\s*end\s*$", re.IGNORECASE)
|
||||
|
||||
# Stack of node IDs representing the nesting of object...end blocks
|
||||
stack: list[str] = [file_nid]
|
||||
|
||||
for lineno, line in enumerate(text.splitlines(), 1):
|
||||
m = obj_re.match(line)
|
||||
if m:
|
||||
class_name = m.group(1)
|
||||
nid = _make_id(stem, class_name)
|
||||
add_node(nid, class_name, lineno)
|
||||
add_edge(stack[-1], nid, "contains", lineno)
|
||||
stack.append(nid)
|
||||
continue
|
||||
|
||||
m = event_re.match(line)
|
||||
if m and len(stack) > 1:
|
||||
handler = m.group(1)
|
||||
handler_nid = _make_id(stem, handler)
|
||||
add_node(handler_nid, f"{handler}()", lineno)
|
||||
add_edge(stack[-1], handler_nid, "references", lineno, context="event")
|
||||
continue
|
||||
|
||||
if end_re.match(line) and len(stack) > 1:
|
||||
stack.pop()
|
||||
|
||||
return {"nodes": nodes, "edges": edges, "input_tokens": 0, "output_tokens": 0}
|
||||
|
||||
def extract_delphi_form(path: Path) -> dict:
|
||||
"""Extract component hierarchy from Delphi .dfm form files.
|
||||
|
||||
.dfm files come in two formats:
|
||||
- Text (same `object Name: TClassName ... end` syntax as .lfm)
|
||||
- Binary (starts with a TPF0/FF0A magic header — unreadable as text)
|
||||
|
||||
Binary .dfm files are skipped gracefully: an empty result is returned
|
||||
so the rest of the pipeline is unaffected. Convert binary forms to
|
||||
text in the Delphi IDE via File → Save As (Text DFM) if you want them
|
||||
indexed.
|
||||
|
||||
Text .dfm files are parsed identically to .lfm: component containment
|
||||
(`contains`) and event handler references (`references`, context "event").
|
||||
"""
|
||||
try:
|
||||
raw = path.read_bytes()
|
||||
except Exception as e:
|
||||
return {"nodes": [], "edges": [], "error": str(e)}
|
||||
|
||||
# Detect binary DFM: Delphi binary resource streams start with FF 0A
|
||||
if raw[:2] == b"\xff\x0a":
|
||||
return {
|
||||
"nodes": [], "edges": [],
|
||||
"error": f"binary DFM (convert to text in Delphi IDE to index): {path.name}",
|
||||
}
|
||||
|
||||
# Text DFM — delegate to the shared form parser (same syntax as .lfm)
|
||||
try:
|
||||
text = raw.decode("utf-8", errors="replace")
|
||||
except Exception as e:
|
||||
return {"nodes": [], "edges": [], "error": str(e)}
|
||||
|
||||
import re
|
||||
str_path = str(path)
|
||||
stem = _file_stem(path)
|
||||
nodes: list[dict] = []
|
||||
edges: list[dict] = []
|
||||
seen_ids: set[str] = set()
|
||||
seen_edge_pairs: set[tuple[str, str, str]] = set()
|
||||
|
||||
def add_node(nid: str, label: str, line: int) -> None:
|
||||
if nid not in seen_ids:
|
||||
seen_ids.add(nid)
|
||||
nodes.append({
|
||||
"id": nid, "label": label, "file_type": "code",
|
||||
"source_file": str_path, "source_location": f"L{line}",
|
||||
})
|
||||
|
||||
def add_edge(
|
||||
src: str, tgt: str, relation: str, line: int,
|
||||
context: str | None = None,
|
||||
) -> None:
|
||||
key = (src, tgt, relation)
|
||||
if key in seen_edge_pairs:
|
||||
return
|
||||
seen_edge_pairs.add(key)
|
||||
edge: dict[str, Any] = {
|
||||
"source": src, "target": tgt, "relation": relation,
|
||||
"confidence": "EXTRACTED", "source_file": str_path,
|
||||
"source_location": f"L{line}", "weight": 1.0,
|
||||
}
|
||||
if context:
|
||||
edge["context"] = context
|
||||
edges.append(edge)
|
||||
|
||||
file_nid = _make_id(str(path))
|
||||
add_node(file_nid, path.name, 1)
|
||||
|
||||
obj_re = re.compile(r"^\s*object\s+\w+\s*:\s*(\w+)", re.IGNORECASE)
|
||||
event_re = re.compile(r"^\s*On\w+\s*=\s*(\w+)", re.IGNORECASE)
|
||||
end_re = re.compile(r"^\s*end\s*$", re.IGNORECASE)
|
||||
stack: list[str] = [file_nid]
|
||||
|
||||
for lineno, line in enumerate(text.splitlines(), 1):
|
||||
m = obj_re.match(line)
|
||||
if m:
|
||||
class_name = m.group(1)
|
||||
nid = _make_id(stem, class_name)
|
||||
add_node(nid, class_name, lineno)
|
||||
add_edge(stack[-1], nid, "contains", lineno)
|
||||
stack.append(nid)
|
||||
continue
|
||||
m = event_re.match(line)
|
||||
if m and len(stack) > 1:
|
||||
handler = m.group(1)
|
||||
handler_nid = _make_id(stem, handler)
|
||||
add_node(handler_nid, f"{handler}()", lineno)
|
||||
add_edge(stack[-1], handler_nid, "references", lineno, context="event")
|
||||
continue
|
||||
if end_re.match(line) and len(stack) > 1:
|
||||
stack.pop()
|
||||
|
||||
return {"nodes": nodes, "edges": edges, "input_tokens": 0, "output_tokens": 0}
|
||||
Reference in New Issue
Block a user