Ausertung von schleifen und Helperfunktionen ergänzt in extract.py

This commit is contained in:
2026-08-09 20:56:24 +02:00
parent 5f11c77ddf
commit 5d227d2f61
44 changed files with 1713 additions and 5 deletions
+114 -3
View File
@@ -109,6 +109,15 @@ class RegisterExtractor(ast.NodeVisitor):
# Klasse, in der wir uns gerade befinden (für cls._table etc.)
self.current_class = None
# Name -> FunctionDef aller Methoden der aktuellen Klasse, um
# Helper-Aufrufe aus __register__ heraus aufzulösen.
self.class_methods = {}
# Aufruf-Kette der Helper-Methoden, in die wir gerade rekursiv
# abgestiegen sind (für Zyklenschutz und die "via"-Provenienz
# der gefundenen Operationen).
self.call_chain = []
# -- Modul-/Klassenebene ------------------------------------------------
def visit_Module(self, node):
@@ -124,9 +133,18 @@ class RegisterExtractor(ast.NodeVisitor):
self.generic_visit(node)
def visit_ClassDef(self, node):
# Alle Methoden der Klasse vormerken, damit __register__-Aufrufe an
# eigene Helper-Methoden (z. B. `cls._migrate_x(module_name)`)
# aufgelöst werden können.
methods = {
item.name: item
for item in node.body
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef))
}
for item in node.body:
if isinstance(item, ast.FunctionDef) and item.name == "__register__":
self.extract_register(node.name, item)
self.extract_register(node.name, item, methods)
self.generic_visit(node)
@@ -147,13 +165,18 @@ class RegisterExtractor(ast.NodeVisitor):
# -- __register__ selbst -------------------------------------------------
def extract_register(self, cls, func):
def extract_register(self, cls, func, methods):
"""Analysiert eine einzelne __register__-Methode."""
operations = []
self.table_vars = {}
self.alias_vars = {}
self.current_class = cls
# Für die Auflösung von Helper-Aufrufen (cls._migrate_x(...)) und
# als Zyklenschutz während der Rekursion in solche Helper.
self.class_methods = methods
self.call_chain = []
model_name = self.model_names.get(cls)
if model_name:
self.table_vars["cls"] = model_name
@@ -338,6 +361,13 @@ class RegisterExtractor(ast.NodeVisitor):
`phase` wechselt von "before_super" auf "after_super", sobald der
super().__register__(...)-Aufruf gefunden wird. `condition` trägt
den Text einer umschließenden if-Bedingung mit, falls vorhanden.
Steigt dabei in ALLE geschachtelten Blöcke ab (if/for/while/with/
try/except/finally), nicht nur in if. Aufrufe innerhalb einer
for-Schleife, eines with-Blocks oder eines try/except wurden vorher
stillschweigend übersprungen, weil nur ast.If speziell behandelt
wurde und alle anderen Compound-Statements kein `.value`-Attribut
besitzen.
"""
for stmt in statements:
@@ -351,10 +381,38 @@ class RegisterExtractor(ast.NodeVisitor):
self.walk_statements(stmt.orelse, operations, phase, condition)
continue
for body in self._nested_bodies(stmt):
self.walk_statements(body, operations, phase, condition)
value = getattr(stmt, "value", None)
if value is not None:
self.walk_expression(value, operations, phase, condition)
@staticmethod
def _nested_bodies(stmt):
"""
Liefert alle Statement-Listen, die direkt in `stmt` geschachtelt
sind (außer bei ast.If, das walk_statements separat behandelt,
weil dort zusätzlich die Bedingung mitgeführt wird).
"""
bodies = []
if isinstance(stmt, (ast.For, ast.AsyncFor, ast.While)):
bodies.append(stmt.body)
bodies.append(stmt.orelse)
elif isinstance(stmt, (ast.With, ast.AsyncWith)):
bodies.append(stmt.body)
elif isinstance(stmt, ast.Try) or (
hasattr(ast, "TryStar") and isinstance(stmt, ast.TryStar)
):
bodies.append(stmt.body)
for handler in stmt.handlers:
bodies.append(handler.body)
bodies.append(stmt.orelse)
bodies.append(stmt.finalbody)
return bodies
def walk_expression(self, node, operations, phase, condition):
if node is None:
return
@@ -365,11 +423,64 @@ class RegisterExtractor(ast.NodeVisitor):
op["phase"] = phase
if condition:
op["condition"] = condition
if self.call_chain:
op["via"] = [dict(entry) for entry in self.call_chain]
operations.append(op)
else:
helper = self.resolve_helper_call(node)
if helper:
method_name, func_node = helper
self.walk_into_helper(
method_name, func_node, node.lineno, operations, phase, condition
)
for child in ast.iter_child_nodes(node):
self.walk_expression(child, operations, phase, condition)
def resolve_helper_call(self, node):
"""
Erkennt Aufrufe der Form `cls.<method>(...)` / `self.<method>(...)`
an eine Methode, die in derselben Klasse definiert ist (z. B. eine
ausgelagerte Migrationsmethode). Cross-Klassen-/Cross-Datei-Aufrufe
werden bewusst nicht aufgelöst.
"""
if not isinstance(node.func, ast.Attribute):
return None
receiver = node.func.value
if not (isinstance(receiver, ast.Name) and receiver.id in ("cls", "self")):
return None
method_name = node.func.attr
if method_name == "__register__":
return None
func_node = self.class_methods.get(method_name)
if func_node is None:
return None
return method_name, func_node
def walk_into_helper(self, method_name, func_node, call_line, operations, phase, condition):
"""
Steigt in eine Helper-Methode ab, als wäre ihr Körper an der
Aufrufstelle inline eingefügt. Mit Zyklenschutz gegen (indirekt)
rekursive Helper-Aufrufe.
"""
if any(entry["method"] == method_name for entry in self.call_chain):
return
self.call_chain.append({"method": method_name, "line": call_line})
# Tabellen-Variablen, die lokal in der Helper-Methode definiert
# werden (z. B. `table = cls.__table__()`), vorab einsammeln.
for stmt in func_node.body:
self.collect_table_assignment(stmt)
self.walk_statements(func_node.body, operations, phase, condition)
self.call_chain.pop()
def is_super_register(self, stmt):
"""True für `super(...).__register__(...)` als eigenständiges Statement."""
if not isinstance(stmt, ast.Expr):
@@ -619,4 +730,4 @@ def main():
if __name__ == "__main__":
main()
main()