Skip to content

Commit a3e22b4

Browse files
committed
feat: add recipe that converts explicit getters to the lombok annotation
1 parent 0bd6e06 commit a3e22b4

File tree

3 files changed

+692
-0
lines changed

3 files changed

+692
-0
lines changed
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Copyright 2021 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.migrate.lombok;
17+
18+
import lombok.AccessLevel;
19+
import lombok.EqualsAndHashCode;
20+
import lombok.Value;
21+
import org.openrewrite.ExecutionContext;
22+
import org.openrewrite.Recipe;
23+
import org.openrewrite.TreeVisitor;
24+
import org.openrewrite.java.JavaIsoVisitor;
25+
import org.openrewrite.java.JavaParser;
26+
import org.openrewrite.java.JavaTemplate;
27+
import org.openrewrite.java.tree.J;
28+
29+
import java.util.HashSet;
30+
import java.util.Optional;
31+
import java.util.Set;
32+
import java.util.StringJoiner;
33+
34+
import static java.util.Comparator.comparing;
35+
import static org.openrewrite.java.tree.JavaType.Variable;
36+
37+
@Value
38+
@EqualsAndHashCode(callSuper = false)
39+
public class ConvertGetter extends Recipe {
40+
41+
@Override
42+
public String getDisplayName() {
43+
//language=markdown
44+
return "Convert getter methods to annotations";
45+
}
46+
47+
@Override
48+
public String getDescription() {
49+
//language=markdown
50+
return new StringJoiner("\n")
51+
.add("Convert trivial getter methods to `@Getter` annotations on their respective fields.")
52+
.add("")
53+
.add("Limitations:")
54+
.add("")
55+
.add(" - Does not add a dependency to Lombok, users need to do that manually")
56+
.add(" - Ignores fields that are declared on the same line as others, e.g. `private int foo, bar;" +
57+
"Users who have such fields are advised to separate them beforehand with " +
58+
"[org.openrewrite.staticanalysis.MultipleVariableDeclaration]" +
59+
"(https://docs.openrewrite.org/recipes/staticanalysis/multiplevariabledeclarations).")
60+
.add(" - Does not offer any of the configuration keys listed in https://projectlombok.org/features/GetterSetter.")
61+
.toString();
62+
}
63+
64+
@Override
65+
public TreeVisitor<?, ExecutionContext> getVisitor() {
66+
return new MethodRemover();
67+
}
68+
69+
70+
@Value
71+
@EqualsAndHashCode(callSuper = false)
72+
private static class MethodRemover extends JavaIsoVisitor<ExecutionContext> {
73+
private static final String FIELDS_TO_DECORATE_KEY = "FIELDS_TO_DECORATE";
74+
75+
@Override
76+
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
77+
78+
//initialize set of fields to annotate
79+
getCursor().putMessage(FIELDS_TO_DECORATE_KEY, new HashSet<Finding>());
80+
81+
//delete methods, note down corresponding fields
82+
J.ClassDeclaration classDeclAfterVisit = super.visitClassDeclaration(classDecl, ctx);
83+
84+
//only thing that can have changed is removal of getter methods
85+
if (classDeclAfterVisit != classDecl) {
86+
//this set collects the fields for which existing methods have already been removed
87+
Set<Finding> fieldsToDecorate = getCursor().pollNearestMessage(FIELDS_TO_DECORATE_KEY);
88+
doAfterVisit(new FieldAnnotator(fieldsToDecorate));
89+
}
90+
return classDeclAfterVisit;
91+
}
92+
93+
@Override
94+
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) {
95+
assert method.getMethodType() != null;
96+
97+
if (LombokUtils.isEffectivelyGetter(method)) {
98+
J.Return return_ = (J.Return) method.getBody().getStatements().get(0);
99+
Variable fieldType = ((J.Identifier) return_.getExpression()).getFieldType();
100+
boolean nameMatch = method.getSimpleName().equals(LombokUtils.deriveGetterMethodName(fieldType));
101+
if (nameMatch){
102+
((Set<Finding>) getCursor().getNearestMessage(FIELDS_TO_DECORATE_KEY))
103+
.add(new Finding(fieldType.getName(), LombokUtils.getAccessLevel(method.getModifiers())));
104+
return null; //delete
105+
}
106+
}
107+
return method;
108+
}
109+
}
110+
111+
@Value
112+
private static class Finding {
113+
String fieldName;
114+
AccessLevel accessLevel;
115+
}
116+
117+
118+
@Value
119+
@EqualsAndHashCode(callSuper = false)
120+
static class FieldAnnotator extends JavaIsoVisitor<ExecutionContext>{
121+
122+
Set<Finding> fieldsToDecorate;
123+
124+
private JavaTemplate getAnnotation(AccessLevel accessLevel) {
125+
JavaTemplate.Builder builder = AccessLevel.PUBLIC.equals(accessLevel)
126+
? JavaTemplate.builder("@Getter\n")
127+
: JavaTemplate.builder("@Getter(AccessLevel." + accessLevel.name() + ")\n")
128+
.imports("lombok.AccessLevel");
129+
130+
return builder
131+
.imports("lombok.Getter")
132+
.javaParser(JavaParser.fromJavaVersion()
133+
.classpath("lombok"))
134+
.build();
135+
}
136+
137+
@Override
138+
public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext ctx) {
139+
140+
//we accept only one var decl per line, see description
141+
if (multiVariable.getVariables().size() > 1) {
142+
return multiVariable;
143+
}
144+
145+
J.VariableDeclarations.NamedVariable variable = multiVariable.getVariables().get(0);
146+
Optional<Finding> field = fieldsToDecorate.stream()
147+
.filter(f -> f.fieldName.equals(variable.getSimpleName()))
148+
.findFirst();
149+
150+
if (!field.isPresent()) {
151+
return multiVariable; //not the field we are looking for
152+
}
153+
154+
J.VariableDeclarations annotated = getAnnotation(field.get().getAccessLevel()).apply(
155+
getCursor(),
156+
multiVariable.getCoordinates().addAnnotation(comparing(J.Annotation::getSimpleName)));
157+
maybeAddImport("lombok.Getter");
158+
maybeAddImport("lombok.AccessLevel");
159+
return annotated;
160+
}
161+
}
162+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package org.openrewrite.java.migrate.lombok;
2+
3+
import com.google.common.collect.ImmutableMap;
4+
import lombok.AccessLevel;
5+
import org.openrewrite.internal.StringUtils;
6+
import org.openrewrite.java.tree.Expression;
7+
import org.openrewrite.java.tree.J;
8+
import org.openrewrite.java.tree.JavaType;
9+
10+
import java.util.Collection;
11+
import java.util.Map;
12+
13+
import static lombok.AccessLevel.*;
14+
import static org.openrewrite.java.tree.J.Modifier.Type.*;
15+
16+
public class LombokUtils {
17+
18+
public static boolean isEffectivelyGetter(J.MethodDeclaration method) {
19+
boolean takesNoParameters = method.getParameters().get(0) instanceof J.Empty;
20+
boolean singularReturn = method.getBody() != null //abstract methods can be null
21+
&& method.getBody().getStatements().size() == 1
22+
&& method.getBody().getStatements().get(0) instanceof J.Return;
23+
24+
if (takesNoParameters && singularReturn) {
25+
Expression returnExpression = ((J.Return) method.getBody().getStatements().get(0)).getExpression();
26+
//returns just an identifier
27+
if (returnExpression instanceof J.Identifier) {
28+
J.Identifier identifier = (J.Identifier) returnExpression;
29+
JavaType.Variable fieldType = identifier.getFieldType();
30+
boolean typeMatch = method.getType().equals(fieldType.getType());
31+
return typeMatch;
32+
}
33+
}
34+
return false;
35+
}
36+
37+
public static String deriveGetterMethodName(JavaType.Variable fieldType) {
38+
boolean isPrimitiveBoolean = JavaType.Variable.Primitive.Boolean.equals(fieldType.getType());
39+
40+
final String fieldName = fieldType.getName();
41+
42+
boolean alreadyStartsWithIs = fieldName.length() >= 3
43+
&& fieldName.substring(0, 3).matches("is[A-Z]");
44+
45+
if (isPrimitiveBoolean)
46+
if (alreadyStartsWithIs)
47+
return fieldName;
48+
else
49+
return "is" + StringUtils.capitalize(fieldName);
50+
51+
return "get" + StringUtils.capitalize(fieldName);
52+
}
53+
54+
public static AccessLevel getAccessLevel(Collection<J.Modifier> modifiers) {
55+
Map<J.Modifier.Type, AccessLevel> map = ImmutableMap.<J.Modifier.Type, AccessLevel>builder()
56+
.put(Public, PUBLIC)
57+
.put(Protected, PROTECTED)
58+
.put(Private, PRIVATE)
59+
.build();
60+
61+
return modifiers.stream()
62+
.map(modifier -> map.getOrDefault(modifier.getType(), AccessLevel.NONE))
63+
.filter(a -> a != AccessLevel.NONE)
64+
.findAny().orElse(AccessLevel.PACKAGE);
65+
}
66+
67+
}

0 commit comments

Comments
 (0)