Skip to content

Commit

Permalink
Recipe that converts explicit setters to the lombok annotation (#625)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Tim te Beek <[email protected]>
  • Loading branch information
4 people authored Dec 15, 2024
1 parent 17de874 commit bb00d0a
Show file tree
Hide file tree
Showing 5 changed files with 729 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2024 the original author or authors.
* <p>
* Licensed under the Moderne Source Available License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://docs.moderne.io/licensing/moderne-source-available-license
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.java.migrate.lombok;

import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.openrewrite.ExecutionContext;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaParser;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.JavaType;

import static java.util.Comparator.comparing;
import static lombok.AccessLevel.PUBLIC;

@Value
@EqualsAndHashCode(callSuper = false)
class FieldAnnotator extends JavaIsoVisitor<ExecutionContext> {

Class<?> annotation;
JavaType field;
AccessLevel accessLevel;

@Override
public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext ctx) {
for (J.VariableDeclarations.NamedVariable variable : multiVariable.getVariables()) {
if (variable.getName().getFieldType() == field) {
maybeAddImport(annotation.getName());
maybeAddImport("lombok.AccessLevel");
String suffix = accessLevel == PUBLIC ? "" : String.format("(AccessLevel.%s)", accessLevel.name());
return JavaTemplate.builder("@" + annotation.getSimpleName() + suffix)
.imports(annotation.getName(), "lombok.AccessLevel")
.javaParser(JavaParser.fromJavaVersion().classpath("lombok"))
.build().apply(getCursor(), multiVariable.getCoordinates().addAnnotation(comparing(J.Annotation::getSimpleName)));
}
}
return multiVariable;
}
}
63 changes: 55 additions & 8 deletions src/main/java/org/openrewrite/java/migrate/lombok/LombokUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,21 @@ static boolean isGetter(J.MethodDeclaration method) {
J.Identifier identifier = (J.Identifier) returnExpression;
if (identifier.getFieldType() != null && declaringType == identifier.getFieldType().getOwner()) {
// Check return: type and matching field name
return hasMatchingTypeAndName(method, identifier.getType(), identifier.getSimpleName());
return hasMatchingTypeAndGetterName(method, identifier.getType(), identifier.getSimpleName());
}
} else if (returnExpression instanceof J.FieldAccess) {
J.FieldAccess fieldAccess = (J.FieldAccess) returnExpression;
Expression target = fieldAccess.getTarget();
if (target instanceof J.Identifier && ((J.Identifier) target).getFieldType() != null &&
declaringType == ((J.Identifier) target).getFieldType().getOwner()) {
// Check return: type and matching field name
return hasMatchingTypeAndName(method, fieldAccess.getType(), fieldAccess.getSimpleName());
return hasMatchingTypeAndGetterName(method, fieldAccess.getType(), fieldAccess.getSimpleName());
}
}
return false;
}

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

