Last active
May 30, 2025 12:45
-
-
Save kibotu/5ead87cb263ae3650f00b4ee192badf2 to your computer and use it in GitHub Desktop.
Migration script for sp android core 13 changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os | |
import subprocess | |
from typing import Dict, Set | |
import re | |
# Constants | |
EVENTS_MODULE = "mover" | |
def checkout_core_repo() -> str: | |
"""Clone and checkout the core repository with shallow clone (no history), return its path""" | |
repo_dir = "android-profis-partner-core" | |
if not os.path.exists(repo_dir): | |
subprocess.run([ | |
"git", "clone", | |
"--depth", "1", | |
"--branch", "release/13.0.0", | |
"[email protected]:check24/android-profis-partner-core.git" | |
], check=True) | |
else: | |
# If directory exists, remove it first to ensure clean shallow clone | |
import shutil | |
shutil.rmtree(repo_dir) | |
subprocess.run([ | |
"git", "clone", | |
"--depth", "1", | |
"--branch", "release/13.0.0", | |
"[email protected]:check24/android-profis-partner-core.git" | |
], check=True) | |
os.chdir(repo_dir) | |
core_path = os.getcwd() | |
os.chdir("..") | |
return core_path | |
def collect_old_imports() -> Set[str]: | |
"""Collect all pluginapi imports from the events module""" | |
target_dirs = [ | |
f"{EVENTS_MODULE}/src/main/java/de/check24/profis/partner/plugin/{EVENTS_MODULE}", | |
f"{EVENTS_MODULE}/src/debug/java/de/check24/profis/partner/plugin/{EVENTS_MODULE}" | |
] | |
# Updated pattern to capture nested imports | |
import_pattern = r"import\s+(de\.check24\.profis\.partner\.pluginapi(?:\.\w+)*(?:\.\w+)+)" | |
old_imports = set() | |
for target_dir in target_dirs: | |
if not os.path.exists(target_dir): | |
continue | |
for root, _, files in os.walk(target_dir): | |
for file_name in files: | |
if file_name.endswith(".kt"): | |
file_path = os.path.join(root, file_name) | |
try: | |
with open(file_path, 'r', encoding='utf-8') as f: | |
content = f.read() | |
matches = re.finditer(import_pattern, content) | |
for match in matches: | |
old_imports.add(match.group(1)) | |
except Exception as e: | |
print(f"Error reading {file_path}: {str(e)}") | |
return old_imports | |
def build_import_mapping(core_path: str, old_imports: Set[str]) -> Dict[str, str]: | |
"""Build a mapping of old imports to new imports, including nested members and extension properties""" | |
import_mapping = {} | |
search_dir = os.path.join(core_path, "pluginapi/src/main/java") | |
if not os.path.exists(search_dir): | |
print(f"Warning: Expected pluginapi directory not found at {search_dir}") | |
return import_mapping | |
# Map full importable names to their new locations | |
name_to_new_import = {} | |
print(f"Scanning for declarations in {search_dir}...") | |
for root, _, files in os.walk(search_dir): | |
for file_name in files: | |
if file_name.endswith(".kt"): | |
file_path = os.path.join(root, file_name) | |
base_name = file_name[:-3] | |
try: | |
with open(file_path, 'r', encoding='utf-8') as f: | |
content = f.read() | |
lines = content.split('\n') | |
package_name = None | |
for line in lines: | |
if line.startswith("package "): | |
package_name = line[8:].strip() | |
break | |
if not package_name: | |
continue | |
# Enhanced patterns for declarations | |
class_pattern = r"(?:class|interface|object|enum\s+class|data\s+class)\s+(\w+)" | |
# More comprehensive extension function pattern | |
extension_func_pattern = r"^(?:\s*(?:@\w+\s*)*)?(?:inline\s+|suspend\s+|infix\s+)*fun\s+(?:<[^>]*>\s+)?(?:[\w.<>?:\s]+)\.(\w+)\s*\(" | |
# Extension property patterns - NEW | |
extension_property_pattern = r"^(?:\s*(?:@\w+\s*)*)?(?:val|var)\s+(?:[\w.<>?:\s]+)\.(\w+)\s*:" | |
extension_property_getter_pattern = r"^(?:\s*(?:@\w+\s*)*)?(?:val|var)\s+([\w.<>?:\s]+)\.(\w+)\s*$" | |
# Top-level function pattern | |
top_level_func_pattern = r"^(?:\s*(?:@\w+\s*)*)?(?:inline\s+|suspend\s+|infix\s+)*fun\s+(?:<[^>]*>\s+)?(\w+)\s*\(" | |
# Properties/constants at top level | |
top_level_property_pattern = r"^(?:\s*(?:@\w+\s*)*)?(?:const\s+)?(?:val|var)\s+(\w+)\s*[:=]" | |
# Companion object members | |
companion_member_pattern = r"^\s*(?:const\s+)?(?:fun|val|var)\s+(\w+)\s*(?:\(|[:=])" | |
current_class = None | |
in_class_block = False | |
in_companion_object = False | |
brace_depth = 0 | |
multiline_function = False | |
function_buffer = "" | |
multiline_property = False | |
property_buffer = "" | |
for line_num, line in enumerate(lines): | |
stripped_line = line.strip() | |
# Handle multiline function declarations | |
if multiline_function: | |
function_buffer += " " + stripped_line | |
if '(' in function_buffer: | |
# Try to parse the complete function | |
ext_func_match = re.search(extension_func_pattern, function_buffer) | |
if ext_func_match: | |
func_name = ext_func_match.group(1) | |
name_to_new_import[func_name] = f"{package_name}.{func_name}" | |
print(f" Found extension function: {func_name} -> {package_name}.{func_name}") | |
multiline_function = False | |
function_buffer = "" | |
continue | |
# Handle multiline property declarations | |
if multiline_property: | |
property_buffer += " " + stripped_line | |
if ':' in property_buffer or 'get()' in property_buffer: | |
# Try to parse the complete property | |
ext_prop_match = re.search(extension_property_pattern, property_buffer) | |
if not ext_prop_match: | |
ext_prop_match = re.search(extension_property_getter_pattern, property_buffer) | |
if ext_prop_match and len(ext_prop_match.groups()) >= 2: | |
prop_name = ext_prop_match.group(2) | |
elif ext_prop_match: | |
prop_name = ext_prop_match.group(1) | |
else: | |
prop_name = None | |
else: | |
prop_name = ext_prop_match.group(1) | |
if prop_name: | |
name_to_new_import[prop_name] = f"{package_name}.{prop_name}" | |
print(f" Found extension property: {prop_name} -> {package_name}.{prop_name}") | |
multiline_property = False | |
property_buffer = "" | |
continue | |
# Count braces to track nesting | |
brace_depth += line.count('{') - line.count('}') | |
# Check if we're entering/exiting class blocks | |
if not in_class_block: | |
class_match = re.search(class_pattern, stripped_line) | |
if class_match: | |
current_class = class_match.group(1) | |
name_to_new_import[current_class] = f"{package_name}.{current_class}" | |
print(f" Found class: {current_class} -> {package_name}.{current_class}") | |
if '{' in line: | |
in_class_block = True | |
continue | |
# Check for extension properties - NEW | |
ext_prop_match = re.search(extension_property_pattern, stripped_line) | |
if ext_prop_match: | |
prop_name = ext_prop_match.group(1) | |
name_to_new_import[prop_name] = f"{package_name}.{prop_name}" | |
print(f" Found extension property: {prop_name} -> {package_name}.{prop_name}") | |
continue | |
# Check for extension property without explicit type (getter style) - NEW | |
if ('val ' in stripped_line or 'var ' in stripped_line) and '.' in stripped_line and 'get()' in lines[line_num:line_num+3]: | |
ext_prop_getter_match = re.search(extension_property_getter_pattern, stripped_line) | |
if ext_prop_getter_match and len(ext_prop_getter_match.groups()) >= 2: | |
prop_name = ext_prop_getter_match.group(2) | |
name_to_new_import[prop_name] = f"{package_name}.{prop_name}" | |
print(f" Found extension property (getter): {prop_name} -> {package_name}.{prop_name}") | |
continue | |
elif not ':' in stripped_line and not '=' in stripped_line: | |
# Might be a multiline property declaration | |
multiline_property = True | |
property_buffer = stripped_line | |
continue | |
# Check for extension functions (including multiline) | |
if 'fun ' in stripped_line and '.' in stripped_line: | |
ext_func_match = re.search(extension_func_pattern, stripped_line) | |
if ext_func_match: | |
func_name = ext_func_match.group(1) | |
name_to_new_import[func_name] = f"{package_name}.{func_name}" | |
print(f" Found extension function: {func_name} -> {package_name}.{func_name}") | |
elif '(' not in stripped_line: | |
# Might be a multiline function declaration | |
multiline_function = True | |
function_buffer = stripped_line | |
continue | |
# Top-level functions | |
top_func_match = re.search(top_level_func_pattern, stripped_line) | |
if top_func_match: | |
func_name = top_func_match.group(1) | |
name_to_new_import[func_name] = f"{package_name}.{func_name}" | |
print(f" Found top-level function: {func_name} -> {package_name}.{func_name}") | |
continue | |
# Top-level properties/constants | |
prop_match = re.search(top_level_property_pattern, stripped_line) | |
if prop_match: | |
prop_name = prop_match.group(1) | |
name_to_new_import[prop_name] = f"{package_name}.{prop_name}" | |
print(f" Found top-level property: {prop_name} -> {package_name}.{prop_name}") | |
continue | |
else: | |
# Inside a class/object | |
if 'companion object' in stripped_line: | |
in_companion_object = True | |
print(f" Entering companion object in {current_class}") | |
# Class members (including companion object members) | |
member_match = re.search(companion_member_pattern, stripped_line) | |
if member_match and current_class: | |
member_name = member_match.group(1) | |
if in_companion_object: | |
# Companion object members can be accessed directly | |
name_to_new_import[member_name] = f"{package_name}.{current_class}.{member_name}" | |
name_to_new_import[f"{current_class}.{member_name}"] = f"{package_name}.{current_class}.{member_name}" | |
print(f" Found companion member: {current_class}.{member_name} -> {package_name}.{current_class}.{member_name}") | |
else: | |
full_name = f"{current_class}.{member_name}" | |
name_to_new_import[full_name] = f"{package_name}.{full_name}" | |
print(f" Found class member: {full_name} -> {package_name}.{full_name}") | |
# Check for extension properties inside objects/classes - NEW | |
if ('val ' in stripped_line or 'var ' in stripped_line) and '.' in stripped_line and current_class: | |
ext_prop_match = re.search(extension_property_pattern, stripped_line) | |
if not ext_prop_match: | |
ext_prop_match = re.search(extension_property_getter_pattern, stripped_line) | |
if ext_prop_match and len(ext_prop_match.groups()) >= 2: | |
prop_name = ext_prop_match.group(2) | |
elif ext_prop_match: | |
prop_name = ext_prop_match.group(1) | |
else: | |
prop_name = None | |
else: | |
prop_name = ext_prop_match.group(1) | |
if prop_name: | |
# For extension properties in objects, make them accessible via object name | |
name_to_new_import[prop_name] = f"{package_name}.{current_class}.{prop_name}" | |
name_to_new_import[f"{current_class}.{prop_name}"] = f"{package_name}.{current_class}.{prop_name}" | |
print(f" Found extension property in {current_class}: {prop_name} -> {package_name}.{current_class}.{prop_name}") | |
# Reset class tracking when exiting class | |
if brace_depth == 0 and in_class_block: | |
in_class_block = False | |
in_companion_object = False | |
current_class = None | |
# Register the file name itself as importable (Kotlin generates *Kt classes for top-level functions) | |
if base_name not in name_to_new_import: | |
name_to_new_import[base_name] = f"{package_name}.{base_name}Kt" | |
print(f" Registered file: {base_name} -> {package_name}.{base_name}Kt") | |
except Exception as e: | |
print(f"Error reading {file_path}: {str(e)}") | |
print(f"\nFound {len(name_to_new_import)} total declarations") | |
# Map old imports to new imports with more flexible matching | |
for old_import in old_imports: | |
import_parts = old_import.split('.') | |
found = False | |
# Try exact match first | |
if old_import in name_to_new_import: | |
import_mapping[old_import] = name_to_new_import[old_import] | |
found = True | |
else: | |
# Try matching the last part (function/property name) | |
last_part = import_parts[-1] | |
if last_part in name_to_new_import: | |
import_mapping[old_import] = name_to_new_import[last_part] | |
found = True | |
else: | |
# Try matching last two parts (Class.member) | |
if len(import_parts) >= 2: | |
last_two = '.'.join(import_parts[-2:]) | |
if last_two in name_to_new_import: | |
import_mapping[old_import] = name_to_new_import[last_two] | |
found = True | |
else: | |
# Try with class name and member | |
class_name = import_parts[-2] | |
member_name = import_parts[-1] | |
potential_key = f"{class_name}.{member_name}" | |
if potential_key in name_to_new_import: | |
import_mapping[old_import] = name_to_new_import[potential_key] | |
found = True | |
# NEW: Special handling for service-like patterns | |
# If the import looks like SomeService.someMethod, try just someMethod | |
if class_name.endswith('Service') or class_name.endswith('Factory') or class_name.endswith('Configuration'): | |
if member_name in name_to_new_import: | |
import_mapping[old_import] = name_to_new_import[member_name] | |
found = True | |
print(f" Mapped service method: {old_import} -> {name_to_new_import[member_name]}") | |
if not found: | |
print(f"Warning: Could not find new location for {old_import}") | |
# Debug: show what we're looking for vs what we have | |
last_part = import_parts[-1] | |
similar_keys = [k for k in name_to_new_import.keys() if last_part.lower() in k.lower()] | |
if similar_keys: | |
print(f" Similar keys found: {similar_keys[:3]}") # Show first 3 matches | |
return import_mapping | |
def update_imports(import_mapping: Dict[str, str]): | |
"""Update imports in the events module""" | |
target_dirs = [ | |
f"{EVENTS_MODULE}/src/main/java/de/check24/profis/partner/plugin/{EVENTS_MODULE}", | |
f"{EVENTS_MODULE}/src/debug/java/de/check24/profis/partner/plugin/{EVENTS_MODULE}" | |
] | |
files_processed = 0 | |
imports_updated = 0 | |
for target_dir in target_dirs: | |
if not os.path.exists(target_dir): | |
print(f"Warning: Directory {target_dir} not found, skipping") | |
continue | |
for root, _, files in os.walk(target_dir): | |
for file_name in files: | |
if file_name.endswith(".kt"): | |
file_path = os.path.join(root, file_name) | |
try: | |
with open(file_path, 'r', encoding='utf-8') as f: | |
content = f.read() | |
updated_content = content | |
file_changed = False | |
for old_import, new_import in import_mapping.items(): | |
old_full_import = f"import {old_import}" | |
new_full_import = f"import {new_import}" | |
if old_full_import in content: | |
updated_content = updated_content.replace( | |
old_full_import, new_full_import | |
) | |
file_changed = True | |
imports_updated += 1 | |
if file_changed: | |
with open(file_path, 'w', encoding='utf-8') as f: | |
f.write(updated_content) | |
print(f"Updated imports in: {file_path}") | |
files_processed += 1 | |
except Exception as e: | |
print(f"Error processing {file_path}: {str(e)}") | |
print(f"\nMigration complete!") | |
print(f"Processed {files_processed} files with {imports_updated} import updates") | |
def main(): | |
try: | |
print(f"Step 1: Collecting existing imports from {EVENTS_MODULE} module...") | |
old_imports = collect_old_imports() | |
if not old_imports: | |
print(f"Warning: No pluginapi imports found in {EVENTS_MODULE} module") | |
return | |
print(f"Found {len(old_imports)} unique pluginapi imports") | |
print("\nStep 2: Checking out core repository (shallow clone)...") | |
core_path = checkout_core_repo() | |
print("\nStep 3: Building import mapping from core repository...") | |
import_mapping = build_import_mapping(core_path, old_imports) | |
if not import_mapping: | |
print("Warning: No import mappings created") | |
return | |
print(f"Created {len(import_mapping)} import mappings") | |
for old, new in list(import_mapping.items())[:5]: # Show first 5 as sample | |
print(f"Will replace: import {old} -> import {new}") | |
if len(import_mapping) > 5: | |
print("...and more") | |
print(f"\nStep 4: Updating imports in {EVENTS_MODULE} module (main and debug)...") | |
update_imports(import_mapping) | |
except subprocess.CalledProcessError as e: | |
print(f"Git operation failed: {str(e)}") | |
except Exception as e: | |
print(f"An error occurred: {str(e)}") | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment