Skip to content

Commit bb00d0a

Browse files
timo-agithub-actions[bot]timtebeek
authored
Recipe that converts explicit setters to the lombok annotation (#625)
* 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 * migrate existing recipe as-is * deactivate getter test for development * Rename and add Lombok tag * fix year in licence header * chore: IntelliJ auto-formatter * apply best practices * light polish * copy from: Also handle field access * minor changes * Minimize changes with `main` branch ahead of rebase to avoid conflicts * Resolve compilation issues * Ensure there is no change for a nested Setter * Extract a reusable FieldAnnotator class * Adopt now shared `FieldAnnotator` for setters as well * Convert most of the checks as used for UseLombokGetter * Add one more style we ought to cover * Add remaining checks to make all tests pass * Move down variable and method closer to usage * Inline variables used only once --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Tim te Beek <timtebeek@gmail.com> Co-authored-by: Tim te Beek <tim@moderne.io>
1 parent 17de874 commit bb00d0a

File tree

5 files changed

+729
-39
lines changed

5 files changed

+729
-39
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
* <p>
4+
* Licensed under the Moderne Source Available License (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://docs.moderne.io/licensing/moderne-source-available-license
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.java.JavaIsoVisitor;
23+
import org.openrewrite.java.JavaParser;
24+
import org.openrewrite.java.JavaTemplate;
25+
import org.openrewrite.java.tree.J;
26+
import org.openrewrite.java.tree.JavaType;
27+
28+
import static java.util.Comparator.comparing;
29+
import static lombok.AccessLevel.PUBLIC;
30+
31+
@Value
32+
@EqualsAndHashCode(callSuper = false)
33+
class FieldAnnotator extends JavaIsoVisitor<ExecutionContext> {
34+
35+
Class<?> annotation;
36+
JavaType field;
37+
AccessLevel accessLevel;
38+
39+
@Override
40+
public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext ctx) {
41+
for (J.VariableDeclarations.NamedVariable variable : multiVariable.getVariables()) {
42+
if (variable.getName().getFieldType() == field) {
43+
maybeAddImport(annotation.getName());
44+
maybeAddImport("lombok.AccessLevel");
45+
String suffix = accessLevel == PUBLIC ? "" : String.format("(AccessLevel.%s)", accessLevel.name());
46+
return JavaTemplate.builder("@" + annotation.getSimpleName() + suffix)
47+
.imports(annotation.getName(), "lombok.AccessLevel")
48+
.javaParser(JavaParser.fromJavaVersion().classpath("lombok"))
49+
.build().apply(getCursor(), multiVariable.getCoordinates().addAnnotation(comparing(J.Annotation::getSimpleName)));
50+
}
51+
}
52+
return multiVariable;
53+
}
54+
}

src/main/java/org/openrewrite/java/migrate/lombok/LombokUtils.java

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,21 +48,21 @@ static boolean isGetter(J.MethodDeclaration method) {
4848
J.Identifier identifier = (J.Identifier) returnExpression;
4949
if (identifier.getFieldType() != null && declaringType == identifier.getFieldType().getOwner()) {
5050
// Check return: type and matching field name
51-
return hasMatchingTypeAndName(method, identifier.getType(), identifier.getSimpleName());
51+
return hasMatchingTypeAndGetterName(method, identifier.getType(), identifier.getSimpleName());
5252
}
5353
} else if (returnExpression instanceof J.FieldAccess) {
5454
J.FieldAccess fieldAccess = (J.FieldAccess) returnExpression;
5555
Expression target = fieldAccess.getTarget();
5656
if (target instanceof J.Identifier && ((J.Identifier) target).getFieldType() != null &&
5757
declaringType == ((J.Identifier) target).getFieldType().getOwner()) {
5858
// Check return: type and matching field name
59-
return hasMatchingTypeAndName(method, fieldAccess.getType(), fieldAccess.getSimpleName());
59+
return hasMatchingTypeAndGetterName(method, fieldAccess.getType(), fieldAccess.getSimpleName());
6060
}
6161
}
6262
return false;
6363
}
6464

