voiceforge / backend /tests /quality /check_syntax.py
lordofgaming
Initial VoiceForge deployment (clean)
673435a
"""
VoiceForge Syntax & Import Checker
-----------------------------------
Validates all Python files for:
- Syntax errors (AST parsing)
- Circular import detection
- Missing __init__.py files
- Undefined imports
"""
import os
import ast
import sys
from pathlib import Path
from collections import defaultdict
def check_syntax(file_path: Path) -> tuple[bool, str]:
"""Check if a Python file has valid syntax"""
try:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
source = f.read()
ast.parse(source)
return True, ""
except SyntaxError as e:
return False, f"Line {e.lineno}: {e.msg}"
def check_init_files(root_dir: Path) -> list[Path]:
"""Find directories missing __init__.py"""
missing = []
for dir_path in root_dir.rglob("*"):
if dir_path.is_dir() and '__pycache__' not in str(dir_path):
py_files = list(dir_path.glob("*.py"))
init_file = dir_path / "__init__.py"
if py_files and not init_file.exists():
missing.append(dir_path)
return missing
def build_import_graph(root_dir: Path) -> dict[str, list[str]]:
"""Build a graph of module imports"""
graph = defaultdict(list)
for py_file in root_dir.rglob("*.py"):
if '__pycache__' in str(py_file):
continue
try:
with open(py_file, 'r', encoding='utf-8', errors='ignore') as f:
source = f.read()
tree = ast.parse(source)
module_name = py_file.stem
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module:
if node.module.startswith('app.'):
graph[module_name].append(node.module.split('.')[-1])
except Exception:
pass
return graph
def detect_circular_imports(graph: dict) -> list[tuple]:
"""Detect circular imports in the dependency graph"""
cycles = []
visited = set()
rec_stack = set()
def dfs(node, path):
visited.add(node)
rec_stack.add(node)
for neighbor in graph.get(node, []):
if neighbor not in visited:
if dfs(neighbor, path + [neighbor]):
return True
elif neighbor in rec_stack:
cycle = path[path.index(neighbor):] + [neighbor]
cycles.append(tuple(cycle))
return True
rec_stack.remove(node)
return False
for node in graph:
if node not in visited:
dfs(node, [node])
return cycles
def run_checks(root_dir: str = "app"):
"""Run all syntax and import checks"""
print("=" * 60)
print("🔧 VoiceForge Syntax & Import Checker")
print("=" * 60)
root = Path(root_dir)
if not root.exists():
print(f"❌ Directory not found: {root_dir}")
sys.exit(1)
all_files = [f for f in root.rglob("*.py") if '__pycache__' not in str(f)]
print(f"\n📁 Checking {len(all_files)} Python files...\n")
syntax_errors = []
# Check syntax
print("1️⃣ SYNTAX CHECK")
print("-" * 40)
for py_file in all_files:
valid, error = check_syntax(py_file)
if not valid:
syntax_errors.append((py_file.relative_to(root), error))
print(f" ❌ {py_file.relative_to(root)}: {error}")
if not syntax_errors:
print(" ✅ All files have valid syntax!")
# Check __init__.py
print("\n2️⃣ MISSING __init__.py")
print("-" * 40)
missing_inits = check_init_files(root)
if missing_inits:
for dir_path in missing_inits:
print(f" ⚠️ {dir_path.relative_to(root)}")
else:
print(" ✅ All packages have __init__.py!")
# Check circular imports
print("\n3️⃣ CIRCULAR IMPORT DETECTION")
print("-" * 40)
graph = build_import_graph(root)
cycles = detect_circular_imports(graph)
if cycles:
for cycle in cycles[:5]:
print(f" ⚠️ Cycle: {' → '.join(cycle)}")
else:
print(" ✅ No circular imports detected!")
print("\n" + "=" * 60)
if syntax_errors:
print("❌ Syntax Check: FAILED")
return 1
else:
print("✅ Syntax Check: PASSED")
return 0
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Check VoiceForge syntax and imports")
parser.add_argument("--path", default="app", help="Root directory to check")
args = parser.parse_args()
sys.exit(run_checks(args.path))