Some syntax errors fixed. Syntax checking added. Release checklist created.
This commit is contained in:
236
tools/validate_syntax.py
Executable file
236
tools/validate_syntax.py
Executable file
@ -0,0 +1,236 @@
|
||||
#!/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()
|
Reference in New Issue
Block a user