-
Notifications
You must be signed in to change notification settings - Fork 101
Recipe to convert explicit getters to the Lombok annotation #623
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 11 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
a3e22b4
feat: add recipe that converts explicit getters to the lombok annotation
timo-a d6eebf2
chore: IntelliJ auto-formatter
timo-a b38f90d
add licence header
timo-a c35b7d6
Apply suggestions from code review
timo-a f06e270
Apply suggestions from code review
timtebeek e6fd1bb
roll back nullable annotation
timo-a 4b7fb67
Light polish
timtebeek 665e73a
Rename and add Lombok tag
timtebeek 91c579b
Also handle field access
timtebeek c7d3feb
Push down method and variable name matching into utils
timtebeek f51e00f
Demonstrate failing case of nested inner class getter
timtebeek 80ec548
fix: year in licence header
timo-a 87495fd
rename method
timo-a 40a44b1
Check that getter methods access fields from the method declaring type
timtebeek 9d2ed73
Remove the need for cursor messaging
timtebeek 8376783
From `visitMethodDeclaration` call `doAfterVisit`
timtebeek 6766243
Drop Guava dependency from LombokUtils
timtebeek 74ddd7d
Merge branch 'main' into lombok/getter
timtebeek 5a7cae1
Compare identity of method type
timtebeek 0fef789
Remove documented limitations
timtebeek File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
91 changes: 91 additions & 0 deletions
91
src/main/java/org/openrewrite/java/migrate/lombok/LombokUtils.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
/* | ||
* Copyright 2024 the original author or authors. | ||
* <p> | ||
* Licensed under the Apache License, Version 2.0 (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://www.apache.org/licenses/LICENSE-2.0 | ||
* <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 com.google.common.collect.ImmutableMap; | ||
import lombok.AccessLevel; | ||
import org.jspecify.annotations.Nullable; | ||
import org.openrewrite.internal.StringUtils; | ||
import org.openrewrite.java.tree.Expression; | ||
import org.openrewrite.java.tree.J; | ||
import org.openrewrite.java.tree.JavaType; | ||
|
||
import java.util.Collection; | ||
import java.util.Map; | ||
|
||
import static lombok.AccessLevel.*; | ||
import static org.openrewrite.java.tree.J.Modifier.Type.*; | ||
|
||
class LombokUtils { | ||
|
||
static boolean isEffectivelyGetter(J.MethodDeclaration method) { | ||
// Check signature: no parameters | ||
if (!(method.getParameters().get(0) instanceof J.Empty) || method.getReturnTypeExpression() == null) { | ||
return false; | ||
} | ||
// Check body: just a return statement | ||
if (method.getBody() == null || | ||
method.getBody().getStatements().size() != 1 || | ||
!(method.getBody().getStatements().get(0) instanceof J.Return)) { | ||
return false; | ||
} | ||
// Check return: type and matching field name | ||
Expression returnExpression = ((J.Return) method.getBody().getStatements().get(0)).getExpression(); | ||
if (returnExpression instanceof J.Identifier) { | ||
J.Identifier identifier = (J.Identifier) returnExpression; | ||
return hasMatchingTypeAndName(method, identifier.getType(), identifier.getSimpleName()); | ||
} else if (returnExpression instanceof J.FieldAccess) { | ||
J.FieldAccess fieldAccess = (J.FieldAccess) returnExpression; | ||
return hasMatchingTypeAndName(method, fieldAccess.getType(), fieldAccess.getSimpleName()); | ||
} | ||
return false; | ||
} | ||
|
||
private static boolean hasMatchingTypeAndName(J.MethodDeclaration method, @Nullable JavaType type, String simpleName) { | ||
if (method.getType().equals(type)) { | ||
String deriveGetterMethodName = deriveGetterMethodName(type, simpleName); | ||
return method.getSimpleName().equals(deriveGetterMethodName); | ||
} | ||
return false; | ||
} | ||
|
||
private static String deriveGetterMethodName(@Nullable JavaType type, String fieldName) { | ||
if (type == JavaType.Primitive.Boolean) { | ||
boolean alreadyStartsWithIs = fieldName.length() >= 3 && | ||
fieldName.substring(0, 3).matches("is[A-Z]"); | ||
if (alreadyStartsWithIs) { | ||
return fieldName; | ||
} else { | ||
return "is" + StringUtils.capitalize(fieldName); | ||
} | ||
} | ||
return "get" + StringUtils.capitalize(fieldName); | ||
} | ||
|
||
static AccessLevel getAccessLevel(Collection<J.Modifier> modifiers) { | ||
Map<J.Modifier.Type, AccessLevel> map = ImmutableMap.<J.Modifier.Type, AccessLevel>builder() | ||
.put(Public, PUBLIC) | ||
.put(Protected, PROTECTED) | ||
.put(Private, PRIVATE) | ||
.build(); | ||
|
||
return modifiers.stream() | ||
.map(modifier -> map.getOrDefault(modifier.getType(), AccessLevel.NONE)) | ||
.filter(a -> a != AccessLevel.NONE) | ||
.findAny().orElse(AccessLevel.PACKAGE); | ||
} | ||
|
||
} |
161 changes: 161 additions & 0 deletions
161
src/main/java/org/openrewrite/java/migrate/lombok/UseLombokGetter.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
/* | ||
* Copyright 2021 the original author or authors. | ||
* <p> | ||
* Licensed under the Apache License, Version 2.0 (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://www.apache.org/licenses/LICENSE-2.0 | ||
* <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.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 java.util.Collections; | ||
import java.util.HashSet; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
|
||
import static java.util.Comparator.comparing; | ||
|
||
@Value | ||
@EqualsAndHashCode(callSuper = false) | ||
public class UseLombokGetter extends Recipe { | ||
|
||
@Override | ||
public String getDisplayName() { | ||
return "Convert getter methods to annotations"; | ||
} | ||
|
||
@Override | ||
public String getDescription() { | ||
//language=markdown | ||
return "Convert trivial getter methods to `@Getter` annotations on their respective fields.\n\n" + | ||
"Limitations:\n\n" + | ||
" - Does not add a dependency to Lombok, users need to do that manually\n" + | ||
" - Ignores fields that are declared on the same line as others, e.g. `private int foo, bar; " + | ||
"Users who have such fields are advised to separate them beforehand with [org.openrewrite.staticanalysis.MultipleVariableDeclaration](https://docs.openrewrite.org/recipes/staticanalysis/multiplevariabledeclarations).\n" + | ||
" - Does not offer any of the configuration keys listed in https://projectlombok.org/features/GetterSetter."; | ||
} | ||
|
||
@Override | ||
public Set<String> getTags() { | ||
return Collections.singleton("lombok"); | ||
} | ||
|
||
@Override | ||
public TreeVisitor<?, ExecutionContext> getVisitor() { | ||
return new MethodRemover(); | ||
} | ||
|
||
@Value | ||
@EqualsAndHashCode(callSuper = false) | ||
private static class MethodRemover extends JavaIsoVisitor<ExecutionContext> { | ||
private static final String FIELDS_TO_DECORATE_KEY = "FIELDS_TO_DECORATE"; | ||
|
||
@Override | ||
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { | ||
|
||
//initialize set of fields to annotate | ||
getCursor().putMessage(FIELDS_TO_DECORATE_KEY, new HashSet<Finding>()); | ||
|
||
//delete methods, note down corresponding fields | ||
J.ClassDeclaration classDeclAfterVisit = super.visitClassDeclaration(classDecl, ctx); | ||
|
||
//only thing that can have changed is removal of getter methods | ||
if (classDeclAfterVisit != classDecl) { | ||
//this set collects the fields for which existing methods have already been removed | ||
Set<Finding> fieldsToDecorate = getCursor().pollNearestMessage(FIELDS_TO_DECORATE_KEY); | ||
doAfterVisit(new FieldAnnotator(fieldsToDecorate)); | ||
} | ||
return classDeclAfterVisit; | ||
} | ||
|
||
@Override | ||
public J.@Nullable MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) { | ||
if (method.getMethodType() != null && LombokUtils.isEffectivelyGetter(method)) { | ||
Set<Finding> set = getCursor().getNearestMessage(FIELDS_TO_DECORATE_KEY); | ||
Expression returnExpression = ((J.Return) method.getBody().getStatements().get(0)).getExpression(); | ||
if (returnExpression instanceof J.Identifier) { | ||
set.add(new Finding( | ||
((J.Identifier) returnExpression).getSimpleName(), | ||
LombokUtils.getAccessLevel(method.getModifiers()))); | ||
return null; | ||
} else if (returnExpression instanceof J.FieldAccess) { | ||
set.add(new Finding( | ||
((J.FieldAccess) returnExpression).getSimpleName(), | ||
LombokUtils.getAccessLevel(method.getModifiers()))); | ||
return null; | ||
} | ||
} | ||
return method; | ||
} | ||
} | ||
|
||
@Value | ||
private static class Finding { | ||
String fieldName; | ||
AccessLevel accessLevel; | ||
} | ||
|
||
@Value | ||
@EqualsAndHashCode(callSuper = false) | ||
static class FieldAnnotator extends JavaIsoVisitor<ExecutionContext> { | ||
|
||
Set<Finding> fieldsToDecorate; | ||
|
||
private JavaTemplate getAnnotation(AccessLevel accessLevel) { | ||
JavaTemplate.Builder builder = AccessLevel.PUBLIC.equals(accessLevel) ? | ||
JavaTemplate.builder("@Getter\n") : | ||
JavaTemplate.builder("@Getter(AccessLevel." + accessLevel.name() + ")\n") | ||
.imports("lombok.AccessLevel"); | ||
|
||
return builder | ||
.imports("lombok.Getter") | ||
.javaParser(JavaParser.fromJavaVersion().classpath("lombok")) | ||
.build(); | ||
} | ||
|
||
@Override | ||
public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext ctx) { | ||
|
||
//we accept only one var decl per line, see description | ||
if (multiVariable.getVariables().size() > 1) { | ||
return multiVariable; | ||
} | ||
|
||
J.VariableDeclarations.NamedVariable variable = multiVariable.getVariables().get(0); | ||
Optional<Finding> field = fieldsToDecorate.stream() | ||
.filter(f -> f.fieldName.equals(variable.getSimpleName())) | ||
.findFirst(); | ||
|
||
if (!field.isPresent()) { | ||
return multiVariable; //not the field we are looking for | ||
} | ||
|
||
J.VariableDeclarations annotated = getAnnotation(field.get().getAccessLevel()).apply( | ||
getCursor(), | ||
multiVariable.getCoordinates().addAnnotation(comparing(J.Annotation::getSimpleName))); | ||
maybeAddImport("lombok.Getter"); | ||
maybeAddImport("lombok.AccessLevel"); | ||
return annotated; | ||
} | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.