diff --git a/scripts/audit_indentation.py b/scripts/audit_indentation.py new file mode 100644 index 00000000..f91bbbc8 --- /dev/null +++ b/scripts/audit_indentation.py @@ -0,0 +1,120 @@ +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() \ No newline at end of file diff --git a/scripts/correct_indentation.py b/scripts/correct_indentation.py new file mode 100644 index 00000000..fc9f1d7d --- /dev/null +++ b/scripts/correct_indentation.py @@ -0,0 +1,167 @@ +import ast +import sys +from pathlib import Path + +ROOT_DIR = Path(__file__).parent.parent + +class IndentationCorrector(ast.NodeVisitor): + def __init__(self, source_lines: list[str]): + self.source_lines = source_lines + self.new_lines: list[str] = [] + self._indentation_stack: list[int] = [0] + + def visit_Module(self, node: ast.Module): + self._process_lines(0, 0) + 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 - 1 + actual_indent = self._get_indent(line_no) + expected_indent = depth + self._process_lines(line_no, expected_indent) + self._indentation_stack.append(depth + 1) + for child in node.body: + self._walk_node(child, depth + 1) + self._indentation_stack.pop() + elif isinstance(node, ast.If): + line_no = node.lineno - 1 + self._process_lines(line_no, depth) + 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 - 1 + self._process_lines(line_no, depth) + 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) + 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] + stripped = line.lstrip() + return len(line) - len(stripped) + + def _process_lines(self, end_line: int, expected_indent: int): + pass + +def correct_file(filepath: Path) -> tuple[bool, 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)) + corrector = IndentationCorrector(source_lines) + corrector.visit(tree) + return False, "Not implemented yet" + except Exception as e: + return False, str(e) + +def fix_indentation_simple(filepath: Path, base_indent: int = 4, target_indent: int = 1) -> tuple[bool, str]: + try: + with open(filepath, "r", encoding="utf-8", newline="") as f: + lines = f.readlines() + + new_lines = [] + changed = False + for line in lines: + stripped = line.lstrip() + if not stripped: + new_lines.append(line) + continue + + leading = len(line) - len(stripped) + if leading > 0: + old_level = leading // base_indent + new_leading = old_level * target_indent + if leading != new_leading: + new_lines.append(" " * new_leading + stripped + "\n" if not line.endswith("\n") else " " * new_leading + stripped) + changed = True + else: + new_lines.append(line) + else: + new_lines.append(line) + + if changed: + with open(filepath, "w", encoding="utf-8", newline="") as f: + f.writelines(new_lines) + return True, "Fixed" + + return False, "No changes needed" + except Exception as e: + return False, str(e) + +def main(): + if len(sys.argv) > 2: + filepath = Path(sys.argv[2]) + base_indent = int(sys.argv[3]) if len(sys.argv) > 3 else 4 + target_indent = int(sys.argv[4]) if len(sys.argv) > 4 else 1 + changed, msg = fix_indentation_simple(filepath, base_indent, target_indent) + print(f"{filepath}: {msg}") + return + + files_to_fix = [ + ("src/fuzzy_anchor.py", 4, 1), + ("src/patch_modal.py", 2, 1), + ("scripts/extract_symbols.py", 4, 1), + ("scripts/tasks/download_fonts.py", 4, 1), + ] + + test_files = [ + ("tests/test_arch_boundary_phase1.py", 2, 1), + ("tests/test_arch_boundary_phase2.py", 2, 1), + ("tests/test_arch_boundary_phase3.py", 2, 1), + ("tests/test_external_editor.py", 4, 1), + ("tests/test_headless_service.py", 4, 1), + ("tests/test_history_manager.py", 4, 1), + ("tests/test_fuzzy_anchor.py", 4, 1), + ("tests/test_gemini_cli_adapter.py", 4, 1), + ("tests/test_ai_client_cli.py", 4, 1), + ("tests/test_api_events.py", 4, 1), + ("tests/test_context_composition_decoupled.py", 4, 1), + ("tests/test_context_composition_phase3.py", 4, 1), + ("tests/test_context_composition_phase4.py", 4, 1), + ("tests/test_diff_viewer.py", 2, 1), + ("tests/test_discussion_takes_gui.py", 4, 1), + ("tests/test_external_mcp_hitl.py", 4, 1), + ("tests/test_gui_discussion_tabs.py", 4, 1), + ("tests/test_gui_stress_performance.py", 4, 1), + ("tests/test_gui_updates.py", 2, 1), + ("tests/test_hot_reloader.py", 4, 1), + ("tests/test_mma_dashboard_refresh.py", 4, 1), + ("tests/test_mma_node_editor.py", 4, 1), + ("tests/test_mma_orchestration_gui.py", 4, 1), + ("tests/test_py_struct_tools.py", 4, 1), + ("tests/test_thinking_persistence.py", 4, 1), + ("tests/test_tier4_interceptor.py", 2, 1), + ("tests/test_tiered_aggregation.py", 4, 1), + ("tests/test_visual_orchestration.py", 4, 1), + ] + + all_files = files_to_fix + test_files + + for rel_path, base, target in all_files: + filepath = ROOT_DIR / rel_path + if filepath.exists(): + changed, msg = fix_indentation_simple(filepath, base, target) + print(f"{rel_path}: {msg}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/fix_indent.py b/scripts/fix_indent.py new file mode 100644 index 00000000..ca8f69d7 --- /dev/null +++ b/scripts/fix_indent.py @@ -0,0 +1,274 @@ +import ast +import sys +from pathlib import Path + +ROOT_DIR = Path(__file__).parent.parent + +class IndentationFixer(ast.NodeVisitor): + def __init__(self, source_lines: list[str]): + self.source_lines = source_lines + self.result_lines: list[str] = [] + self._pending_lines: list[tuple[int, str]] = [] + self._current_depth = 0 + + def fix(self) -> list[str]: + self._process_pending(0) + self.visit_Module(ast.parse("".join(self.source_lines))) + return self.result_lines + + 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 _is_docstring_or_comment(self, line: str) -> bool: + stripped = line.lstrip() + if stripped.startswith('#'): + return True + if stripped.startswith('"""') or stripped.startswith("'''"): + return True + return False + + def _process_pending(self, target_depth: int): + while self._pending_lines: + line_no, line = self._pending_lines[0] + stripped = line.lstrip() + actual = len(line) - len(stripped) + expected = target_depth + + if actual == expected: + self.result_lines.append(line) + self._pending_lines.pop(0) + elif actual < expected: + break + elif actual > expected: + self.result_lines.append(" " * expected + stripped) + self._pending_lines.pop(0) + + def visit_Module(self, node: ast.Module): + for child in node.body: + self._walk_node(child) + self.generic_visit(node) + + def _walk_node(self, node: ast.AST): + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + lineno = node.lineno + for i, (old_no, old_line) in enumerate(self._pending_lines): + if old_no == lineno: + self._pending_lines.pop(i) + break + + actual = self._get_indent(lineno) + expected = self._current_depth + line = self.source_lines[lineno - 1] + stripped = line.lstrip() + + if actual != expected: + self.result_lines.append(" " * expected + stripped) + else: + self.result_lines.append(line) + + self._current_depth += 1 + for child in node.body: + self._walk_node(child) + self._current_depth -= 1 + + elif isinstance(node, ast.If): + lineno = node.lineno + line = self.source_lines[lineno - 1] + stripped = line.lstrip() + actual = self._get_indent(lineno) + expected = self._current_depth + + if actual != expected: + self.result_lines.append(" " * expected + stripped) + else: + self.result_lines.append(line) + + self._current_depth += 1 + for child in node.body: + self._walk_node(child) + self._current_depth -= 1 + + if node.orelse: + self._current_depth += 1 + self._walk_node(node.orelse) + self._current_depth -= 1 + + elif isinstance(node, (ast.For, ast.While, ast.With)): + lineno = node.lineno + line = self.source_lines[lineno - 1] + stripped = line.lstrip() + actual = self._get_indent(lineno) + expected = self._current_depth + + if actual != expected: + self.result_lines.append(" " * expected + stripped) + else: + self.result_lines.append(line) + + self._current_depth += 1 + for child in node.body: + self._walk_node(child) + self._current_depth -= 1 + + if isinstance(node, ast.For) and node.orelse: + self._current_depth += 1 + for child in node.orelse: + self._walk_node(child) + self._current_depth -= 1 + + elif isinstance(node, ast.Try): + for child in node.body: + self._walk_node(child) + for handler in node.handlers: + self._current_depth += 1 + for child in handler.body: + self._walk_node(child) + self._current_depth -= 1 + if node.orelse: + self._current_depth += 1 + for child in node.orelse: + self._walk_node(child) + self._current_depth -= 1 + if node.finalbody: + self._current_depth += 1 + for child in node.finalbody: + self._walk_node(child) + self._current_depth -= 1 + + else: + self.generic_visit(node) + +def fix_file_ast(filepath: Path) -> tuple[bool, 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)) + + fixer = IndentationFixer(source_lines) + new_lines = fixer.fix() + + new_source = "\n".join(new_lines) + if new_source == source: + return False, "No changes" + + with open(filepath, "w", encoding="utf-8", newline="") as f: + f.write(new_source) + + ast.parse(new_source) + return True, "Fixed" + + except SyntaxError as e: + return False, f"Syntax error: {e}" + except Exception as e: + return False, str(e) + +def fix_file_simple(filepath: Path, base_indent: int = 4) -> tuple[bool, str]: + try: + with open(filepath, "r", encoding="utf-8", newline="") as f: + lines = f.readlines() + + in_docstring = False + new_lines = [] + changed = False + + for line in lines: + stripped = line.lstrip() + if not stripped: + new_lines.append(line) + continue + + if not in_docstring: + if stripped.startswith('#'): + new_lines.append(line) + continue + if '"""' in stripped or "'''" in stripped: + triple_pos = max(stripped.find('"""') if '"""' in stripped else 999, + stripped.find("'''") if "'''" in stripped else 999) + if triple_pos == 0: + in_docstring = True + new_lines.append(line) + continue + + if in_docstring: + if '"""' in stripped or "'''" in stripped: + in_docstring = False + new_lines.append(line) + continue + + leading = len(line) - len(stripped) + if leading > 0: + level = leading // base_indent + new_leading = level + if leading != new_leading: + new_lines.append(" " * new_leading + stripped + ("\n" if not line.endswith("\n") else "")) + changed = True + else: + new_lines.append(line) + else: + new_lines.append(line) + + if changed: + with open(filepath, "w", encoding="utf-8", newline="") as f: + f.writelines(new_lines) + return True, "Fixed" + return False, "No changes needed" + + except Exception as e: + return False, str(e) + +def main(): + if len(sys.argv) > 1: + filepath = Path(sys.argv[1]) + changed, msg = fix_file_simple(filepath) + print(f"{filepath}: {msg}") + return + + files_to_fix = [ + ("src/fuzzy_anchor.py", 4), + ("src/patch_modal.py", 2), + ("scripts/extract_symbols.py", 4), + ("scripts/tasks/download_fonts.py", 4), + ("tests/test_arch_boundary_phase1.py", 2), + ("tests/test_arch_boundary_phase2.py", 2), + ("tests/test_arch_boundary_phase3.py", 2), + ("tests/test_external_editor.py", 4), + ("tests/test_headless_service.py", 4), + ("tests/test_history_manager.py", 4), + ("tests/test_fuzzy_anchor.py", 4), + ("tests/test_gemini_cli_adapter.py", 4), + ("tests/test_ai_client_cli.py", 4), + ("tests/test_api_events.py", 4), + ("tests/test_context_composition_decoupled.py", 4), + ("tests/test_context_composition_phase3.py", 4), + ("tests/test_context_composition_phase4.py", 4), + ("tests/test_diff_viewer.py", 2), + ("tests/test_discussion_takes_gui.py", 4), + ("tests/test_external_mcp_hitl.py", 4), + ("tests/test_gui_discussion_tabs.py", 4), + ("tests/test_gui_stress_performance.py", 4), + ("tests/test_gui_updates.py", 2), + ("tests/test_hot_reloader.py", 4), + ("tests/test_mma_dashboard_refresh.py", 4), + ("tests/test_mma_node_editor.py", 4), + ("tests/test_mma_orchestration_gui.py", 4), + ("tests/test_py_struct_tools.py", 4), + ("tests/test_thinking_persistence.py", 4), + ("tests/test_tier4_interceptor.py", 2), + ("tests/test_tiered_aggregation.py", 4), + ("tests/test_visual_orchestration.py", 4), + ] + + for rel_path, base_indent in files_to_fix: + filepath = ROOT_DIR / rel_path + if filepath.exists(): + changed, msg = fix_file_simple(filepath, base_indent) + print(f"{rel_path}: {msg}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/fix_indent_ast.py b/scripts/fix_indent_ast.py new file mode 100644 index 00000000..ccb2331f --- /dev/null +++ b/scripts/fix_indent_ast.py @@ -0,0 +1,104 @@ +import ast +import sys +from pathlib import Path + +ROOT_DIR = Path(__file__).parent.parent + +class PythonIndentationFixer(ast.NodeVisitor): + def __init__(self, source_lines: list[str]): + self.source_lines = source_lines + self.result: list[str] = [] + self._depth = 0 + self._pending: list[tuple[int, str]] = [] + + def fix(self) -> str: + tree = ast.parse("".join(self.source_lines)) + self._walk_module(tree) + return "\n".join(self.result) + + def _get_line(self, lineno: int) -> str: + if 0 < lineno <= len(self.source_lines): + return self.source_lines[lineno - 1] + return "" + + def _walk_module(self, node: ast.Module): + for item in node.body: + self._process_item(item, 0) + self.generic_visit(node) + + def _process_item(self, node: ast.AST, base_depth: int): + lineno = node.lineno + line = self._get_line(lineno) + stripped = line.lstrip() + leading = len(line) - len(stripped) + expected = base_depth + + if leading != expected: + self.result.append(" " * expected + stripped) + else: + self.result.append(line.rstrip("\n")) + + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + body_depth = base_depth + 1 + for child in node.body: + self._process_item(child, body_depth) + elif isinstance(node, (ast.If, ast.For, ast.While, ast.With, ast.Try)): + body_depth = base_depth + 1 + for child in node.body: + self._process_item(child, body_depth) + if isinstance(node, ast.If) and node.orelse: + self._process_item(node.orelse, base_depth + 1) + if isinstance(node, ast.For) and node.orelse: + for child in node.orelse: + self._process_item(child, body_depth) + elif isinstance(node, ast.Try): + for handler in node.handlers: + for child in handler.body: + self._process_item(child, body_depth) + if node.orelse: + for child in node.orelse: + self._process_item(child, body_depth) + if node.finalbody: + for child in node.finalbody: + self._process_item(child, body_depth) + else: + self.generic_visit(node) + + def generic_visit(self, node: ast.AST): + pass + +def fix_file_ast(filepath: Path) -> tuple[bool, str]: + try: + with open(filepath, "r", encoding="utf-8", newline="") as f: + source = f.read() + + lines = source.splitlines() + fixer = PythonIndentationFixer(lines) + new_source = fixer.fix() + + ast.parse(new_source) + + if new_source == source: + return False, "No changes needed" + + with open(filepath, "w", encoding="utf-8", newline="") as f: + f.write(new_source) + + return True, "Fixed" + except SyntaxError as e: + return False, f"SyntaxError: {e}" + except Exception as e: + return False, str(e) + +def main(): + if len(sys.argv) > 1: + filepath = Path(sys.argv[1]) + changed, msg = fix_file_ast(filepath) + print(f"{filepath}: {msg}") + return + + print("AST-based Python indentation fixer") + print("Usage: python fix_indent_ast.py ") + +if __name__ == "__main__": + main() \ No newline at end of file