diff --git a/rewrite-java/src/main/java/org/openrewrite/java/LombokUtilityClass.java b/rewrite-java/src/main/java/org/openrewrite/java/LombokUtilityClass.java new file mode 100644 index 00000000000..8b67e5ea9a8 --- /dev/null +++ b/rewrite-java/src/main/java/org/openrewrite/java/LombokUtilityClass.java @@ -0,0 +1,185 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * 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 + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.tree.Flag; +import org.openrewrite.java.tree.J; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +import static java.util.Comparator.comparing; + +/** + * TODO: Check the following criteria: + * - Lombok in dependencies + + * - All methods of class are static + + * - No instances of given class + + * - All static attributes are final + + *

+ * TODO: Perform the transformation: + * - Add the annotation + + * - Remove static from all attributes and methods + + *

+ * TODO: Features to consider: + * - Transformation: Add Lombok config if not present + supported configuration options for utility class + * - Transformation: Replace instantiation with static calls to methods --> na + * - Anonymous classes ??? + * - Reflection ??? + */ +public class LombokUtilityClass extends Recipe { + + @Override + public String getDisplayName() { + return "Lombok UtilityClass"; + } + + @Override + public String getDescription() { + return "This recipe will check if any class is transformable (only static methods in class)" + + " into the Lombok UtilityClass and will perform the change if applicable."; + } + + @Override + public TreeVisitor getVisitor() { + return new ChangeVisitor(); + } + + + private static class ChangeVisitor extends JavaIsoVisitor { + + @Override + public J.ClassDeclaration visitClassDeclaration( + final J.ClassDeclaration classDecl, + final ExecutionContext executionContext + ) { + if (!CheckVisitor.shouldPerformChanges(classDecl)) { + return super.visitClassDeclaration(classDecl, executionContext); + } + + maybeAddImport("lombok.experimental.UtilityClass", false); + return super.visitClassDeclaration( + JavaTemplate + .builder("@UtilityClass") + .imports("lombok.experimental.UtilityClass") + .javaParser(JavaParser.fromJavaVersion().classpath("lombok")) + .build() + .apply( + getCursor(), + classDecl.getCoordinates().addAnnotation(comparing(J.Annotation::getSimpleName)) + ), + executionContext + ); + } + + @Override + public J.MethodDeclaration visitMethodDeclaration( + final J.MethodDeclaration method, + final ExecutionContext executionContext + ) { + final J.ClassDeclaration classDecl = getCursor().firstEnclosingOrThrow(J.ClassDeclaration.class); + if (!CheckVisitor.shouldPerformChanges(classDecl)) { + return super.visitMethodDeclaration(method, executionContext); + } + + return super.visitMethodDeclaration( + method.withModifiers(method.getModifiers().stream() + .filter(m -> m.getType() != J.Modifier.Type.Static) + .collect(Collectors.toList())), + executionContext + ); + } + + @Override + public J.VariableDeclarations visitVariableDeclarations( + final J.VariableDeclarations multiVariable, + final ExecutionContext executionContext + ) { + final J.ClassDeclaration classDecl = getCursor().firstEnclosingOrThrow(J.ClassDeclaration.class); + if (!CheckVisitor.shouldPerformChanges(classDecl)) { + return super.visitVariableDeclarations(multiVariable, executionContext); + } + + return super.visitVariableDeclarations( + multiVariable + .withModifiers(multiVariable.getModifiers().stream() + .filter(m -> m.getType() != J.Modifier.Type.Static) + .collect(Collectors.toList()) + ) + .withVariables(multiVariable.getVariables().stream() + .map(v -> v.withName(v.getName().withSimpleName(v.getName().getSimpleName().toLowerCase()))) + .collect(Collectors.toList()) + ), + executionContext + ); + } + } + + private static class CheckVisitor extends JavaIsoVisitor { + + @Override + public J.ClassDeclaration visitClassDeclaration( + final J.ClassDeclaration classDecl, + final AtomicBoolean shouldPerformChanges + ) { + if (classDecl.getType().hasFlags(Flag.Interface)) { + shouldPerformChanges.set(false); + } + if (classDecl.hasModifier(J.Modifier.Type.Abstract)) { + shouldPerformChanges.set(false); + } + if (classDecl.getLeadingAnnotations().stream().anyMatch(a -> "UtilityClass".equals(a.getSimpleName()))) { + shouldPerformChanges.set(false); + } + return super.visitClassDeclaration(classDecl, shouldPerformChanges); + } + + @Override + public J.MethodDeclaration visitMethodDeclaration( + final J.MethodDeclaration method, + final AtomicBoolean shouldPerformChanges + ) { + if (!method.hasModifier(J.Modifier.Type.Static)) { + shouldPerformChanges.set(false); + } + + if (method.getSimpleName().equalsIgnoreCase("main")) { + shouldPerformChanges.set(false); + } + return super.visitMethodDeclaration(method, shouldPerformChanges); + } + + @Override + public J.VariableDeclarations.NamedVariable visitVariable( + final J.VariableDeclarations.NamedVariable variable, + final AtomicBoolean shouldPerformChanges + ) { + if (variable.isField(getCursor()) + && variable.getVariableType() != null + && !variable.getVariableType().hasFlags(Flag.Static, Flag.Final)) { + shouldPerformChanges.set(false); + } + return super.visitVariable(variable, shouldPerformChanges); + } + + private static boolean shouldPerformChanges(final J.ClassDeclaration classDecl) { + return new CheckVisitor().reduce(classDecl, new AtomicBoolean(true)).get(); + } + } +} diff --git a/rewrite-java/src/test/java/org/openrewrite/java/LombokUtilityClassTest.java b/rewrite-java/src/test/java/org/openrewrite/java/LombokUtilityClassTest.java new file mode 100644 index 00000000000..f067871ffda --- /dev/null +++ b/rewrite-java/src/test/java/org/openrewrite/java/LombokUtilityClassTest.java @@ -0,0 +1,385 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * 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 + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +/** + * Cases to test: + * - inheritance + * - instantiations of changed classes + * - constructor + */ +class LombokUtilityClassTest implements RewriteTest { + + @Nested + class ShouldApplyLombokUtility implements RewriteTest { + + @Test + void givenOneStaticMethod() { + rewriteRun( + recipeSpec -> recipeSpec.recipe(new LombokUtilityClass()), + java( + """ + public class A { + public static int add(final int x, final int y) { + return x + y; + } + } + """, + """ + import lombok.experimental.UtilityClass; + + @UtilityClass + public class A { + public int add(final int x, final int y) { + return x + y; + } + } + """ + ) + ); + } + + @Test + void givenOneStaticFinalMember() { + rewriteRun( + recipeSpec -> recipeSpec.recipe(new LombokUtilityClass()), + java( + """ + public class A { + public static final int C = 0; + } + """, + """ + import lombok.experimental.UtilityClass; + + @UtilityClass + public class A { + public final int c = 0; + } + """ + ) + ); + } + + @Test + void givenMultipleStaticFinalMembers() { + rewriteRun( + recipeSpec -> recipeSpec.recipe(new LombokUtilityClass()), + java( + """ + public class A { + public static final int A, B, C = 0; + } + """, + """ + import lombok.experimental.UtilityClass; + + @UtilityClass + public class A { + public final int a, b, c = 0; + } + """ + ) + ); + } + + @Test + void givenStaticInnerClassWithOneStaticMethod() { + rewriteRun( + recipeSpec -> recipeSpec.recipe(new LombokUtilityClass()), + java( + """ + public class A { + public int add(final int x, final int y) { + return x + y; + } + + private static class B { + private static int substract(final int x, final int y) { + return x - y; + } + } + } + """, + """ + import lombok.experimental.UtilityClass; + + public class A { + public int add(final int x, final int y) { + return x + y; + } + + @UtilityClass + private static class B { + private int substract(final int x, final int y) { + return x - y; + } + } + } + """ + ) + ); + } + + @Test + void givenInnerClassWithOneStaticMethod() { + rewriteRun( + recipeSpec -> recipeSpec.recipe(new LombokUtilityClass()), + java( + """ + public class A { + public int add(final int x, final int y) { + return x + y; + } + + private class B { + private static int substract(final int x, final int y) { + return x - y; + } + } + } + """, + """ + import lombok.experimental.UtilityClass; + + public class A { + public int add(final int x, final int y) { + return x + y; + } + + @UtilityClass + private class B { + private int substract(final int x, final int y) { + return x - y; + } + } + } + """ + ) + ); + } + + @Test + void givenNotPublicClassWithOneStaticMethod() { + rewriteRun( + recipeSpec -> recipeSpec.recipe(new LombokUtilityClass()), + java( + """ + public class A { + public int add(final int x, final int y) { + return x + y; + } + } + class B { + public static int substract(final int x, final int y) { + return x - y; + } + } + """, + """ + import lombok.experimental.UtilityClass; + + public class A { + public int add(final int x, final int y) { + return x + y; + } + } + + @UtilityClass + class B { + public int substract(final int x, final int y) { + return x - y; + } + } + """ + ) + ); + } + } + + @Nested + class ShouldNotApplyLombokUtility implements RewriteTest { + @Test + void givenStaticMemberIsNotFinal() { + rewriteRun( + recipeSpec -> recipeSpec.recipe(new LombokUtilityClass()), + java( + """ + public class A { + public static int C = 0; + } + """ + ) + ); + } + + @Test + void givenMethodIsNotStatic() { + rewriteRun( + recipeSpec -> recipeSpec.recipe(new LombokUtilityClass()), + java( + """ + public class A { + public int add(final int x, final int y) { + return x + y; + } + } + """ + ) + ); + } + + @Test + void givenMain() { + rewriteRun( + recipeSpec -> recipeSpec.recipe(new LombokUtilityClass()), + java( + """ + public class A { + public static void main(String[] args) { + } + } + """ + ) + ); + } + + @Test + void givenAbstractClass() { + rewriteRun( + recipeSpec -> recipeSpec.recipe(new LombokUtilityClass()), + java( + """ + public abstract class A { + public static void doSmth() { + }; + } + """ + ) + ); + } + + @Test + void givenInterface() { + rewriteRun( + recipeSpec -> recipeSpec.recipe(new LombokUtilityClass()), + java( + """ + public interface A { + int CONST = 1; + static void doSmth() { + } + } + """ + ) + ); + } + + // FIXME: use messaging on getCursor() to notify class of existing methods or fields? + @Test + void givenEmptyClass() { + rewriteRun( + recipeSpec -> recipeSpec.recipe(new LombokUtilityClass()), + java( + """ + public class A { + } + """ + ) + ); + } + + } + + @Test + void shouldNotChangeClassWhenStaticMethodOfChangedClassIsCalled() { + rewriteRun( + recipeSpec -> recipeSpec.recipe(new LombokUtilityClass()), + java( + """ + public class A { + public static int add(final int x, final int y) { + return x + y; + } + } + """, + """ + import lombok.experimental.UtilityClass; + + @UtilityClass + public class A { + public int add(final int x, final int y) { + return x + y; + } + } + """ + ), + java(""" + public class B { + public int add(final int x, final int y) { + return A.add(x, y); + } + } + """ + ) + ); + } + + + @Test + void shoulOnlyUpgradeRelevantToUtilityClass() { + rewriteRun( + recipeSpec -> recipeSpec.recipe(new LombokUtilityClass()), + java( + """ + public class A { + public static int add(final int x, final int y) { + return x + y; + } + } + """, + """ + import lombok.experimental.UtilityClass; + + @UtilityClass + public class A { + public int add(final int x, final int y) { + return x + y; + } + } + """ + ), + java(""" + public class B { + public int add(final int x, final int y) { + return x + y; + } + } + """ + ) + ); + } + + + + +} \ No newline at end of file