#!/usr/bin/env python3 # Copyright (c) 2025 OPEN CASCADE SAS # # This file is part of Open CASCADE Technology software library. # # This library is free software; you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License version 2.1 as published # by the Free Software Foundation, with special exception defined in the file # OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT # distribution for complete text of the license and disclaimer of any warranty. # # Alternatively, this file may be used under the terms of Open CASCADE # commercial license or contractual agreement. """ Migration Verification Script for OCCT 8.0.0 This script scans the codebase to find remaining legacy patterns that should have been migrated. Useful for: 1. Verifying migration completeness 2. Finding patterns that were missed 3. Identifying files that may need manual review Usage: python3 verify_migration.py [options] Options: --verbose Show all matches --output=FILE Write report to file --json Output as JSON """ import os import re import sys import json import argparse from pathlib import Path from typing import List, Dict, Tuple from dataclasses import dataclass, field, asdict from collections import defaultdict @dataclass class PatternMatch: """A single pattern match.""" file: str line_number: int line_content: str pattern: str @dataclass class VerificationReport: """Complete verification report.""" total_files_scanned: int = 0 patterns_checked: List[str] = field(default_factory=list) matches_by_pattern: Dict[str, List[PatternMatch]] = field(default_factory=lambda: defaultdict(list)) summary: Dict[str, int] = field(default_factory=dict) def add_match(self, pattern: str, match: PatternMatch): self.matches_by_pattern[pattern].append(match) def compute_summary(self): for pattern, matches in self.matches_by_pattern.items(): self.summary[pattern] = len(matches) class MigrationVerifier: """Verifies that migration patterns have been applied.""" # Files to skip (core infrastructure where patterns are defined) SKIP_FILES = { 'Standard_Handle.hxx', 'Standard_TypeDef.hxx', 'Standard_Transient.hxx', 'Standard_Transient.cxx', 'Standard_Type.hxx', 'Standard_Type.cxx', 'Standard_Macro.hxx', 'Standard_Std.hxx', 'Standard_Boolean.hxx', 'Standard_Integer.hxx', 'Standard_Real.hxx', 'Standard_Real.cxx', 'Standard_ShortReal.hxx', 'Standard_Character.hxx', 'Standard_Byte.hxx', 'Standard_PrimitiveTypes.hxx', 'occ.hxx', # Migration scripts themselves 'occt_modernize.py', 'migrate_handles.py', 'migrate_standard_types.py', 'migrate_macros.py', 'verify_migration.py', } # Patterns to check for (should NOT be present after migration) LEGACY_PATTERNS = { # Handle patterns 'Handle(Class)': r'\bHandle\([A-Z][a-zA-Z0-9_]*\)', '::DownCast': r'::DownCast\(', # Standard types 'Standard_Boolean': r'\bStandard_Boolean\b', 'Standard_Integer': r'\bStandard_Integer\b', 'Standard_Real': r'\bStandard_Real\b', 'Standard_ShortReal': r'\bStandard_ShortReal\b', 'Standard_Character': r'\bStandard_Character\b', 'Standard_Byte': r'\bStandard_Byte\b', 'Standard_Address': r'\bStandard_Address\b', # Standard values 'Standard_True': r'\bStandard_True\b', 'Standard_False': r'\bStandard_False\b', # Legacy includes '#include ': r'#include\s*[<"]Standard_Boolean\.hxx[>"]', '#include ': r'#include\s*[<"]Standard_Integer\.hxx[>"]', '#include ': r'#include\s*[<"]Standard_Real\.hxx[>"]', # Legacy macros (should be replaced with C++ keywords) 'Standard_OVERRIDE': r'\bStandard_OVERRIDE\b', 'Standard_NODISCARD': r'\bStandard_NODISCARD\b', 'Standard_FALLTHROUGH': r'\bStandard_FALLTHROUGH\b', 'Standard_Noexcept': r'\bStandard_Noexcept\b', 'Standard_DELETE': r'\bStandard_DELETE\b', 'Standard_THREADLOCAL': r'\bStandard_THREADLOCAL\b', 'Standard_ATOMIC': r'\bStandard_ATOMIC\s*\(', 'Standard_UNUSED': r'\bStandard_UNUSED\b', # Redundant handle macros (should be removed) 'DEFINE_STANDARD_HANDLE': r'\bDEFINE_STANDARD_HANDLE\s*\(', } # New patterns that SHOULD be present after migration NEW_PATTERNS = { 'occ::handle<': r'occ::handle<[A-Z]', 'occ::down_cast<': r'occ::down_cast<[A-Z]', } def __init__(self, src_dir: str, verbose: bool = False): self.src_dir = Path(src_dir) self.verbose = verbose self.report = VerificationReport() def should_skip(self, file_path: Path) -> bool: """Check if file should be skipped.""" return file_path.name in self.SKIP_FILES def read_file(self, file_path: Path) -> List[str]: """Read file lines.""" try: with open(file_path, 'r', encoding='utf-8') as f: return f.readlines() except UnicodeDecodeError: with open(file_path, 'r', encoding='latin-1') as f: return f.readlines() def get_source_files(self) -> List[Path]: """Get all source files.""" files = [] for ext in ('*.hxx', '*.cxx', '*.lxx', '*.pxx'): files.extend(self.src_dir.rglob(ext)) return sorted(files) def scan_file(self, file_path: Path, patterns: Dict[str, str]) -> List[PatternMatch]: """Scan a file for patterns.""" matches = [] if self.should_skip(file_path): return matches try: lines = self.read_file(file_path) for line_num, line in enumerate(lines, 1): for pattern_name, regex in patterns.items(): if re.search(regex, line): matches.append(PatternMatch( file=str(file_path), line_number=line_num, line_content=line.strip()[:100], pattern=pattern_name )) except Exception as e: print(f"Error scanning {file_path}: {e}", file=sys.stderr) return matches def verify_legacy_patterns(self) -> VerificationReport: """Verify that legacy patterns have been removed.""" files = self.get_source_files() self.report.total_files_scanned = len(files) self.report.patterns_checked = list(self.LEGACY_PATTERNS.keys()) print(f"Scanning {len(files)} files for legacy patterns...") for i, file_path in enumerate(files, 1): if i % 500 == 0: print(f"Progress: {i}/{len(files)}") matches = self.scan_file(file_path, self.LEGACY_PATTERNS) for match in matches: self.report.add_match(match.pattern, match) self.report.compute_summary() return self.report def verify_new_patterns(self) -> Dict[str, int]: """Verify that new patterns are present (migration was applied).""" files = self.get_source_files() counts = {name: 0 for name in self.NEW_PATTERNS} print(f"Scanning {len(files)} files for new patterns...") for file_path in files: if self.should_skip(file_path): continue try: content = self.read_file(file_path) content_str = ''.join(content) for pattern_name, regex in self.NEW_PATTERNS.items(): matches = re.findall(regex, content_str) counts[pattern_name] += len(matches) except Exception: pass return counts def print_report(self): """Print the verification report.""" print("\n" + "=" * 70) print("OCCT Migration Verification Report") print("=" * 70) print(f"\nFiles scanned: {self.report.total_files_scanned}") print("\n" + "-" * 70) print("Legacy Patterns (should be ZERO after migration)") print("-" * 70) all_clean = True for pattern in sorted(self.report.summary.keys()): count = self.report.summary[pattern] status = "OK" if count == 0 else f"REMAINING: {count}" if count > 0: all_clean = False print(f" {pattern:40s}: {status}") if all_clean: print("\n All legacy patterns have been migrated!") else: print("\n Some legacy patterns remain. See details below.") # Show details for remaining patterns if self.verbose and not all_clean: print("\n" + "-" * 70) print("Detailed Matches (first 5 per pattern)") print("-" * 70) for pattern, matches in self.report.matches_by_pattern.items(): if matches: print(f"\n{pattern} ({len(matches)} matches):") for match in matches[:5]: print(f" {match.file}:{match.line_number}") print(f" {match.line_content}") if len(matches) > 5: print(f" ... and {len(matches) - 5} more") print("\n" + "=" * 70) def save_report_json(self, output_file: str): """Save report as JSON.""" data = { 'total_files_scanned': self.report.total_files_scanned, 'patterns_checked': self.report.patterns_checked, 'summary': self.report.summary, 'matches': { pattern: [asdict(m) for m in matches] for pattern, matches in self.report.matches_by_pattern.items() } } with open(output_file, 'w') as f: json.dump(data, f, indent=2) print(f"\nReport saved to: {output_file}") def save_report_text(self, output_file: str): """Save report as text.""" with open(output_file, 'w') as f: f.write("OCCT Migration Verification Report\n") f.write("=" * 70 + "\n\n") f.write(f"Files scanned: {self.report.total_files_scanned}\n\n") f.write("Summary:\n") f.write("-" * 70 + "\n") for pattern in sorted(self.report.summary.keys()): count = self.report.summary[pattern] f.write(f" {pattern:40s}: {count}\n") f.write("\n\nDetailed Matches:\n") f.write("-" * 70 + "\n") for pattern, matches in self.report.matches_by_pattern.items(): if matches: f.write(f"\n{pattern} ({len(matches)} matches):\n") for match in matches: f.write(f" {match.file}:{match.line_number}\n") f.write(f" {match.line_content}\n") print(f"\nReport saved to: {output_file}") def main(): parser = argparse.ArgumentParser(description='OCCT Migration Verification Script') parser.add_argument('src_directory', nargs='?', default='.', help='Source directory to scan') parser.add_argument('--verbose', '-v', action='store_true', help='Show detailed matches') parser.add_argument('--output', dest='output_file', help='Save report to file') parser.add_argument('--json', action='store_true', help='Output report as JSON') parser.add_argument('--check-new', action='store_true', help='Also check for new pattern usage') args = parser.parse_args() verifier = MigrationVerifier( src_dir=args.src_directory, verbose=args.verbose ) # Verify legacy patterns verifier.verify_legacy_patterns() verifier.print_report() # Check new patterns if requested if args.check_new: print("\n" + "-" * 70) print("New Pattern Usage (confirms migration was applied)") print("-" * 70) new_counts = verifier.verify_new_patterns() for pattern, count in new_counts.items(): print(f" {pattern:40s}: {count}") # Save report if requested if args.output_file: if args.json: verifier.save_report_json(args.output_file) else: verifier.save_report_text(args.output_file) # Return non-zero if legacy patterns remain remaining = sum(verifier.report.summary.values()) sys.exit(0 if remaining == 0 else 1) if __name__ == '__main__': main()