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) {
+ }
+}