[ADD] dbg2lsp.py: AutoLISP-Funktionen automatisch mit ssg_dbg instrumentieren

Neues Entwickler-Tool (bin/dbg2lsp.bat + lib/dbg2lsp.py), das das manuelle
Einfuegen von dbgf/dbg/dbgreturn/dbgopen/dbgclose automatisiert:
- --method NAME: dbgf + dbg je Parameter + dbgreturn um die letzte Rumpf-Form
- --recursive: verfolgt den Aufrufgraphen ueber Dateigrenzen hinweg (Builtins
  werden automatisch uebersprungen, da sie kein defun im Suchpfad haben)
- --add-open DATEI METHODE: dbgopen/dbgclose fuer einen Einstiegspunkt
  (z.B. ein c:BEFEHL-Kommando), inkl. aller (exit)-Stellen
Dokumentiert in doc/dbg2lsp.md, Kurzeintrag in CLAUDE.md.

Ausserdem: set_attributs.py aus doc/tools.md und CLAUDE.md entfernt - das
Skript existiert nicht mehr in lib/.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 15:04:45 +02:00
parent 1d5d7c141e
commit d7bcfc3edb
5 changed files with 949 additions and 167 deletions
+725
View File
@@ -0,0 +1,725 @@
"""
Instrumentiert AutoLISP-Funktionen (.lsp) automatisch mit den ssg_dbg.lsp-
Routinen (dbgf, dbg, dbgreturn, dbgopen, dbgclose) - so wie es beim manuellen
Debuggen (siehe z.B. Lisp/KreiselInsert.lsp::ils-eckrad-insert) bereits
gemacht wird.
Fuer eine gegebene Funktion (--method) wird eingefuegt:
- (dbgf "funktionsname") direkt als erste Form im Rumpf
- (dbg 'parameter) je Eingabeparameter (die Locals nach "/" NICHT)
- (dbgreturn <letzte Rumpf-Form>) die letzte Form des Rumpfs wird
eingeklammert, damit ihr Rueckgabe-
wert mitgeloggt wird
Mit --recursive wird zusaetzlich jede Funktion instrumentiert, die im Rumpf
(auch verschachtelt, aber NICHT innerhalb von 'quotierten Listen) aufgerufen
wird - vorausgesetzt, ihr (defun ...) laesst sich in einem der --search-dir-
Verzeichnisse finden (Bibliotheks-/AutoLISP-Builtins wie princ, command,
strcat etc. haben dort kein defun und werden dadurch automatisch NICHT
angefasst). Der Aufrufgraph wird rekursiv weiterverfolgt (mit --max-depth
begrenzt), einmal besuchte Funktionen werden nicht doppelt instrumentiert.
Mit --add-open DATEI METHODE wird in der angegebenen Methode zusaetzlich
- (dbgopen "<dbg-datei>" "<envvar>") als erste Rumpf-Form
- (dbgclose) vor jedem (exit)-Aufruf im Rumpf
UND am Ende des Rumpfs (falls die
letzte Form nicht bereits (exit) ist)
eingefuegt - fuer den Einstiegspunkt (typischerweise ein c:BEFEHL-Kommando),
der die .dbg-Datei fuer einen ganzen Testlauf oeffnen/schliessen soll.
--add-open ist unabhaengig von --method/--recursive nutzbar und mehrfach
angebbar.
Aufruf-Beispiele:
python dbg2lsp.py tests/test_mubea.lsp --method mubea:build-separator-one
python dbg2lsp.py tests/test_mubea.lsp --method mubea:build-kreisel --recursive
python dbg2lsp.py tests/test_mubea.lsp --add-open tests/test_mubea.lsp c:TEST_MUBEA
python dbg2lsp.py tests/test_mubea.lsp --method mubea:build-kreisel --recursive \\
--add-open tests/test_mubea.lsp c:TEST_MUBEA --dry-run
Details, Grenzen und weitere Beispiele: siehe doc/dbg2lsp.md
"""
import argparse
import difflib
import os
import sys
# ---------------------------------------------------------------------------
# ssg_dbg.lsp-Funktionen, die NIE instrumentiert oder in den Aufrufgraphen
# aufgenommen werden duerfen - sonst wuerde --recursive versuchen, dbgf
# selbst mit dbgf zu instrumentieren, sobald es auf eine bereits (manuell
# oder durch einen frueheren Lauf) instrumentierte Funktion trifft.
# ---------------------------------------------------------------------------
DBG_FRAMEWORK_NAMES = {
"dbgf", "dbg", "dbgp", "dbgmsg", "dbgreturn", "dbgopen", "dbgclose",
"dbgflush", "dbgon", "dbgoff",
"dbg-flush", "dbg-bufwrite", "dbg-tabs", "dbg-tostring", "dbg-list-tostring",
}
DEFAULT_DBG_ENVVAR = "DXFM_LOG"
# ---------------------------------------------------------------------------
# Zeichenweises Scannen mit String-/Kommentar-Bewusstsein (identische Logik
# wie beim manuellen Klammer-Balance-Check waehrend der Mubea-Instrumentierung)
# ---------------------------------------------------------------------------
class LispSyntaxError(Exception):
pass
def _skip_string(text, i):
"""i zeigt auf das oeffnende '\"'. Gibt den Index NACH dem schliessenden '\"' zurueck."""
n = len(text)
i += 1
while i < n:
c = text[i]
if c == "\\":
i += 2
continue
if c == '"':
return i + 1
i += 1
raise LispSyntaxError("Nicht geschlossener String ab Position %d" % i)
def _skip_line_comment(text, i):
nl = text.find("\n", i)
return len(text) if nl == -1 else nl + 1
def find_matching_paren(text, open_idx):
"""open_idx zeigt auf '('. Gibt den Index der zugehoerigen ')' zurueck."""
n = len(text)
if text[open_idx] != "(":
raise LispSyntaxError("Position %d ist keine '('" % open_idx)
depth = 0
i = open_idx
while i < n:
c = text[i]
if c == '"':
i = _skip_string(text, i)
continue
if c == ";":
i = _skip_line_comment(text, i)
continue
if c == "(":
depth += 1
elif c == ")":
depth -= 1
if depth == 0:
return i
i += 1
raise LispSyntaxError("Keine schliessende Klammer fuer '(' ab Position %d gefunden" % open_idx)
def _skip_ws_and_comments(text, i):
n = len(text)
while i < n:
c = text[i]
if c in " \t\r\n":
i += 1
continue
if c == ";":
i = _skip_line_comment(text, i)
continue
break
return i
_TOKEN_STOP_CHARS = set(" \t\r\n()\";'")
def read_next_token(text, i, limit):
"""Liest ab i (nach Whitespace/Kommentaren) das naechste Token (Symbol/Zahl)
bis zum naechsten Trennzeichen. Gibt (token, end_index) oder (None, i) zurueck,
wenn an Position i eine '(' / '"' / EOF folgt (kein einfaches Token)."""
i = _skip_ws_and_comments(text, i)
if i >= limit:
return None, i
if text[i] in "(\"'":
return None, i
start = i
while i < limit and text[i] not in _TOKEN_STOP_CHARS:
i += 1
if i == start:
return None, i
return text[start:i], i
def iter_top_level_spans(text, start, end):
"""Liefert (form_start, form_end) fuer jede balancierte Top-Level-Form
zwischen start und end (end exklusiv, typischerweise die Position der
schliessenden ')' des umgebenden defun). Reine Atome (Symbole/Zahlen ohne
umschliessende Klammer) werden ebenfalls als Form erkannt."""
spans = []
i = start
while i < end:
i = _skip_ws_and_comments(text, i)
if i >= end:
break
c = text[i]
if c == "(":
close = find_matching_paren(text, i)
spans.append((i, close + 1))
i = close + 1
elif c == "'":
# Quotiertes Atom/Liste - als eine Form ab dem Apostroph behandeln.
j = i + 1
if j < end and text[j] == "(":
close = find_matching_paren(text, j)
spans.append((i, close + 1))
i = close + 1
else:
tok, tend = read_next_token(text, j, end)
spans.append((i, tend))
i = tend
elif c == '"':
send = _skip_string(text, i)
spans.append((i, send))
i = send
else:
tok, tend = read_next_token(text, i, end)
if tok is None:
i += 1
else:
spans.append((i, tend))
i = tend
return spans
# ---------------------------------------------------------------------------
# (defun ...) - Erkennung
# ---------------------------------------------------------------------------
class DefunInfo:
def __init__(self, name, path, defun_start, defun_end,
arglist_start, arglist_end, body_start, body_end,
params, locals_):
self.name = name
self.path = path
self.defun_start = defun_start
self.defun_end = defun_end # Position NACH der schliessenden ')' des defun
self.arglist_start = arglist_start
self.arglist_end = arglist_end
self.body_start = body_start
self.body_end = body_end # Position der schliessenden ')' des defun (exklusiv)
self.params = params
self.locals = locals_
def __repr__(self):
return "DefunInfo(%r in %s)" % (self.name, self.path)
def _parse_arglist(text, arglist_start, arglist_end):
"""arglist_start zeigt auf '(' der Parameterliste, arglist_end auf die
zugehoerige ')'. Gibt (params, locals) zurueck - locals ist alles nach
einem "/"-Trenner (leer, falls keiner vorhanden)."""
params = []
locals_ = []
seen_slash = False
i = arglist_start + 1
while i < arglist_end:
tok, tend = read_next_token(text, i, arglist_end)
if tok is None:
# z.B. eine verschachtelte Liste als Defaultwert (in AutoLISP
# eigentlich nicht ueblich) - ueberspringen statt abzubrechen.
i = _skip_ws_and_comments(text, i)
if i < arglist_end and text[i] == "(":
i = find_matching_paren(text, i) + 1
continue
i += 1
continue
if tok == "/":
seen_slash = True
elif seen_slash:
locals_.append(tok)
else:
params.append(tok)
i = tend
return params, locals_
def find_defuns_in_text(text, path):
"""Findet alle (defun NAME (...) ...) Top-Level-Definitionen im Text."""
defuns = []
i = 0
n = len(text)
while i < n:
c = text[i]
if c == '"':
i = _skip_string(text, i)
continue
if c == ";":
i = _skip_line_comment(text, i)
continue
if c == "(":
close = find_matching_paren(text, i)
head_tok, head_end = read_next_token(text, i + 1, close)
if head_tok is not None and head_tok.lower() == "defun":
name_tok, name_end = read_next_token(text, head_end, close)
if name_tok:
al_start = _skip_ws_and_comments(text, name_end)
if al_start < close and text[al_start] == "(":
al_end = find_matching_paren(text, al_start)
params, locals_ = _parse_arglist(text, al_start, al_end)
body_start = al_end + 1
defuns.append(DefunInfo(
name=name_tok, path=path,
defun_start=i, defun_end=close + 1,
arglist_start=al_start, arglist_end=al_end,
body_start=body_start, body_end=close,
params=params, locals_=locals_,
))
i = close + 1
continue
i += 1
return defuns
def build_defun_index(paths):
"""path -> Dateitext (einmal eingelesen) und name.lower() -> DefunInfo
(erster Treffer gewinnt; mehrfach definierte Namen sind in dieser
Code-Basis nicht vorgesehen)."""
file_texts = {}
index = {}
for path in paths:
with open(path, "r", encoding="utf-8") as f:
text = f.read()
file_texts[path] = text
for d in find_defuns_in_text(text, path):
index.setdefault(d.name.lower(), d)
return file_texts, index
# ---------------------------------------------------------------------------
# Aufruf-Erkennung (fuer --recursive und --add-open/exit-Suche)
# ---------------------------------------------------------------------------
def scan_calls(text, start, end):
"""Liefert eine Liste von (head_name, call_start, call_end) fuer jede
NICHT quotierte Aufruf-Form (...) im Bereich [start, end) - beliebig
verschachtelt, aber nicht innerhalb von 'quotierten Listen (deren Inhalt
ist reine Datenliste, dort kann nichts "aufgerufen" werden)."""
calls = []
def walk(s, e):
i = s
while i < e:
c = text[i]
if c == '"':
i = _skip_string(text, i)
continue
if c == ";":
i = _skip_line_comment(text, i)
continue
if c == "'":
j = i + 1
if j < e and text[j] == "(":
# quotierte Liste: reine Daten, nicht weiter absteigen
close = find_matching_paren(text, j)
i = close + 1
continue
i = j
continue
if c == "(":
close = find_matching_paren(text, i)
head_tok, head_end = read_next_token(text, i + 1, close)
if head_tok and head_tok[0] not in "0123456789.\"'":
calls.append((head_tok, i, close + 1))
walk(i + 1, close)
i = close + 1
continue
i += 1
walk(start, end)
return calls
# ---------------------------------------------------------------------------
# Hilfsfunktionen fuer Einfuegetexte
# ---------------------------------------------------------------------------
def _line_indent(text, pos):
"""Fuehrender Whitespace der Zeile, in der pos liegt (fuer die Einrueckung
neu eingefuegter Debug-Zeilen)."""
nl = text.rfind("\n", 0, pos)
line_start = nl + 1
i = line_start
while i < pos and text[i] in " \t":
i += 1
indent = text[line_start:i]
return indent if indent else " "
def _body_indent(text, body_start, body_end):
"""Einrueckung der ERSTEN echten Rumpf-Form. NICHT einfach _line_indent
auf body_start anwenden: bei mehrzeiligen Parameterlisten (z.B.
"(defun foo (a b /\n c d)" - "/"-Locals auf eigener Zeile) liegt
body_start mitten in der Fortsetzungszeile der Parameterliste, deren
Ausrichtung nichts mit der eigentlichen Rumpf-Einrueckung zu tun hat."""
first = _skip_ws_and_comments(text, body_start)
if first >= body_end:
return " "
return _line_indent(text, first)
def _already_has_call(text, body_start, body_end, head_name):
spans = iter_top_level_spans(text, body_start, body_end)
if not spans:
return False
fs, fe = spans[0]
if text[fs] != "(":
return False
tok, _ = read_next_token(text, fs + 1, fe)
return bool(tok) and tok.lower() == head_name.lower()
# ---------------------------------------------------------------------------
# Edit-Sammlung: (position, prioritaet, text) - reine Einfuegungen, angewandt
# in absteigender Position (und bei Gleichstand nach Prioritaet), damit
# fruehere Positionen gueltig bleiben.
# ---------------------------------------------------------------------------
class EditSet:
def __init__(self):
self.edits = {} # path -> list[(pos, prio, text)]
def insert(self, path, pos, prio, text):
self.edits.setdefault(path, []).append((pos, prio, text))
def apply(self, file_texts):
"""Gibt {path: neuer_text} fuer alle Dateien mit mindestens einer
Einfuegung zurueck."""
result = {}
for path, items in self.edits.items():
text = file_texts[path]
# Bei gleicher Position: kleinere Prioritaet zuerst im Ergebnis-
# text (prio dient als "vor/nach"-Reihenfolge an derselben Stelle).
items_sorted = sorted(items, key=lambda t: (t[0], t[1]))
out = []
last = 0
# Gruppieren nach Position, damit mehrere Einfuegungen an
# derselben Stelle in Prioritaets-Reihenfolge zusammenkommen.
grouped = {}
order = []
for pos, prio, ins_text in items_sorted:
if pos not in grouped:
grouped[pos] = []
order.append(pos)
grouped[pos].append(ins_text)
for pos in order:
out.append(text[last:pos])
out.extend(grouped[pos])
last = pos
out.append(text[last:])
result[path] = "".join(out)
return result
# ---------------------------------------------------------------------------
# Instrumentierung
# ---------------------------------------------------------------------------
def instrument_method(defun, file_texts, edits, force=False, log=print):
"""Fuegt (dbgf ...), (dbg 'param) je Parameter und (dbgreturn ...) um die
letzte Rumpf-Form ein."""
text = file_texts[defun.path]
if not force and _already_has_call(text, defun.body_start, defun.body_end, "dbgf"):
log(" [skip] %s (%s) ist bereits instrumentiert (dbgf vorhanden) - --force zum Erzwingen" %
(defun.name, defun.path))
return False
indent = _body_indent(text, defun.body_start, defun.body_end)
lines = ['(dbgf "%s")' % defun.name]
for p in defun.params:
lines.append("(dbg '%s)" % p)
# Kein Trailing-Newline/-Indent: body_start hat im Original bereits sein
# eigenes "\n<indent>" vor der ersten Rumpf-Form, das bleibt unveraendert
# stehen und schliesst direkt an die letzte hier eingefuegte Zeile an.
insertion = "\n" + "\n".join(indent + l for l in lines)
edits.insert(defun.path, defun.body_start, 10, insertion)
spans = iter_top_level_spans(text, defun.body_start, defun.body_end)
if spans:
last_start, last_end = spans[-1]
edits.insert(defun.path, last_start, 20, "(dbgreturn ")
edits.insert(defun.path, last_end, 21, ")")
else:
log(" [warn] %s (%s) hat einen leeren Rumpf - kein dbgreturn eingefuegt" %
(defun.name, defun.path))
log(" [ok] %s (%s): dbgf + %d x dbg + dbgreturn" %
(defun.name, defun.path, len(defun.params)))
return True
def instrument_add_open(defun, file_texts, edits, dbg_file, dbg_envvar, force=False, log=print):
"""Fuegt (dbgopen "dbg_file" "dbg_envvar") als erste Rumpf-Form ein sowie
(dbgclose) vor jedem (exit)-Aufruf und (sofern die letzte Rumpf-Form nicht
selbst ein (exit) ist) am Ende des Rumpfs."""
text = file_texts[defun.path]
if not force and _already_has_call(text, defun.body_start, defun.body_end, "dbgopen"):
log(" [skip] %s (%s) hat bereits (dbgopen ...) - --force zum Erzwingen" %
(defun.name, defun.path))
return False
indent = _body_indent(text, defun.body_start, defun.body_end)
# Kein Trailing-Newline/-Indent (siehe instrument_method): die naechste
# Einfuegung an derselben Position (z.B. dbgf durch --method) oder das
# Original selbst liefert das fuehrende "\n<indent>" bereits mit.
open_line = "\n" + indent + '(dbgopen "%s" "%s")' % (dbg_file, dbg_envvar)
edits.insert(defun.path, defun.body_start, 5, open_line)
calls = scan_calls(text, defun.body_start, defun.body_end)
exit_calls = [c for c in calls if c[0].lower() == "exit"]
for _, cstart, _cend in exit_calls:
cindent = _line_indent(text, cstart)
edits.insert(defun.path, cstart, 15, "(dbgclose)\n" + cindent)
spans = iter_top_level_spans(text, defun.body_start, defun.body_end)
last_is_exit = False
if spans:
last_start, last_end = spans[-1]
tok, _ = read_next_token(text, last_start + 1, last_end) if text[last_start] == "(" else (None, 0)
last_is_exit = bool(tok) and tok.lower() == "exit"
if not last_is_exit:
edits.insert(defun.path, defun.body_end, 25, indent + "(dbgclose)\n")
log(" [ok] %s (%s): dbgopen + dbgclose (%d exit-Stellen + Rumpfende)" %
(defun.name, defun.path, len(exit_calls)))
return True
# ---------------------------------------------------------------------------
# Rekursion ueber den Aufrufgraphen
# ---------------------------------------------------------------------------
def instrument_recursive(start_defun, index, file_texts, edits, max_depth, exclude, force, log):
visited = set()
def recurse(defun, depth):
key = defun.name.lower()
if key in visited:
return
visited.add(key)
instrument_method(defun, file_texts, edits, force=force, log=log)
if depth >= max_depth:
log(" [info] max-depth (%d) bei %s erreicht - keine weiteren Aufrufe verfolgt" %
(max_depth, defun.name))
return
text = file_texts[defun.path]
calls = scan_calls(text, defun.body_start, defun.body_end)
seen_here = set()
for head, _s, _e in calls:
head_l = head.lower()
if head_l in seen_here:
continue
seen_here.add(head_l)
if head_l in DBG_FRAMEWORK_NAMES or head_l in exclude:
continue
callee = index.get(head_l)
if callee is None or callee.name.lower() == key:
continue
recurse(callee, depth + 1)
recurse(start_defun, 0)
# ---------------------------------------------------------------------------
# Pfad-/Umgebungshilfen
# ---------------------------------------------------------------------------
def find_repo_root(start_path):
cur = os.path.abspath(start_path)
if os.path.isfile(cur):
cur = os.path.dirname(cur)
while True:
if os.path.isdir(os.path.join(cur, ".git")) or os.path.isdir(os.path.join(cur, "Lisp")):
return cur
parent = os.path.dirname(cur)
if parent == cur:
return None
cur = parent
def resolve_file(path, repo_root):
if os.path.isfile(path):
return os.path.abspath(path)
if repo_root:
candidate = os.path.join(repo_root, path)
if os.path.isfile(candidate):
return os.path.abspath(candidate)
return None
def collect_lsp_files(dirs):
paths = []
seen = set()
for d in dirs:
if not os.path.isdir(d):
continue
for root, _dirnames, filenames in os.walk(d):
for fn in filenames:
if fn.lower().endswith(".lsp"):
full = os.path.abspath(os.path.join(root, fn))
if full not in seen:
seen.add(full)
paths.append(full)
return paths
def dbg_file_name_for(method_name):
"""Leitet einen .dbg-Dateinamen aus einem Methodennamen ab, z.B.
"c:TEST_MUBEA" -> "test_mubea.dbg", "mubea:build-kreisel" -> "build-kreisel.dbg"."""
base = method_name.split(":")[-1]
return base.lower() + ".dbg"
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main(argv=None):
parser = argparse.ArgumentParser(
prog="dbg2lsp.py",
description="Instrumentiert AutoLISP-Funktionen mit ssg_dbg.lsp (dbgf/dbg/dbgreturn/dbgopen/dbgclose).",
)
parser.add_argument("file", help="Anker-.lsp-Datei (Verzeichnis wird immer mit durchsucht)")
parser.add_argument("--method", action="append", default=[],
metavar="NAME",
help="Funktionsname, der mit dbgf/dbg/dbgreturn instrumentiert wird. Mehrfach angebbar.")
parser.add_argument("--recursive", action="store_true",
help="Auch alle im Rumpf aufgerufenen projekteigenen Funktionen instrumentieren (rekursiv)")
parser.add_argument("--search-dir", action="append", default=[],
metavar="DIR",
help="Weiteres Verzeichnis nach .lsp-Dateien durchsuchen (fuer --recursive). "
"Default: Lisp/ und das Verzeichnis von FILE.")
parser.add_argument("--max-depth", type=int, default=3,
help="Maximale Rekursionstiefe fuer --recursive (Default: 3)")
parser.add_argument("--exclude", action="append", default=[],
metavar="NAME",
help="Funktionsname von --recursive ausschliessen (mehrfach angebbar)")
parser.add_argument("--add-open", nargs=2, action="append", default=[],
metavar=("DATEI", "METHODE"),
help="(dbgopen ...)/(dbgclose) in METHODE (aus DATEI) einfuegen. Mehrfach angebbar.")
parser.add_argument("--dbg-file", default=None,
help="Dateiname fuer (dbgopen ...) (Default: aus dem --add-open-Methodennamen abgeleitet, z.B. test_mubea.dbg)")
parser.add_argument("--dbg-envvar", default=DEFAULT_DBG_ENVVAR,
help='Umgebungsvariable fuer (dbgopen ...) (Default: "%s")' % DEFAULT_DBG_ENVVAR)
parser.add_argument("--force", action="store_true",
help="Auch instrumentieren, wenn bereits (dbgf ...)/(dbgopen ...) vorhanden zu sein scheint")
parser.add_argument("--dry-run", action="store_true",
help="Nur ein Diff anzeigen, keine Dateien schreiben")
args = parser.parse_args(argv)
repo_root = find_repo_root(args.file) or find_repo_root(".")
main_file = resolve_file(args.file, repo_root)
if main_file is None:
print("FEHLER: Datei nicht gefunden: %s" % args.file, file=sys.stderr)
return 1
search_dirs = [os.path.dirname(main_file)]
if repo_root:
search_dirs.append(os.path.join(repo_root, "Lisp"))
for d in args.search_dir:
resolved = d if os.path.isdir(d) else os.path.join(repo_root or ".", d)
search_dirs.append(resolved)
for f, _m in args.add_open:
resolved = resolve_file(f, repo_root)
if resolved:
search_dirs.append(os.path.dirname(resolved))
lsp_files = set(collect_lsp_files(search_dirs))
lsp_files.add(main_file)
lsp_files = sorted(lsp_files)
file_texts, index = build_defun_index(lsp_files)
if not args.method and not args.add_open:
print("FEHLER: mindestens --method oder --add-open angeben.", file=sys.stderr)
return 1
edits = EditSet()
exclude = {n.lower() for n in args.exclude}
touched_any = False
for method_name in args.method:
defun = index.get(method_name.lower())
if defun is None:
print("FEHLER: Funktion '%s' nicht gefunden (durchsucht: %s)" %
(method_name, ", ".join(lsp_files)), file=sys.stderr)
continue
print("== --method %s ==" % method_name)
if args.recursive:
instrument_recursive(defun, index, file_texts, edits,
max_depth=args.max_depth, exclude=exclude,
force=args.force, log=print)
else:
instrument_method(defun, file_texts, edits, force=args.force, log=print)
touched_any = True
for file_arg, method_name in args.add_open:
resolved = resolve_file(file_arg, repo_root)
if resolved is None:
print("FEHLER: Datei nicht gefunden: %s" % file_arg, file=sys.stderr)
continue
# Falls die Datei noch nicht im Index ist (z.B. ausserhalb der
# Search-Dirs), jetzt einlesen und mit aufnehmen.
if resolved not in file_texts:
with open(resolved, "r", encoding="utf-8") as f:
file_texts[resolved] = f.read()
for d in find_defuns_in_text(file_texts[resolved], resolved):
index.setdefault(d.name.lower(), d)
defun = index.get(method_name.lower())
if defun is None or defun.path != resolved:
# ggf. gleichnamige Funktion aus anderer Datei ignorieren -
# explizit in der genannten Datei suchen.
local_defuns = find_defuns_in_text(file_texts[resolved], resolved)
defun = next((d for d in local_defuns if d.name.lower() == method_name.lower()), None)
if defun is None:
print("FEHLER: Funktion '%s' nicht in %s gefunden" % (method_name, file_arg), file=sys.stderr)
continue
print("== --add-open %s %s ==" % (file_arg, method_name))
dbg_file = args.dbg_file or dbg_file_name_for(method_name)
instrument_add_open(defun, file_texts, edits, dbg_file=dbg_file, dbg_envvar=args.dbg_envvar,
force=args.force, log=print)
touched_any = True
if not touched_any:
return 1
changed = edits.apply(file_texts)
if not changed:
print("Keine Aenderungen vorgenommen.")
return 0
for path, new_text in changed.items():
old_text = file_texts[path]
if args.dry_run:
rel = os.path.relpath(path, repo_root) if repo_root else path
diff = difflib.unified_diff(
old_text.splitlines(keepends=True),
new_text.splitlines(keepends=True),
fromfile=rel, tofile=rel,
)
sys.stdout.writelines(diff)
else:
with open(path, "w", encoding="utf-8", newline="") as f:
f.write(new_text)
print("Geschrieben: %s" % path)
return 0
if __name__ == "__main__":
sys.exit(main())