diff --git a/rewrite-java-test/src/test/java/org/openrewrite/java/AddOrUpdateAnnotationAttributeTest.java b/rewrite-java-test/src/test/java/org/openrewrite/java/AddOrUpdateAnnotationAttributeTest.java index fe45a9774b6..ac1b1bc8117 100755 --- a/rewrite-java-test/src/test/java/org/openrewrite/java/AddOrUpdateAnnotationAttributeTest.java +++ b/rewrite-java-test/src/test/java/org/openrewrite/java/AddOrUpdateAnnotationAttributeTest.java @@ -25,7 +25,7 @@ class AddOrUpdateAnnotationAttributeTest implements RewriteTest { @Test void addValueAttribute() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", null, "hello", null)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", null, "hello", null, null)), java( """ package org.example; @@ -56,7 +56,7 @@ public class A { @Test void addValueAttributeClass() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", null, "Integer.class", null)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", null, "Integer.class", null, null)), java( """ package org.example; @@ -87,7 +87,7 @@ public class A { @Test void addValueAttributeFullyQualifiedClass() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", null, "java.math.BigDecimal.class", null)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", null, "java.math.BigDecimal.class", null, null)), java( """ package org.example; @@ -118,7 +118,7 @@ public class A { @Test void updateValueAttribute() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", null, "hello", null)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", null, "hello", null, null)), java( """ package org.example; @@ -150,7 +150,7 @@ public class A { @Test void updateValueAttributeClass() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", null, "Integer.class", null)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", null, "Integer.class", null, null)), java( """ package org.example; @@ -182,7 +182,7 @@ public class A { @Test void removeValueAttribute() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", null, null, null)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", null, null, null, null)), java( """ package org.example; @@ -214,7 +214,7 @@ public class A { @Test void removeValueAttributeClass() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", null, null, null)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", null, null, null, null)), java( """ package org.example; @@ -245,7 +245,7 @@ public class A { @Test void addNamedAttribute() { - rewriteRun(spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.junit.Test", "timeout", "500", null)), + rewriteRun(spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.junit.Test", "timeout", "500", null, null)), java( """ package org.junit; @@ -282,7 +282,7 @@ void foo() { @Test void replaceAttribute() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.junit.Test", "timeout", "500", null)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.junit.Test", "timeout", "500", null, null)), java( """ package org.junit; @@ -319,7 +319,7 @@ void foo() { @Test void removeAttribute() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.junit.Test", "timeout", null, null)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.junit.Test", "timeout", null, null, null)), java( """ package org.junit; @@ -356,7 +356,7 @@ void foo() { @Test void preserveExistingAttributes() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.junit.Test", "timeout", "500", null)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.junit.Test", "timeout", "500", null, null)), java( """ package org.junit; @@ -394,7 +394,7 @@ void foo() { @Test void implicitValueToExplicitValue() { - rewriteRun(spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.junit.Test", "other", "1", null)), + rewriteRun(spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.junit.Test", "other", "1", null, null)), java( """ package org.junit; @@ -431,7 +431,7 @@ void foo() { @Test void implicitValueToExplicitValueClass() { - rewriteRun(spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.junit.Test", "other", "1", null)), + rewriteRun(spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.junit.Test", "other", "1", null, null)), java( """ package org.junit; @@ -469,7 +469,7 @@ void foo() { @Test void dontChangeWhenSetToAddOnly() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.junit.Test", "other", "1", true)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.junit.Test", "other", "1", true, false)), java( """ package org.junit; @@ -500,6 +500,7 @@ void arrayInAnnotationAttribute() { "org.example.Foo", "array", "newTest", + false, false)), java( """ @@ -535,6 +536,7 @@ void arrayInputMoreThanOneInAnnotationAttribute() { "org.example.Foo", "array", "newTest1,newTest2", + false, false)), java( """ @@ -570,6 +572,7 @@ void addArrayInputInAnnotationAttribute() { "org.example.Foo", "array", "newTest1,newTest2", + false, false)), java( """ @@ -598,6 +601,42 @@ public class A { ); } + @Test + void addArrayInputInAnnotationAttributeWithAppendTrue() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute( + "org.example.Foo", + "array", + "newTest1,newTest2", + false, + true)), + java( + """ + package org.example; + public @interface Foo { + String[] array() default {}; + } + """ + ), + java( + """ + import org.example.Foo; + + @Foo + public class A { + } + """, + """ + import org.example.Foo; + + @Foo(array = {"newTest1", "newTest2"}) + public class A { + } + """ + ) + ); + } + @Test void addArrayInputInAnnotationAttributeEmptyBraces() { rewriteRun( @@ -605,7 +644,8 @@ void addArrayInputInAnnotationAttributeEmptyBraces() { "org.example.Foo", "array", "newTest1,newTest2", - false)), + false, + null)), java( """ package org.example; @@ -640,7 +680,8 @@ void removeArrayInputInAnnotationAttribute() { "org.example.Foo", "array", null, - null)), + null, + false)), java( """ package org.example; @@ -675,7 +716,8 @@ void addOtherAttributeInArrayAnnotation() { "org.example.Foo", "string", "test", - null)), + null, + false)), java( """ package org.example; @@ -703,4 +745,151 @@ public class A { ) ); } + + @Test + void appendSingleValueToExistingArrayAttribute() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute( + "org.example.Foo", + "array", + "b", + false, + true)), + java( + """ + package org.example; + public @interface Foo { + String[] array() default {}; + } + """ + ), + java( + """ + import org.example.Foo; + + @Foo(array = {"a"}) + public class A { + } + """, + """ + import org.example.Foo; + + @Foo(array = {"a", "b"}) + public class A { + } + """ + ) + ); + } + + @Test + void appendMultipleValuesToExistingArrayAttribute() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute( + "org.example.Foo", + "array", + "b,c", + false, + true)), + java( + """ + package org.example; + public @interface Foo { + String[] array() default {}; + } + """ + ), + java( + """ + import org.example.Foo; + + @Foo(array = {"a"}) + public class A { + } + """, + """ + import org.example.Foo; + + @Foo(array = {"a", "b", "c"}) + public class A { + } + """ + ) + ); + } + + @Test + void appendMultipleValuesToExistingArrayAttributeWithOverlap() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute( + "org.example.Foo", + "array", + "b,c", + false, + true)), + java( + """ + package org.example; + public @interface Foo { + String[] array() default {}; + } + """ + ), + java( + """ + import org.example.Foo; + + @Foo(array = {"a", "b"}) + public class A { + } + """, + """ + import org.example.Foo; + + @Foo(array = {"a", "b", "c"}) + public class A { + } + """ + ) + ); + } + + @Test + void appendMultipleValuesToExistingArrayAttributeNonSet() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute( + "org.example.Foo", + "array", + "b,c", + true, + true)), + java( + """ + package org.example; + public @interface Foo { + String[] array() default {}; + } + + public class A { + } + """ + ), + java( + """ + import org.example.Foo; + + @Foo(array = {"a", "b"}) + public class A { + } + """, + """ + import org.example.Foo; + + @Foo(array = {"a", "b", "b", "c"}) + public class A { + } + """ + ) + ); + } } diff --git a/rewrite-java/src/main/java/org/openrewrite/java/AddOrUpdateAnnotationAttribute.java b/rewrite-java/src/main/java/org/openrewrite/java/AddOrUpdateAnnotationAttribute.java index 39c40e48843..be098009e16 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/AddOrUpdateAnnotationAttribute.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/AddOrUpdateAnnotationAttribute.java @@ -17,22 +17,24 @@ import lombok.EqualsAndHashCode; import lombok.Value; +import lombok.With; import org.jspecify.annotations.Nullable; import org.openrewrite.*; import org.openrewrite.internal.ListUtils; import org.openrewrite.java.search.UsesType; -import org.openrewrite.java.tree.Expression; -import org.openrewrite.java.tree.J; -import org.openrewrite.java.tree.JavaType; -import org.openrewrite.java.tree.TypeUtils; +import org.openrewrite.java.tree.*; +import org.openrewrite.marker.Marker; import org.openrewrite.marker.Markers; import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; +import static org.openrewrite.Tree.randomId; + @Value @EqualsAndHashCode(callSuper = false) public class AddOrUpdateAnnotationAttribute extends Recipe { @@ -70,6 +72,14 @@ public String getDescription() { @Nullable Boolean addOnly; + @Option(displayName = "Append array", + description = "If the attribute is an array, setting this option to `true` will append the value(s). " + + "In conjunction with `addOnly`, it is possible to control duplicates: " + + "`addOnly=true`, always append. " + + "`addOnly=false`, only append if the value is not already present.") + @Nullable + Boolean appendArray; + @Override public TreeVisitor getVisitor() { return Preconditions.check(new UsesType<>(annotationType, false), new JavaIsoVisitor() { @@ -123,10 +133,28 @@ public J.Annotation visitAnnotation(J.Annotation a, ExecutionContext ctx) { return null; } - if (as.getAssignment() instanceof J.NewArray){ + if (as.getAssignment() instanceof J.NewArray) { List jLiteralList = ((J.NewArray) as.getAssignment()).getInitializer(); String attributeValueCleanedUp = attributeValue.replaceAll("\\s+","").replaceAll("[\\s+{}\"]",""); List attributeList = Arrays.asList(attributeValueCleanedUp.contains(",") ? attributeValueCleanedUp.split(",") : new String[]{attributeValueCleanedUp}); + + if (as.getMarkers().findFirst(AlreadyAppended.class).filter(ap -> ap.getValues().equals(newAttributeValue)).isPresent()) { + return as; + } + + if (Boolean.TRUE.equals(appendArray)) { + boolean changed = false; + for (String attrListValues : attributeList) { + String newAttributeListValue = maybeQuoteStringArgument(attributeName, attrListValues, finalA); + if (Boolean.FALSE.equals(addOnly) && attributeValIsAlreadyPresent(jLiteralList, newAttributeListValue)) { + continue; + } + changed = true; + jLiteralList.add(new J.Literal(randomId(), Space.EMPTY, Markers.EMPTY, newAttributeListValue, newAttributeListValue, null, JavaType.Primitive.String)); + } + return changed ? as.withAssignment(((J.NewArray) as.getAssignment()).withInitializer(jLiteralList)) + .withMarkers(as.getMarkers().add(new AlreadyAppended(randomId(), newAttributeValue))) : as; + } int m = 0; for (int i = 0; i< Objects.requireNonNull(jLiteralList).size(); i++){ if (i >= attributeList.size()){ @@ -152,7 +180,7 @@ public J.Annotation visitAnnotation(J.Annotation a, ExecutionContext ctx) { } for (int j = m; j < attributeList.size(); j++){ String newAttributeListValue = maybeQuoteStringArgument(attributeName, attributeList.get(j), finalA); - jLiteralList.add(j, new J.Literal(Tree.randomId(), jLiteralList.get(j-1).getPrefix(), Markers.EMPTY, newAttributeListValue, newAttributeListValue, null, JavaType.Primitive.String)); + jLiteralList.add(j, new J.Literal(randomId(), jLiteralList.get(j - 1).getPrefix(), Markers.EMPTY, newAttributeListValue, newAttributeListValue, null, JavaType.Primitive.String)); } } @@ -255,4 +283,23 @@ private static boolean attributeIsString(@Nullable String attributeName, J.Annot } return false; } + + private static boolean attributeValIsAlreadyPresent(List expression, @Nullable String attributeValue) { + for (Expression e : expression) { + if (e instanceof J.Literal) { + J.Literal literal = (J.Literal) e; + if (literal.getValueSource() != null && literal.getValueSource().equals(attributeValue)) { + return true; + } + } + } + return false; + } + + @Value + @With + private static class AlreadyAppended implements Marker { + UUID id; + String values; + } } diff --git a/rewrite-java/src/main/java/org/openrewrite/java/recipes/MissingOptionExample.java b/rewrite-java/src/main/java/org/openrewrite/java/recipes/MissingOptionExample.java index 51d668ac0d2..847f84fdc87 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/recipes/MissingOptionExample.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/recipes/MissingOptionExample.java @@ -80,7 +80,7 @@ public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ct } } - AddOrUpdateAnnotationAttribute addOrUpdateAnnotationAttribute = new AddOrUpdateAnnotationAttribute(ORG_OPENREWRITE_OPTION, "example", "TODO Provide a usage example for the docs", true); + AddOrUpdateAnnotationAttribute addOrUpdateAnnotationAttribute = new AddOrUpdateAnnotationAttribute(ORG_OPENREWRITE_OPTION, "example", "TODO Provide a usage example for the docs", true, false); return (J.Annotation) addOrUpdateAnnotationAttribute.getVisitor().visitNonNull(an, ctx, getCursor().getParent()); } });