Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<artifact>-<major>-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.
12 changes: 6 additions & 6 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Binary file modified src/main/resources/META-INF/rewrite/classpath.tsv.gz
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -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 <artifactId>");
if (args.length < 2) {
System.err.println("Usage: InlineMethodCallsRecipeGenerator <artifactId> <outputDirectory>");
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<InlineMeMethod> 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<TypeTable.ClassDefinition> classes,
List<InlineMeMethod> 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,
Expand Down Expand Up @@ -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 `<constructor>` name, not on the simple class name
if ("<init>".equals(methodName)) {
methodName = className.substring(className.lastIndexOf('.') + 1);
methodName = "<constructor>";
}

// Parse method descriptor to extract parameter types
String descriptor = member.getDescriptor();
String paramPattern = parseMethodParameters(descriptor);
List<String> 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<String> 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<String> parseMethodParameters(String descriptor) {
List<String> paramTypes = new ArrayList<>();
if (!descriptor.startsWith("(")) {
return "()";
return paramTypes;
}

List<String> 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<String> 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<String> 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) {
Expand Down Expand Up @@ -212,20 +298,22 @@ private static int getTypeLength(String descriptor, int start) {
};
}

private static void generateYamlRecipes(List<InlineMeMethod> methods) throws IOException {
private static void generateYamlRecipes(List<InlineMeMethod> 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");
yaml.append("# Recipes generated for `@InlineMe` annotated methods in `")
.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");
Expand Down Expand Up @@ -260,7 +348,7 @@ private static void generateYamlRecipes(List<InlineMeMethod> 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);
}

Expand Down
Binary file not shown.
Loading