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 6ea6db24cf3..4f0c045ed7d 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 @@ -536,6 +536,80 @@ public class A { ); } + @Test + void arrayInAnnotationValueAttribute() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute( + "org.example.Foo", + null, + "newTest", + null, + false, + false)), + java( + """ + package org.example; + public @interface Foo { + String[] value() default {}; + } + """ + ), + java( + """ + import org.example.Foo; + + @Foo({"oldTest"}) + public class A { + } + """, + """ + import org.example.Foo; + + @Foo({"newTest"}) + public class A { + } + """ + ) + ); + } + + @Test + void arrayInAnnotationExplicitValueAttribute() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute( + "org.example.Foo", + null, + "newTest", + null, + false, + false)), + java( + """ + package org.example; + public @interface Foo { + String[] value() default {}; + } + """ + ), + java( + """ + import org.example.Foo; + + @Foo(value = {"oldTest"}) + public class A { + } + """, + """ + import org.example.Foo; + + @Foo(value = {"newTest"}) + public class A { + } + """ + ) + ); + } + @Test void arrayInputMoreThanOneInAnnotationAttribute() { rewriteRun( @@ -573,6 +647,117 @@ public class A { ); } + @Test + void arrayInputMoreThanOneInAnnotationLiteralAttribute() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute( + "org.example.Foo", + "array", + "newTest1,newTest2", + null, + false, + false)), + java( + """ + package org.example; + public @interface Foo { + String[] array() default {}; + } + """ + ), + java( + """ + import org.example.Foo; + + @Foo(array = "oldTest") + public class A { + } + """, + """ + import org.example.Foo; + + @Foo(array = {"newTest1", "newTest2"}) + public class A { + } + """ + ) + ); + } + + @Test + void arrayInputMoreThanOneInAnnotationLiteralValueAttribute() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute( + "org.example.Foo", + null, + "newTest1,newTest2", + null, + false, + false)), + java( + """ + package org.example; + public @interface Foo { + String[] value() default {}; + } + """ + ), + java( + """ + import org.example.Foo; + + @Foo("oldTest") + public class A { + } + """, + """ + import org.example.Foo; + + @Foo({"newTest1", "newTest2"}) + public class A { + } + """ + ) + ); + } + + @Test + void arrayInputMoreThanOneInAnnotationValueAttribute() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute( + "org.example.Foo", + null, + "newTest1,newTest2", + null, + false, + false)), + java( + """ + package org.example; + public @interface Foo { + String[] value() default {}; + } + """ + ), + java( + """ + import org.example.Foo; + + @Foo({"oldTest"}) + public class A { + } + """, + """ + import org.example.Foo; + + @Foo({"newTest1", "newTest2"}) + public class A { + } + """ + ) + ); + } + @Test void addArrayInputInAnnotationAttribute() { rewriteRun( @@ -759,6 +944,74 @@ public class A { ); } + @Test + void addOtherAttributeInArrayValueAnnotation() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute( + "org.example.Foo", + "string", + "test", + null, + null, + false)), + java( + """ + package org.example; + public @interface Foo { + String[] value() default {}; + String string() default ""; + } + """ + ), + java( + """ + import org.example.Foo; + + @Foo({"newTest1", "newTest2"}) + public class A { + } + """, + """ + import org.example.Foo; + + @Foo(string = "test", value = {"newTest1", "newTest2"}) + public class A { + } + """ + ) + ); + } + + @Test + void dontChangeWhenAttributeDoesNotMatchAtAll() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute( + "org.example.Foo", + "array", + "b", + null, + false, + true)), + java( + """ + package org.example; + public @interface Foo { + String[] value() default {}; + } + """ + ), + java( + """ + import org.example.Foo; + + @Foo({"a"}) + public class A { + } + """ + ) + ); + } + @Test void appendSingleValueToExistingArrayAttribute() { rewriteRun( @@ -796,6 +1049,43 @@ public class A { ); } + @Test + void appendSingleValueToExistingArrayValueAttribute() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute( + "org.example.Foo", + null, + "b", + null, + false, + true)), + java( + """ + package org.example; + public @interface Foo { + String[] value() default {}; + } + """ + ), + java( + """ + import org.example.Foo; + + @Foo({"a"}) + public class A { + } + """, + """ + import org.example.Foo; + + @Foo({"a", "b"}) + public class A { + } + """ + ) + ); + } + @Test void appendMultipleValuesToExistingArrayAttribute() { rewriteRun( @@ -833,6 +1123,43 @@ public class A { ); } + @Test + void appendMultipleValuesToExistingArrayValueAttribute() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute( + "org.example.Foo", + null, + "b,c", + null, + false, + true)), + java( + """ + package org.example; + public @interface Foo { + String[] value() default {}; + } + """ + ), + java( + """ + import org.example.Foo; + + @Foo({"a"}) + public class A { + } + """, + """ + import org.example.Foo; + + @Foo({"a", "b", "c"}) + public class A { + } + """ + ) + ); + } + @Test void appendMultipleValuesToExistingArrayAttributeWithOverlap() { rewriteRun( @@ -870,6 +1197,43 @@ public class A { ); } + @Test + void appendMultipleValuesToExistingArrayValueAttributeWithOverlap() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute( + "org.example.Foo", + null, + "b,c", + null, + false, + true)), + java( + """ + package org.example; + public @interface Foo { + String[] value() default {}; + } + """ + ), + java( + """ + import org.example.Foo; + + @Foo({"a", "b"}) + public class A { + } + """, + """ + import org.example.Foo; + + @Foo({"a", "b", "c"}) + public class A { + } + """ + ) + ); + } + @Test void appendMultipleValuesToExistingArrayAttributeNonSet() { rewriteRun( @@ -910,6 +1274,46 @@ public class A { ); } + @Test + void appendMultipleValuesToExistingArrayValueAttributeNonSet() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute( + "org.example.Foo", + null, + "b,c", + null, + true, + true)), + java( + """ + package org.example; + public @interface Foo { + String[] value() default {}; + } + + public class A { + } + """ + ), + java( + """ + import org.example.Foo; + + @Foo({"a", "b"}) + public class A { + } + """, + """ + import org.example.Foo; + + @Foo({"a", "b", "b", "c"}) + public class A { + } + """ + ) + ); + } + @Test void updateFieldAccessAttribute() { rewriteRun( 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 cee402360ef..accedac3c03 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/AddOrUpdateAnnotationAttribute.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/AddOrUpdateAnnotationAttribute.java @@ -36,6 +36,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; +import static java.util.Collections.emptyList; import static java.util.Objects.requireNonNull; import static org.openrewrite.Tree.randomId; @@ -66,7 +67,7 @@ public String getDescription() { String attributeName; @Option(displayName = "Attribute value", - description = "The value to set the attribute to. Set to `null` to remove the attribute.", + description = "The value to set the attribute to. If the attribute is an array, provide values separated by comma to add multiple attributes at once. Set to `null` to remove the attribute.", required = false, example = "500") @Nullable @@ -112,23 +113,12 @@ public J.Annotation visitAnnotation(J.Annotation a, ExecutionContext ctx) { } if (attributeName == null || "value".equals(attributeName)) { - return JavaTemplate.builder("#{}") - .contextSensitive() - .build() - .apply(getCursor(), a.getCoordinates().replaceArguments(), newAttributeValue); + return JavaTemplate + .apply("#{}", getCursor(), a.getCoordinates().replaceArguments(), newAttributeValue); } else { - String newAttributeValueResult = newAttributeValue; - if (((JavaType.FullyQualified) requireNonNull(a.getAnnotationType().getType())).getMethods().stream().anyMatch(method -> method.getReturnType().toString().equals("java.lang.String[]"))) { - String attributeValueCleanedUp = attributeValue.replaceAll("\\s+", "").replaceAll("[\\s+{}\"]", ""); - List attributeList = Arrays.asList(attributeValueCleanedUp.contains(",") ? attributeValueCleanedUp.split(",") : new String[]{attributeValueCleanedUp}); - newAttributeValueResult = attributeList.stream() - .map(String::valueOf) - .collect(Collectors.joining("\", \"", "{\"", "\"}")); - } - return JavaTemplate.builder("#{} = #{}") - .contextSensitive() - .build() - .apply(getCursor(), a.getCoordinates().replaceArguments(), attributeName, newAttributeValueResult); + String newAttributeValueResult = isArray(a) ? getAttributeValuesAsString() : newAttributeValue; + return JavaTemplate + .apply("#{} = #{}", getCursor(), a.getCoordinates().replaceArguments(), attributeName, newAttributeValueResult); } } else { // First assume the value exists amongst the arguments and attempt to update it @@ -153,8 +143,7 @@ public J.Annotation visitAnnotation(J.Annotation a, ExecutionContext ctx) { if (as.getAssignment() instanceof J.NewArray) { List initializerList = requireNonNull(((J.NewArray) as.getAssignment()).getInitializer()); - String attributeValueCleanedUp = attributeValue.replaceAll("\\s+", "").replaceAll("[\\s+{}\"]", ""); - List attributeList = Arrays.asList(attributeValueCleanedUp.contains(",") ? attributeValueCleanedUp.split(",") : new String[]{attributeValueCleanedUp}); + List attributeList = getAttributeValues(); if (as.getMarkers().findFirst(AlreadyAppended.class).filter(ap -> ap.getValues().equals(newAttributeValue)).isPresent()) { return as; @@ -171,10 +160,8 @@ public J.Annotation visitAnnotation(J.Annotation a, ExecutionContext ctx) { continue; } changed = true; - Expression e = requireNonNull(((J.Annotation) JavaTemplate.builder(newAttributeListValue) - .contextSensitive() - .build() - .apply(getCursor(), finalA.getCoordinates().replaceArguments())) + Expression e = requireNonNull(((J.Annotation) JavaTemplate + .apply(newAttributeListValue, getCursor(), finalA.getCoordinates().replaceArguments())) .getArguments()).get(0); initializerList.add(e); } @@ -225,14 +212,21 @@ public J.Annotation visitAnnotation(J.Annotation a, ExecutionContext ctx) { if (!valueMatches(value, oldAttributeValue)) { return it; } + if (isArray(finalA)) { + //noinspection ConstantConditions + return as.withAssignment(((J.Annotation) JavaTemplate + .apply("#{}", getCursor(), finalA.getCoordinates().replaceArguments(), getAttributeValuesAsString())) + .getArguments().get(0)); + } return as.withAssignment(value.withValue(newAttributeValue).withValueSource(newAttributeValue)); } else if (exp instanceof J.FieldAccess) { if (Boolean.TRUE.equals(addOnly) || oldAttributeValue != null) { return it; } - int index = finalA.getArguments().indexOf(as); - as = (J.Assignment) ((J.Annotation) JavaTemplate.apply("#{} = #{}", getCursor(), as.getCoordinates().replace(), var.getSimpleName(), newAttributeValue)).getArguments().get(index); - return as; + //noinspection ConstantConditions + return ((J.Annotation) JavaTemplate + .apply("#{} = #{}", getCursor(), as.getCoordinates().replace(), var.getSimpleName(), newAttributeValue)) + .getArguments().get(finalA.getArguments().indexOf(as)); } } } else if (it instanceof J.Literal) { @@ -242,23 +236,24 @@ public J.Annotation visitAnnotation(J.Annotation a, ExecutionContext ctx) { if (newAttributeValue == null) { return null; } - J.Literal value = (J.Literal) it; - if (newAttributeValue.equals(value.getValueSource()) || Boolean.TRUE.equals(addOnly)) { + J.Literal literal = (J.Literal) it; + if (newAttributeValue.equals(literal.getValueSource()) || Boolean.TRUE.equals(addOnly)) { return it; } - if (!valueMatches(value, oldAttributeValue)) { + if (!valueMatches(literal, oldAttributeValue)) { return it; } - return ((J.Literal) it).withValue(newAttributeValue).withValueSource(newAttributeValue); + if (isArray(finalA)) { + //noinspection ConstantConditions + return ((J.Annotation) JavaTemplate + .apply("#{}", getCursor(), finalA.getCoordinates().replaceArguments(), getAttributeValuesAsString())) + .getArguments().get(0); + } + return literal.withValue(newAttributeValue).withValueSource(newAttributeValue); } else if (oldAttributeValue == null) { // Without an oldAttributeValue and an attributeName not matching `value` we want to add an extra argument to the annotation. // Make the attribute name explicit, before we add the new value below - //noinspection ConstantConditions - return ((J.Annotation) JavaTemplate.builder("value = #{}") - .contextSensitive() - .build() - .apply(getCursor(), finalA.getCoordinates().replaceArguments(), it) - ).getArguments().get(0); + return createAnnotationAssignment(finalA, "value", it); } } else if (it instanceof J.FieldAccess) { // The only way anything except an assignment can appear is if there's an implicit assignment to "value" @@ -275,16 +270,12 @@ public J.Annotation visitAnnotation(J.Annotation a, ExecutionContext ctx) { return it; } //noinspection ConstantConditions - return ((J.Annotation) JavaTemplate.apply(newAttributeValue, getCursor(), finalA.getCoordinates().replaceArguments())) + return ((J.Annotation) JavaTemplate + .apply(newAttributeValue, getCursor(), finalA.getCoordinates().replaceArguments())) .getArguments().get(0); } else { // Make the attribute name explicit, before we add the new value below - //noinspection ConstantConditions - return ((J.Annotation) JavaTemplate.builder("value = #{any()}") - .contextSensitive() - .build() - .apply(getCursor(), finalA.getCoordinates().replaceArguments(), it)) - .getArguments().get(0); + return createAnnotationAssignment(finalA, "value", it); } } else if (it instanceof J.NewArray) { if (it.getMarkers().findFirst(AlreadyAppended.class).filter(ap -> ap.getValues().equals(newAttributeValue)).isPresent()) { @@ -295,10 +286,13 @@ public J.Annotation visitAnnotation(J.Annotation a, ExecutionContext ctx) { return null; } + if (attributeName != null && !attributeValue.equals("value")) { + return isAnnotationWithOnlyValueMethod(finalA) ? it : createAnnotationAssignment(finalA, "value", it); + } + J.NewArray arrayValue = (J.NewArray) it; List initializerList = requireNonNull(arrayValue.getInitializer()); - String attributeValueCleanedUp = attributeValue.replaceAll("\\s+", "").replaceAll("[\\s+{}\"]", ""); - List attributeList = Arrays.asList(attributeValueCleanedUp.contains(",") ? attributeValueCleanedUp.split(",") : new String[]{attributeValueCleanedUp}); + List attributeList = getAttributeValues(); if (Boolean.TRUE.equals(appendArray)) { boolean changed = false; @@ -309,10 +303,8 @@ public J.Annotation visitAnnotation(J.Annotation a, ExecutionContext ctx) { } changed = true; - Expression e = requireNonNull(((J.Annotation) JavaTemplate.builder(newAttributeListValue) - .contextSensitive() - .build() - .apply(getCursor(), finalA.getCoordinates().replaceArguments())) + Expression e = requireNonNull(((J.Annotation) JavaTemplate + .apply(newAttributeListValue, getCursor(), finalA.getCoordinates().replaceArguments())) .getArguments()).get(0); initializerList.add(e); } @@ -353,10 +345,8 @@ 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); - Expression e = requireNonNull(((J.Annotation) JavaTemplate.builder(newAttributeListValue) - .contextSensitive() - .build() - .apply(getCursor(), finalA.getCoordinates().replaceArguments())) + Expression e = requireNonNull(((J.Annotation) JavaTemplate + .apply(newAttributeListValue, getCursor(), finalA.getCoordinates().replaceArguments())) .getArguments()).get(0); initializerList.add(j, e); } @@ -370,24 +360,56 @@ public J.Annotation visitAnnotation(J.Annotation a, ExecutionContext ctx) { if (newArgs != currentArgs) { a = a.withArguments(newArgs); } - if (!foundOrSetAttributeWithDesiredValue.get() && !attributeValIsAlreadyPresent(newArgs, newAttributeValue) && oldAttributeValue == null) { + if (!foundOrSetAttributeWithDesiredValue.get() && !attributeValIsAlreadyPresent(newArgs, newAttributeValue) && oldAttributeValue == null && !isAnnotationWithOnlyValueMethod(a)) { // There was no existing value to update and no requirements on a pre-existing old value, so add a new value into the argument list - String effectiveName = (attributeName == null) ? "value" : attributeName; - //noinspection ConstantConditions - J.Assignment as = (J.Assignment) ((J.Annotation) JavaTemplate.builder("#{} = #{}") - .contextSensitive() - .build() - .apply(getCursor(), a.getCoordinates().replaceArguments(), effectiveName, newAttributeValue)) - .getArguments().get(0); + String effectiveName = attributeName == null ? "value" : attributeName; + J.Assignment as = createAnnotationAssignment(a, effectiveName, newAttributeValue); a = a.withArguments(ListUtils.concat(as, a.getArguments())); } } a = maybeAutoFormat(original, a, ctx); return a; } + + private J.Assignment createAnnotationAssignment(J.Annotation annotation, String name, @Nullable Object parameter) { + //noinspection ConstantConditions + return (J.Assignment) ((J.Annotation) JavaTemplate + .apply(name + " = " + (parameter instanceof J ? "#{any()}" : "#{}"), getCursor(), annotation.getCoordinates().replaceArguments(), parameter)) + .getArguments().get(0); + } }); } + private List getAttributeValues() { + if (attributeValue == null) { + return emptyList(); + } + String attributeValueCleanedUp = attributeValue.replaceAll("\\s+", "").replaceAll("[\\s+{}\"]", ""); + return Arrays.asList(attributeValueCleanedUp.contains(",") ? attributeValueCleanedUp.split(",") : new String[]{attributeValueCleanedUp}); + } + + private String getAttributeValuesAsString() { + return getAttributeValues().stream() + .map(String::valueOf) + .collect(Collectors.joining("\", \"", "{\"", "\"}")); + } + + private boolean isArray(J.Annotation annotation) { + String effectiveName = attributeName == null ? "value" : attributeName; + List methods = ((JavaType.FullyQualified) requireNonNull(annotation.getAnnotationType().getType())).getMethods(); + for (JavaType.Method method : methods) { + if (effectiveName.equals(method.getName()) && method.getReturnType().toString().equals("java.lang.String[]")) { + return true; + } + } + return false; + } + + private static boolean isAnnotationWithOnlyValueMethod(J.Annotation annotation) { + return ((JavaType.FullyQualified) requireNonNull(annotation.getAnnotationType().getType())).getMethods().size() == 1 && + ((JavaType.FullyQualified) requireNonNull(annotation.getAnnotationType().getType())).getMethods().get(0).getName().equals("value"); + } + private static boolean valueMatches(@Nullable Expression expression, @Nullable String oldAttributeValue) { if (expression == null) { return oldAttributeValue == null;