Skip to content

Commit 4c84462

Browse files
authored
Add @InlineMe and InlineMethodCalls recipe to replace annotated methods (#5953)
* Add `@InlineMe` and `InlineMethodCalls` recipe to replace annotated methods * Handle repeated arguments * Polish description * Add a test for using an argument twice
1 parent 425c565 commit 4c84462

File tree

3 files changed

+824
-0
lines changed

3 files changed

+824
-0
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.java;
17+
18+
import java.lang.annotation.Documented;
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Target;
21+
22+
@Documented
23+
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
24+
public @interface InlineMe {
25+
String replacement();
26+
27+
String[] imports() default {};
28+
29+
String[] staticImports() default {};
30+
}
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.java;
17+
18+
import lombok.AccessLevel;
19+
import lombok.Getter;
20+
import lombok.Value;
21+
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;
26+
import org.openrewrite.java.tree.*;
27+
28+
import java.util.*;
29+
import java.util.regex.Matcher;
30+
import java.util.regex.Pattern;
31+
32+
import static java.lang.String.format;
33+
import static java.util.Collections.emptySet;
34+
import static java.util.Objects.requireNonNull;
35+
import static java.util.stream.Collectors.toMap;
36+
import static java.util.stream.Collectors.toSet;
37+
38+
public class InlineMethodCalls extends Recipe {
39+
40+
private static final String INLINE_ME = "InlineMe";
41+
42+
@Override
43+
public String getDisplayName() {
44+
return "Inline methods annotated with `@InlineMe`";
45+
}
46+
47+
@Override
48+
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.";
52+
}
53+
54+
@Override
55+
public TreeVisitor<?, ExecutionContext> getVisitor() {
56+
// XXX Preconditions can not yet pick up the `@InlineMe` annotation on methods used
57+
return new JavaVisitor<ExecutionContext>() {
58+
@Override
59+
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;
64+
}
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);
78+
}
79+
80+
@Override
81+
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;
90+
}
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);
100+
}
101+
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;
118+
}
119+
120+
private void removeAndAddImports(MethodCall method, Set<String> templateImports, Set<String> templateStaticImports) {
121+
Set<String> originalImports = findOriginalImports(method);
122+
123+
// Remove regular and static imports that are no longer needed
124+
for (String originalImport : originalImports) {
125+
if (!templateImports.contains(originalImport) &&
126+
!templateStaticImports.contains(originalImport)) {
127+
maybeRemoveImport(originalImport);
128+
}
129+
}
130+
131+
// Add new regular imports needed by the template
132+
for (String importStr : templateImports) {
133+
if (!originalImports.contains(importStr)) {
134+
maybeAddImport(importStr);
135+
}
136+
}
137+
138+
// Add new static imports needed by the template
139+
for (String staticImport : templateStaticImports) {
140+
if (!originalImports.contains(staticImport)) {
141+
int lastDot = staticImport.lastIndexOf('.');
142+
if (0 < lastDot) {
143+
maybeAddImport(
144+
staticImport.substring(0, lastDot),
145+
staticImport.substring(lastDot + 1));
146+
}
147+
}
148+
}
149+
}
150+
151+
private Set<String> findOriginalImports(MethodCall method) {
152+
// Collect all regular and static imports used in the original method call
153+
return new JavaVisitor<Set<String>>() {
154+
@Override
155+
public @Nullable JavaType visitType(@Nullable JavaType javaType, Set<String> strings) {
156+
JavaType jt = super.visitType(javaType, strings);
157+
if (jt instanceof JavaType.FullyQualified) {
158+
strings.add(((JavaType.FullyQualified) jt).getFullyQualifiedName());
159+
}
160+
return jt;
161+
}
162+
163+
@Override
164+
public J visitMethodInvocation(J.MethodInvocation methodInvocation, Set<String> staticImports) {
165+
J.MethodInvocation mi = (J.MethodInvocation) super.visitMethodInvocation(methodInvocation, staticImports);
166+
// Check if this is a static method invocation without a select (meaning it might be statically imported)
167+
JavaType.Method methodType = mi.getMethodType();
168+
if (mi.getSelect() == null && methodType != null && methodType.hasFlags(Flag.Static)) {
169+
staticImports.add(format("%s.%s",
170+
methodType.getDeclaringType().getFullyQualifiedName(),
171+
methodType.getName()));
172+
}
173+
return mi;
174+
}
175+
176+
@Override
177+
public J visitIdentifier(J.Identifier identifier, Set<String> staticImports) {
178+
J.Identifier id = (J.Identifier) super.visitIdentifier(identifier, staticImports);
179+
// Check if this is a static field reference
180+
JavaType.Variable fieldType = id.getFieldType();
181+
if (fieldType != null && fieldType.hasFlags(Flag.Static)) {
182+
if (fieldType.getOwner() instanceof JavaType.FullyQualified) {
183+
staticImports.add(format("%s.%s",
184+
((JavaType.FullyQualified) fieldType.getOwner()).getFullyQualifiedName(),
185+
fieldType.getName()));
186+
}
187+
}
188+
return id;
189+
}
190+
}.reduce(method, new HashSet<>());
191+
}
192+
193+
private J avoidMethodSelfReferences(MethodCall original, J replacement) {
194+
JavaType.Method replacementMethodType = replacement instanceof MethodCall ?
195+
((MethodCall) replacement).getMethodType() : null;
196+
if (replacementMethodType == null) {
197+
return replacement;
198+
}
199+
200+
Cursor cursor = getCursor();
201+
while ((cursor = cursor.getParent()) != null) {
202+
Object value = cursor.getValue();
203+
204+
JavaType.Method cursorMethodType;
205+
if (value instanceof MethodCall) {
206+
cursorMethodType = ((MethodCall) value).getMethodType();
207+
} else if (value instanceof J.MethodDeclaration) {
208+
cursorMethodType = ((J.MethodDeclaration) value).getMethodType();
209+
} else {
210+
continue;
211+
}
212+
if (TypeUtils.isOfType(replacementMethodType, cursorMethodType)) {
213+
return original;
214+
}
215+
}
216+
return replacement;
217+
}
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.getParameterNames());
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, List<String> originalParameterNames) {
264+
String templateString = original instanceof J.MethodInvocation &&
265+
((J.MethodInvocation) original).getSelect() == null &&
266+
replacement.startsWith("this.") ?
267+
replacement.replaceFirst("^this.\\b", "") :
268+
replacement.replaceAll("\\bthis\\b", "#{this:any()}");
269+
for (String parameterName : originalParameterNames) {
270+
// Replace parameter names with their values in the templateString
271+
templateString = templateString
272+
.replaceFirst(format("\\b%s\\b", parameterName), format("#{%s:any()}", parameterName))
273+
.replaceAll(format("(?<!\\{)\\b%s\\b", parameterName), format("#{%s}", parameterName));
274+
}
275+
return templateString;
276+
}
277+
278+
private static List<Object> createParameters(String templateString, MethodCall original) {
279+
Map<String, Expression> lookup = new HashMap<>();
280+
if (original instanceof J.MethodInvocation) {
281+
Expression select = ((J.MethodInvocation) original).getSelect();
282+
if (select != null) {
283+
lookup.put("this", select);
284+
}
285+
}
286+
List<String> originalParameterNames = requireNonNull(original.getMethodType()).getParameterNames();
287+
for (int i = 0; i < originalParameterNames.size(); i++) {
288+
String originalName = originalParameterNames.get(i);
289+
Expression originalValue = original.getArguments().get(i);
290+
lookup.put(originalName, originalValue);
291+
}
292+
List<Object> parameters = new ArrayList<>();
293+
Matcher matcher = TEMPLATE_IDENTIFIER.matcher(templateString);
294+
while (matcher.find()) {
295+
Expression o = lookup.get(matcher.group(1));
296+
if (o != null) {
297+
parameters.add(o);
298+
}
299+
}
300+
return parameters;
301+
}
302+
}
303+
304+
@Value
305+
private static class Template {
306+
String string;
307+
Object[] parameters;
308+
}
309+
}

0 commit comments

Comments
 (0)