diff --git a/src/main/java/org/openrewrite/staticanalysis/AddSerialAnnotationToserialVersionUID.java b/src/main/java/org/openrewrite/staticanalysis/AddSerialAnnotationToserialVersionUID.java new file mode 100644 index 0000000000..9c209d93c7 --- /dev/null +++ b/src/main/java/org/openrewrite/staticanalysis/AddSerialAnnotationToserialVersionUID.java @@ -0,0 +1,93 @@ +/* + * Copyright 2024 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.staticanalysis; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.internal.lang.NonNull; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.search.FindAnnotations; +import org.openrewrite.java.search.UsesJavaVersion; +import org.openrewrite.java.search.UsesType; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.TypeUtils; + +import java.time.Duration; +import java.util.Comparator; + +public class AddSerialAnnotationToserialVersionUID extends Recipe { + @Override + public String getDisplayName() { + return "Add `@Serial` annotation to `serialVersionUID`"; + } + + @Override + public String getDescription() { + return "Annotation any `serialVersionUID` fields with `@Serial` to indicate it's part of the serialization mechanism."; + } + + @Override + public Duration getEstimatedEffortPerOccurrence() { + return Duration.ofMinutes(1); + } + + @Override + @NonNull + public TreeVisitor getVisitor() { + return Preconditions.check( + Preconditions.and( + new UsesJavaVersion<>(14), + new UsesType<>("java.io.Serializable", true) + ), + new JavaIsoVisitor() { + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext executionContext) { + if (TypeUtils.isAssignableTo("java.io.Serializable", classDecl.getType())) { + return super.visitClassDeclaration(classDecl, executionContext); + } + return classDecl; + } + + @Override + public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext ctx) { + J.VariableDeclarations vd = super.visitVariableDeclarations(multiVariable, ctx); + if (isPrivateStaticFinalLongSerialVersionUID(vd) && + FindAnnotations.find(vd, "@java.io.Serial").isEmpty()) { + maybeAddImport("java.io.Serial"); + return JavaTemplate.builder("@Serial") + .imports("java.io.Serial") + .build() + .apply(getCursor(), vd.getCoordinates().addAnnotation(Comparator.comparing(J.Annotation::getSimpleName))); + } + return vd; + } + + private boolean isPrivateStaticFinalLongSerialVersionUID(J.VariableDeclarations vd) { + return vd.hasModifier(J.Modifier.Type.Private) && + vd.hasModifier(J.Modifier.Type.Static) && + vd.hasModifier(J.Modifier.Type.Final) && + TypeUtils.asPrimitive(vd.getType()) == JavaType.Primitive.Long && + vd.getVariables().size() == 1 && + "serialVersionUID".equals(vd.getVariables().get(0).getSimpleName()); + } + } + ); + } +} diff --git a/src/test/java/org/openrewrite/staticanalysis/AddSerialAnnotationToserialVersionUIDTest.java b/src/test/java/org/openrewrite/staticanalysis/AddSerialAnnotationToserialVersionUIDTest.java new file mode 100644 index 0000000000..5b18adc2d1 --- /dev/null +++ b/src/test/java/org/openrewrite/staticanalysis/AddSerialAnnotationToserialVersionUIDTest.java @@ -0,0 +1,166 @@ +/* + * Copyright 2024 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.staticanalysis; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; +import static org.openrewrite.java.Assertions.javaVersion; + +class AddSerialAnnotationToserialVersionUIDTest implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new AddSerialAnnotationToserialVersionUID()) + .allSources(sourceSpec -> sourceSpec.markers(javaVersion(17))); + } + + @DocumentExample + @Test + void addSerialAnnotation() { + rewriteRun( + //language=java + java( + """ + import java.io.Serializable; + + class Example implements Serializable { + private static final long serialVersionUID = 1L; + } + """, + """ + import java.io.Serial; + import java.io.Serializable; + + class Example implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + } + """ + ) + ); + } + + @Test + void shouldNoopIfAlreadyPresent() { + rewriteRun( + //language=java + java( + """ + import java.io.Serializable; + import java.io.Serial; + + class Example implements Serializable { + String var1 = "first variable"; + @Serial + private static final long serialVersionUID = 1L; + int var3 = 666; + } + """ + ) + ); + } + + @Test + void shouldNotAnnotateNonSerializableClass() { + rewriteRun( + //language=java + java( + """ + class Example { + private static final long serialVersionUID = 1L; + } + """ + ) + ); + } + + @Test + void shouldNotAnnotateOnJava11() { + rewriteRun( + //language=java + java( + """ + import java.io.Serializable; + + class Example implements Serializable { + private static final long serialVersionUID = 1L; + } + """, + spec -> spec.markers(javaVersion(11)) + ) + ); + } + + @Test + void shouldNotAnnotateOtherFields() { + rewriteRun( + //language=java + java( + """ + import java.io.Serializable; + + class Example implements Serializable { + static final long serialVersionUID = 1L; + private final long serialVersionUID = 1L; + private static long serialVersionUID = 1L; + private static final int serialVersionUID = 1L; + private static final long foo = 1L; + + void doSomething() { + long serialVersionUID = 1L; + } + } + """ + ) + ); + } + + @Test + void shouldAnnotatedFieldsInInnerClasses() { + rewriteRun( + //language=java + java( + """ + import java.io.Serializable; + + class Outer implements Serializable { + private static final long serialVersionUID = 1; + static class Inner implements Serializable { + private static final long serialVersionUID = 1; + } + } + """, + """ + import java.io.Serial; + import java.io.Serializable; + + class Outer implements Serializable { + @Serial + private static final long serialVersionUID = 1; + static class Inner implements Serializable { + @Serial + private static final long serialVersionUID = 1; + } + } + """ + ) + ); + } +}