274 lines
9.3 KiB
Python
274 lines
9.3 KiB
Python
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() |