| #!/usr/bin/env python3 |
| |
| import os |
| import subprocess |
| import sys |
| from subprocess import CompletedProcess |
| |
| |
| def usage(): |
| print("""Usage: jspecify_update.py <dir> |
| This script updates all projects in a directory to use JSpecify annotations instead of AndroidX |
| nullness annotations. If no directory is provided, uses the root frameworks/support dir. |
| """) |
| sys.exit(1) |
| |
| |
| def run_gradle_task(gradle_path: str, task: str, args: list[str] | None = None) -> \ |
| CompletedProcess[bytes]: |
| """Runs a Gradle task ans returns the result.""" |
| cmd = [gradle_path, task] + (args or []) |
| return subprocess.run(cmd, capture_output=True) |
| |
| |
| def update_annotations(dir_path: str, gradle_path: str) -> bool: |
| """If the project in dir_path has a lint task, runs lintFix to shift all AndroidX nullness |
| annotations to type-use position. Returns whether any updates were applied.""" |
| tasks_result = run_gradle_task(gradle_path, "tasks") |
| if "lintFix" not in str(tasks_result.stdout): |
| print(f"Lint task does not exist for {dir_path}") |
| return False |
| |
| fix_result = run_gradle_task(gradle_path, "lintFix", ["-Pandroidx.useJSpecifyAnnotations=true"]) |
| return fix_result.returncode != 0 |
| |
| |
| def update_imports_for_file(filepath: str) -> bool: |
| """Replaces AndroidX nullness annotation imports with JSpecify imports. Returns whether any |
| updates were needed.""" |
| with open(filepath, "r") as f: |
| lines = f.readlines() |
| |
| replacements = { |
| "import androidx.annotation.NonNull": "import org.jspecify.annotations.NonNull;\n", |
| "import androidx.annotation.Nullable": "import org.jspecify.annotations.Nullable;\n" |
| } |
| updated_count = 0 |
| for i, line in enumerate(lines): |
| for target, replacement in replacements.items(): |
| if target in line: |
| lines[i] = replacement |
| updated_count += 1 |
| break |
| |
| if updated_count == len(replacements): |
| break |
| |
| replaced = updated_count > 0 |
| if replaced: |
| with open(filepath, "w") as f: |
| f.writelines(lines) |
| return replaced |
| |
| |
| def reformat_files(gradle_path: str) -> None: |
| """Runs the java formatter to fix imports for the specified files.""" |
| run_gradle_task(gradle_path, "javaFormat", ["--fix-imports-only"]), |
| |
| |
| def update_imports(dir_path: str, gradle_path: str) -> bool: |
| """For each java file in the directory, replaces the AndroidX nullness imports with JSpecify |
| imports and runs the java formatter to correct the new import order.""" |
| java_files = [] |
| for sub_dir_path, _, filenames in os.walk(dir_path): |
| for filename in filenames: |
| (_, ext) = os.path.splitext(filename) |
| if ext == ".java": |
| file_path = os.path.join(sub_dir_path, filename) |
| if update_imports_for_file(file_path): |
| java_files.append(file_path) |
| |
| if java_files: |
| reformat_files(gradle_path) |
| return java_files |
| |
| |
| def add_jspecify_dependency(build_gradle_path: str) -> None: |
| """Adds a JSpecify dependency to the build file.""" |
| with open(build_gradle_path, "r") as f: |
| lines = f.readlines() |
| |
| jspecify_dependency = " api(libs.jspecify)\n" |
| if jspecify_dependency in lines: |
| print(f"JSpecify dependency already present for {build_gradle_path}") |
| return |
| |
| dependencies_start = None |
| for i in range(len(lines)): |
| line = lines[i] |
| if line.startswith("dependencies {"): |
| dependencies_start = i |
| break |
| |
| if not dependencies_start: |
| print(f"No dependencies block found for {build_gradle_path}") |
| return |
| |
| lines.insert(dependencies_start + 1, " api(libs.jspecify)\n") |
| with open(build_gradle_path, "w") as f: |
| f.writelines(lines) |
| |
| |
| def process_dir(dir_path: str, root_dir_path: str) -> None: |
| """Updates the directory to use JSpecify annotations.""" |
| print(f"Processing {dir_path}") |
| os.chdir(dir_path) |
| gradle_path = os.path.join(root_dir_path, "gradlew") |
| if update_annotations(dir_path, gradle_path): |
| print(f"Lint fixes applied in {dir_path}") |
| if update_imports(dir_path, gradle_path): |
| add_jspecify_dependency(os.path.join(dir_path, "build.gradle")) |
| |
| |
| def main(start_dir: str | None) -> None: |
| # Location of this script: under support/development |
| script_path = os.path.realpath(__file__) |
| # Move up to the support dir |
| support_dir = os.path.dirname(os.path.dirname(script_path)) |
| |
| # Search the specified directory, or the support dir if there wasn't one |
| if not start_dir: |
| start_dir = support_dir |
| else: |
| start_dir = os.path.abspath(start_dir) |
| for dir_path, _, filenames in os.walk(start_dir): |
| if dir_path == support_dir: |
| continue |
| if "build.gradle" in filenames: |
| process_dir(dir_path, support_dir) |
| |
| |
| if __name__ == '__main__': |
| if len(sys.argv) == 1: |
| main(None) |
| elif len(sys.argv) == 2: |
| main(sys.argv[1]) |
| else: |
| usage() |