#!/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. """ OCCT 8.0.0 Modernization Script This script performs the following transformations: 1. Handle(ClassName) -> occ::handle 2. Handle(ClassName)::DownCast(...) -> occ::down_cast(...) 3. ->IsKind(STANDARD_TYPE(T)) -> occ::is_kind(...) 4. ->IsInstance(STANDARD_TYPE(T)) -> occ::is_instance(...) 5. Standard_Boolean -> bool 6. Standard_Integer -> int 7. Standard_Real -> double 8. Standard_True -> true 9. Standard_False -> false 10. Standard_Address -> void* 11. Standard_ShortReal -> float 12. Standard_Character -> char 13. Standard_Byte -> unsigned char Note: Standard_*.hxx includes are NOT removed as they contain utility functions (Abs, IsEven, Square, RealEpsilon, etc.) Usage: python3 occt_modernize.py [options] Options: --dry-run Preview changes without modifying files --backup Create .bak backup files before modification --verbose Show detailed progress --phase=N Run specific phase (1=handles, 2=downcast, 3=iskind, 4=types) --exclude=PATTERN Exclude files matching pattern (can be repeated) --verify Check for remaining patterns after migration --log=FILE Write detailed log to file --stats Show statistics only, don't modify files """ import os import re import sys import argparse import shutil from pathlib import Path from typing import List, Tuple, Dict, Set, Optional from dataclasses import dataclass, field from datetime import datetime import json @dataclass class MigrationStats: """Statistics for migration operations.""" files_processed: int = 0 files_modified: int = 0 handle_replacements: int = 0 downcast_replacements: int = 0 iskind_replacements: int = 0 isinstance_replacements: int = 0 type_replacements: Dict[str, int] = field(default_factory=dict) errors: List[str] = field(default_factory=list) skipped_files: List[str] = field(default_factory=list) def to_dict(self) -> dict: return { 'files_processed': self.files_processed, 'files_modified': self.files_modified, 'handle_replacements': self.handle_replacements, 'downcast_replacements': self.downcast_replacements, 'iskind_replacements': self.iskind_replacements, 'isinstance_replacements': self.isinstance_replacements, 'type_replacements': self.type_replacements, 'errors': self.errors, 'skipped_files': self.skipped_files } class OCCTModernizer: """Main class for OCCT code modernization.""" # Files to exclude from migration (core infrastructure) EXCLUDE_FILES = { 'Standard_Handle.hxx', 'Standard_TypeDef.hxx', 'Standard_Transient.hxx', 'Standard_Transient.cxx', 'Standard_Type.hxx', 'Standard_Type.cxx', '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', } # Standard type mappings TYPE_MAPPINGS = { 'Standard_Boolean': 'bool', 'Standard_Integer': 'int', 'Standard_Real': 'double', 'Standard_ShortReal': 'float', 'Standard_Character': 'char', 'Standard_Byte': 'unsigned char', 'Standard_Address': 'void*', } # Value mappings VALUE_MAPPINGS = { 'Standard_True': 'true', 'Standard_False': 'false', } def __init__(self, src_dir: str, dry_run: bool = False, backup: bool = False, verbose: bool = False, exclude_patterns: List[str] = None): self.src_dir = Path(src_dir) self.dry_run = dry_run self.backup = backup self.verbose = verbose self.exclude_patterns = exclude_patterns or [] self.stats = MigrationStats() self.log_entries: List[str] = [] def log(self, message: str, level: str = 'INFO'): """Log a message.""" timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') entry = f"[{timestamp}] [{level}] {message}" self.log_entries.append(entry) if self.verbose or level in ('ERROR', 'WARNING'): print(entry) def should_process_file(self, file_path: Path) -> bool: """Check if a file should be processed.""" # Check file extension if file_path.suffix not in ('.hxx', '.cxx', '.lxx', '.pxx'): return False # Check excluded files if file_path.name in self.EXCLUDE_FILES: self.stats.skipped_files.append(str(file_path)) return False # Check exclude patterns for pattern in self.exclude_patterns: if re.search(pattern, str(file_path)): self.stats.skipped_files.append(str(file_path)) return False return True def get_source_files(self) -> List[Path]: """Get all source files to process.""" files = [] for ext in ('*.hxx', '*.cxx', '*.lxx', '*.pxx'): files.extend(self.src_dir.rglob(ext)) return sorted(files) def create_backup(self, file_path: Path): """Create a backup of the file.""" if self.backup and not self.dry_run: backup_path = file_path.with_suffix(file_path.suffix + '.bak') shutil.copy2(file_path, backup_path) def read_file(self, file_path: Path) -> str: """Read file content with encoding handling.""" try: with open(file_path, 'r', encoding='utf-8') as f: return f.read() except UnicodeDecodeError: with open(file_path, 'r', encoding='latin-1') as f: return f.read() def write_file(self, file_path: Path, content: str): """Write content to file.""" if not self.dry_run: with open(file_path, 'w', encoding='utf-8') as f: f.write(content) # ========================================================================= # Phase 1: Handle(Class) -> occ::handle # ========================================================================= def migrate_handle_pattern(self, content: str) -> Tuple[str, int]: """ Replace Handle(ClassName) with occ::handle. Does NOT replace Handle(ClassName)::DownCast patterns (handled in phase 2). """ count = 0 # Pattern: Handle(ClassName) not followed by ::DownCast # We need to be careful with nested parentheses and templates pattern = r'\bHandle\(([A-Z][a-zA-Z0-9_]*)\)(?!::DownCast)' def replace_handle(match): nonlocal count class_name = match.group(1) count += 1 return f'occ::handle<{class_name}>' new_content = re.sub(pattern, replace_handle, content) return new_content, count # ========================================================================= # Phase 2: Handle(Class)::DownCast -> occ::down_cast # ========================================================================= def migrate_downcast_pattern(self, content: str) -> Tuple[str, int]: """ Replace Handle(ClassName)::DownCast(...) with occ::down_cast(...). """ count = 0 # Pattern: Handle(ClassName)::DownCast( pattern = r'\bHandle\(([A-Z][a-zA-Z0-9_]*)\)::DownCast\(' def replace_downcast(match): nonlocal count class_name = match.group(1) count += 1 return f'occ::down_cast<{class_name}>(' new_content = re.sub(pattern, replace_downcast, content) return new_content, count # ========================================================================= # Phase 3: IsKind/IsInstance -> occ::is_kind/occ::is_instance # ========================================================================= def migrate_iskind_isinstance_pattern(self, content: str) -> Tuple[str, int, int]: """ Replace ->IsKind(STANDARD_TYPE(T)) with occ::is_kind(...). Replace ->IsInstance(STANDARD_TYPE(T)) with occ::is_instance(...). Returns (new_content, iskind_count, isinstance_count). """ iskind_count = 0 isinstance_count = 0 # Pattern for IsKind: ->IsKind(STANDARD_TYPE(ClassName)) # We need to capture the object before -> and the class name iskind_pattern = r'(\w+)->IsKind\s*\(\s*STANDARD_TYPE\s*\(\s*([A-Z][a-zA-Z0-9_]*)\s*\)\s*\)' def replace_iskind(match): nonlocal iskind_count obj_name = match.group(1) class_name = match.group(2) iskind_count += 1 return f'occ::is_kind<{class_name}>({obj_name})' content = re.sub(iskind_pattern, replace_iskind, content) # Pattern for IsInstance: ->IsInstance(STANDARD_TYPE(ClassName)) isinstance_pattern = r'(\w+)->IsInstance\s*\(\s*STANDARD_TYPE\s*\(\s*([A-Z][a-zA-Z0-9_]*)\s*\)\s*\)' def replace_isinstance(match): nonlocal isinstance_count obj_name = match.group(1) class_name = match.group(2) isinstance_count += 1 return f'occ::is_instance<{class_name}>({obj_name})' content = re.sub(isinstance_pattern, replace_isinstance, content) return content, iskind_count, isinstance_count # ========================================================================= # Phase 4: Standard_* types -> native types # ========================================================================= def migrate_standard_types(self, content: str) -> Tuple[str, Dict[str, int]]: """ Replace Standard_* types with native C++ types. """ counts = {} # Replace type mappings for old_type, new_type in self.TYPE_MAPPINGS.items(): # Use word boundary to avoid partial matches pattern = rf'\b{old_type}\b' matches = len(re.findall(pattern, content)) if matches > 0: content = re.sub(pattern, new_type, content) counts[old_type] = matches # Replace value mappings for old_val, new_val in self.VALUE_MAPPINGS.items(): pattern = rf'\b{old_val}\b' matches = len(re.findall(pattern, content)) if matches > 0: content = re.sub(pattern, new_val, content) counts[old_val] = matches return content, counts # ========================================================================= # Main processing # ========================================================================= def process_file(self, file_path: Path, phases: Set[int]) -> bool: """ Process a single file through specified phases. Returns True if the file was modified. """ if not self.should_process_file(file_path): return False self.stats.files_processed += 1 self.log(f"Processing: {file_path}") try: original_content = self.read_file(file_path) content = original_content modified = False # Phase 2 must come before Phase 1 to avoid double-processing # We need to handle DownCast first, then remaining Handle patterns # Phase 2: DownCast patterns (must be first!) if 2 in phases: content, downcast_count = self.migrate_downcast_pattern(content) if downcast_count > 0: self.stats.downcast_replacements += downcast_count modified = True self.log(f" DownCast replacements: {downcast_count}") # Phase 1: Handle patterns (after DownCast!) if 1 in phases: content, handle_count = self.migrate_handle_pattern(content) if handle_count > 0: self.stats.handle_replacements += handle_count modified = True self.log(f" Handle replacements: {handle_count}") # Phase 3: IsKind/IsInstance patterns if 3 in phases: content, iskind_count, isinstance_count = self.migrate_iskind_isinstance_pattern(content) if iskind_count > 0: self.stats.iskind_replacements += iskind_count modified = True self.log(f" IsKind replacements: {iskind_count}") if isinstance_count > 0: self.stats.isinstance_replacements += isinstance_count modified = True self.log(f" IsInstance replacements: {isinstance_count}") # Phase 4: Standard_* types if 4 in phases: content, type_counts = self.migrate_standard_types(content) if type_counts: for type_name, count in type_counts.items(): self.stats.type_replacements[type_name] = \ self.stats.type_replacements.get(type_name, 0) + count modified = True self.log(f" Type replacements: {type_counts}") if modified: self.create_backup(file_path) self.write_file(file_path, content) self.stats.files_modified += 1 return True except Exception as e: error_msg = f"Error processing {file_path}: {e}" self.stats.errors.append(error_msg) self.log(error_msg, 'ERROR') return False def run(self, phases: Set[int] = None): """Run the modernization process.""" if phases is None: phases = {1, 2, 3, 4} self.log(f"Starting OCCT modernization") self.log(f"Source directory: {self.src_dir}") self.log(f"Phases to run: {sorted(phases)}") self.log(f"Dry run: {self.dry_run}") files = self.get_source_files() total_files = len(files) self.log(f"Found {total_files} source files") for i, file_path in enumerate(files, 1): if i % 100 == 0: print(f"Progress: {i}/{total_files} files processed...") self.process_file(file_path, phases) self.log(f"\nMigration complete!") self.print_summary() def print_summary(self): """Print migration summary.""" print("\n" + "=" * 60) print("OCCT Modernization Summary") print("=" * 60) print(f"Files processed: {self.stats.files_processed}") print(f"Files modified: {self.stats.files_modified}") print(f"Files skipped: {len(self.stats.skipped_files)}") print(f"Handle replacements: {self.stats.handle_replacements}") print(f"DownCast replacements: {self.stats.downcast_replacements}") print(f"IsKind replacements: {self.stats.iskind_replacements}") print(f"IsInstance replacements:{self.stats.isinstance_replacements}") if self.stats.type_replacements: print("\nType replacements:") for type_name, count in sorted(self.stats.type_replacements.items()): print(f" {type_name}: {count}") if self.stats.errors: print(f"\nErrors ({len(self.stats.errors)}):") for error in self.stats.errors[:10]: print(f" {error}") if len(self.stats.errors) > 10: print(f" ... and {len(self.stats.errors) - 10} more") print("=" * 60) def verify(self) -> Dict[str, int]: """Verify remaining patterns after migration.""" remaining = { 'Handle(': 0, '::DownCast': 0, '->IsKind(STANDARD_TYPE': 0, '->IsInstance(STANDARD_TYPE': 0, 'Standard_Boolean': 0, 'Standard_Integer': 0, 'Standard_Real': 0, 'Standard_True': 0, 'Standard_False': 0, } files = self.get_source_files() for file_path in files: if not self.should_process_file(file_path): continue try: content = self.read_file(file_path) for pattern in remaining: remaining[pattern] += len(re.findall(re.escape(pattern), content)) except Exception: pass print("\n" + "=" * 60) print("Verification - Remaining Patterns") print("=" * 60) for pattern, count in remaining.items(): status = "OK" if count == 0 else f"REMAINING: {count}" print(f" {pattern:20s}: {status}") print("=" * 60) return remaining def save_log(self, log_file: str): """Save log to file.""" with open(log_file, 'w') as f: f.write('\n'.join(self.log_entries)) f.write('\n\nStatistics:\n') f.write(json.dumps(self.stats.to_dict(), indent=2)) def main(): parser = argparse.ArgumentParser( description='OCCT 8.0.0 Modernization Script', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__ ) parser.add_argument('src_directory', nargs='?', default='.', help='Source directory to process (default: current directory)') parser.add_argument('--dry-run', action='store_true', help='Preview changes without modifying files') parser.add_argument('--backup', action='store_true', help='Create .bak backup files before modification') parser.add_argument('--verbose', '-v', action='store_true', help='Show detailed progress') parser.add_argument('--phase', type=int, action='append', dest='phases', help='Run specific phase (1=handles, 2=downcast, 3=types, 4=includes)') parser.add_argument('--exclude', action='append', dest='exclude_patterns', help='Exclude files matching pattern (regex)') parser.add_argument('--verify', action='store_true', help='Check for remaining patterns after migration') parser.add_argument('--log', dest='log_file', help='Write detailed log to file') parser.add_argument('--stats', action='store_true', help='Show statistics only, don\'t modify files') args = parser.parse_args() # Determine phases to run if args.phases: phases = set(args.phases) else: phases = {1, 2, 3, 4} # Validate phases valid_phases = {1, 2, 3, 4} if not phases.issubset(valid_phases): print(f"Error: Invalid phase(s). Valid phases are: {valid_phases}") sys.exit(1) # Create modernizer modernizer = OCCTModernizer( src_dir=args.src_directory, dry_run=args.dry_run or args.stats, backup=args.backup, verbose=args.verbose, exclude_patterns=args.exclude_patterns or [] ) # Run migration or verification if args.verify: modernizer.verify() else: modernizer.run(phases) # Save log if requested if args.log_file: modernizer.save_log(args.log_file) print(f"\nLog saved to: {args.log_file}") # Return non-zero if there were errors sys.exit(1 if modernizer.stats.errors else 0) if __name__ == '__main__': main()