Skip to content

Commit 64b80fe

Browse files
timo-agithub-actions[bot]timtebeek
authored
Recipe to convert explicit getters to the Lombok annotation (#623)
* feat: add recipe that converts explicit getters to the lombok annotation * chore: IntelliJ auto-formatter * add licence header Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * roll back nullable annotation * Light polish * Rename and add Lombok tag * Also handle field access * Push down method and variable name matching into utils * Demonstrate failing case of nested inner class getter * fix: year in licence header had copy-pasted from the example recipe * rename method * Check that getter methods access fields from the method declaring type * Remove the need for cursor messaging * From `visitMethodDeclaration` call `doAfterVisit` * Drop Guava dependency from LombokUtils * Compare identity of method type * Remove documented limitations --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Tim te Beek <[email protected]> Co-authored-by: Tim te Beek <[email protected]>
1 parent 2c654e8 commit 64b80fe

File tree

3 files changed

+737
-0
lines changed

3 files changed

+737
-0
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright 2024 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 org.jspecify.annotations.Nullable;
20+
import org.openrewrite.internal.StringUtils;
21+
import org.openrewrite.java.tree.Expression;
22+
import org.openrewrite.java.tree.J;
23+
import org.openrewrite.java.tree.JavaType;
24+
25+
import static lombok.AccessLevel.*;
26+
import static org.openrewrite.java.tree.J.Modifier.Type.*;
27+
28+
class LombokUtils {
29+
30+
static boolean isGetter(J.MethodDeclaration method) {
31+
if (method.getMethodType() == null) {
32+
return false;
33+
}
34+
// Check signature: no parameters
35+
if (!(method.getParameters().get(0) instanceof J.Empty) || method.getReturnTypeExpression() == null) {
36+
return false;
37+
}
38+
// Check body: just a return statement
39+
if (method.getBody() == null ||
40+
method.getBody().getStatements().size() != 1 ||
41+
!(method.getBody().getStatements().get(0) instanceof J.Return)) {
42+
return false;
43+
}
44+
// Check field is declared on method type
45+
JavaType.FullyQualified declaringType = method.getMethodType().getDeclaringType();
46+
Expression returnExpression = ((J.Return) method.getBody().getStatements().get(0)).getExpression();
47+
if (returnExpression instanceof J.Identifier) {
48+
J.Identifier identifier = (J.Identifier) returnExpression;
49+
if (identifier.getFieldType() != null && declaringType == identifier.getFieldType().getOwner()) {
50+
// Check return: type and matching field name
51+
return hasMatchingTypeAndName(method, identifier.getType(), identifier.getSimpleName());
52+
}
53+
} else if (returnExpression instanceof J.FieldAccess) {
54+
J.FieldAccess fieldAccess = (J.FieldAccess) returnExpression;
55+
Expression target = fieldAccess.getTarget();
56+
if (target instanceof J.Identifier && ((J.Identifier) target).getFieldType() != null &&
57+
declaringType == ((J.Identifier) target).getFieldType().getOwner()) {
58+
// Check return: type and matching field name
59+
return hasMatchingTypeAndName(method, fieldAccess.getType(), fieldAccess.getSimpleName());
60+
}
61+
}
62+
return false;
63+
}
64+
65+
private static boolean hasMatchingTypeAndName(J.MethodDeclaration method, @Nullable JavaType type, String simpleName) {
66+
if (method.getType() == type) {
67+
String deriveGetterMethodName = deriveGetterMethodName(type, simpleName);
68+
return method.getSimpleName().equals(deriveGetterMethodName);
69+
}
70+
return false;
71+
}
72+
73+
private static String deriveGetterMethodName(@Nullable JavaType type, String fieldName) {
74+
if (type == JavaType.Primitive.Boolean) {
75+
boolean alreadyStartsWithIs = fieldName.length() >= 3 &&
76+
fieldName.substring(0, 3).matches("is[A-Z]");
77+
if (alreadyStartsWithIs) {
78+
return fieldName;
79+
} else {
80+
return "is" + StringUtils.capitalize(fieldName);
81+
}
82+
}
83+
return "get" + StringUtils.capitalize(fieldName);
84+
}
85+
86+
static AccessLevel getAccessLevel(J.MethodDeclaration modifiers) {
87+
if (modifiers.hasModifier(Public)) {
88+
return PUBLIC;
89+
} else if (modifiers.hasModifier(Protected)) {
90+
return PROTECTED;
91+
} else if (modifiers.hasModifier(Private)) {
92+
return PRIVATE;
93+
}
94+
return PACKAGE;
95+
}
96+
97+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2024 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.jspecify.annotations.Nullable;
22+
import org.openrewrite.ExecutionContext;
23+
import org.openrewrite.Recipe;
24+
import org.openrewrite.TreeVisitor;
25+
import org.openrewrite.java.JavaIsoVisitor;
26+
import org.openrewrite.java.JavaParser;
27+
import org.openrewrite.java.JavaTemplate;
28+
import org.openrewrite.java.tree.Expression;
29+
import org.openrewrite.java.tree.J;
30+
import org.openrewrite.java.tree.JavaType;
31+
32+
import java.util.Collections;
33+
import java.util.List;
34+
import java.util.Set;
35+
36+
import static java.util.Comparator.comparing;
37+
import static lombok.AccessLevel.PUBLIC;
38+
39+
@Value
40+
@EqualsAndHashCode(callSuper = false)
41+
public class UseLombokGetter extends Recipe {
42+
43+
@Override
44+
public String getDisplayName() {
45+
return "Convert getter methods to annotations";
46+
}
47+
48+
@Override
49+
public String getDescription() {
50+
//language=markdown
51+
return "Convert trivial getter methods to `@Getter` annotations on their respective fields.";
52+
}
53+
54+
@Override
55+
public Set<String> getTags() {
56+
return Collections.singleton("lombok");
57+
}
58+
59+
@Override
60+
public TreeVisitor<?, ExecutionContext> getVisitor() {
61+
return new JavaIsoVisitor<ExecutionContext>() {
62+
@Override
63+
public J.@Nullable MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) {
64+
if (LombokUtils.isGetter(method)) {
65+
Expression returnExpression = ((J.Return) method.getBody().getStatements().get(0)).getExpression();
66+
if (returnExpression instanceof J.Identifier &&
67+
((J.Identifier) returnExpression).getFieldType() != null) {
68+
doAfterVisit(new FieldAnnotator(
69+
((J.Identifier) returnExpression).getFieldType(),
70+
LombokUtils.getAccessLevel(method)));
71+
return null;
72+
} else if (returnExpression instanceof J.FieldAccess &&
73+
((J.FieldAccess) returnExpression).getName().getFieldType() != null) {
74+
doAfterVisit(new FieldAnnotator(
75+
((J.FieldAccess) returnExpression).getName().getFieldType(),
76+
LombokUtils.getAccessLevel(method)));
77+
return null;
78+
}
79+
}
80+
return method;
81+
}
82+
};
83+
}
84+
85+
86+
@Value
87+
@EqualsAndHashCode(callSuper = false)
88+
static class FieldAnnotator extends JavaIsoVisitor<ExecutionContext> {
89+
90+
JavaType field;
91+
AccessLevel accessLevel;
92+
93+
@Override
94+
public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext ctx) {
95+
for (J.VariableDeclarations.NamedVariable variable : multiVariable.getVariables()) {
96+
if (variable.getName().getFieldType() == field) {
97+
maybeAddImport("lombok.Getter");
98+
maybeAddImport("lombok.AccessLevel");
99+
String suffix = accessLevel == PUBLIC ? "" : String.format("(AccessLevel.%s)", accessLevel.name());
100+
return JavaTemplate.builder("@Getter" + suffix)
101+
.imports("lombok.Getter", "lombok.AccessLevel")
102+
.javaParser(JavaParser.fromJavaVersion().classpath("lombok"))
103+
.build().apply(getCursor(), multiVariable.getCoordinates().addAnnotation(comparing(J.Annotation::getSimpleName)));
104+
}
105+
}
106+
return multiVariable;
107+
}
108+
}
109+
}

0 commit comments

Comments
 (0)