Skip to content

Instantly share code, notes, and snippets.

@kibotu
Last active May 30, 2025 12:45
Show Gist options
  • Save kibotu/5ead87cb263ae3650f00b4ee192badf2 to your computer and use it in GitHub Desktop.
Save kibotu/5ead87cb263ae3650f00b4ee192badf2 to your computer and use it in GitHub Desktop.
Migration script for sp android core 13 changes.
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