236 lines
9.0 KiB
Python
Executable File
236 lines
9.0 KiB
Python
Executable File
#!/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() |