diff --git a/.gemini/skills/java-ast-refactoring/SKILL.md b/.gemini/skills/java-ast-refactoring/SKILL.md new file mode 100644 index 000000000..96ca08ec2 --- /dev/null +++ b/.gemini/skills/java-ast-refactoring/SKILL.md @@ -0,0 +1,30 @@ +--- +name: java-ast-refactoring +description: "AST-aware Java refactoring using OpenRewrite. Use when asked to structurally refactor Java code, change class names, change method signatures/overloads, replace builder patterns, modify annotations, or perform cross-file structural replacements. Note: Renaming fields or local variables/parameters is not supported natively via simple YAML recipes in the standard openrewrite modules." +--- + +# AST-Aware Java Refactoring + +This skill uses OpenRewrite to perform Abstract Syntax Tree (AST) based refactoring on Java codebases. This is highly preferred over text-based regex or python scripts because it understands Java semantics, correctly updates imports, and preserves formatting. + +## Parameter and Field Renaming (Last Resort) + +Because OpenRewrite's YAML recipes do not natively support simple variable or field renaming, a custom script is provided: +```bash +python3 .gemini/skills/java-ast-refactoring/scripts/safe_rename.py +``` +**CRITICAL:** Running this python script is a LAST RESORT. It is a regex-based token replacement that ignores strings and comments, but it lacks true AST understanding. ALWAYS prefer using OpenRewrite recipes (`rewrite.yml`) for structural changes like renaming classes, methods, or moving targets, as OpenRewrite correctly handles imports, types, and cross-file references safely. + +## Usage + +1. Create a `rewrite.yml` recipe file in the workspace root. Refer to `.gemini/skills/java-ast-refactoring/references/rewrite_recipes.md` for syntax. +2. Execute the script: +```bash +./.gemini/skills/java-ast-refactoring/scripts/run_rewrite.sh rewrite.yml +``` +3. The script will safely apply the AST transformations and then automatically run `./gradlew spotlessApply` and `google-java-format --replace` on the modified files to automatically fix any Checkstyle line-length and import ordering issues caused by longer/shorter identifiers. Verify the output using `git diff`. +4. **MANDATORY:** Always run `./gradlew build -x test` (or the equivalent compile task) after running OpenRewrite to ensure no compilation errors were introduced. + +## Known Limitations & Troubleshooting +* **Static Imports Dropped on Class Rename:** When using `ChangeType` to rename a class, OpenRewrite may sometimes drop static imports for fields/constants belonging to the old class instead of updating them to the new class. If compilation fails due to "cannot find symbol" for a constant after a class rename, manually restore the static import (e.g., `import static com.new.ClassName.CONSTANT;`). +* **Continuous Improvement:** If any new issues or edge cases are found while running the refactoring (e.g., build failures, formatting issues, or missed transformations), proactively update this skill file (`SKILL.md`) and its accompanying scripts (`scripts/run_rewrite.sh`, `scripts/safe_rename.py`) to permanently fix the issue for future use. diff --git a/.gemini/skills/java-ast-refactoring/references/rewrite_recipes.md b/.gemini/skills/java-ast-refactoring/references/rewrite_recipes.md new file mode 100644 index 000000000..b18349c75 --- /dev/null +++ b/.gemini/skills/java-ast-refactoring/references/rewrite_recipes.md @@ -0,0 +1,82 @@ +# OpenRewrite Recipe Reference + +OpenRewrite uses declarative YAML recipes to perform structural refactorings. + +## Recipe Structure + +A recipe file must have a `type`, a `name` (which you will activate), and a `recipeList` containing specific core recipes to execute sequentially. + +```yaml +type: specs.openrewrite.org/v1beta/recipe +name: com.example.MyRefactoring +recipeList: + - : + : +``` + +## Core Recipes for Common Operations + +### 1. Change Method Name +```yaml + - org.openrewrite.java.ChangeMethodName: + methodPattern: java.util.Collections emptyList() + newMethodName: emptyList +``` + +### 2. Change Method Target to Static +Moves a method call to a new static method target. Useful for replacing custom utility methods with standard ones. +```yaml + - org.openrewrite.java.ChangeMethodTargetToStatic: + methodPattern: google.registry.model.eppinput.EppInputs createDomain(java.lang.String, java.lang.String) + fullyQualifiedTargetTypeName: google.registry.model.domain.DomainCommand.Create + returnType: google.registry.model.domain.DomainCommand.Create.Builder +``` + +### 3. Change Type (Rename/Move Class) +Updates the class name and automatically updates all imports across the codebase. +*Note: OpenRewrite occasionally drops `import static` references to fields inside the renamed class. Be prepared to manually restore them if a compilation error occurs.* +```yaml + - org.openrewrite.java.ChangeType: + oldFullyQualifiedTypeName: org.joda.time.Instant + newFullyQualifiedTypeName: java.time.Instant +``` + +### 4. Remove Unused Imports +```yaml + - org.openrewrite.java.RemoveUnusedImports +``` + +### 5. Change Annotation +```yaml + - org.openrewrite.java.ChangeAnnotation: + annotationPattern: @org.junit.Ignore + newAnnotation: @org.junit.jupiter.api.Disabled +``` + +### 6. Remove Annotation +```yaml + - org.openrewrite.java.RemoveAnnotation: + annotationPattern: @java.lang.SuppressWarnings("unchecked") +``` + +### 7. Change Method Arguments +Reorders or removes arguments based on a target signature. `newArgumentTemplate` uses 0-based indexing. +```yaml + - org.openrewrite.java.ChangeMethodAccessLevel: + methodPattern: com.google.common.collect.ImmutableList of(..) + newAccessLevel: protected +``` + +### 8. Add Import +```yaml + - org.openrewrite.java.AddImport: + type: java.util.List +``` + +## Method Patterns +OpenRewrite uses a specific pointcut expression language for `methodPattern`: +* `[return-type] [fully-qualified-class-name] [method-name]([parameter-types])` +* `*` matches any type. +* `..` matches any number of parameters. +* Example: `java.lang.String split(java.lang.String, int)` +* Example: `* java.util.List add(..)` diff --git a/.gemini/skills/java-ast-refactoring/scripts/run_rewrite.sh b/.gemini/skills/java-ast-refactoring/scripts/run_rewrite.sh new file mode 100755 index 000000000..cf8859864 --- /dev/null +++ b/.gemini/skills/java-ast-refactoring/scripts/run_rewrite.sh @@ -0,0 +1,87 @@ +#!/bin/bash +# Copyright 2026 The Nomulus Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Wrapper script to dynamically execute OpenRewrite without modifying build.gradle + +if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1 +fi + +RECIPE_FILE=$(realpath "$1") +if [ ! -f "$RECIPE_FILE" ]; then + echo "Error: Recipe file $RECIPE_FILE not found." + exit 1 +fi + +# Extract the name of the recipe from the YAML to activate it +RECIPE_NAME=$(grep -oP '(?<=name: ).*' "$RECIPE_FILE" | head -n 1) + +if [ -z "$RECIPE_NAME" ]; then + echo "Error: Could not extract 'name:' from $RECIPE_FILE" + exit 1 +fi + +INIT_SCRIPT="rewrite-init.gradle" + +cat << EOF > "$INIT_SCRIPT" +initscript { + repositories { + mavenCentral() + gradlePluginPortal() + } + dependencies { + classpath("org.openrewrite.rewrite:org.openrewrite.rewrite.gradle.plugin:7.33.0") + } +} + +rootProject { + apply plugin: org.openrewrite.gradle.RewritePlugin + + rewrite { + activeRecipe("$RECIPE_NAME") + } + + dependencies { + rewrite("org.openrewrite.recipe:rewrite-testing-frameworks:2.14.0") + rewrite("org.openrewrite.recipe:rewrite-migrate-java:2.11.0") + rewrite("org.openrewrite.recipe:rewrite-spring:5.7.0") + } +} + +allprojects { + apply plugin: org.openrewrite.gradle.RewritePlugin +} +EOF + +# Copy the recipe file to the workspace root temporarily so OpenRewrite finds it +cp "$RECIPE_FILE" ./rewrite.yml + +echo "Executing OpenRewrite recipe: $RECIPE_NAME" +./gradlew --init-script "$INIT_SCRIPT" rewriteRun --no-parallel --no-configuration-cache + +echo "Running code formatters to fix Checkstyle line-length and import ordering..." +./gradlew spotlessApply + +# Automatically handle line-wrapping and formatting for all files modified by OpenRewrite +MODIFIED_JAVA_FILES=$(git diff --name-only --diff-filter=d | grep "\.java$" || true) +if [ -n "$MODIFIED_JAVA_FILES" ]; then + echo "Applying google-java-format to all modified Java files to enforce LineLength..." + echo "$MODIFIED_JAVA_FILES" | xargs -r google-java-format --replace +fi + +# Clean up temporary files +rm "$INIT_SCRIPT" +rm ./rewrite.yml diff --git a/.gemini/skills/java-ast-refactoring/scripts/safe_rename.py b/.gemini/skills/java-ast-refactoring/scripts/safe_rename.py new file mode 100644 index 000000000..fe2508f3a --- /dev/null +++ b/.gemini/skills/java-ast-refactoring/scripts/safe_rename.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# Copyright 2026 The Nomulus Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +import re +import os + +def usage(): + print("Usage: python safe_rename.py ") + print("Safely renames an identifier in a Java file, ignoring strings and comments.") + sys.exit(1) + +def main(): + if len(sys.argv) < 4: + usage() + + filepath = sys.argv[1] + old_name = sys.argv[2] + new_name = sys.argv[3] + + if not os.path.exists(filepath): + print(f"Error: File {filepath} not found.") + sys.exit(1) + + with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + + # Regex to tokenize Java source safely. + token_pattern = re.compile( + r'(?P"(?:\\.|[^"\\])*")|' + r'(?P\'(?:\\.|[^\'\\])*\')|' + r'(?P//.*)|' + r'(?P/\*[\s\S]*?\*/)|' + r'(?P[a-zA-Z_$][a-zA-Z0-9_$]*)' + ) + + def replacer(match): + if match.group('ident') == old_name: + return new_name + return match.group(0) + + new_content = token_pattern.sub(replacer, content) + + if content == new_content: + print(f"No occurrences of '{old_name}' found to rename in {filepath}.") + else: + with open(filepath, 'w', encoding='utf-8') as f: + f.write(new_content) + print(f"Successfully renamed '{old_name}' to '{new_name}' in {filepath}.") + +if __name__ == '__main__': + main()