Skip to content

Commit 6fa7386

Browse files
authored
Inline annotated methods through generated recipes to support multiple versions (#6059)
* Generate declarative recipe to inline method calls * Adjust `InlineMethodCalls` to take in options instead of method type annotations * Drop explicit constructors * Support `.classpathFromResources` * Extract a generate method * Generate classpathFromResources arguments as well * Apply formatter * Tweak markdown * Include the parameter types in the generated recipe method patterns * Expect and write Gzipped output stream * Accept any `InlineMe` annotation * Minimize indentation * Add incubating annotation * Add a third argument for the generated recipe name * Trim the descriptions * Apply suggestions * Apply suggestions * Remove local `Template` class * Delete rewrite-java/src/test/resources/test-typetable.tsv
1 parent 02a598d commit 6fa7386

File tree

5 files changed

+674
-253
lines changed

5 files changed

+674
-253
lines changed

rewrite-java/src/main/java/org/openrewrite/java/InlineMethodCalls.java

Lines changed: 122 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,11 @@
1515
*/
1616
package org.openrewrite.java;
1717

18-
import lombok.AccessLevel;
19-
import lombok.Getter;
18+
import lombok.EqualsAndHashCode;
2019
import lombok.Value;
2120
import org.jspecify.annotations.Nullable;
22-
import org.openrewrite.Cursor;
23-
import org.openrewrite.ExecutionContext;
24-
import org.openrewrite.Recipe;
25-
import org.openrewrite.TreeVisitor;
21+
import org.openrewrite.*;
22+
import org.openrewrite.java.search.UsesMethod;
2623
import org.openrewrite.java.tree.*;
2724

2825
import java.util.*;
@@ -32,89 +29,81 @@
3229
import static java.lang.String.format;
3330
import static java.util.Collections.emptySet;
3431
import static java.util.Objects.requireNonNull;
35-
import static java.util.stream.Collectors.toMap;
36-
import static java.util.stream.Collectors.toSet;
3732

33+
@Incubating(since = "8.63.0")
34+
@EqualsAndHashCode(callSuper = false)
35+
@Value
3836
public class InlineMethodCalls extends Recipe {
37+
private static final Pattern TEMPLATE_IDENTIFIER = Pattern.compile("#\\{(\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*):any\\(.*?\\)}");
3938

40-
private static final String INLINE_ME = "InlineMe";
39+
@Option(displayName = "Method pattern",
40+
description = "A method pattern that is used to find matching method invocations.",
41+
example = "com.google.common.base.Preconditions checkNotNull(..)")
42+
String methodPattern;
43+
44+
@Option(displayName = "Replacement template",
45+
description = "The replacement template for the method invocation. Parameters can be referenced using their names from the original method.",
46+
example = "java.util.Objects.requireNonNull(#{p0})")
47+
String replacement;
48+
49+
@Option(displayName = "Imports",
50+
description = "List of regular imports to add when the replacement is made.",
51+
required = false,
52+
example = "[\"java.util.Objects\"]")
53+
@Nullable
54+
Set<String> imports;
55+
56+
@Option(displayName = "Static imports",
57+
description = "List of static imports to add when the replacement is made.",
58+
required = false,
59+
example = "[\"java.util.Collections.emptyList\"]")
60+
@Nullable
61+
Set<String> staticImports;
62+
63+
@Option(displayName = "Classpath from resources",
64+
description = "List of paths to JAR files on the classpath for parsing the replacement template.",
65+
required = false,
66+
example = "[\"guava-33.4.8-jre\"]")
67+
@Nullable
68+
Set<String> classpathFromResources;
4169

4270
@Override
4371
public String getDisplayName() {
44-
return "Inline methods annotated with `@InlineMe`";
72+
return "Inline method calls";
4573
}
4674

4775
@Override
4876
public String getDescription() {
49-
return "Apply inlinings as defined by Error Prone's [`@InlineMe` annotation](https://errorprone.info/docs/inlineme), " +
50-
"or compatible annotations. Uses the template and method arguments to replace method calls. " +
51-
"Supports both methods invocations and constructor calls, with optional new imports.";
77+
return "Inline method calls using a template replacement pattern. " +
78+
"Supports both method invocations and constructor calls, with optional imports.";
5279
}
5380

5481
@Override
5582
public TreeVisitor<?, ExecutionContext> getVisitor() {
56-
// XXX Preconditions can not yet pick up the `@InlineMe` annotation on methods used
57-
return new JavaVisitor<ExecutionContext>() {
83+
MethodMatcher matcher = new MethodMatcher(methodPattern, true);
84+
return Preconditions.check(new UsesMethod<>(methodPattern), new JavaVisitor<ExecutionContext>() {
5885
@Override
5986
public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
60-
J.MethodInvocation mi = (J.MethodInvocation) super.visitMethodInvocation(method, ctx);
61-
InlineMeValues values = findInlineMeValues(mi.getMethodType());
62-
if (values == null) {
63-
return mi;
87+
if (matcher.matches(method)) {
88+
return replaceMethodCall(method, ctx);
6489
}
65-
Template template = values.template(mi);
66-
if (template == null) {
67-
return mi;
68-
}
69-
removeAndAddImports(method, values.getImports(), values.getStaticImports());
70-
J replacement = JavaTemplate.builder(template.getString())
71-
.contextSensitive()
72-
.imports(values.getImports().toArray(new String[0]))
73-
.staticImports(values.getStaticImports().toArray(new String[0]))
74-
.javaParser(JavaParser.fromJavaVersion().classpath(JavaParser.runtimeClasspath()))
75-
.build()
76-
.apply(updateCursor(mi), mi.getCoordinates().replace(), template.getParameters());
77-
return avoidMethodSelfReferences(mi, replacement);
90+
return super.visitMethodInvocation(method, ctx);
7891
}
7992

8093
@Override
8194
public J visitNewClass(J.NewClass newClass, ExecutionContext ctx) {
82-
J.NewClass nc = (J.NewClass) super.visitNewClass(newClass, ctx);
83-
InlineMeValues values = findInlineMeValues(nc.getConstructorType());
84-
if (values == null) {
85-
return nc;
86-
}
87-
Template template = values.template(nc);
88-
if (template == null) {
89-
return nc;
95+
if (matcher.matches(newClass)) {
96+
return replaceMethodCall(newClass, ctx);
9097
}
91-
removeAndAddImports(newClass, values.getImports(), values.getStaticImports());
92-
J replacement = JavaTemplate.builder(template.getString())
93-
.contextSensitive()
94-
.imports(values.getImports().toArray(new String[0]))
95-
.staticImports(values.getStaticImports().toArray(new String[0]))
96-
.javaParser(JavaParser.fromJavaVersion().classpath(JavaParser.runtimeClasspath()))
97-
.build()
98-
.apply(updateCursor(nc), nc.getCoordinates().replace(), template.getParameters());
99-
return avoidMethodSelfReferences(nc, replacement);
98+
return super.visitNewClass(newClass, ctx);
10099
}
101100

102-
private @Nullable InlineMeValues findInlineMeValues(JavaType.@Nullable Method methodType) {
103-
if (methodType == null) {
104-
return null;
105-
}
106-
List<String> parameterNames = methodType.getParameterNames();
107-
if (!parameterNames.isEmpty() && "arg0".equals(parameterNames.get(0))) {
108-
return null; // We need `-parameters` before we're able to substitute parameters in the template
109-
}
110-
111-
List<JavaType.FullyQualified> annotations = methodType.getAnnotations();
112-
for (JavaType.FullyQualified annotation : annotations) {
113-
if (INLINE_ME.equals(annotation.getClassName())) {
114-
return InlineMeValues.parse((JavaType.Annotation) annotation);
115-
}
116-
}
117-
return null;
101+
private J replaceMethodCall(MethodCall methodCall, ExecutionContext ctx) {
102+
Set<String> importsSet = imports != null ? imports : emptySet();
103+
Set<String> staticImportsSet = staticImports != null ? staticImports : emptySet();
104+
removeAndAddImports(methodCall, importsSet, staticImportsSet);
105+
J applied = applyJavaTemplate(methodCall, getCursor(), importsSet, staticImportsSet, ctx);
106+
return avoidMethodSelfReferences(methodCall, applied);
118107
}
119108

120109
private void removeAndAddImports(MethodCall method, Set<String> templateImports, Set<String> templateStaticImports) {
@@ -152,10 +141,10 @@ private Set<String> findOriginalImports(MethodCall method) {
152141
// Collect all regular and static imports used in the original method call
153142
return new JavaVisitor<Set<String>>() {
154143
@Override
155-
public @Nullable JavaType visitType(@Nullable JavaType javaType, Set<String> strings) {
156-
JavaType jt = super.visitType(javaType, strings);
144+
public @Nullable JavaType visitType(@Nullable JavaType javaType, Set<String> imports) {
145+
JavaType jt = super.visitType(javaType, imports);
157146
if (jt instanceof JavaType.FullyQualified) {
158-
strings.add(((JavaType.FullyQualified) jt).getFullyQualifiedName());
147+
imports.add(((JavaType.FullyQualified) jt).getFullyQualifiedName());
159148
}
160149
return jt;
161150
}
@@ -190,6 +179,70 @@ public J visitIdentifier(J.Identifier identifier, Set<String> staticImports) {
190179
}.reduce(method, new HashSet<>());
191180
}
192181

182+
J applyJavaTemplate(MethodCall methodCall, Cursor cursor, Set<String> importsSet, Set<String> staticImportsSet, ExecutionContext ctx) {
183+
JavaType.Method methodType = requireNonNull(methodCall.getMethodType());
184+
String string = createTemplateString(methodCall, methodType);
185+
Object[] parameters = createParameters(string, methodCall);
186+
187+
JavaTemplate.Builder templateBuilder = JavaTemplate.builder(string)
188+
.contextSensitive()
189+
.imports(importsSet.toArray(new String[0]))
190+
.staticImports(staticImportsSet.toArray(new String[0]));
191+
if (classpathFromResources != null && !classpathFromResources.isEmpty()) {
192+
templateBuilder.javaParser(JavaParser.fromJavaVersion()
193+
.classpathFromResources(ctx, classpathFromResources.toArray(new String[0])));
194+
}
195+
return templateBuilder.build()
196+
.apply(cursor, methodCall.getCoordinates().replace(), parameters);
197+
}
198+
199+
private String createTemplateString(MethodCall original, JavaType.Method methodType) {
200+
String templateString;
201+
if (original instanceof J.NewClass && replacement.startsWith("this(")) {
202+
// For constructor-to-constructor replacement, replace "this" with "new ClassName"
203+
templateString = "new " + methodType.getDeclaringType().getClassName() + replacement.substring(4);
204+
} else if (original instanceof J.MethodInvocation &&
205+
((J.MethodInvocation) original).getSelect() == null &&
206+
replacement.startsWith("this.")) {
207+
templateString = replacement.substring(5);
208+
} else {
209+
templateString = replacement.replaceAll("\\bthis\\b", "#{this:any()}");
210+
}
211+
List<String> originalParameterNames = methodType.getParameterNames();
212+
for (String parameterName : originalParameterNames) {
213+
// Replace parameter names with their values in the templateString
214+
templateString = templateString
215+
.replaceFirst(format("\\b%s\\b", parameterName), format("#{%s:any()}", parameterName))
216+
.replaceAll(format("(?<!\\{)\\b%s\\b", parameterName), format("#{%s}", parameterName));
217+
}
218+
return templateString;
219+
}
220+
221+
private Object[] createParameters(String templateString, MethodCall original) {
222+
Map<String, Expression> lookup = new HashMap<>();
223+
if (original instanceof J.MethodInvocation) {
224+
Expression select = ((J.MethodInvocation) original).getSelect();
225+
if (select != null) {
226+
lookup.put("this", select);
227+
}
228+
}
229+
List<String> originalParameterNames = requireNonNull(original.getMethodType()).getParameterNames();
230+
for (int i = 0; i < originalParameterNames.size(); i++) {
231+
String originalName = originalParameterNames.get(i);
232+
Expression originalValue = original.getArguments().get(i);
233+
lookup.put(originalName, originalValue);
234+
}
235+
List<Object> parameters = new ArrayList<>();
236+
Matcher matcher = TEMPLATE_IDENTIFIER.matcher(templateString);
237+
while (matcher.find()) {
238+
Expression o = lookup.get(matcher.group(1));
239+
if (o != null) {
240+
parameters.add(o);
241+
}
242+
}
243+
return parameters.toArray();
244+
}
245+
193246
private J avoidMethodSelfReferences(MethodCall original, J replacement) {
194247
JavaType.Method replacementMethodType = replacement instanceof MethodCall ?
195248
((MethodCall) replacement).getMethodType() : null;
@@ -215,102 +268,6 @@ private J avoidMethodSelfReferences(MethodCall original, J replacement) {
215268
}
216269
return replacement;
217270
}
218-
};
219-
}
220-
221-
@Value
222-
private static class InlineMeValues {
223-
private static final Pattern TEMPLATE_IDENTIFIER = Pattern.compile("#\\{(\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*):any\\(.*?\\)}");
224-
225-
@Getter(AccessLevel.NONE)
226-
String replacement;
227-
228-
Set<String> imports;
229-
Set<String> staticImports;
230-
231-
static InlineMeValues parse(JavaType.Annotation annotation) {
232-
Map<String, Object> collect = annotation.getValues().stream().collect(toMap(
233-
e -> ((JavaType.Method) e.getElement()).getName(),
234-
JavaType.Annotation.ElementValue::getValue
235-
));
236-
// Parse imports and static imports from the annotation values
237-
return new InlineMeValues(
238-
(String) collect.get("replacement"),
239-
parseImports(collect.get("imports")),
240-
parseImports(collect.get("staticImports")));
241-
}
242-
243-
private static Set<String> parseImports(@Nullable Object importsValue) {
244-
if (importsValue instanceof List) {
245-
return ((List<?>) importsValue).stream()
246-
.map(Object::toString)
247-
.collect(toSet());
248-
}
249-
return emptySet();
250-
}
251-
252-
@Nullable
253-
Template template(MethodCall original) {
254-
JavaType.Method methodType = original.getMethodType();
255-
if (methodType == null) {
256-
return null;
257-
}
258-
String templateString = createTemplateString(original, replacement, methodType);
259-
List<Object> parameters = createParameters(templateString, original);
260-
return new Template(templateString, parameters.toArray(new Object[0]));
261-
}
262-
263-
private static String createTemplateString(MethodCall original, String replacement, JavaType.Method methodType) {
264-
String templateString;
265-
if (original instanceof J.NewClass && replacement.startsWith("this(")) {
266-
// For constructor-to-constructor replacement, replace "this" with "new ClassName"
267-
templateString = "new " + methodType.getDeclaringType().getClassName() + replacement.substring(4);
268-
} else if (original instanceof J.MethodInvocation &&
269-
((J.MethodInvocation) original).getSelect() == null &&
270-
replacement.startsWith("this.")) {
271-
templateString = replacement.substring(5);
272-
} else {
273-
templateString = replacement.replaceAll("\\bthis\\b", "#{this:any()}");
274-
}
275-
List<String> originalParameterNames = methodType.getParameterNames();
276-
for (String parameterName : originalParameterNames) {
277-
// Replace parameter names with their values in the templateString
278-
templateString = templateString
279-
.replaceFirst(format("\\b%s\\b", parameterName), format("#{%s:any()}", parameterName))
280-
.replaceAll(format("(?<!\\{)\\b%s\\b", parameterName), format("#{%s}", parameterName));
281-
}
282-
return templateString;
283-
}
284-
285-
private static List<Object> createParameters(String templateString, MethodCall original) {
286-
Map<String, Expression> lookup = new HashMap<>();
287-
if (original instanceof J.MethodInvocation) {
288-
Expression select = ((J.MethodInvocation) original).getSelect();
289-
if (select != null) {
290-
lookup.put("this", select);
291-
}
292-
}
293-
List<String> originalParameterNames = requireNonNull(original.getMethodType()).getParameterNames();
294-
for (int i = 0; i < originalParameterNames.size(); i++) {
295-
String originalName = originalParameterNames.get(i);
296-
Expression originalValue = original.getArguments().get(i);
297-
lookup.put(originalName, originalValue);
298-
}
299-
List<Object> parameters = new ArrayList<>();
300-
Matcher matcher = TEMPLATE_IDENTIFIER.matcher(templateString);
301-
while (matcher.find()) {
302-
Expression o = lookup.get(matcher.group(1));
303-
if (o != null) {
304-
parameters.add(o);
305-
}
306-
}
307-
return parameters;
308-
}
309-
}
310-
311-
@Value
312-
private static class Template {
313-
String string;
314-
Object[] parameters;
271+
});
315272
}
316273
}

0 commit comments

Comments
 (0)