diff --git a/build.gradle.kts b/build.gradle.kts index 754ebaf..e28f7f7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -104,4 +104,20 @@ tasks { mainClass = "org.openrewrite.recipe.quarkus.internal.AggregateQuarkusUpdates" classpath = sourceSets.getByName("test").runtimeClasspath } + val generateInlineGuavaMethods by registering(JavaExec::class) { + group = "generate" + description = "Generate Quarkus migration aggregation Recipes." + mainClass = "org.openrewrite.java.internal.parser.InlineMethodCallsRecipeGenerator" + classpath = sourceSets.getByName("test").runtimeClasspath + args("guava") + finalizedBy("licenseFormat") + } + val generateInlineLog4jMethods by registering(JavaExec::class) { + group = "generate" + description = "Generate Quarkus migration aggregation Recipes." + mainClass = "org.openrewrite.java.internal.parser.InlineMethodCallsRecipeGenerator" + classpath = sourceSets.getByName("test").runtimeClasspath + args("log4j-api") + finalizedBy("licenseFormat") + } } diff --git a/src/main/resources/META-INF/rewrite/inline-guava-methods.yml b/src/main/resources/META-INF/rewrite/inline-guava-33-methods.yml similarity index 99% rename from src/main/resources/META-INF/rewrite/inline-guava-methods.yml rename to src/main/resources/META-INF/rewrite/inline-guava-33-methods.yml index 3a0cb12..c83ddf5 100644 --- a/src/main/resources/META-INF/rewrite/inline-guava-methods.yml +++ b/src/main/resources/META-INF/rewrite/inline-guava-33-methods.yml @@ -1,3 +1,4 @@ +# # Copyright 2025 the original author or authors. #

# Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,7 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# Generated InlineMe recipes from TypeTable + +# +# Recipes generated for `@InlineMe` annotated methods in `com.google.guava:guava:33.5.0-jre` # type: specs.openrewrite.org/v1beta/recipe @@ -22,7 +25,6 @@ description: >- Automatically generated recipes to inline method calls based on `@InlineMe` annotations discovered in the type table. recipeList: - # From com.google.guava:guava:33.5.0-jre - org.openrewrite.java.InlineMethodCalls: methodPattern: 'com.google.common.primitives.Booleans hashCode(boolean)' replacement: 'Boolean.hashCode(value)' diff --git a/src/main/resources/META-INF/rewrite/inline-log4j-api-methods.yml b/src/main/resources/META-INF/rewrite/inline-log4j-api-2-methods.yml similarity index 93% rename from src/main/resources/META-INF/rewrite/inline-log4j-api-methods.yml rename to src/main/resources/META-INF/rewrite/inline-log4j-api-2-methods.yml index 3f5bd3d..d194a99 100644 --- a/src/main/resources/META-INF/rewrite/inline-log4j-api-methods.yml +++ b/src/main/resources/META-INF/rewrite/inline-log4j-api-2-methods.yml @@ -1,3 +1,4 @@ +# # Copyright 2025 the original author or authors. #

# Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,24 +13,18 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# Generated InlineMe recipes from TypeTable + +# +# Recipes generated for `@InlineMe` annotated methods in `org.apache.logging.log4j:log4j-api:2.25.2` # type: specs.openrewrite.org/v1beta/recipe name: org.apache.logging.log4j.InlineLog4jApiMethods -displayName: Inline `log4j-api-2` methods annotated with `@InlineMe` +displayName: Inline `log4j-api` methods annotated with `@InlineMe` description: >- Automatically generated recipes to inline method calls based on `@InlineMe` annotations discovered in the type table. recipeList: - # From org.apache.logging.log4j:log4j-api:2.25.2 - - org.openrewrite.java.InlineMethodCalls: - methodPattern: 'org.apache.logging.log4j.message.ParameterizedMessage ParameterizedMessage(java.lang.String, java.lang.String[], java.lang.Throwable)' - replacement: 'this(pattern, Arrays.stream(args).toArray(Object[]::new), throwable)' - imports: - - 'java.util.Arrays' - classpathFromResources: - - 'log4j-api-2' - org.openrewrite.java.InlineMethodCalls: methodPattern: 'org.apache.logging.log4j.message.StructuredDataId StructuredDataId(java.lang.String, int, java.lang.String[], java.lang.String[])' replacement: 'this(name, String.valueOf(enterpriseNumber), required, optional)' @@ -45,6 +40,13 @@ recipeList: replacement: 'this.makeId(defaultId, String.valueOf(anEnterpriseNumber))' classpathFromResources: - 'log4j-api-2' + - org.openrewrite.java.InlineMethodCalls: + methodPattern: 'org.apache.logging.log4j.message.ParameterizedMessage ParameterizedMessage(java.lang.String, java.lang.String[], java.lang.Throwable)' + replacement: 'this(pattern, Arrays.stream(args).toArray(Object[]::new), throwable)' + imports: + - 'java.util.Arrays' + classpathFromResources: + - 'log4j-api-2' - org.openrewrite.java.InlineMethodCalls: methodPattern: 'org.apache.logging.log4j.MarkerManager getMarker(java.lang.String, org.apache.logging.log4j.Marker)' replacement: 'MarkerManager.getMarker(name).addParents(parent)' diff --git a/src/test/java/com/google/guava/InlineGuavaMethodsTest.java b/src/test/java/com/google/guava/InlineGuavaMethodsTest.java index 65167f7..873c67d 100644 --- a/src/test/java/com/google/guava/InlineGuavaMethodsTest.java +++ b/src/test/java/com/google/guava/InlineGuavaMethodsTest.java @@ -26,9 +26,7 @@ class InlineGuavaMethodsTest implements RewriteTest { @Override public void defaults(RecipeSpec spec) { - spec.recipeFromResource( - "/META-INF/rewrite/inline-guava-methods.yml", - "com.google.guava.InlineGuavaMethods"); + spec.recipeFromResources("com.google.guava.InlineGuavaMethods"); } @DocumentExample diff --git a/src/test/java/org/openrewrite/java/internal/parser/InlineMethodCallsRecipeGenerator.java b/src/test/java/org/openrewrite/java/internal/parser/InlineMethodCallsRecipeGenerator.java new file mode 100644 index 0000000..a6c098e --- /dev/null +++ b/src/test/java/org/openrewrite/java/internal/parser/InlineMethodCallsRecipeGenerator.java @@ -0,0 +1,278 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * 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 + *