static AccessLevel getAccessLevel(J.MethodDeclaration modifiers) {
if (modifiers.hasModifier(Public)) {
static boolean isSetter(J.MethodDeclaration method) {
// Check return type: void
if (method.getType() != JavaType.Primitive.Void) {
return false;
}
// Check signature: single parameter
if (method.getParameters().size() != 1 || method.getParameters().get(0) instanceof J.Empty) {
return false;
}
// Check body: just an assignment
if (method.getBody() == null || //abstract methods can be null
method.getBody().getStatements().size() != 1 ||
!(method.getBody().getStatements().get(0) instanceof J.Assignment)) {
return false;
}

// Check there's no up/down cast between parameter and field
J.VariableDeclarations.NamedVariable param = ((J.VariableDeclarations) method.getParameters().get(0)).getVariables().get(0);
Expression variable = ((J.Assignment) method.getBody().getStatements().get(0)).getVariable();
if (param.getType() != variable.getType()) {
return false;
}

// Method name has to match
JavaType.FullyQualified declaringType = method.getMethodType().getDeclaringType();
if (variable instanceof J.Identifier) {
J.Identifier assignedVar = (J.Identifier) variable;
if (hasMatchingSetterMethodName(method, assignedVar.getSimpleName())) {
// Check field is declared on method type
return assignedVar.getFieldType() != null && declaringType == assignedVar.getFieldType().getOwner();
}
} else if (variable instanceof J.FieldAccess) {
J.FieldAccess assignedField = (J.FieldAccess) variable;
if (hasMatchingSetterMethodName(method, assignedField.getSimpleName())) {
Expression target = assignedField.getTarget();
// Check field is declared on method type
return target instanceof J.Identifier && ((J.Identifier) target).getFieldType() != null &&
declaringType == ((J.Identifier) target).getFieldType().getOwner();
}
}

return false;
}

private static boolean hasMatchingSetterMethodName(J.MethodDeclaration method, String simpleName) {
return method.getSimpleName().equals("set" + StringUtils.capitalize(simpleName));
}

static AccessLevel getAccessLevel(J.MethodDeclaration methodDeclaration) {
if (methodDeclaration.hasModifier(Public)) {
return PUBLIC;
} else if (modifiers.hasModifier(Protected)) {
} else if (methodDeclaration.hasModifier(Protected)) {
return PROTECTED;
} else if (modifiers.hasModifier(Private)) {
} else if (methodDeclaration.hasModifier(Private)) {
return PRIVATE;
}
return PACKAGE;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,21 @@
*/
package org.openrewrite.java.migrate.lombok;

import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Value;
import org.jspecify.annotations.Nullable;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaParser;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.tree.Expression;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.JavaType;

import java.util.Collections;
import java.util.List;
import java.util.Set;

import static java.util.Comparator.comparing;
import static lombok.AccessLevel.PUBLIC;

@Value
@EqualsAndHashCode(callSuper = false)
Expand Down Expand Up @@ -66,12 +61,14 @@ public TreeVisitor<?, ExecutionContext> getVisitor() {
if (returnExpression instanceof J.Identifier &&
((J.Identifier) returnExpression).getFieldType() != null) {
doAfterVisit(new FieldAnnotator(
Getter.class,
((J.Identifier) returnExpression).getFieldType(),
LombokUtils.getAccessLevel(method)));
return null;
} else if (returnExpression instanceof J.FieldAccess &&
((J.FieldAccess) returnExpression).getName().getFieldType() != null) {
doAfterVisit(new FieldAnnotator(
Getter.class,
((J.FieldAccess) returnExpression).getName().getFieldType(),
LombokUtils.getAccessLevel(method)));
return null;
Expand All @@ -81,29 +78,4 @@ public TreeVisitor<?, ExecutionContext> getVisitor() {
}
};
}


@Value
@EqualsAndHashCode(callSuper = false)
static class FieldAnnotator extends JavaIsoVisitor<ExecutionContext> {

JavaType field;
AccessLevel accessLevel;

@Override
public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext ctx) {
for (J.VariableDeclarations.NamedVariable variable : multiVariable.getVariables()) {
if (variable.getName().getFieldType() == field) {
maybeAddImport("lombok.Getter");
maybeAddImport("lombok.AccessLevel");
String suffix = accessLevel == PUBLIC ? "" : String.format("(AccessLevel.%s)", accessLevel.name());
return JavaTemplate.builder("@Getter" + suffix)
.imports("lombok.Getter", "lombok.AccessLevel")
.javaParser(JavaParser.fromJavaVersion().classpath("lombok"))
.build().apply(getCursor(), multiVariable.getCoordinates().addAnnotation(comparing(J.Annotation::getSimpleName)));
}
}
return multiVariable;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright 2024 the original author or authors.
* <p>
* Licensed under the Moderne Source Available License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://docs.moderne.io/licensing/moderne-source-available-license
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.java.migrate.lombok;

import lombok.EqualsAndHashCode;
import lombok.Setter;
import lombok.Value;
import org.jspecify.annotations.Nullable;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.tree.Expression;
import org.openrewrite.java.tree.J;

import java.util.Collections;
import java.util.Set;

@Value
@EqualsAndHashCode(callSuper = false)
public class UseLombokSetter extends Recipe {

@Override
public String getDisplayName() {
return "Convert setter methods to annotations";
}

@Override
public String getDescription() {
return "Convert trivial setter methods to `@Setter` annotations on their respective fields.";
}

@Override
public Set<String> getTags() {
return Collections.singleton("lombok");
}

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.@Nullable MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) {
if (LombokUtils.isSetter(method)) {
Expression assignmentVariable = ((J.Assignment) method.getBody().getStatements().get(0)).getVariable();
if (assignmentVariable instanceof J.FieldAccess &&
((J.FieldAccess) assignmentVariable).getName().getFieldType() != null) {
doAfterVisit(new FieldAnnotator(Setter.class,
((J.FieldAccess) assignmentVariable).getName().getFieldType(),
LombokUtils.getAccessLevel(method)));
return null; //delete

} else if (assignmentVariable instanceof J.Identifier &&
((J.Identifier) assignmentVariable).getFieldType() != null) {
doAfterVisit(new FieldAnnotator(Setter.class,
((J.Identifier) assignmentVariable).getFieldType(),
LombokUtils.getAccessLevel(method)));
return null; //delete
}
}
return method;
}
};
}
}
Loading

0 comments on commit bb00d0a

Please sign in to comment.