65-
private static boolean hasMatchingTypeAndName(J.MethodDeclaration method, @Nullable JavaType type, String simpleName) {
65+
private static boolean hasMatchingTypeAndGetterName(J.MethodDeclaration method, @Nullable JavaType type, String simpleName) {
6666
if (method.getType() == type) {
6767
String deriveGetterMethodName = deriveGetterMethodName(type, simpleName);
6868
return method.getSimpleName().equals(deriveGetterMethodName);
@@ -83,15 +83,62 @@ private static String deriveGetterMethodName(@Nullable JavaType type, String fie
8383
return "get" + StringUtils.capitalize(fieldName);
8484
}
8585

86-
static AccessLevel getAccessLevel(J.MethodDeclaration modifiers) {
87-
if (modifiers.hasModifier(Public)) {
86+
static boolean isSetter(J.MethodDeclaration method) {
87+
// Check return type: void
88+
if (method.getType() != JavaType.Primitive.Void) {
89+
return false;
90+
}
91+
// Check signature: single parameter
92+
if (method.getParameters().size() != 1 || method.getParameters().get(0) instanceof J.Empty) {
93+
return false;
94+
}
95+
// Check body: just an assignment
96+
if (method.getBody() == null || //abstract methods can be null
97+
method.getBody().getStatements().size() != 1 ||
98+
!(method.getBody().getStatements().get(0) instanceof J.Assignment)) {
99+
return false;
100+
}
101+
102+
// Check there's no up/down cast between parameter and field
103+
J.VariableDeclarations.NamedVariable param = ((J.VariableDeclarations) method.getParameters().get(0)).getVariables().get(0);
104+
Expression variable = ((J.Assignment) method.getBody().getStatements().get(0)).getVariable();
105+
if (param.getType() != variable.getType()) {
106+
return false;
107+
}
108+
109+
// Method name has to match
110+
JavaType.FullyQualified declaringType = method.getMethodType().getDeclaringType();
111+
if (variable instanceof J.Identifier) {
112+
J.Identifier assignedVar = (J.Identifier) variable;
113+
if (hasMatchingSetterMethodName(method, assignedVar.getSimpleName())) {
114+
// Check field is declared on method type
115+
return assignedVar.getFieldType() != null && declaringType == assignedVar.getFieldType().getOwner();
116+
}
117+
} else if (variable instanceof J.FieldAccess) {
118+
J.FieldAccess assignedField = (J.FieldAccess) variable;
119+
if (hasMatchingSetterMethodName(method, assignedField.getSimpleName())) {
120+
Expression target = assignedField.getTarget();
121+
// Check field is declared on method type
122+
return target instanceof J.Identifier && ((J.Identifier) target).getFieldType() != null &&
123+
declaringType == ((J.Identifier) target).getFieldType().getOwner();
124+
}
125+
}
126+
127+
return false;
128+
}
129+
130+
private static boolean hasMatchingSetterMethodName(J.MethodDeclaration method, String simpleName) {
131+
return method.getSimpleName().equals("set" + StringUtils.capitalize(simpleName));
132+
}
133+
134+
static AccessLevel getAccessLevel(J.MethodDeclaration methodDeclaration) {
135+
if (methodDeclaration.hasModifier(Public)) {
88136
return PUBLIC;
89-
} else if (modifiers.hasModifier(Protected)) {
137+
} else if (methodDeclaration.hasModifier(Protected)) {
90138
return PROTECTED;
91-
} else if (modifiers.hasModifier(Private)) {
139+
} else if (methodDeclaration.hasModifier(Private)) {
92140
return PRIVATE;
93141
}
94142
return PACKAGE;
95143
}
96-
97144
}

src/main/java/org/openrewrite/java/migrate/lombok/UseLombokGetter.java

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,21 @@
1515
*/
1616
package org.openrewrite.java.migrate.lombok;
1717

18-
import lombok.AccessLevel;
1918
import lombok.EqualsAndHashCode;
19+
import lombok.Getter;
2020
import lombok.Value;
2121
import org.jspecify.annotations.Nullable;
2222
import org.openrewrite.ExecutionContext;
2323
import org.openrewrite.Recipe;
2424
import org.openrewrite.TreeVisitor;
2525
import org.openrewrite.java.JavaIsoVisitor;
26-
import org.openrewrite.java.JavaParser;
27-
import org.openrewrite.java.JavaTemplate;
2826
import org.openrewrite.java.tree.Expression;
2927
import org.openrewrite.java.tree.J;
30-
import org.openrewrite.java.tree.JavaType;
3128

3229
import java.util.Collections;
33-
import java.util.List;
3430
import java.util.Set;
3531

3632
import static java.util.Comparator.comparing;
37-
import static lombok.AccessLevel.PUBLIC;
3833

3934
@Value
4035
@EqualsAndHashCode(callSuper = false)
@@ -66,12 +61,14 @@ public TreeVisitor<?, ExecutionContext> getVisitor() {
6661
if (returnExpression instanceof J.Identifier &&
6762
((J.Identifier) returnExpression).getFieldType() != null) {
6863
doAfterVisit(new FieldAnnotator(
64+
Getter.class,
6965
((J.Identifier) returnExpression).getFieldType(),
7066
LombokUtils.getAccessLevel(method)));
7167
return null;
7268
} else if (returnExpression instanceof J.FieldAccess &&
7369
((J.FieldAccess) returnExpression).getName().getFieldType() != null) {
7470
doAfterVisit(new FieldAnnotator(
71+
Getter.class,
7572
((J.FieldAccess) returnExpression).getName().getFieldType(),
7673
LombokUtils.getAccessLevel(method)));
7774
return null;
@@ -81,29 +78,4 @@ public TreeVisitor<?, ExecutionContext> getVisitor() {
8178
}
8279
};
8380
}
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-
}
10981
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
* <p>
4+
* Licensed under the Moderne Source Available License (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://docs.moderne.io/licensing/moderne-source-available-license
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.EqualsAndHashCode;
19+
import lombok.Setter;
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.tree.Expression;
27+
import org.openrewrite.java.tree.J;
28+
29+
import java.util.Collections;
30+
import java.util.Set;
31+
32+
@Value
33+
@EqualsAndHashCode(callSuper = false)
34+
public class UseLombokSetter extends Recipe {
35+
36+
@Override
37+
public String getDisplayName() {
38+
return "Convert setter methods to annotations";
39+
}
40+
41+
@Override
42+
public String getDescription() {
43+
return "Convert trivial setter methods to `@Setter` annotations on their respective fields.";
44+
}
45+
46+
@Override
47+
public Set<String> getTags() {
48+
return Collections.singleton("lombok");
49+
}
50+
51+
@Override
52+
public TreeVisitor<?, ExecutionContext> getVisitor() {
53+
return new JavaIsoVisitor<ExecutionContext>() {
54+
@Override
55+
public J.@Nullable MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) {
56+
if (LombokUtils.isSetter(method)) {
57+
Expression assignmentVariable = ((J.Assignment) method.getBody().getStatements().get(0)).getVariable();
58+
if (assignmentVariable instanceof J.FieldAccess &&
59+
((J.FieldAccess) assignmentVariable).getName().getFieldType() != null) {
60+
doAfterVisit(new FieldAnnotator(Setter.class,
61+
((J.FieldAccess) assignmentVariable).getName().getFieldType(),
62+
LombokUtils.getAccessLevel(method)));
63+
return null; //delete
64+
65+
} else if (assignmentVariable instanceof J.Identifier &&
66+
((J.Identifier) assignmentVariable).getFieldType() != null) {
67+
doAfterVisit(new FieldAnnotator(Setter.class,
68+
((J.Identifier) assignmentVariable).getFieldType(),
69+
LombokUtils.getAccessLevel(method)));
70+
return null; //delete
71+
}
72+
}
73+
return method;
74+
}
75+
};
76+
}
77+
}

0 commit comments

Comments
 (0)