#!/usr/bin/env python3 """ Fenrir Syntax Validation Tool Validates Python syntax across the entire Fenrir codebase without writing cache files. Designed to catch syntax errors before packaging or releases. Usage: python3 tools/validate_syntax.py # Validate all Python files python3 tools/validate_syntax.py --fix # Fix common issues automatically python3 tools/validate_syntax.py --check-only # Exit with non-zero if errors found """ import ast import os import sys import argparse import tempfile from pathlib import Path class SyntaxValidator: def __init__(self): self.errors = [] self.warnings = [] self.fixed = [] def validate_file(self, filepath): """Validate syntax of a single Python file.""" try: with open(filepath, 'r', encoding='utf-8') as f: content = f.read() # Parse with AST (catches syntax errors) ast.parse(content, filename=str(filepath)) return True, content except SyntaxError as e: error_msg = f"{filepath}:{e.lineno}: {e.msg}" self.errors.append((filepath, e, content)) return False, content except UnicodeDecodeError as e: error_msg = f"{filepath}: Unicode decode error: {e}" self.errors.append((filepath, e, None)) return False, None except Exception as e: error_msg = f"{filepath}: Unexpected error: {e}" self.errors.append((filepath, e, None)) return False, None def fix_common_issues(self, filepath, content): """Attempt to fix common syntax issues automatically.""" if not content: return False, content original_content = content fixed_issues = [] # Fix unterminated f-strings (the main issue from the email) lines = content.split('\n') modified = False for i, line in enumerate(lines): # Look for f-strings that span multiple lines incorrectly if 'f"' in line and line.count('"') % 2 == 1: # Check if this looks like a broken multi-line f-string indent = len(line) - len(line.lstrip()) # Look ahead for continuation j = i + 1 while j < len(lines) and lines[j].strip(): next_line = lines[j] next_indent = len(next_line) - len(next_line.lstrip()) # If next line is indented more and has closing brace/quote if (next_indent > indent and ('"' in next_line or '}' in next_line)): # Try to fix by joining lines properly combined_line = line.rstrip() continuation = next_line.strip() if continuation.startswith(('"', '}', 'str(e)', 'self.', 'fenrirVersion.')): # Fix common patterns if 'str(e)}' in continuation: fixed_line = line.replace('f"', 'f"').rstrip() + '{' + continuation.replace('"', '') + '}' elif continuation.startswith('"'): fixed_line = line + continuation else: fixed_line = line.rstrip() + continuation lines[i] = fixed_line lines[j] = '' # Remove the continuation line modified = True fixed_issues.append(f"Line {i+1}: Fixed multi-line f-string") break j += 1 if modified: content = '\n'.join(lines) # Clean up empty lines that were created content = '\n'.join(line for line in content.split('\n') if line.strip() or not line) # Verify the fix worked try: ast.parse(content, filename=str(filepath)) self.fixed.append((filepath, fixed_issues)) return True, content except SyntaxError: # Fix didn't work, return original return False, original_content return False, content def scan_directory(self, directory, fix_mode=False): """Scan directory for Python files and validate them.""" python_files = [] # Find all Python files for root, dirs, files in os.walk(directory): # Skip cache and build directories dirs[:] = [d for d in dirs if not d.startswith(('__pycache__', '.git', 'build', 'dist'))] for file in files: if file.endswith('.py'): python_files.append(Path(root) / file) print(f"Validating {len(python_files)} Python files...") valid_count = 0 fixed_count = 0 for filepath in sorted(python_files): is_valid, content = self.validate_file(filepath) if is_valid: valid_count += 1 print(f"āœ“ {filepath}") else: print(f"āœ— {filepath}") if fix_mode and content: # Try to fix the file was_fixed, fixed_content = self.fix_common_issues(filepath, content) if was_fixed: # Write the fixed content back with open(filepath, 'w', encoding='utf-8') as f: f.write(fixed_content) print(f" → Fixed automatically") fixed_count += 1 # Re-validate is_valid_now, _ = self.validate_file(filepath) if is_valid_now: valid_count += 1 return valid_count, len(python_files), fixed_count def print_summary(self, valid_count, total_count, fixed_count=0): """Print validation summary.""" print(f"\n{'='*60}") print(f"SYNTAX VALIDATION SUMMARY") print(f"{'='*60}") print(f"Valid files: {valid_count}/{total_count}") print(f"Invalid files: {total_count - valid_count}") if fixed_count > 0: print(f"Auto-fixed: {fixed_count}") if self.errors: print(f"\nERRORS ({len(self.errors)}):") for filepath, error, _ in self.errors: if isinstance(error, SyntaxError): print(f" {filepath}:{error.lineno}: {error.msg}") else: print(f" {filepath}: {error}") if self.fixed: print(f"\nAUTO-FIXES APPLIED ({len(self.fixed)}):") for filepath, fixes in self.fixed: print(f" {filepath}:") for fix in fixes: print(f" - {fix}") success_rate = (valid_count / total_count) * 100 if total_count > 0 else 0 print(f"\nSuccess rate: {success_rate:.1f}%") return len(self.errors) == 0 def main(): parser = argparse.ArgumentParser(description='Validate Python syntax in Fenrir codebase') parser.add_argument('--fix', action='store_true', help='Attempt to fix common syntax issues automatically') parser.add_argument('--check-only', action='store_true', help='Exit with non-zero code if syntax errors found') parser.add_argument('--directory', default='src/fenrirscreenreader', help='Directory to scan (default: src/fenrirscreenreader)') args = parser.parse_args() # Find project root script_dir = Path(__file__).parent project_root = script_dir.parent target_dir = project_root / args.directory if not target_dir.exists(): print(f"Error: Directory {target_dir} does not exist") sys.exit(1) print(f"Fenrir Syntax Validator") print(f"Target directory: {target_dir}") print(f"Fix mode: {'ON' if args.fix else 'OFF'}") print() validator = SyntaxValidator() valid_count, total_count, fixed_count = validator.scan_directory(target_dir, fix_mode=args.fix) all_valid = validator.print_summary(valid_count, total_count, fixed_count) if args.check_only and not all_valid: print(f"\nValidation failed: {total_count - valid_count} files have syntax errors") sys.exit(1) elif not all_valid: print(f"\nWarning: {total_count - valid_count} files have syntax errors") if not args.fix: print("Run with --fix to attempt automatic fixes") sys.exit(1) else: print(f"\nāœ“ All {total_count} files have valid syntax") sys.exit(0) if __name__ == '__main__': main()