diff --git a/README.md b/README.md index 8417d52..f324cb4 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,29 @@ This project implements a [Rewrite module](https://github.com/openrewrite/rewrite) that bundles OpenRewrite recipes maintained by third parties. These recipes are not maintained by the OpenRewrite team, but are still useful for migrating codebases. +## Updating the `@InlineMe` recipes in rewrite-migrate-java + +`InlineMethodCallsRecipeGenerator` turns the `@InlineMe` annotations found in the type tables into an `InlineMethodCalls` recipe list. +The generated YAML is not shipped from here, but copied into the module that owns those recipes, such as [rewrite-migrate-java](https://github.com/openrewrite/rewrite-migrate-java) for Guava. + +1. Refresh the type tables, such that the `+` versions in `recipeDependencies` resolve to the latest releases. + ```bash + ./gradlew createTypeTable createTestTypeTable --refresh-dependencies + ``` + Commit both `src/main/resources/META-INF/rewrite/classpath.tsv.gz` and `src/test/resources/META-INF/rewrite/classpath.tsv.gz`; `parserClasspath` artifacts land in the first, `testParserClasspath` artifacts in the second. +2. Generate the recipes, which prints how many were found for each artifact. + ```bash + ./gradlew generateInlineGuavaMethods generateInlineLog4jMethods + ``` + Each task writes `build/generated/META-INF/rewrite/inline---methods.yml`, with a header naming the exact version the method patterns were generated from, and pointing back here. +3. Copy the generated file into the target repository. + ```bash + cp build/generated/META-INF/rewrite/inline-guava-33-methods.yml \ + ../rewrite-migrate-java/src/main/resources/META-INF/rewrite/ + ``` +4. There, pin `parserClasspath` to the version named in the header of the generated file, and run `./gradlew createTypeTable --refresh-dependencies`, such that the `classpathFromResources` entries resolve against the version the method patterns were generated from. +5. Finish with `./gradlew licenseFormat` to add the license header the generated file lacks, and `./gradlew recipeCsvGenerate` to update the recipe count in `recipes.csv`. + ## Contributing We appreciate all types of contributions. See the [contributing guide](https://github.com/openrewrite/.github/blob/main/CONTRIBUTING.md) for detailed instructions on how to get started. diff --git a/build.gradle.kts b/build.gradle.kts index c3627bb..d980640 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -173,20 +173,20 @@ tasks { args("src/main/resources/META-INF/rewrite/picnic.yml") finalizedBy("licenseFormat") } + // The generated recipes are not shipped from here, but copied into rewrite-migrate-java + val inlineMethodsOutputDir = "build/generated/META-INF/rewrite" val generateInlineGuavaMethods by registering(JavaExec::class) { group = "generate" - description = "Generate Quarkus migration aggregation Recipes." + description = "Generate `InlineMethodCalls` recipes for `@InlineMe` methods in Guava." mainClass = "org.openrewrite.java.internal.parser.InlineMethodCallsRecipeGenerator" classpath = sourceSets.getByName("test").runtimeClasspath - args("guava") - finalizedBy("licenseFormat") + args("guava", inlineMethodsOutputDir) } val generateInlineLog4jMethods by registering(JavaExec::class) { group = "generate" - description = "Generate Quarkus migration aggregation Recipes." + description = "Generate `InlineMethodCalls` recipes for `@InlineMe` methods in Log4j API." mainClass = "org.openrewrite.java.internal.parser.InlineMethodCallsRecipeGenerator" classpath = sourceSets.getByName("test").runtimeClasspath - args("log4j-api") - finalizedBy("licenseFormat") + args("log4j-api", inlineMethodsOutputDir) } } diff --git a/src/main/resources/META-INF/rewrite/classpath.tsv.gz b/src/main/resources/META-INF/rewrite/classpath.tsv.gz index 3408370..a764c8b 100644 Binary files a/src/main/resources/META-INF/rewrite/classpath.tsv.gz and b/src/main/resources/META-INF/rewrite/classpath.tsv.gz differ diff --git a/src/test/java/org/openrewrite/java/internal/parser/InlineMethodCallsRecipeGenerator.java b/src/test/java/org/openrewrite/java/internal/parser/InlineMethodCallsRecipeGenerator.java index 0a1740b..d1782bd 100644 --- a/src/test/java/org/openrewrite/java/internal/parser/InlineMethodCallsRecipeGenerator.java +++ b/src/test/java/org/openrewrite/java/internal/parser/InlineMethodCallsRecipeGenerator.java @@ -25,54 +25,83 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.zip.GZIPInputStream; +import static java.util.Collections.emptyList; import static java.util.Comparator.comparing; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.joining; public class InlineMethodCallsRecipeGenerator { + /** + * ASM is a `runtime` scoped dependency of `rewrite-java`, so `Opcodes` is not on the compile classpath. + */ + private static final int ACC_BRIDGE = 0x0040; + private static final int ACC_SYNTHETIC = 0x1000; + public static void main(String[] args) { - if (args.length < 1) { - System.err.println("Usage: InlineMethodCallsRecipeGenerator "); + if (args.length < 2) { + System.err.println("Usage: InlineMethodCallsRecipeGenerator "); System.exit(1); } - generate(args[0]); + generate(args[0], Path.of(args[1])); } - static void generate(String artifactId) { + static void generate(String artifactId, Path outputDirectory) { 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, classBytes) -> { - // 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); - } - } - } + TypeTable.Reader.Options options = TypeTable.Reader.Options.builder() + .artifactMatcher(artifactIdVersion -> artifactIdVersion.startsWith(artifactId + '-')) + .build(); + try { + // Read the type tables of this project only; dependencies ship their own for other versions + for (String sourceSet : List.of("main", "test")) { + Path typeTable = Path.of("src", sourceSet, "resources").resolve(TypeTable.DEFAULT_RESOURCE_PATH); + if (!Files.exists(typeTable)) { + continue; } - }); + try (InputStream is = Files.newInputStream(typeTable); InputStream inflate = new GZIPInputStream(is)) { + reader.parseTsvAndProcess(inflate, options, (gav, classes, nestedTypes, classBytes) -> + collectInlineMeMethods(gav, classes.values(), inlineMethods)); + } + } - generateYamlRecipes(inlineMethods); + if (inlineMethods.isEmpty()) { + throw new IllegalStateException("No `@InlineMe` annotated methods found for " + artifactId + + "; is it listed as a `parserClasspath` or `testParserClasspath` dependency, and is the type table up to date?"); + } + generateYamlRecipes(inlineMethods, outputDirectory); } catch (IOException e) { throw new RuntimeException(e); } } + private static void collectInlineMeMethods( + TypeTable.GroupArtifactVersion gav, + Collection classes, + List inlineMethods) { + for (TypeTable.ClassDefinition classDef : classes) { + for (TypeTable.Member member : classDef.getMembers()) { + // Bridge methods carry a copy of the annotation of the method they delegate to + if ((member.getAccess() & (ACC_BRIDGE | ACC_SYNTHETIC)) != 0) { + continue; + } + + String annotations = member.getAnnotations(); + if (annotations != null && annotations.contains("InlineMe")) { + InlineMeMethod inlineMethod = extractInlineMeMethod(gav, classDef, member); + if (inlineMethod != null) { + inlineMethods.add(inlineMethod); + } + } + } + } + } + private static @Nullable InlineMeMethod extractInlineMeMethod( TypeTable.GroupArtifactVersion gav, TypeTable.ClassDefinition classDef, @@ -142,35 +171,92 @@ private static String buildMethodPattern(TypeTable.ClassDefinition classDef, Typ String className = classDef.getName().replace('/', '.'); String methodName = member.getName(); - // For constructors, use the class name + // `MethodMatcher` matches constructors on the `` name, not on the simple class name if ("".equals(methodName)) { - methodName = className.substring(className.lastIndexOf('.') + 1); + methodName = ""; } - // Parse method descriptor to extract parameter types - String descriptor = member.getDescriptor(); - String paramPattern = parseMethodParameters(descriptor); + List paramTypes = parseMethodParameters(member.getDescriptor()); - return className + " " + methodName + paramPattern; + // The erasure of a type variable can never match, as call sites resolve it to the argument type + List signatureParams = parseSignatureParameters(member.getSignature()); + if (signatureParams.size() == paramTypes.size()) { + for (int i = 0; i < signatureParams.size(); i++) { + String typeVariable = signatureParams.get(i); + if (typeVariable != null) { + paramTypes.set(i, typeVariable); + } + } + } + + return className + " " + methodName + "(" + String.join(", ", paramTypes) + ")"; } - private static String parseMethodParameters(String descriptor) { + private static List parseMethodParameters(String descriptor) { + List paramTypes = new ArrayList<>(); if (!descriptor.startsWith("(")) { - return "()"; + return paramTypes; } - List paramTypes = new ArrayList<>(); int i = 1; // Skip opening '(' while (i < descriptor.length() && descriptor.charAt(i) != ')') { - String type = parseType(descriptor, i); - paramTypes.add(type); + paramTypes.add(parseType(descriptor, i)); i += getTypeLength(descriptor, i); } + return paramTypes; + } + + /** + * Parse the parameters out of a JVMS 4.7.9.1 generic method signature, returning {@code *} for + * each parameter that is a type variable, and {@code null} for any other parameter. + */ + private static List parseSignatureParameters(@Nullable String signature) { + if (signature == null) { + return emptyList(); + } + int open = signature.indexOf('('); // Skip any formal type parameters + if (open == -1) { + return emptyList(); + } - if (paramTypes.isEmpty()) { - return "()"; + List paramTypes = new ArrayList<>(); + int i = open + 1; + while (i < signature.length() && signature.charAt(i) != ')') { + int dimensions = 0; + while (signature.charAt(i) == '[') { + dimensions++; + i++; + } + boolean typeVariable = signature.charAt(i) == 'T'; + paramTypes.add(typeVariable ? "*" + "[]".repeat(dimensions) : null); + i = endOfSignatureType(signature, i); + } + return paramTypes; + } + + private static int endOfSignatureType(String signature, int start) { + char c = signature.charAt(start); + if (c != 'L' && c != 'T') { + return start + 1; // Primitive + } + // Scan past any nested type arguments to the terminating semicolon + int depth = 0; + for (int i = start; i < signature.length(); i++) { + switch (signature.charAt(i)) { + case '<': + depth++; + break; + case '>': + depth--; + break; + case ';': + if (depth == 0) { + return i + 1; + } + break; + } } - return "(" + String.join(", ", paramTypes) + ")"; + throw new IllegalArgumentException("Unterminated type at index " + start + " in signature " + signature); } private static String parseType(String descriptor, int start) { @@ -212,13 +298,14 @@ private static int getTypeLength(String descriptor, int start) { }; } - private static void generateYamlRecipes(List methods) throws IOException { + private static void generateYamlRecipes(List methods, Path outputDirectory) 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/test/resources/META-INF/rewrite/inline-%s-methods.yml".formatted(firstMethod.classpathResource)); + Path outputPath = outputDirectory.resolve("inline-%s-methods.yml".formatted(firstMethod.classpathResource)); + Files.createDirectories(outputDirectory); StringBuilder yaml = new StringBuilder(); yaml.append("#\n"); @@ -226,6 +313,7 @@ private static void generateYamlRecipes(List methods) throws IOE .append(gav.getGroupId()).append(":") .append(gav.getArtifactId()).append(":") .append(gav.getVersion()).append("`\n"); + yaml.append("# by `InlineMethodCallsRecipeGenerator` in https://github.com/openrewrite/rewrite-third-party\n"); yaml.append("#\n\n"); yaml.append("type: specs.openrewrite.org/v1beta/recipe\n"); @@ -260,7 +348,7 @@ private static void generateYamlRecipes(List methods) throws IOE yaml.append(" - '").append(escapeYaml(method.classpathResource)).append("'\n"); } - Files.write(outputPath, yaml.toString().getBytes()); + Files.writeString(outputPath, yaml); System.out.println("Generated " + methods.size() + " inline recipes to " + outputPath); } diff --git a/src/test/resources/META-INF/rewrite/classpath.tsv.gz b/src/test/resources/META-INF/rewrite/classpath.tsv.gz new file mode 100644 index 0000000..6384694 Binary files /dev/null and b/src/test/resources/META-INF/rewrite/classpath.tsv.gz differ