import ast import sys from pathlib import Path ROOT_DIR = Path(__file__).parent.parent class IndentationAnalyzer(ast.NodeVisitor): def __init__(self, source_lines: list[str]): self.source_lines = source_lines self.violations: list[tuple[int, int, int, str]] = [] def visit_Module(self, node: ast.Module): for child in node.body: self._walk_node(child, 0) self.generic_visit(node) def _walk_node(self, node: ast.AST, depth: int): if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): line_no = node.lineno expected_indent = depth actual_indent = self._get_indent(line_no) if actual_indent != expected_indent: self.violations.append((line_no, actual_indent, expected_indent, f"{node.__class__.__name__} {node.name}")) body_depth = depth + 1 for child in node.body: self._walk_node(child, body_depth) elif isinstance(node, ast.If): line_no = node.lineno expected_indent = depth actual_indent = self._get_indent(line_no) if actual_indent != expected_indent: self.violations.append((line_no, actual_indent, expected_indent, "if statement")) for child in node.body: self._walk_node(child, depth + 1) if node.orelse: self._walk_node(node.orelse, depth + 1) elif isinstance(node, (ast.For, ast.While, ast.With)): line_no = node.lineno expected_indent = depth actual_indent = self._get_indent(line_no) if actual_indent != expected_indent: self.violations.append((line_no, actual_indent, expected_indent, node.__class__.__name__)) for child in node.body: self._walk_node(child, depth + 1) if isinstance(node, ast.For) and node.orelse: self._walk_node(node.orelse, depth + 1) elif isinstance(node, ast.Try): for child in node.body: self._walk_node(child, depth + 1) for handler in node.handlers: self._walk_node(handler, depth + 1) if node.orelse: self._walk_node(node.orelse, depth + 1) if node.finalbody: self._walk_node(node.finalbody, depth + 1) elif isinstance(node, (ast.Assign, ast.Expr, ast.Return, ast.Pass, ast.Raise, ast.Assert)): pass else: self.generic_visit(node) def _get_indent(self, lineno: int) -> int: if lineno <= 0 or lineno > len(self.source_lines): return 0 line = self.source_lines[lineno - 1] stripped = line.lstrip() return len(line) - len(stripped) def audit_file(filepath: Path) -> list[tuple[int, int, int, str]]: try: with open(filepath, "r", encoding="utf-8", newline="") as f: source = f.read() source_lines = source.splitlines() tree = ast.parse(source, filename=str(filepath)) analyzer = IndentationAnalyzer(source_lines) analyzer.visit(tree) return analyzer.violations except SyntaxError: return [] except Exception: return [] def main(): dirs = ["src", "tests", "scripts", "conductor"] total_files = 0 files_with_violations = 0 total_violations = 0 results: dict[str, list[tuple]] = {} for dir_name in dirs: dir_path = ROOT_DIR / dir_name if not dir_path.exists(): continue py_files = list(dir_path.rglob("*.py")) for py_file in sorted(py_files): total_files += 1 violations = audit_file(py_file) if violations: files_with_violations += 1 total_violations += len(violations) rel = str(py_file.relative_to(dir_path)) results[f"{dir_name}/{rel}"] = violations print(f"Total files scanned: {total_files}") print(f"Files with violations: {files_with_violations}") print(f"Total violations: {total_violations}") print() for path, violations in sorted(results.items()): print(f"{path}: {len(violations)} violations") for line_no, actual, expected, desc in violations[:5]: print(f" Line {line_no}: actual={actual}, expected={expected} for {desc}") if len(violations) > 5: print(f" ... and {len(violations) - 5} more") if __name__ == "__main__": main()