+ * https://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. + */ +package org.openrewrite.java.internal.parser; + +import org.jspecify.annotations.Nullable; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.internal.StringUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.zip.GZIPInputStream; + +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.joining; + +public class InlineMethodCallsRecipeGenerator { + + public static void main(String[] args) { + if (args.length < 1) { + System.err.println("Usage: InlineMethodCallsRecipeGenerator "); + System.exit(1); + } + generate(args[0]); + } + + static void generate(String artifactId) { + List inlineMethods = new ArrayList<>(); + + TypeTable.Reader reader = new TypeTable.Reader(new InMemoryExecutionContext()); + try (InputStream is = ClassLoader.getSystemResourceAsStream(TypeTable.DEFAULT_RESOURCE_PATH); InputStream inflate = new GZIPInputStream(is)) { + TypeTable.Reader.Options options = TypeTable.Reader.Options.builder() + .artifactMatcher(artifactIdVersion -> artifactIdVersion.startsWith(artifactId + '-')) + .build(); + reader.parseTsvAndProcess(inflate, options, (gav, classes, nestedTypes) -> { + // Process each class in this GAV + for (TypeTable.ClassDefinition classDef : classes.values()) { + // Process each member (method/constructor) in the class + for (TypeTable.Member member : classDef.getMembers()) { + // Check if member has @InlineMe annotation + String annotations = member.getAnnotations(); + if (annotations != null && annotations.contains("InlineMe")) { + InlineMeMethod inlineMethod = extractInlineMeMethod(gav, classDef, member); + if (inlineMethod != null) { + inlineMethods.add(inlineMethod); + } + } + } + } + }); + + generateYamlRecipes(inlineMethods); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static @Nullable InlineMeMethod extractInlineMeMethod( + TypeTable.GroupArtifactVersion gav, + TypeTable.ClassDefinition classDef, + TypeTable.Member member) { + try { + // Parse the annotations to find @InlineMe + List annotations = + AnnotationDeserializer.parseAnnotations(requireNonNull(member.getAnnotations())); + for (AnnotationDeserializer.AnnotationInfo annotation : annotations) { + if (!annotation.getDescriptor().endsWith("/InlineMe;")) { + continue; + } + + List attributes = annotation.getAttributes(); + if (attributes == null) { + continue; + } + + // Extract annotation values + String replacement = null; + List imports = new ArrayList<>(); + List staticImports = new ArrayList<>(); + + for (AnnotationDeserializer.AttributeInfo attr : attributes) { + switch (attr.getName()) { + case "replacement": + replacement = (String) attr.getValue(); + break; + case "imports": + if (attr.getValue() instanceof Object[]) { + for (Object imp : (Object[]) attr.getValue()) { + imports.add((String) imp); + } + } + break; + case "staticImports": + if (attr.getValue() instanceof Object[]) { + for (Object imp : (Object[]) attr.getValue()) { + staticImports.add((String) imp); + } + } + break; + } + } + + if (replacement != null) { + // Build the method pattern + String methodPattern = buildMethodPattern(classDef, member); + String classpathResource = gav.getArtifactId() + "-" + gav.getVersion().substring(0, gav.getVersion().indexOf('.')); + return new InlineMeMethod( + gav, + methodPattern, + replacement, + imports, + staticImports, + classpathResource); + } + } + } catch (Exception e) { + System.err.println("Failed to parse annotations for " + classDef.getName() + "." + member.getName() + ": " + e.getMessage()); + } + + return null; + } + + private static String buildMethodPattern(TypeTable.ClassDefinition classDef, TypeTable.Member member) { + String className = classDef.getName().replace('/', '.'); + String methodName = member.getName(); + + // For constructors, use the class name + if ("".equals(methodName)) { + methodName = className.substring(className.lastIndexOf('.') + 1); + } + + // Parse method descriptor to extract parameter types + String descriptor = member.getDescriptor(); + String paramPattern = parseMethodParameters(descriptor); + + return className + " " + methodName + paramPattern; + } + + private static String parseMethodParameters(String descriptor) { + if (!descriptor.startsWith("(")) { + return "()"; + } + + List paramTypes = new ArrayList<>(); + int i = 1; // Skip opening '(' + while (i < descriptor.length() && descriptor.charAt(i) != ')') { + String type = parseType(descriptor, i); + paramTypes.add(type); + i += getTypeLength(descriptor, i); + } + + if (paramTypes.isEmpty()) { + return "()"; + } + return "(" + String.join(", ", paramTypes) + ")"; + } + + private static String parseType(String descriptor, int start) { + char c = descriptor.charAt(start); + return switch (c) { + case 'B' -> "byte"; + case 'C' -> "char"; + case 'D' -> "double"; + case 'F' -> "float"; + case 'I' -> "int"; + case 'J' -> "long"; + case 'S' -> "short"; + case 'Z' -> "boolean"; + case 'V' -> "void"; + case 'L' -> { + // Object type - extract class name + int semicolon = descriptor.indexOf(';', start); + String className = descriptor.substring(start + 1, semicolon); + yield className.replace('/', '.'); + } + case '[' -> { + // Array type + String elementType = parseType(descriptor, start + 1); + yield elementType + "[]"; + } + default -> "Object"; // Fallback + }; + } + + private static int getTypeLength(String descriptor, int start) { + char c = descriptor.charAt(start); + return switch (c) { + case 'B', 'C', 'D', 'F', 'I', 'J', 'S', 'Z', 'V' -> 1; + // Object type - find the semicolon + case 'L' -> descriptor.indexOf(';', start) - start + 1; + // Array type - recurse for element type + case '[' -> 1 + getTypeLength(descriptor, start + 1); + default -> 1; + }; + } + + private static void generateYamlRecipes(List methods) throws IOException { + InlineMeMethod firstMethod = methods.getFirst(); + TypeTable.GroupArtifactVersion gav = firstMethod.gav(); + String moduleName = Arrays.stream(gav.getArtifactId().split("-")) + .map(StringUtils::capitalize) + .collect(joining()); + Path outputPath = Path.of("src/main/resources/META-INF/rewrite/inline-%s-methods.yml".formatted(firstMethod.classpathResource)); + + StringBuilder yaml = new StringBuilder(); + yaml.append("#\n"); + yaml.append("# Recipes generated for `@InlineMe` annotated methods in `") + .append(gav.getGroupId()).append(":") + .append(gav.getArtifactId()).append(":") + .append(gav.getVersion()).append("`\n"); + yaml.append("#\n\n"); + + yaml.append("type: specs.openrewrite.org/v1beta/recipe\n"); + yaml.append("name: ").append(gav.getGroupId()).append(".Inline").append(moduleName).append("Methods").append("\n"); + yaml.append("displayName: Inline `").append(gav.getArtifactId()).append("` methods annotated with `@InlineMe`\n"); + yaml.append("description: >-\n"); + yaml.append(" Automatically generated recipes to inline method calls based on `@InlineMe` annotations\n"); + yaml.append(" discovered in the type table.\n"); + yaml.append("recipeList:\n"); + + for (InlineMeMethod method : methods) { + yaml.append(" - org.openrewrite.java.InlineMethodCalls:\n"); + yaml.append(" methodPattern: '").append(escapeYaml(method.methodPattern)).append("'\n"); + yaml.append(" replacement: '").append(escapeYaml(method.replacement)).append("'\n"); + + if (!method.imports.isEmpty()) { + yaml.append(" imports:\n"); + for (String imp : method.imports) { + yaml.append(" - '").append(escapeYaml(imp)).append("'\n"); + } + } + + if (!method.staticImports.isEmpty()) { + yaml.append(" staticImports:\n"); + for (String imp : method.staticImports) { + yaml.append(" - '").append(escapeYaml(imp)).append("'\n"); + } + } + + yaml.append(" classpathFromResources:\n"); + yaml.append(" - '").append(escapeYaml(method.classpathResource)).append("'\n"); + } + + Files.write(outputPath, yaml.toString().getBytes()); + System.out.println("Generated " + methods.size() + " inline recipes to " + outputPath); + } + + private static String escapeYaml(String value) { + // Escape single quotes by doubling them + return value.replace("'", "''"); + } + + private record InlineMeMethod( + TypeTable.GroupArtifactVersion gav, + String methodPattern, + String replacement, + List imports, + List staticImports, + String classpathResource) { + } +}