mirror of
https://github.com/Open-Cascade-SAS/OCCT.git
synced 2026-05-11 18:32:35 +08:00
- Added automated migration scripts for handle syntax, standard types, and macros - Deprecated legacy `Standard_*` types and macros in favor of native C++ equivalents - Introduced modern `occ` namespace with template-based type checking helpers - Enhanced NCollection macros to support variadic arguments for complex template types- Added automated migration scripts for handle syntax, standard types, and macros - Deprecated legacy `Standard_*` types and macros in favor of native C++ equivalents - Introduced modern `occ` namespace with template-based type checking helpers - Enhanced NCollection macros to support variadic arguments for complex template types
541 lines
20 KiB
Python
Executable File
541 lines
20 KiB
Python
Executable File
#!/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<ClassName>
|
|
2. Handle(ClassName)::DownCast(...) -> occ::down_cast<ClassName>(...)
|
|
3. ->IsKind(STANDARD_TYPE(T)) -> occ::is_kind<T>(...)
|
|
4. ->IsInstance(STANDARD_TYPE(T)) -> occ::is_instance<T>(...)
|
|
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] <src_directory>
|
|
|
|
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<Class>
|
|
# =========================================================================
|
|
|
|
def migrate_handle_pattern(self, content: str) -> Tuple[str, int]:
|
|
"""
|
|
Replace Handle(ClassName) with occ::handle<ClassName>.
|
|
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<Class>
|
|
# =========================================================================
|
|
|
|
def migrate_downcast_pattern(self, content: str) -> Tuple[str, int]:
|
|
"""
|
|
Replace Handle(ClassName)::DownCast(...) with occ::down_cast<ClassName>(...).
|
|
"""
|
|
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<T>(...).
|
|
Replace ->IsInstance(STANDARD_TYPE(T)) with occ::is_instance<T>(...).
|
|
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()
|