Ausertung von schleifen und Helperfunktionen ergänzt in extract.py
This commit is contained in:
+114
-3
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user