From 8165b9d0f5971ebb9c72f77151b4e9122f10aec5 Mon Sep 17 00:00:00 2001 From: Steve Elliott Date: Wed, 13 Aug 2025 17:01:22 -0400 Subject: [PATCH 1/8] Improving support for `appendArray` when going from literal to array, either via single value or a comma-separated list of values --- .../AddOrUpdateAnnotationAttributeTest.java | 33 ++++++++++++++++++- .../java/AddOrUpdateAnnotationAttribute.java | 26 ++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) 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 d0d2af4209..fcebd4ac57 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 @@ -60,6 +60,37 @@ public class A { ); } + @Test + void literalToListFromSingleValueUsingAppendArray() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", "bars", "xyz", null, null, true)), + //language=java + java( + """ + package org.example; + public @interface Foo { + String[] bars() default {}; + } + """ + ), + //language=java + java( + """ + import org.example.Foo; + + @Foo(bars = "abc") + public class A {} + """, + """ + import org.example.Foo; + + @Foo(bars = {"abc", "xyz"}) + public class A {} + """ + ) + ); + } + @Test void addValueAttributeClass() { rewriteRun( @@ -748,7 +779,7 @@ public class A { """ import org.example.Foo; - @Foo(array = {"newTest1", "newTest2"}) + @Foo(array = {"oldTest", "newTest1", "newTest2"}) 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 497b58dcba..9bb1fc53db 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/AddOrUpdateAnnotationAttribute.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/AddOrUpdateAnnotationAttribute.java @@ -34,6 +34,7 @@ import static java.util.Collections.*; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; import static org.openrewrite.Tree.randomId; import static org.openrewrite.java.tree.Space.SINGLE_SPACE; import static org.openrewrite.marker.Markers.EMPTY; @@ -172,6 +173,16 @@ public J.Annotation visitAnnotation(J.Annotation original, ExecutionContext ctx) if (!valueMatches(exp, oldAttributeValue) || newAttributeValue.equals(((J.Literal) exp).getValueSource())) { return as; } + if (TRUE.equals(appendArray) && attributeIsArray(annotation)) { + List updatedList = updateInitializer(annotation, singletonList(as.getAssignment()), getAttributeValues()); + Expression flattenedList = createAnnotationLiteralFromString( + annotation, + wrapValues(updatedList.stream() + .map(e -> ((J.Literal) e).getValueSource()) + .collect(toList()), true) + ); + return as.withAssignment(flattenedList); + } return as.withAssignment(createAnnotationLiteral(annotation, newAttributeValue)); } if (exp instanceof J.FieldAccess) { @@ -247,6 +258,12 @@ private Expression createAnnotationLiteral(J.Annotation annotation, String newAt .getArguments().get(0); } + private Expression createAnnotationLiteralFromString(J.Annotation annotation, String updatedAttributeValue) { + //noinspection ConstantConditions + return JavaTemplate.apply("#{}", getCursor(), annotation.getCoordinates().replaceArguments(), updatedAttributeValue) + .getArguments().get(0); + } + private J.Assignment createAnnotationAssignment(J.Annotation annotation, String name, @Nullable Object parameter) { //noinspection ConstantConditions return (J.Assignment) JavaTemplate.apply(name + " = " + (parameter instanceof J ? "#{any()}" : "#{}"), getCursor(), annotation.getCoordinates().replaceArguments(), parameter) @@ -325,7 +342,14 @@ private List getAttributeValues() { } private String getAttributeValuesAsString() { - return getAttributeValues().stream().map(String::valueOf).collect(joining("\", \"", "{\"", "\"}")); + return wrapValues(getAttributeValues(), false); + } + + private String wrapValues(List<@Nullable String> values, boolean quoteless) { + if (quoteless) { + return values.stream().map(String::valueOf).collect(joining(", ", "{", "}")); + } + return values.stream().map(String::valueOf).collect(joining("\", \"", "{\"", "\"}")); } private static boolean isAnnotationWithOnlyValueMethod(J.Annotation annotation) { From d650bed741b5eb0fd1ad79e2bdb9278706cf3684 Mon Sep 17 00:00:00 2001 From: Steve Elliott Date: Thu, 14 Aug 2025 17:31:35 -0400 Subject: [PATCH 2/8] WiP --- .../AddOrUpdateAnnotationAttributeTest.java | 652 ++++++++++++++++++ .../java/AddOrUpdateAnnotationAttribute.java | 39 +- 2 files changed, 682 insertions(+), 9 deletions(-) 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 fcebd4ac57..96dc8dea3b 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 @@ -21,12 +21,664 @@ import org.junit.jupiter.api.Test; import org.openrewrite.DocumentExample; import org.openrewrite.Issue; +import org.openrewrite.test.RecipeSpec; import org.openrewrite.test.RewriteTest; import org.openrewrite.test.SourceSpec; import static org.openrewrite.java.Assertions.java; class AddOrUpdateAnnotationAttributeTest implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec.parser(JavaParser.fromJavaVersion().dependsOn( + //language=java + """ + package org.example; + public @interface FooDefaultString { + String value() default ""; + String a() default ""; + String[] b() default {}; + Class c() default null; + Class[] d() default {}; + } + """, + //language=java + """ + package org.example; + public @interface FooDefaultStringArray { + String[] value() default {}; + String a() default ""; + String[] b() default {}; + Class c() default null; + Class[] d() default {}; + } + """, + //language=java + """ + package org.example; + public @interface FooDefaultClass { + Class value() default null; + String a() default ""; + String[] b() default {}; + Class c() default null; + Class[] d() default {}; + } + """, + //language=java + """ + package org.example; + public @interface FooDefaultClassArray { + Class[] value() default {}; + String a() default ""; + String[] b() default {}; + Class c() default null; + Class[] d() default {}; + } + """ + )); + } + + @Nested + class UsingImplicitAttributeName { + @Nested + class UsingNullAttributeValue { + // TODO: Check on `addOnly` and `appendArray` effects + @Nested + class WithLiteralTypeAttribute { + @Test + void literalAttribute_absent_doesNothing() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, null, null, null, null)), + //language=java + java( + """ + import org.example.FooDefaultString; + @FooDefaultString + @FooDefaultString() + public class A {} + """ + ), + //language=java + java( + """ + import org.example.FooDefaultString; + @FooDefaultString(a = "a") + public class B {} + """ + ) + ); + } + + @Test + void literalAttribute_existing_usingNullOldAttributeValue_removesSafely() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, null, null, null, null)), + //language=java + java( + """ + import org.example.FooDefaultString; + @FooDefaultString("a") + @FooDefaultString(value = "b") + public class A {} + """, + """ + import org.example.FooDefaultString; + @FooDefaultString + @FooDefaultString + public class A {} + """ + ), + //language=java + java( + """ + import org.example.FooDefaultString; + @FooDefaultString("a", a = "z") + @FooDefaultString(value = "b", a = "y") + public class B {} + """, + """ + import org.example.FooDefaultString; + @FooDefaultString(a = "z") + @FooDefaultString(a = "y") + public class B {} + """ + ) + ); + } + + @Test + void literalAttribute_existing_usingProvidedOldAttributeValue_removesSafelyOnlyMatched() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, null, "a", null, null)), + //language=java + java( + """ + import org.example.FooDefaultString; + @FooDefaultString("a") + @FooDefaultString("b") + @FooDefaultString(value = "a") + @FooDefaultString(value = "b") + public class A {} + """, + """ + import org.example.FooDefaultString; + @FooDefaultString + @FooDefaultString("b") + @FooDefaultString + @FooDefaultString("b") + public class A {} + """ + ), + //language=java + java( + """ + import org.example.FooDefaultString; + @FooDefaultString("a", a = "a") + @FooDefaultString("b", a = "a") + @FooDefaultString(value = "a", a = "a") + @FooDefaultString(value = "b", a = "a") + public class B {} + """, + """ + import org.example.FooDefaultString; + @FooDefaultString(a = "a") + @FooDefaultString("b", a = "a") + @FooDefaultString(a = "a") + @FooDefaultString(value = "b", a = "a") + public class B {} + """ + ) + ); + } + } + + @Nested + class WithLiteralArrayTypeAttribute { + @Test + void literalArrayAttribute_absent_doesNothing() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", null, null, null, null, null)), + //language=java + java( + """ + import org.example.FooDefaultStringArray; + @FooDefaultStringArray + @FooDefaultStringArray() + public class A {} + """ + ), + //language=java + java( + """ + import org.example.FooDefaultStringArray; + @FooDefaultStringArray(b = "a") + @FooDefaultStringArray(b = {"b"}) + public class B {} + """ + ) + ); + } + + @Test + void literalArrayAttribute_existing_usingNullOldAttributeValue_removesSafely() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", null, null, null, null, null)), + //language=java + java( + """ + import org.example.FooDefaultStringArray; + @FooDefaultStringArray("a") + @FooDefaultStringArray(value = "b") + @FooDefaultStringArray({"c"}) + @FooDefaultStringArray(value = {"d"}) + public class A {} + """, + """ + import org.example.FooDefaultStringArray; + @FooDefaultStringArray + @FooDefaultStringArray + @FooDefaultStringArray + @FooDefaultStringArray + public class A {} + """ + ), + //language=java + java( + """ + import org.example.FooDefaultStringArray; + @FooDefaultStringArray("a", b = {"z"}) + @FooDefaultStringArray(value = "b", b = {"y"}) + @FooDefaultStringArray({"c"}, b = {"x"}) + @FooDefaultStringArray(value = {"d"}, b = {"w"}) + public class B {} + """, + """ + import org.example.FooDefaultStringArray; + @FooDefaultStringArray(b = {"z"}) + @FooDefaultStringArray(b = {"y"}) + @FooDefaultStringArray(b = {"x"}) + @FooDefaultStringArray(b = {"w"}) + public class B {} + """ + ) + ); + } + + @Test + void literalArrayAttribute_existing_usingProvidedOldAttributeValue_removesSafelyOnlyMatched() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", null, null, "a", null, null)), + //language=java + java( + """ + import org.example.FooDefaultStringArray; + @FooDefaultStringArray("a") + @FooDefaultStringArray("b") + @FooDefaultStringArray(value = "a") + @FooDefaultStringArray(value = "b") + @FooDefaultStringArray({"a"}) + @FooDefaultStringArray({"b", "a", "c"}) + @FooDefaultStringArray(value = {"a"}) + @FooDefaultStringArray(value = {"b", "a", "c"}) + public class A {} + """, + """ + import org.example.FooDefaultStringArray; + @FooDefaultStringArray + @FooDefaultStringArray("b") + @FooDefaultStringArray + @FooDefaultStringArray("b") + @FooDefaultStringArray + @FooDefaultStringArray({"b", "c"}) + @FooDefaultStringArray + @FooDefaultStringArray({"b", "c"}) + public class A {} + """ + ), + //language=java + java( + """ + import org.example.FooDefaultStringArray; + @FooDefaultStringArray("a", b = {"a"}) + @FooDefaultStringArray("b", b = {"a"}) + @FooDefaultStringArray(value = "a", b = {"a"}) + @FooDefaultStringArray(value = "b", b = {"a"}) + @FooDefaultStringArray({"a"}, b = {"a"}) + @FooDefaultStringArray({"b", "a", "c"}, b = {"a"}) + @FooDefaultStringArray(value = {"a"}, b = {"a"}) + @FooDefaultStringArray(value = {"b", "a", "c"}, b = {"a"}) + public class B {} + """, + """ + import org.example.FooDefaultStringArray; + @FooDefaultStringArray(b = {"a"}) + @FooDefaultStringArray("b", b = {"a"}) + @FooDefaultStringArray(b = {"a"}) + @FooDefaultStringArray(value = "b", b = {"a"}) + @FooDefaultStringArray(b = {"a"}) + @FooDefaultStringArray({"b", "c"}, b = {"a"}) + @FooDefaultStringArray(b = {"a"}) + @FooDefaultStringArray(value = {"b", "c"}, b = {"a"}) + public class B {} + """ + ) + ); + } + } + + @Nested + class WithClassTypeAttribute { + @Test + void classAttribute_absent_doesNothing() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultClass", null, null, null, null, null)), + //language=java + java( + """ + import org.example.FooDefaultClass; + @FooDefaultClass + @FooDefaultClass() + public class A {} + """ + ), + //language=java + java( + """ + import org.example.FooDefaultClass; + @FooDefaultClass(c = Integer.class) + public class B {} + """ + ) + ); + } + + @Test + void classAttribute_existing_usingNullOldAttributeValue_removesSafely() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultClass", null, null, null, null, null)), + //language=java + java( + """ + import org.example.FooDefaultClass; + @FooDefaultClass(Integer.class) + @FooDefaultClass(Long.class) + public class A {} + """, + """ + import org.example.FooDefaultClass; + @FooDefaultClass + @FooDefaultClass + public class A {} + """ + ), + //language=java + java( + """ + import org.example.FooDefaultClass; + @FooDefaultClass(Integer.class, c = Short.class) + @FooDefaultClass(value = Long.class, c = Byte.class) + public class B {} + """, + """ + import org.example.FooDefaultClass; + @FooDefaultClass(c = Short.class) + @FooDefaultClass(c = Byte.class) + public class B {} + """ + ) + ); + } + + @Test + void classArrayAttribute_existing_usingProvidedOldAttributeValue_removesSafelyOnlyMatched() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultClassArray", null, null, "Integer.class", null, null)), + //language=java + java( + """ + import org.example.FooDefaultClassArray; + @FooDefaultClassArray(Integer.class) + @FooDefaultClassArray(Long.class) + @FooDefaultClassArray(value = Integer.class) + @FooDefaultClassArray(value = Long.class) + @FooDefaultClassArray({Integer.class}) + @FooDefaultClassArray({Long.class, Integer.class, Short.class}) + @FooDefaultClassArray(value = {Integer.class}) + @FooDefaultClassArray(value = {Long.class, Integer.class, Short.class}) + public class A {} + """, + """ + import org.example.FooDefaultClassArray; + @FooDefaultClassArray + @FooDefaultClassArray(Long.class) + @FooDefaultClassArray + @FooDefaultClassArray(Long.class) + @FooDefaultClassArray + @FooDefaultClassArray({Long.class, Short.class}) + @FooDefaultClassArray + @FooDefaultClassArray({Long.class, Short.class}) + public class A {} + """ + ), + //language=java + java( + """ + import org.example.FooDefaultClassArray; + @FooDefaultClassArray(Integer.class, d = {Integer.class}) + @FooDefaultClassArray(Long.class, d = {Integer.class}) + @FooDefaultClassArray(value = Integer.class, d = {Integer.class}) + @FooDefaultClassArray(value = Long.class, d = {Integer.class}) + @FooDefaultClassArray({Integer.class}, d = {Integer.class}) + @FooDefaultClassArray({Long.class, Integer.class, Short.class}, d = {Integer.class}) + @FooDefaultClassArray(value = {Integer.class}, d = {Integer.class}) + @FooDefaultClassArray(value = {Long.class, Integer.class, Short.class}, d = {Integer.class}) + public class B {} + """, + """ + import org.example.FooDefaultClassArray; + @FooDefaultClassArray(d = {Integer.class}) + @FooDefaultClassArray(Long.class, d = {Integer.class}) + @FooDefaultClassArray(d = {Integer.class}) + @FooDefaultClassArray(value = Long.class, d = {Integer.class}) + @FooDefaultClassArray(d = {Integer.class}) + @FooDefaultClassArray({Long.class, Short.class}, d = {Integer.class}) + @FooDefaultClassArray(d = {Integer.class}) + @FooDefaultClassArray(value = {Long.class, Short.class}, d = {Integer.class}) + public class B {} + """ + ) + ); + } + } + + + + + + + + + @Test + void classArrayAttribute_absent_doesNothing() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultClassArray", null, null, null, null, null)), + //language=java + java( + """ + import org.example.FooDefaultClassArray; + @FooDefaultClassArray + public class A { + @FooDefaultClassArray() + public class AInner {} + } + """ + ), + //language=java + java( + """ + import org.example.FooDefaultClassArray; + @FooDefaultClassArray(d = Integer.class) + public class B { + @FooDefaultClassArray(d = {Long.class}) + public class BInner {} + } + """ + ) + ); + } + } + + @Nested + class WhenImplicitIsLiteralType { + @Test + void unsetImplicitToLiteral_Adds() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "hello", null, null, null)), + //language=java + java( + """ + import org.example.FooDefaultString; + @FooDefaultString + public class A {} + """, + """ + import org.example.FooDefaultString; + @FooDefaultString("hello") + public class A {} + """ + ), + //language=java + java( + """ + import org.example.FooDefaultString; + @FooDefaultString() + public class B {} + """, + """ + import org.example.FooDefaultString; + @FooDefaultString("hello") + public class B {} + """ + ) + ); + } + + @Test + void existingImplicitToLiteral_Updates_AndDropsAttributeName() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "bonjour", null, null, null)), + //language=java + java( + """ + import org.example.FooDefaultString; + @FooDefaultString("hello") + public class A {} + """, + """ + import org.example.FooDefaultString; + @FooDefaultString("bonjour") + public class A {} + """ + ), + //language=java + java( + """ + import org.example.FooDefaultString; + @FooDefaultString(value = "hello") + public class B {} + """, + """ + import org.example.FooDefaultString; + @FooDefaultString("bonjour") + public class B {} + """ + ) + ); + } + + @Test + void existingImplicitToNull_Removes() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, null, null, null, null)), + //language=java + java( + """ + import org.example.FooDefaultString; + @FooDefaultString("hello") + public class A {} + """, + """ + import org.example.FooDefaultString; + @FooDefaultString + public class A {} + """ + ), + //language=java + java( + """ + import org.example.FooDefaultString; + @FooDefaultString(value = "hello") + public class B {} + """, + """ + import org.example.FooDefaultString; + @FooDefaultString + public class B {} + """ + ) + ); + } + + @Test + void existingOtherAttributes_AndUnsetImplicitToLiteral_AddsSafely_AndAddsAttributeName() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "bonjour", null, null, null)), + //language=java + java( + """ + import org.example.FooDefaultString; + @FooDefaultString(additionalValue = "hello", additionalValues = {"hi"}) + public class A {} + """, + """ + import org.example.FooDefaultString; + @FooDefaultString(value = "bonjour", additionalValue = "hello", additionalValues = {"hi"}) + public class A {} + """ + ) + ); + } + + @Test + void existingOtherAttributes_AndExistingImplicitToLiteral_AddsSafely_AndRetainsAttributeNameUsage() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "bonjour", null, null, null)), + //language=java + java( + """ + import org.example.FooDefaultString; + @FooDefaultString(value = "blah", additionalValue = "hello", additionalValues = {"hi"}) + public class A {} + """, + """ + import org.example.FooDefaultString; + @FooDefaultString(value = "bonjour", additionalValue = "hello", additionalValues = {"hi"}) + public class A {} + """ + ), + // TODO: Unclear whether the after actually should have had `value = "bonjour"` in reality + //language=java + java( + """ + import org.example.FooDefaultString; + @FooDefaultString("blah", additionalValue = "hello", additionalValues = {"hi"}) + public class B {} + """, + """ + import org.example.FooDefaultString; + @FooDefaultString("bonjour", additionalValue = "hello", additionalValues = {"hi"}) + public class B {} + """ + ) + ); + } + + @Test + void existingOtherAttributes_AndExistingImplicitToNull_RemovesSafely() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, null, null, null, null)), + //language=java + java( + """ + import org.example.FooDefaultString; + @FooDefaultString(value = "blah", additionalValue = "hello", additionalValues = {"hi"}) + public class A {} + """, + """ + import org.example.FooDefaultString; + @FooDefaultString(value = "bonjour", additionalValue = "hello", additionalValues = {"hi"}) + public class A {} + """ + ), + // TODO: Unclear whether the after actually should have had `value = "bonjour"` in reality + //language=java + java( + """ + import org.example.FooDefaultString; + @FooDefaultString("blah", additionalValue = "hello", additionalValues = {"hi"}) + public class B {} + """, + """ + import org.example.FooDefaultString; + @FooDefaultString("bonjour", additionalValue = "hello", additionalValues = {"hi"}) + public class B {} + """ + ) + ); + } + } + + } @DocumentExample @Test 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 9bb1fc53db..4b4e7c3343 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/AddOrUpdateAnnotationAttribute.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/AddOrUpdateAnnotationAttribute.java @@ -23,10 +23,7 @@ import org.openrewrite.internal.ListUtils; import org.openrewrite.internal.StringUtils; 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 java.util.*; @@ -160,10 +157,22 @@ public J.Annotation visitAnnotation(J.Annotation original, ExecutionContext ctx) (attributeName != null && !attributeName.equals(var_.getSimpleName()))) { return as; } + Expression exp = as.getAssignment(); if (newAttributeValue == null) { - return null; + if (exp instanceof J.NewArray) { + List initializerList = requireNonNull(((J.NewArray) exp).getInitializer()); + List updatedList = updateInitializer(annotation, initializerList, getAttributeValues()); + if (updatedList.isEmpty()) { + return null; + } + return as.withAssignment(((J.NewArray) exp) + .withInitializer(updatedList)); + } + if (valueMatches(as.getAssignment(), oldAttributeValue)) { + return null; + } + return as; } - Expression exp = as.getAssignment(); if (exp instanceof J.NewArray) { List initializerList = requireNonNull(((J.NewArray) exp).getInitializer()); return as.withAssignment(((J.NewArray) exp) @@ -202,7 +211,7 @@ public J.Annotation visitAnnotation(J.Annotation original, ExecutionContext ctx) private @Nullable Expression update(J.Literal literal, J.Annotation annotation, @Nullable String newAttributeValue) { // The only way anything except an assignment can appear is if there's an implicit assignment to "value" if ("value".equals(attributeName())) { - if (newAttributeValue == null) { + if (newAttributeValue == null && valueMatches(literal, oldAttributeValue)) { return null; } if (!valueMatches(literal, oldAttributeValue) || newAttributeValue.equals(literal.getValueSource())) { @@ -243,7 +252,12 @@ public J.Annotation visitAnnotation(J.Annotation original, ExecutionContext ctx) private @Nullable Expression update(J.NewArray arrayValue, J.Annotation annotation, @Nullable String newAttributeValue) { if (newAttributeValue == null) { - return null; + List initializerList = requireNonNull(arrayValue.getInitializer()); + List updatedList = updateInitializer(annotation, initializerList, getAttributeValues()); + if (updatedList.isEmpty()) { + return null; + } + return arrayValue.withInitializer(updatedList); } if (attributeName != null && !"value".equals(attributeValue)) { return isAnnotationWithOnlyValueMethod(annotation) ? arrayValue : createAnnotationAssignment(annotation, "value", arrayValue); @@ -289,13 +303,20 @@ private List updateInitializer(J.Annotation annotation, List { - if (it instanceof J.Literal && oldAttributeValue.equals(((J.Literal) it).getValue())) { + if (it instanceof J.Literal && valueMatches(it, oldAttributeValue)) { List newItemsList = new ArrayList<>(); for (String attribute : attributeList) { J.Literal newLiteral = new J.Literal(randomId(), SINGLE_SPACE, EMPTY, attribute, maybeQuoteStringArgument(annotation, attribute), null, JavaType.Primitive.String); newItemsList.add(newLiteral); } return newItemsList; + } else if (it instanceof J.FieldAccess && valueMatches(it, oldAttributeValue)) { + List newItemsList = new ArrayList<>(); + for (String attribute : attributeList) { + J.FieldAccess newFieldAccess = TypeTree.build(attribute); + newItemsList.add(newFieldAccess); + } + return newItemsList; } return it; }); From e57630bfc0bd3acda8da20d4cd38187d308756bb Mon Sep 17 00:00:00 2001 From: Steve Elliott Date: Fri, 15 Aug 2025 17:31:06 -0400 Subject: [PATCH 3/8] Additional WiP - finished implict attribute name w/ null attributeValue, partway through implicit attribute name w/ literal attributeValue --- .../AddOrUpdateAnnotationAttributeTest.java | 954 +++++++++++++----- .../java/AddOrUpdateAnnotationAttribute.java | 38 +- 2 files changed, 732 insertions(+), 260 deletions(-) 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 96dc8dea3b..4eb257378c 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 @@ -34,46 +34,71 @@ public void defaults(RecipeSpec spec) { //language=java """ package org.example; + import java.lang.annotation.Repeatable; + @interface FdsWrapper { FooDefaultString[] value(); } + @Repeatable(FdsWrapper.class) public @interface FooDefaultString { String value() default ""; String a() default ""; String[] b() default {}; - Class c() default null; + Class c() default Number.class; Class[] d() default {}; } """, //language=java """ package org.example; + import java.lang.annotation.Repeatable; + @interface FdsaWrapper { FooDefaultStringArray[] value(); } + @Repeatable(FdsaWrapper.class) public @interface FooDefaultStringArray { String[] value() default {}; String a() default ""; String[] b() default {}; - Class c() default null; + Class c() default Number.class; Class[] d() default {}; } """, //language=java """ package org.example; + import java.lang.annotation.Repeatable; + @interface FdcWrapper { FooDefaultClass[] value(); } + @Repeatable(FdcWrapper.class) public @interface FooDefaultClass { - Class value() default null; + Class value() default Number.class; String a() default ""; String[] b() default {}; - Class c() default null; + Class c() default Number.class; Class[] d() default {}; } """, //language=java """ package org.example; + import java.lang.annotation.Repeatable; + @interface FdcaWrapper { FooDefaultClassArray[] value(); } + @Repeatable(FdcaWrapper.class) public @interface FooDefaultClassArray { Class[] value() default {}; String a() default ""; String[] b() default {}; - Class c() default null; + Class c() default Number.class; Class[] d() default {}; } + """, + //language=java + """ + package org.example; + public class Const { + public class X { + public class Y { + public static final String SOME_CONST = "a"; + public static final String OTHER_CONST = "b"; + public static final String THIRD_CONST = "c"; + } + } + } """ )); } @@ -82,7 +107,6 @@ public void defaults(RecipeSpec spec) { class UsingImplicitAttributeName { @Nested class UsingNullAttributeValue { - // TODO: Check on `addOnly` and `appendArray` effects @Nested class WithLiteralTypeAttribute { @Test @@ -109,6 +133,35 @@ public class B {} ); } + @Test + void literalAttribute_existing_usingAddOnly_doesNothing() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, null, null, true, null)), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString("a") + @FooDefaultString(Const.X.Y.SOME_CONST) + @FooDefaultString(value = "b") + @FooDefaultString(value = Const.X.Y.SOME_CONST) + public class A {} + """ + ), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString(value = "a", a = "z") + @FooDefaultString(value = Const.X.Y.SOME_CONST, a = "z") + public class B {} + """ + ) + ); + } + @Test void literalAttribute_existing_usingNullOldAttributeValue_removesSafely() { rewriteRun( @@ -116,30 +169,129 @@ void literalAttribute_existing_usingNullOldAttributeValue_removesSafely() { //language=java java( """ + import org.example.Const; import org.example.FooDefaultString; @FooDefaultString("a") + @FooDefaultString(Const.X.Y.SOME_CONST) @FooDefaultString(value = "b") + @FooDefaultString(value = Const.X.Y.SOME_CONST) + public class A {} + """, + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString + @FooDefaultString + @FooDefaultString + @FooDefaultString + public class A {} + """ + ), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString(value = "a", a = "z1") + @FooDefaultString(value = Const.X.Y.SOME_CONST, a = "z2") + public class B {} + """, + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString(a = "z1") + @FooDefaultString(a = "z2") + public class B {} + """ + ) + ); + } + + @Disabled("We can't support this right now, as there is no reference to the actual string literal of the constant") + @Test + void literalAttribute_existing_asConst_usingProvidedOldAttributeValue_ofConstValue_removesSafelyOnlyMatched() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, null, "a", null, null)), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString(Const.X.Y.SOME_CONST) + @FooDefaultString(Const.X.Y.OTHER_CONST) + @FooDefaultString(value = Const.X.Y.SOME_CONST) + @FooDefaultString(value = Const.X.Y.OTHER_CONST) public class A {} """, """ + import org.example.Const; import org.example.FooDefaultString; @FooDefaultString + @FooDefaultString(Const.X.Y.OTHER_CONST) @FooDefaultString + @FooDefaultString(Const.X.Y.OTHER_CONST) public class A {} """ ), //language=java java( """ + import org.example.Const; import org.example.FooDefaultString; - @FooDefaultString("a", a = "z") - @FooDefaultString(value = "b", a = "y") + @FooDefaultString(value = Const.X.Y.SOME_CONST, a = Const.X.Y.SOME_CONST) + @FooDefaultString(value = Const.X.Y.OTHER_CONST, a = Const.X.Y.SOME_CONST) public class B {} """, """ + import org.example.Const; import org.example.FooDefaultString; - @FooDefaultString(a = "z") - @FooDefaultString(a = "y") + @FooDefaultString(a = Const.X.Y.SOME_CONST) + @FooDefaultString(value = Const.X.Y.OTHER_CONST, a = Const.X.Y.SOME_CONST) + public class B {} + """ + ) + ); + } + + @Test + void literalAttribute_existing_asConst_usingProvidedOldAttributeValue_ofConstRef_removesSafelyOnlyMatched() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, null, "Const.X.Y.SOME_CONST", null, null)), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString(Const.X.Y.SOME_CONST) + @FooDefaultString(Const.X.Y.OTHER_CONST) + @FooDefaultString(value = Const.X.Y.SOME_CONST) + @FooDefaultString(value = Const.X.Y.OTHER_CONST) + public class A {} + """, + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString + @FooDefaultString(Const.X.Y.OTHER_CONST) + @FooDefaultString + @FooDefaultString(Const.X.Y.OTHER_CONST) + public class A {} + """ + ), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString(value = Const.X.Y.SOME_CONST, a = Const.X.Y.SOME_CONST) + @FooDefaultString(value = Const.X.Y.OTHER_CONST, a = Const.X.Y.SOME_CONST) + public class B {} + """, + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString(a = Const.X.Y.SOME_CONST) + @FooDefaultString(value = Const.X.Y.OTHER_CONST, a = Const.X.Y.SOME_CONST) public class B {} """ ) @@ -173,8 +325,6 @@ public class A {} java( """ import org.example.FooDefaultString; - @FooDefaultString("a", a = "a") - @FooDefaultString("b", a = "a") @FooDefaultString(value = "a", a = "a") @FooDefaultString(value = "b", a = "a") public class B {} @@ -182,8 +332,6 @@ public class B {} """ import org.example.FooDefaultString; @FooDefaultString(a = "a") - @FooDefaultString("b", a = "a") - @FooDefaultString(a = "a") @FooDefaultString(value = "b", a = "a") public class B {} """ @@ -219,6 +367,36 @@ public class B {} ); } + @Test + void literalArrayAttribute_existing_usingAddOnly_doesNothing() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", null, null, null, true, null)), + //language=java + java( + """ + import org.example.FooDefaultStringArray; + @FooDefaultStringArray("a") + @FooDefaultStringArray(value = "b") + @FooDefaultStringArray({}) + @FooDefaultStringArray({"c"}) + @FooDefaultStringArray(value = {}) + @FooDefaultStringArray(value = {"d"}) + public class A {} + """ + ), + //language=java + java( + """ + import org.example.FooDefaultStringArray; + @FooDefaultStringArray(value = "a", b = {"z"}) + @FooDefaultStringArray(value = {}, b = {"y"}) + @FooDefaultStringArray(value = {"d"}, b = {"x"}) + public class B {} + """ + ) + ); + } + @Test void literalArrayAttribute_existing_usingNullOldAttributeValue_removesSafely() { rewriteRun( @@ -226,38 +404,171 @@ void literalArrayAttribute_existing_usingNullOldAttributeValue_removesSafely() { //language=java java( """ + import org.example.Const; import org.example.FooDefaultStringArray; @FooDefaultStringArray("a") + @FooDefaultStringArray(Const.X.Y.SOME_CONST) @FooDefaultStringArray(value = "b") + @FooDefaultStringArray(Const.X.Y.SOME_CONST) + @FooDefaultStringArray({}) @FooDefaultStringArray({"c"}) + @FooDefaultStringArray({Const.X.Y.SOME_CONST}) + @FooDefaultStringArray(value = {}) @FooDefaultStringArray(value = {"d"}) + @FooDefaultStringArray(value = {Const.X.Y.SOME_CONST}) + public class A {} + """, + """ + import org.example.Const; + import org.example.FooDefaultStringArray; + @FooDefaultStringArray + @FooDefaultStringArray + @FooDefaultStringArray + @FooDefaultStringArray + @FooDefaultStringArray + @FooDefaultStringArray + @FooDefaultStringArray + @FooDefaultStringArray + @FooDefaultStringArray + @FooDefaultStringArray public class A {} + """ + ), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultStringArray; + @FooDefaultStringArray(value = "b", b = {"z1"}) + @FooDefaultStringArray(value = Const.X.Y.SOME_CONST, b = {"z2"}) + @FooDefaultStringArray(value = {}, b = {"y1"}) + @FooDefaultStringArray(value = {"d"}, b = {"y2"}) + @FooDefaultStringArray(value = {Const.X.Y.SOME_CONST}, b = {"y3"}) + public class B {} """, """ + import org.example.Const; + import org.example.FooDefaultStringArray; + @FooDefaultStringArray(b = {"z1"}) + @FooDefaultStringArray(b = {"z2"}) + @FooDefaultStringArray(b = {"y1"}) + @FooDefaultStringArray(b = {"y2"}) + @FooDefaultStringArray(b = {"y3"}) + public class B {} + """ + ) + ); + } + + @Disabled("We can't support this right now, as there is no reference to the actual string literal of the constant") + @Test + void literalArrayAttribute_existing_asConst_usingProvidedOldAttributeValue_ofConstValue_removesSafelyOnlyMatched() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", null, null, "a", null, null)), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultStringArray; + @FooDefaultStringArray(Const.X.Y.SOME_CONST) + @FooDefaultStringArray(Const.X.Y.OTHER_CONST) + @FooDefaultStringArray(value = Const.X.Y.SOME_CONST) + @FooDefaultStringArray(value = Const.X.Y.OTHER_CONST) + @FooDefaultStringArray({Const.X.Y.SOME_CONST}) + @FooDefaultStringArray({Const.X.Y.OTHER_CONST, Const.X.Y.SOME_CONST, Const.X.Y.THIRD_CONST}) + @FooDefaultStringArray(value = {Const.X.Y.SOME_CONST}) + @FooDefaultStringArray(value = {Const.X.Y.OTHER_CONST, Const.X.Y.SOME_CONST, Const.X.Y.THIRD_CONST}) + public class A {} + """, + """ + import org.example.Const; import org.example.FooDefaultStringArray; @FooDefaultStringArray + @FooDefaultStringArray(Const.X.Y.OTHER_CONST) @FooDefaultStringArray + @FooDefaultStringArray(Const.X.Y.OTHER_CONST) @FooDefaultStringArray + @FooDefaultStringArray({Const.X.Y.OTHER_CONST, Const.X.Y.THIRD_CONST}) @FooDefaultStringArray + @FooDefaultStringArray({Const.X.Y.OTHER_CONST, Const.X.Y.THIRD_CONST}) public class A {} """ ), //language=java java( """ + import org.example.Const; import org.example.FooDefaultStringArray; - @FooDefaultStringArray("a", b = {"z"}) - @FooDefaultStringArray(value = "b", b = {"y"}) - @FooDefaultStringArray({"c"}, b = {"x"}) - @FooDefaultStringArray(value = {"d"}, b = {"w"}) + @FooDefaultStringArray(value = Const.X.Y.SOME_CONST, b = {Const.X.Y.SOME_CONST}) + @FooDefaultStringArray(value = Const.X.Y.OTHER_CONST, b = {Const.X.Y.SOME_CONST}) + @FooDefaultStringArray(value = {Const.X.Y.SOME_CONST}, b = {Const.X.Y.SOME_CONST}) + @FooDefaultStringArray(value = {Const.X.Y.OTHER_CONST, Const.X.Y.SOME_CONST, Const.X.Y.THIRD_CONST}, b = {Const.X.Y.SOME_CONST}) public class B {} """, """ + import org.example.Const; import org.example.FooDefaultStringArray; - @FooDefaultStringArray(b = {"z"}) - @FooDefaultStringArray(b = {"y"}) - @FooDefaultStringArray(b = {"x"}) - @FooDefaultStringArray(b = {"w"}) + @FooDefaultStringArray(b = {Const.X.Y.SOME_CONST}) + @FooDefaultStringArray(value = Const.X.Y.OTHER_CONST, b = {Const.X.Y.SOME_CONST}) + @FooDefaultStringArray(b = {Const.X.Y.SOME_CONST}) + @FooDefaultStringArray(value = {Const.X.Y.OTHER_CONST, Const.X.Y.THIRD_CONST}, b = {Const.X.Y.SOME_CONST}) + public class B {} + """ + ) + ); + } + + @Test + void literalArrayAttribute_existing_asConst_usingProvidedOldAttributeValue_ofConstRef_removesSafelyOnlyMatched() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", null, null, "Const.X.Y.SOME_CONST", null, null)), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultStringArray; + @FooDefaultStringArray(Const.X.Y.SOME_CONST) + @FooDefaultStringArray(Const.X.Y.OTHER_CONST) + @FooDefaultStringArray(value = Const.X.Y.SOME_CONST) + @FooDefaultStringArray(value = Const.X.Y.OTHER_CONST) + @FooDefaultStringArray({Const.X.Y.SOME_CONST}) + @FooDefaultStringArray({Const.X.Y.OTHER_CONST, Const.X.Y.SOME_CONST, Const.X.Y.THIRD_CONST}) + @FooDefaultStringArray(value = {Const.X.Y.SOME_CONST}) + @FooDefaultStringArray(value = {Const.X.Y.OTHER_CONST, Const.X.Y.SOME_CONST, Const.X.Y.THIRD_CONST}) + public class A {} + """, + """ + import org.example.Const; + import org.example.FooDefaultStringArray; + @FooDefaultStringArray + @FooDefaultStringArray(Const.X.Y.OTHER_CONST) + @FooDefaultStringArray + @FooDefaultStringArray(Const.X.Y.OTHER_CONST) + @FooDefaultStringArray + @FooDefaultStringArray({Const.X.Y.OTHER_CONST, Const.X.Y.THIRD_CONST}) + @FooDefaultStringArray + @FooDefaultStringArray({Const.X.Y.OTHER_CONST, Const.X.Y.THIRD_CONST}) + public class A {} + """ + ), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultStringArray; + @FooDefaultStringArray(value = Const.X.Y.SOME_CONST, b = {Const.X.Y.SOME_CONST}) + @FooDefaultStringArray(value = Const.X.Y.OTHER_CONST, b = {Const.X.Y.SOME_CONST}) + @FooDefaultStringArray(value = {Const.X.Y.SOME_CONST}, b = {Const.X.Y.SOME_CONST}) + @FooDefaultStringArray(value = {Const.X.Y.OTHER_CONST, Const.X.Y.SOME_CONST, Const.X.Y.THIRD_CONST}, b = {Const.X.Y.SOME_CONST}) + public class B {} + """, + """ + import org.example.Const; + import org.example.FooDefaultStringArray; + @FooDefaultStringArray(b = {Const.X.Y.SOME_CONST}) + @FooDefaultStringArray(value = Const.X.Y.OTHER_CONST, b = {Const.X.Y.SOME_CONST}) + @FooDefaultStringArray(b = {Const.X.Y.SOME_CONST}) + @FooDefaultStringArray(value = {Const.X.Y.OTHER_CONST, Const.X.Y.THIRD_CONST}, b = {Const.X.Y.SOME_CONST}) public class B {} """ ) @@ -276,8 +587,10 @@ void literalArrayAttribute_existing_usingProvidedOldAttributeValue_removesSafely @FooDefaultStringArray("b") @FooDefaultStringArray(value = "a") @FooDefaultStringArray(value = "b") + @FooDefaultStringArray({}) @FooDefaultStringArray({"a"}) @FooDefaultStringArray({"b", "a", "c"}) + @FooDefaultStringArray(value = {}) @FooDefaultStringArray(value = {"a"}) @FooDefaultStringArray(value = {"b", "a", "c"}) public class A {} @@ -289,8 +602,10 @@ public class A {} @FooDefaultStringArray @FooDefaultStringArray("b") @FooDefaultStringArray + @FooDefaultStringArray @FooDefaultStringArray({"b", "c"}) @FooDefaultStringArray + @FooDefaultStringArray @FooDefaultStringArray({"b", "c"}) public class A {} """ @@ -299,12 +614,9 @@ public class A {} java( """ import org.example.FooDefaultStringArray; - @FooDefaultStringArray("a", b = {"a"}) - @FooDefaultStringArray("b", b = {"a"}) @FooDefaultStringArray(value = "a", b = {"a"}) @FooDefaultStringArray(value = "b", b = {"a"}) - @FooDefaultStringArray({"a"}, b = {"a"}) - @FooDefaultStringArray({"b", "a", "c"}, b = {"a"}) + @FooDefaultStringArray(value = {}, b = {"a"}) @FooDefaultStringArray(value = {"a"}, b = {"a"}) @FooDefaultStringArray(value = {"b", "a", "c"}, b = {"a"}) public class B {} @@ -312,11 +624,8 @@ public class B {} """ import org.example.FooDefaultStringArray; @FooDefaultStringArray(b = {"a"}) - @FooDefaultStringArray("b", b = {"a"}) - @FooDefaultStringArray(b = {"a"}) @FooDefaultStringArray(value = "b", b = {"a"}) @FooDefaultStringArray(b = {"a"}) - @FooDefaultStringArray({"b", "c"}, b = {"a"}) @FooDefaultStringArray(b = {"a"}) @FooDefaultStringArray(value = {"b", "c"}, b = {"a"}) public class B {} @@ -352,6 +661,30 @@ public class B {} ); } + @Test + void classAttribute_existing_usingAddOnly_doesNothing() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultClass", null, null, null, true, null)), + //language=java + java( + """ + import org.example.FooDefaultClass; + @FooDefaultClass(Integer.class) + @FooDefaultClass(Long.class) + public class A {} + """ + ), + //language=java + java( + """ + import org.example.FooDefaultClass; + @FooDefaultClass(value = Integer.class, c = Long.class) + public class B {} + """ + ) + ); + } + @Test void classAttribute_existing_usingNullOldAttributeValue_removesSafely() { rewriteRun( @@ -375,13 +708,11 @@ public class A {} java( """ import org.example.FooDefaultClass; - @FooDefaultClass(Integer.class, c = Short.class) @FooDefaultClass(value = Long.class, c = Byte.class) public class B {} """, """ import org.example.FooDefaultClass; - @FooDefaultClass(c = Short.class) @FooDefaultClass(c = Byte.class) public class B {} """ @@ -389,6 +720,152 @@ public class B {} ); } + @Test + void classAttribute_existing_usingProvidedOldAttributeValue_removesSafelyOnlyMatched() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultClass", null, null, "Integer.class", null, null)), + //language=java + java( + """ + import org.example.FooDefaultClass; + @FooDefaultClass(Integer.class) + @FooDefaultClass(Long.class) + @FooDefaultClass(value = Integer.class) + @FooDefaultClass(value = Long.class) + public class A {} + """, + """ + import org.example.FooDefaultClass; + @FooDefaultClass + @FooDefaultClass(Long.class) + @FooDefaultClass + @FooDefaultClass(Long.class) + public class A {} + """ + ), + //language=java + java( + """ + import org.example.FooDefaultClass; + @FooDefaultClass(value = Integer.class, c = Integer.class) + @FooDefaultClass(value = Long.class, c = Integer.class) + public class B {} + """, + """ + import org.example.FooDefaultClass; + @FooDefaultClass(c = Integer.class) + @FooDefaultClass(value = Long.class, c = Integer.class) + public class B {} + """ + ) + ); + } + } + + @Nested + class WithClassArrayTypeAttribute { + @Test + void classArrayAttribute_absent_doesNothing() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultClassArray", null, null, null, null, null)), + //language=java + java( + """ + import org.example.FooDefaultClassArray; + @FooDefaultClassArray + @FooDefaultClassArray() + public class A {} + """ + ), + //language=java + java( + """ + import org.example.FooDefaultClassArray; + @FooDefaultClassArray(d = Integer.class) + @FooDefaultClassArray(d = {Long.class}) + public class B {} + """ + ) + ); + } + + @Test + void classArrayAttribute_existing_usingAddOnly_doesNothing() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultClassArray", null, null, null, true, null)), + //language=java + java( + """ + import org.example.FooDefaultClassArray; + @FooDefaultClassArray(Integer.class) + @FooDefaultClassArray(value = Long.class) + @FooDefaultClassArray({}) + @FooDefaultClassArray({Short.class}) + @FooDefaultClassArray(value = {}) + @FooDefaultClassArray(value = {Byte.class}) + public class A {} + """ + ), + //language=java + java( + """ + import org.example.FooDefaultClassArray; + @FooDefaultClassArray(value = Long.class, d = {Byte.class}) + @FooDefaultClassArray(value = {}, d = {Integer.class}) + @FooDefaultClassArray(value = {Long.class}, d = {Integer.class}) + public class B {} + """ + ) + ); + } + + @Test + void classArrayAttribute_existing_usingNullOldAttributeValue_removesSafely() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultClassArray", null, null, null, null, null)), + //language=java + java( + """ + import org.example.FooDefaultClassArray; + @FooDefaultClassArray(Integer.class) + @FooDefaultClassArray(value = Long.class) + @FooDefaultClassArray({}) + @FooDefaultClassArray({Short.class}) + @FooDefaultClassArray(value = {}) + @FooDefaultClassArray(value = {Byte.class}) + public class A {} + """, + """ + import org.example.FooDefaultClassArray; + @FooDefaultClassArray + @FooDefaultClassArray + @FooDefaultClassArray + @FooDefaultClassArray + @FooDefaultClassArray + @FooDefaultClassArray + public class A {} + """ + ), + //language=java + java( + """ + import org.example.FooDefaultClassArray; + @FooDefaultClassArray(value = Long.class, d = {Byte.class}) + @FooDefaultClassArray(value = {}, d = {Float.class}) + @FooDefaultClassArray(value = {Long.class}, d = {Integer.class}) + public class B {} + """, + """ + import org.example.FooDefaultClassArray; + @FooDefaultClassArray(d = {Byte.class}) + @FooDefaultClassArray(d = {Float.class}) + @FooDefaultClassArray(d = {Integer.class}) + public class B {} + """ + ) + ); + } + @Test void classArrayAttribute_existing_usingProvidedOldAttributeValue_removesSafelyOnlyMatched() { rewriteRun( @@ -401,8 +878,10 @@ void classArrayAttribute_existing_usingProvidedOldAttributeValue_removesSafelyOn @FooDefaultClassArray(Long.class) @FooDefaultClassArray(value = Integer.class) @FooDefaultClassArray(value = Long.class) + @FooDefaultClassArray({}) @FooDefaultClassArray({Integer.class}) @FooDefaultClassArray({Long.class, Integer.class, Short.class}) + @FooDefaultClassArray(value = {}) @FooDefaultClassArray(value = {Integer.class}) @FooDefaultClassArray(value = {Long.class, Integer.class, Short.class}) public class A {} @@ -414,8 +893,10 @@ public class A {} @FooDefaultClassArray @FooDefaultClassArray(Long.class) @FooDefaultClassArray + @FooDefaultClassArray @FooDefaultClassArray({Long.class, Short.class}) @FooDefaultClassArray + @FooDefaultClassArray @FooDefaultClassArray({Long.class, Short.class}) public class A {} """ @@ -424,12 +905,9 @@ public class A {} java( """ import org.example.FooDefaultClassArray; - @FooDefaultClassArray(Integer.class, d = {Integer.class}) - @FooDefaultClassArray(Long.class, d = {Integer.class}) @FooDefaultClassArray(value = Integer.class, d = {Integer.class}) @FooDefaultClassArray(value = Long.class, d = {Integer.class}) - @FooDefaultClassArray({Integer.class}, d = {Integer.class}) - @FooDefaultClassArray({Long.class, Integer.class, Short.class}, d = {Integer.class}) + @FooDefaultClassArray(value = {}, d = {Integer.class}) @FooDefaultClassArray(value = {Integer.class}, d = {Integer.class}) @FooDefaultClassArray(value = {Long.class, Integer.class, Short.class}, d = {Integer.class}) public class B {} @@ -437,11 +915,8 @@ public class B {} """ import org.example.FooDefaultClassArray; @FooDefaultClassArray(d = {Integer.class}) - @FooDefaultClassArray(Long.class, d = {Integer.class}) - @FooDefaultClassArray(d = {Integer.class}) @FooDefaultClassArray(value = Long.class, d = {Integer.class}) @FooDefaultClassArray(d = {Integer.class}) - @FooDefaultClassArray({Long.class, Short.class}, d = {Integer.class}) @FooDefaultClassArray(d = {Integer.class}) @FooDefaultClassArray(value = {Long.class, Short.class}, d = {Integer.class}) public class B {} @@ -450,236 +925,205 @@ public class B {} ); } } - - - - - - - - - @Test - void classArrayAttribute_absent_doesNothing() { - rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultClassArray", null, null, null, null, null)), - //language=java - java( - """ - import org.example.FooDefaultClassArray; - @FooDefaultClassArray - public class A { - @FooDefaultClassArray() - public class AInner {} - } - """ - ), - //language=java - java( - """ - import org.example.FooDefaultClassArray; - @FooDefaultClassArray(d = Integer.class) - public class B { - @FooDefaultClassArray(d = {Long.class}) - public class BInner {} - } - """ - ) - ); - } } @Nested - class WhenImplicitIsLiteralType { - @Test - void unsetImplicitToLiteral_Adds() { - rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "hello", null, null, null)), - //language=java - java( - """ - import org.example.FooDefaultString; - @FooDefaultString - public class A {} - """, - """ - import org.example.FooDefaultString; - @FooDefaultString("hello") - public class A {} - """ - ), - //language=java - java( - """ - import org.example.FooDefaultString; - @FooDefaultString() - public class B {} - """, - """ - import org.example.FooDefaultString; - @FooDefaultString("hello") - public class B {} - """ - ) - ); - } + class UsingLiteralAttributeValue { + @Nested + class WithLiteralTypeAttribute { + @Test + void literalAttribute_absent_usingNullOldAttributeValue_addsSafely() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "a", null, null, null)), + //language=java + java( + """ + import org.example.FooDefaultString; + @FooDefaultString + @FooDefaultString() + @FooDefaultString(a = "b") + public class A {} + """, + """ + import org.example.FooDefaultString; + @FooDefaultString("a") + @FooDefaultString("a") + @FooDefaultString(value = "a", a = "b") + public class A {} + """ + ) + ); + } - @Test - void existingImplicitToLiteral_Updates_AndDropsAttributeName() { - rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "bonjour", null, null, null)), - //language=java - java( - """ - import org.example.FooDefaultString; - @FooDefaultString("hello") - public class A {} - """, - """ - import org.example.FooDefaultString; - @FooDefaultString("bonjour") - public class A {} - """ - ), - //language=java - java( - """ - import org.example.FooDefaultString; - @FooDefaultString(value = "hello") - public class B {} - """, - """ - import org.example.FooDefaultString; - @FooDefaultString("bonjour") - public class B {} - """ - ) - ); - } + @Test + void literalAttribute_absent_usingLiteralOldAttributeValue_doesNothing() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "a", "b", null, null)), + //language=java + java( + """ + import org.example.FooDefaultString; + @FooDefaultString + @FooDefaultString() + @FooDefaultString(a = "b") + public class A {} + """ + ) + ); + } - @Test - void existingImplicitToNull_Removes() { - rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, null, null, null, null)), - //language=java - java( - """ - import org.example.FooDefaultString; - @FooDefaultString("hello") - public class A {} - """, - """ - import org.example.FooDefaultString; - @FooDefaultString - public class A {} - """ - ), - //language=java - java( - """ - import org.example.FooDefaultString; - @FooDefaultString(value = "hello") - public class B {} - """, - """ - import org.example.FooDefaultString; - @FooDefaultString - public class B {} - """ - ) - ); - } + @Test + void literalAttribute_existing_usingAddOnly_doesNothing() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "a", null, true, null)), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString("b") + @FooDefaultString(Const.X.Y.OTHER_CONST) + @FooDefaultString(value = "b") + @FooDefaultString(value = Const.X.Y.OTHER_CONST) + public class A {} + """ + ) + ); + } - @Test - void existingOtherAttributes_AndUnsetImplicitToLiteral_AddsSafely_AndAddsAttributeName() { - rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "bonjour", null, null, null)), - //language=java - java( - """ - import org.example.FooDefaultString; - @FooDefaultString(additionalValue = "hello", additionalValues = {"hi"}) - public class A {} - """, - """ - import org.example.FooDefaultString; - @FooDefaultString(value = "bonjour", additionalValue = "hello", additionalValues = {"hi"}) - public class A {} - """ - ) - ); - } + @Test + void literalAttribute_existing_usingNullOldAttributeValue_updatesSafely() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "a", null, null, null)), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString("b") + @FooDefaultString(Const.X.Y.OTHER_CONST) + @FooDefaultString(value = "c") + @FooDefaultString(value = Const.X.Y.THIRD_CONST) + public class A {} + """, + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString("a") + @FooDefaultString("a") + @FooDefaultString("a") + @FooDefaultString("a") + public class A {} + """ + ) + ); + } - @Test - void existingOtherAttributes_AndExistingImplicitToLiteral_AddsSafely_AndRetainsAttributeNameUsage() { - rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "bonjour", null, null, null)), - //language=java - java( - """ - import org.example.FooDefaultString; - @FooDefaultString(value = "blah", additionalValue = "hello", additionalValues = {"hi"}) - public class A {} - """, - """ - import org.example.FooDefaultString; - @FooDefaultString(value = "bonjour", additionalValue = "hello", additionalValues = {"hi"}) - public class A {} - """ - ), - // TODO: Unclear whether the after actually should have had `value = "bonjour"` in reality - //language=java - java( - """ - import org.example.FooDefaultString; - @FooDefaultString("blah", additionalValue = "hello", additionalValues = {"hi"}) - public class B {} - """, - """ - import org.example.FooDefaultString; - @FooDefaultString("bonjour", additionalValue = "hello", additionalValues = {"hi"}) - public class B {} - """ - ) - ); - } + @Disabled("We can't support this right now, as there is no reference to the actual string literal of the constant") + @Test + void literalAttribute_existing_asConst_usingProvidedOldAttributeValue_ofConstValue_updatesSafelyOnlyMatched() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "b", "a", null, null)), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString(Const.X.Y.SOME_CONST) + @FooDefaultString(Const.X.Y.OTHER_CONST) + @FooDefaultString(value = Const.X.Y.SOME_CONST) + @FooDefaultString(value = Const.X.Y.OTHER_CONST) + public class A {} + """, + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString("b") + @FooDefaultString(Const.X.Y.OTHER_CONST) + @FooDefaultString("b") + @FooDefaultString(Const.X.Y.OTHER_CONST) + public class A {} + """ + ), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString(value = Const.X.Y.SOME_CONST, a = Const.X.Y.SOME_CONST) + @FooDefaultString(value = Const.X.Y.OTHER_CONST, a = Const.X.Y.SOME_CONST) + public class B {} + """, + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString(value = "b", a = Const.X.Y.SOME_CONST) + @FooDefaultString(value = Const.X.Y.OTHER_CONST, a = Const.X.Y.SOME_CONST) + public class B {} + """ + ) + ); + } - @Test - void existingOtherAttributes_AndExistingImplicitToNull_RemovesSafely() { - rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, null, null, null, null)), - //language=java - java( - """ - import org.example.FooDefaultString; - @FooDefaultString(value = "blah", additionalValue = "hello", additionalValues = {"hi"}) - public class A {} - """, - """ - import org.example.FooDefaultString; - @FooDefaultString(value = "bonjour", additionalValue = "hello", additionalValues = {"hi"}) - public class A {} - """ - ), - // TODO: Unclear whether the after actually should have had `value = "bonjour"` in reality - //language=java - java( - """ - import org.example.FooDefaultString; - @FooDefaultString("blah", additionalValue = "hello", additionalValues = {"hi"}) - public class B {} - """, - """ - import org.example.FooDefaultString; - @FooDefaultString("bonjour", additionalValue = "hello", additionalValues = {"hi"}) - public class B {} - """ - ) - ); + @Test + void literalAttribute_existing_asConst_usingProvidedOldAttributeValue_ofConstRef_updatesSafelyOnlyMatched() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "b", "Const.X.Y.SOME_CONST", null, null)), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString(Const.X.Y.SOME_CONST) + @FooDefaultString(Const.X.Y.OTHER_CONST) + @FooDefaultString(value = Const.X.Y.SOME_CONST) + @FooDefaultString(value = Const.X.Y.OTHER_CONST) + public class A {} + """, + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString("b") + @FooDefaultString(Const.X.Y.OTHER_CONST) + @FooDefaultString("b") + @FooDefaultString(Const.X.Y.OTHER_CONST) + public class A {} + """ + )/*, + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString(value = Const.X.Y.SOME_CONST, a = Const.X.Y.SOME_CONST) + @FooDefaultString(value = Const.X.Y.OTHER_CONST, a = Const.X.Y.SOME_CONST) + public class B {} + """, + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString(value = "b", a = Const.X.Y.SOME_CONST) + @FooDefaultString(value = Const.X.Y.OTHER_CONST, a = Const.X.Y.SOME_CONST) + public class B {} + """ + )*/ + ); + } + + // TODO: `literalAttribute_existing_usingProvidedOldAttributeValue_updatesSafelyOnlyMatched` } + + // TODO: nested `WithLiteralArrayTypeAttribute` + // TODO: nested `WithClassTypeAttribute` + // TODO: nested `WithClassArrayTypeAttribute` } + // TODO: Check on `addOnly` and `appendArray` effects + // TODO: Pull other checks from below up, such as not getting confused by fields of the same name during qualified constant checks } + // TODO: all the above, but for explicit attribute + @DocumentExample @Test void addValueAttribute() { 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 4b4e7c3343..b6730e85ef 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/AddOrUpdateAnnotationAttribute.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/AddOrUpdateAnnotationAttribute.java @@ -140,7 +140,7 @@ public J.Annotation visitAnnotation(J.Annotation original, ExecutionContext ctx) } // ADD the value into the argument list when there was no existing value to update and no requirements on a pre-existing old value, e.g. @Foo(name="old") to @Foo(value="new", name="old") - if (oldAttributeValue == null && newAttributeValue != null && !attributeNameOrValIsAlreadyPresent(a.getArguments(), getAttributeValues())) { + if (oldAttributeValue == null && newAttributeValue != null && !attributeNameAlreadyPresent(a)) { J.Assignment as = createAnnotationAssignment(a, attributeName(), newAttributeValue); a = a.withArguments(ListUtils.concat(as, a.getArguments())); } @@ -195,7 +195,7 @@ public J.Annotation visitAnnotation(J.Annotation original, ExecutionContext ctx) return as.withAssignment(createAnnotationLiteral(annotation, newAttributeValue)); } if (exp instanceof J.FieldAccess) { - if (oldAttributeValue != null) { + if (!valueMatches(exp, oldAttributeValue) || newAttributeValue.equals(exp.toString())) { return as; } if (isFullyQualifiedClass() && getFullyQualifiedClass(newAttributeValue).equals(exp.toString())) { @@ -211,8 +211,11 @@ public J.Annotation visitAnnotation(J.Annotation original, ExecutionContext ctx) private @Nullable Expression update(J.Literal literal, J.Annotation annotation, @Nullable String newAttributeValue) { // The only way anything except an assignment can appear is if there's an implicit assignment to "value" if ("value".equals(attributeName())) { - if (newAttributeValue == null && valueMatches(literal, oldAttributeValue)) { - return null; + if (newAttributeValue == null) { + if (valueMatches(literal, oldAttributeValue)) { + return null; + } + return literal; } if (!valueMatches(literal, oldAttributeValue) || newAttributeValue.equals(literal.getValueSource())) { return literal; @@ -231,7 +234,10 @@ public J.Annotation visitAnnotation(J.Annotation original, ExecutionContext ctx) // The only way anything except an assignment can appear is if there's an implicit assignment to "value" if ("value".equals(attributeName())) { if (newAttributeValue == null) { - return null; + if (valueMatches(fieldAccess, oldAttributeValue)) { + return null; + } + return fieldAccess; } if (isFullyQualifiedClass() && getFullyQualifiedClass(newAttributeValue).equals(fieldAccess.toString())) { return fieldAccess; @@ -317,6 +323,8 @@ private List updateInitializer(J.Annotation annotation, List(); } return it; }); @@ -459,4 +467,24 @@ private boolean attributeNameOrValIsAlreadyPresent(Expression e, Collection v } return false; } + + private boolean attributeNameAlreadyPresent(J.Annotation a) { + List existingArguments = a.getArguments(); + if (existingArguments == null) { + return false; + } + for (Expression e : a.getArguments()) { + if (e instanceof J.Assignment) { + J.Assignment as = (J.Assignment) e; + if (as.getVariable() instanceof J.Identifier) { + if (((J.Identifier) as.getVariable()).getSimpleName().equals(attributeName())) { + return true; + } + } + } else if (attributeName().equals("value")) { + return true; + } + } + return false; + } } From 44935720c2a5481497328cb782c74db825004ed3 Mon Sep 17 00:00:00 2001 From: Steve Elliott Date: Mon, 18 Aug 2025 17:40:18 -0400 Subject: [PATCH 4/8] WiP --- .../AddOrUpdateAnnotationAttributeTest.java | 1937 ++++++++++------- .../java/AddOrUpdateAnnotationAttribute.java | 93 +- 2 files changed, 1183 insertions(+), 847 deletions(-) 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 4eb257378c..bf637bb339 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 @@ -32,17 +32,49 @@ class AddOrUpdateAnnotationAttributeTest implements RewriteTest { public void defaults(RecipeSpec spec) { spec.parser(JavaParser.fromJavaVersion().dependsOn( //language=java + """ + package org.example; + public enum FooEnum { ONE, TWO, THREE, FOUR, JUNK } + """, + //language=java + """ + package org.example; + public class Const { + public class X { + public class Y { + public static final String FIRST_CONST = "a"; + public static final String SECOND_CONST = "b"; + public static final String THIRD_CONST = "c"; + public static final String FOURTH_CONST = "d"; + } + } + } + """, + //language=java + """ + package org.example; + public @interface FooDefaultBase { + String str() default ""; + String[] strArr() default {}; + Class cla() default Number.class; + Class[] claArr() default {}; + FooEnum enu() default FooEnum.JUNK; + FooEnum[] enuArr() default {}; + long lon() default 0L; + long[] lonArr() default {}; + boolean boo() default false; + boolean[] booArr() default {}; + } + """, + //language=java """ package org.example; import java.lang.annotation.Repeatable; @interface FdsWrapper { FooDefaultString[] value(); } @Repeatable(FdsWrapper.class) - public @interface FooDefaultString { + public @interface FooDefaultString extends FooDefaultBase { String value() default ""; - String a() default ""; - String[] b() default {}; - Class c() default Number.class; - Class[] d() default {}; + // TODO: need to figure out why it can't see interface inherited methods } """, //language=java @@ -51,12 +83,8 @@ public void defaults(RecipeSpec spec) { import java.lang.annotation.Repeatable; @interface FdsaWrapper { FooDefaultStringArray[] value(); } @Repeatable(FdsaWrapper.class) - public @interface FooDefaultStringArray { + public @interface FooDefaultStringArray extends FooDefaultBase { String[] value() default {}; - String a() default ""; - String[] b() default {}; - Class c() default Number.class; - Class[] d() default {}; } """, //language=java @@ -65,12 +93,8 @@ public void defaults(RecipeSpec spec) { import java.lang.annotation.Repeatable; @interface FdcWrapper { FooDefaultClass[] value(); } @Repeatable(FdcWrapper.class) - public @interface FooDefaultClass { + public @interface FooDefaultClass extends FooDefaultBase { Class value() default Number.class; - String a() default ""; - String[] b() default {}; - Class c() default Number.class; - Class[] d() default {}; } """, //language=java @@ -79,25 +103,68 @@ public void defaults(RecipeSpec spec) { import java.lang.annotation.Repeatable; @interface FdcaWrapper { FooDefaultClassArray[] value(); } @Repeatable(FdcaWrapper.class) - public @interface FooDefaultClassArray { + public @interface FooDefaultClassArray extends FooDefaultBase { Class[] value() default {}; - String a() default ""; - String[] b() default {}; - Class c() default Number.class; - Class[] d() default {}; } """, //language=java """ package org.example; - public class Const { - public class X { - public class Y { - public static final String SOME_CONST = "a"; - public static final String OTHER_CONST = "b"; - public static final String THIRD_CONST = "c"; - } - } + import java.lang.annotation.Repeatable; + @interface FdeWrapper { FooDefaultEnum[] value(); } + @Repeatable(FdeWrapper.class) + public @interface FooDefaultEnum extends FooDefaultBase { + FooEnum value() default FooEnum.JUNK; + } + """, + //language=java + """ + package org.example; + import java.lang.annotation.Repeatable; + @interface FdeaWrapper { FooDefaultEnumArray[] value(); } + @Repeatable(FdeaWrapper.class) + public @interface FooDefaultEnumArray extends FooDefaultBase { + FooEnum[] value() default {}; + } + """, + //language=java + """ + package org.example; + import java.lang.annotation.Repeatable; + @interface FdlWrapper { FooDefaultLong[] value(); } + @Repeatable(FdlWrapper.class) + public @interface FooDefaultLong extends FooDefaultBase { + long value() default 0L; + } + """, + //language=java + """ + package org.example; + import java.lang.annotation.Repeatable; + @interface FdlaWrapper { FooDefaultLongArray[] value(); } + @Repeatable(FdlaWrapper.class) + public @interface FooDefaultLongArray extends FooDefaultBase { + long[] value() default {}; + } + """, + //language=java + """ + package org.example; + import java.lang.annotation.Repeatable; + @interface FdbWrapper { FooDefaultBoolean[] value(); } + @Repeatable(FdbWrapper.class) + public @interface FooDefaultBoolean extends FooDefaultBase { + boolean value() default false; + } + """, + //language=java + """ + package org.example; + import java.lang.annotation.Repeatable; + @interface FdbaWrapper { FooDefaultBooleanArray[] value(); } + @Repeatable(FdbaWrapper.class) + public @interface FooDefaultBooleanArray extends FooDefaultBase { + boolean[] value() default {}; } """ )); @@ -126,7 +193,7 @@ public class A {} java( """ import org.example.FooDefaultString; - @FooDefaultString(a = "a") + @FooDefaultString(str = "a") public class B {} """ ) @@ -143,9 +210,9 @@ void literalAttribute_existing_usingAddOnly_doesNothing() { import org.example.Const; import org.example.FooDefaultString; @FooDefaultString("a") - @FooDefaultString(Const.X.Y.SOME_CONST) + @FooDefaultString(Const.X.Y.FIRST_CONST) @FooDefaultString(value = "b") - @FooDefaultString(value = Const.X.Y.SOME_CONST) + @FooDefaultString(value = Const.X.Y.FIRST_CONST) public class A {} """ ), @@ -154,8 +221,8 @@ public class A {} """ import org.example.Const; import org.example.FooDefaultString; - @FooDefaultString(value = "a", a = "z") - @FooDefaultString(value = Const.X.Y.SOME_CONST, a = "z") + @FooDefaultString(value = "a", str = "z") + @FooDefaultString(value = Const.X.Y.FIRST_CONST, str = "z") public class B {} """ ) @@ -172,9 +239,9 @@ void literalAttribute_existing_usingNullOldAttributeValue_removesSafely() { import org.example.Const; import org.example.FooDefaultString; @FooDefaultString("a") - @FooDefaultString(Const.X.Y.SOME_CONST) + @FooDefaultString(Const.X.Y.FIRST_CONST) @FooDefaultString(value = "b") - @FooDefaultString(value = Const.X.Y.SOME_CONST) + @FooDefaultString(value = Const.X.Y.FIRST_CONST) public class A {} """, """ @@ -192,61 +259,15 @@ public class A {} """ import org.example.Const; import org.example.FooDefaultString; - @FooDefaultString(value = "a", a = "z1") - @FooDefaultString(value = Const.X.Y.SOME_CONST, a = "z2") - public class B {} - """, - """ - import org.example.Const; - import org.example.FooDefaultString; - @FooDefaultString(a = "z1") - @FooDefaultString(a = "z2") - public class B {} - """ - ) - ); - } - - @Disabled("We can't support this right now, as there is no reference to the actual string literal of the constant") - @Test - void literalAttribute_existing_asConst_usingProvidedOldAttributeValue_ofConstValue_removesSafelyOnlyMatched() { - rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, null, "a", null, null)), - //language=java - java( - """ - import org.example.Const; - import org.example.FooDefaultString; - @FooDefaultString(Const.X.Y.SOME_CONST) - @FooDefaultString(Const.X.Y.OTHER_CONST) - @FooDefaultString(value = Const.X.Y.SOME_CONST) - @FooDefaultString(value = Const.X.Y.OTHER_CONST) - public class A {} - """, - """ - import org.example.Const; - import org.example.FooDefaultString; - @FooDefaultString - @FooDefaultString(Const.X.Y.OTHER_CONST) - @FooDefaultString - @FooDefaultString(Const.X.Y.OTHER_CONST) - public class A {} - """ - ), - //language=java - java( - """ - import org.example.Const; - import org.example.FooDefaultString; - @FooDefaultString(value = Const.X.Y.SOME_CONST, a = Const.X.Y.SOME_CONST) - @FooDefaultString(value = Const.X.Y.OTHER_CONST, a = Const.X.Y.SOME_CONST) + @FooDefaultString(value = "a", str = "z1") + @FooDefaultString(value = Const.X.Y.FIRST_CONST, str = "z2") public class B {} """, """ import org.example.Const; import org.example.FooDefaultString; - @FooDefaultString(a = Const.X.Y.SOME_CONST) - @FooDefaultString(value = Const.X.Y.OTHER_CONST, a = Const.X.Y.SOME_CONST) + @FooDefaultString(str = "z1") + @FooDefaultString(str = "z2") public class B {} """ ) @@ -256,25 +277,25 @@ public class B {} @Test void literalAttribute_existing_asConst_usingProvidedOldAttributeValue_ofConstRef_removesSafelyOnlyMatched() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, null, "Const.X.Y.SOME_CONST", null, null)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, null, "Const.X.Y.FIRST_CONST", null, null)), //language=java java( """ import org.example.Const; import org.example.FooDefaultString; - @FooDefaultString(Const.X.Y.SOME_CONST) - @FooDefaultString(Const.X.Y.OTHER_CONST) - @FooDefaultString(value = Const.X.Y.SOME_CONST) - @FooDefaultString(value = Const.X.Y.OTHER_CONST) + @FooDefaultString(Const.X.Y.FIRST_CONST) + @FooDefaultString(Const.X.Y.SECOND_CONST) + @FooDefaultString(value = Const.X.Y.FIRST_CONST) + @FooDefaultString(value = Const.X.Y.SECOND_CONST) public class A {} """, """ import org.example.Const; import org.example.FooDefaultString; @FooDefaultString - @FooDefaultString(Const.X.Y.OTHER_CONST) + @FooDefaultString(Const.X.Y.SECOND_CONST) @FooDefaultString - @FooDefaultString(Const.X.Y.OTHER_CONST) + @FooDefaultString(Const.X.Y.SECOND_CONST) public class A {} """ ), @@ -283,15 +304,15 @@ public class A {} """ import org.example.Const; import org.example.FooDefaultString; - @FooDefaultString(value = Const.X.Y.SOME_CONST, a = Const.X.Y.SOME_CONST) - @FooDefaultString(value = Const.X.Y.OTHER_CONST, a = Const.X.Y.SOME_CONST) + @FooDefaultString(value = Const.X.Y.FIRST_CONST, str = Const.X.Y.FIRST_CONST) + @FooDefaultString(value = Const.X.Y.SECOND_CONST, str = Const.X.Y.FIRST_CONST) public class B {} """, """ import org.example.Const; import org.example.FooDefaultString; - @FooDefaultString(a = Const.X.Y.SOME_CONST) - @FooDefaultString(value = Const.X.Y.OTHER_CONST, a = Const.X.Y.SOME_CONST) + @FooDefaultString(str = Const.X.Y.FIRST_CONST) + @FooDefaultString(value = Const.X.Y.SECOND_CONST, str = Const.X.Y.FIRST_CONST) public class B {} """ ) @@ -325,14 +346,14 @@ public class A {} java( """ import org.example.FooDefaultString; - @FooDefaultString(value = "a", a = "a") - @FooDefaultString(value = "b", a = "a") + @FooDefaultString(value = "a", str = "a") + @FooDefaultString(value = "b", str = "a") public class B {} """, """ import org.example.FooDefaultString; - @FooDefaultString(a = "a") - @FooDefaultString(value = "b", a = "a") + @FooDefaultString(str = "a") + @FooDefaultString(value = "b", str = "a") public class B {} """ ) @@ -359,8 +380,8 @@ public class A {} java( """ import org.example.FooDefaultStringArray; - @FooDefaultStringArray(b = "a") - @FooDefaultStringArray(b = {"b"}) + @FooDefaultStringArray(strArr = "a") + @FooDefaultStringArray(strArr = {"b"}) public class B {} """ ) @@ -388,9 +409,9 @@ public class A {} java( """ import org.example.FooDefaultStringArray; - @FooDefaultStringArray(value = "a", b = {"z"}) - @FooDefaultStringArray(value = {}, b = {"y"}) - @FooDefaultStringArray(value = {"d"}, b = {"x"}) + @FooDefaultStringArray(value = "a", strArr = {"z"}) + @FooDefaultStringArray(value = {}, strArr = {"y"}) + @FooDefaultStringArray(value = {"d"}, strArr = {"x"}) public class B {} """ ) @@ -407,15 +428,15 @@ void literalArrayAttribute_existing_usingNullOldAttributeValue_removesSafely() { import org.example.Const; import org.example.FooDefaultStringArray; @FooDefaultStringArray("a") - @FooDefaultStringArray(Const.X.Y.SOME_CONST) + @FooDefaultStringArray(Const.X.Y.FIRST_CONST) @FooDefaultStringArray(value = "b") - @FooDefaultStringArray(Const.X.Y.SOME_CONST) + @FooDefaultStringArray(Const.X.Y.FIRST_CONST) @FooDefaultStringArray({}) @FooDefaultStringArray({"c"}) - @FooDefaultStringArray({Const.X.Y.SOME_CONST}) + @FooDefaultStringArray({Const.X.Y.FIRST_CONST}) @FooDefaultStringArray(value = {}) @FooDefaultStringArray(value = {"d"}) - @FooDefaultStringArray(value = {Const.X.Y.SOME_CONST}) + @FooDefaultStringArray(value = {Const.X.Y.FIRST_CONST}) public class A {} """, """ @@ -439,79 +460,21 @@ public class A {} """ import org.example.Const; import org.example.FooDefaultStringArray; - @FooDefaultStringArray(value = "b", b = {"z1"}) - @FooDefaultStringArray(value = Const.X.Y.SOME_CONST, b = {"z2"}) - @FooDefaultStringArray(value = {}, b = {"y1"}) - @FooDefaultStringArray(value = {"d"}, b = {"y2"}) - @FooDefaultStringArray(value = {Const.X.Y.SOME_CONST}, b = {"y3"}) - public class B {} - """, - """ - import org.example.Const; - import org.example.FooDefaultStringArray; - @FooDefaultStringArray(b = {"z1"}) - @FooDefaultStringArray(b = {"z2"}) - @FooDefaultStringArray(b = {"y1"}) - @FooDefaultStringArray(b = {"y2"}) - @FooDefaultStringArray(b = {"y3"}) - public class B {} - """ - ) - ); - } - - @Disabled("We can't support this right now, as there is no reference to the actual string literal of the constant") - @Test - void literalArrayAttribute_existing_asConst_usingProvidedOldAttributeValue_ofConstValue_removesSafelyOnlyMatched() { - rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", null, null, "a", null, null)), - //language=java - java( - """ - import org.example.Const; - import org.example.FooDefaultStringArray; - @FooDefaultStringArray(Const.X.Y.SOME_CONST) - @FooDefaultStringArray(Const.X.Y.OTHER_CONST) - @FooDefaultStringArray(value = Const.X.Y.SOME_CONST) - @FooDefaultStringArray(value = Const.X.Y.OTHER_CONST) - @FooDefaultStringArray({Const.X.Y.SOME_CONST}) - @FooDefaultStringArray({Const.X.Y.OTHER_CONST, Const.X.Y.SOME_CONST, Const.X.Y.THIRD_CONST}) - @FooDefaultStringArray(value = {Const.X.Y.SOME_CONST}) - @FooDefaultStringArray(value = {Const.X.Y.OTHER_CONST, Const.X.Y.SOME_CONST, Const.X.Y.THIRD_CONST}) - public class A {} - """, - """ - import org.example.Const; - import org.example.FooDefaultStringArray; - @FooDefaultStringArray - @FooDefaultStringArray(Const.X.Y.OTHER_CONST) - @FooDefaultStringArray - @FooDefaultStringArray(Const.X.Y.OTHER_CONST) - @FooDefaultStringArray - @FooDefaultStringArray({Const.X.Y.OTHER_CONST, Const.X.Y.THIRD_CONST}) - @FooDefaultStringArray - @FooDefaultStringArray({Const.X.Y.OTHER_CONST, Const.X.Y.THIRD_CONST}) - public class A {} - """ - ), - //language=java - java( - """ - import org.example.Const; - import org.example.FooDefaultStringArray; - @FooDefaultStringArray(value = Const.X.Y.SOME_CONST, b = {Const.X.Y.SOME_CONST}) - @FooDefaultStringArray(value = Const.X.Y.OTHER_CONST, b = {Const.X.Y.SOME_CONST}) - @FooDefaultStringArray(value = {Const.X.Y.SOME_CONST}, b = {Const.X.Y.SOME_CONST}) - @FooDefaultStringArray(value = {Const.X.Y.OTHER_CONST, Const.X.Y.SOME_CONST, Const.X.Y.THIRD_CONST}, b = {Const.X.Y.SOME_CONST}) + @FooDefaultStringArray(value = "b", strArr = {"z1"}) + @FooDefaultStringArray(value = Const.X.Y.FIRST_CONST, strArr = {"z2"}) + @FooDefaultStringArray(value = {}, strArr = {"y1"}) + @FooDefaultStringArray(value = {"d"}, strArr = {"y2"}) + @FooDefaultStringArray(value = {Const.X.Y.FIRST_CONST}, strArr = {"y3"}) public class B {} """, """ import org.example.Const; import org.example.FooDefaultStringArray; - @FooDefaultStringArray(b = {Const.X.Y.SOME_CONST}) - @FooDefaultStringArray(value = Const.X.Y.OTHER_CONST, b = {Const.X.Y.SOME_CONST}) - @FooDefaultStringArray(b = {Const.X.Y.SOME_CONST}) - @FooDefaultStringArray(value = {Const.X.Y.OTHER_CONST, Const.X.Y.THIRD_CONST}, b = {Const.X.Y.SOME_CONST}) + @FooDefaultStringArray(strArr = {"z1"}) + @FooDefaultStringArray(strArr = {"z2"}) + @FooDefaultStringArray(strArr = {"y1"}) + @FooDefaultStringArray(strArr = {"y2"}) + @FooDefaultStringArray(strArr = {"y3"}) public class B {} """ ) @@ -521,33 +484,33 @@ public class B {} @Test void literalArrayAttribute_existing_asConst_usingProvidedOldAttributeValue_ofConstRef_removesSafelyOnlyMatched() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", null, null, "Const.X.Y.SOME_CONST", null, null)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", null, null, "Const.X.Y.FIRST_CONST", null, null)), //language=java java( """ import org.example.Const; import org.example.FooDefaultStringArray; - @FooDefaultStringArray(Const.X.Y.SOME_CONST) - @FooDefaultStringArray(Const.X.Y.OTHER_CONST) - @FooDefaultStringArray(value = Const.X.Y.SOME_CONST) - @FooDefaultStringArray(value = Const.X.Y.OTHER_CONST) - @FooDefaultStringArray({Const.X.Y.SOME_CONST}) - @FooDefaultStringArray({Const.X.Y.OTHER_CONST, Const.X.Y.SOME_CONST, Const.X.Y.THIRD_CONST}) - @FooDefaultStringArray(value = {Const.X.Y.SOME_CONST}) - @FooDefaultStringArray(value = {Const.X.Y.OTHER_CONST, Const.X.Y.SOME_CONST, Const.X.Y.THIRD_CONST}) + @FooDefaultStringArray(Const.X.Y.FIRST_CONST) + @FooDefaultStringArray(Const.X.Y.SECOND_CONST) + @FooDefaultStringArray(value = Const.X.Y.FIRST_CONST) + @FooDefaultStringArray(value = Const.X.Y.SECOND_CONST) + @FooDefaultStringArray({Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray({Const.X.Y.SECOND_CONST, Const.X.Y.FIRST_CONST, Const.X.Y.THIRD_CONST}) + @FooDefaultStringArray(value = {Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray(value = {Const.X.Y.SECOND_CONST, Const.X.Y.FIRST_CONST, Const.X.Y.THIRD_CONST}) public class A {} """, """ import org.example.Const; import org.example.FooDefaultStringArray; @FooDefaultStringArray - @FooDefaultStringArray(Const.X.Y.OTHER_CONST) + @FooDefaultStringArray(Const.X.Y.SECOND_CONST) @FooDefaultStringArray - @FooDefaultStringArray(Const.X.Y.OTHER_CONST) + @FooDefaultStringArray(Const.X.Y.SECOND_CONST) @FooDefaultStringArray - @FooDefaultStringArray({Const.X.Y.OTHER_CONST, Const.X.Y.THIRD_CONST}) + @FooDefaultStringArray({Const.X.Y.SECOND_CONST, Const.X.Y.THIRD_CONST}) @FooDefaultStringArray - @FooDefaultStringArray({Const.X.Y.OTHER_CONST, Const.X.Y.THIRD_CONST}) + @FooDefaultStringArray({Const.X.Y.SECOND_CONST, Const.X.Y.THIRD_CONST}) public class A {} """ ), @@ -556,19 +519,19 @@ public class A {} """ import org.example.Const; import org.example.FooDefaultStringArray; - @FooDefaultStringArray(value = Const.X.Y.SOME_CONST, b = {Const.X.Y.SOME_CONST}) - @FooDefaultStringArray(value = Const.X.Y.OTHER_CONST, b = {Const.X.Y.SOME_CONST}) - @FooDefaultStringArray(value = {Const.X.Y.SOME_CONST}, b = {Const.X.Y.SOME_CONST}) - @FooDefaultStringArray(value = {Const.X.Y.OTHER_CONST, Const.X.Y.SOME_CONST, Const.X.Y.THIRD_CONST}, b = {Const.X.Y.SOME_CONST}) + @FooDefaultStringArray(value = Const.X.Y.FIRST_CONST, strArr = {Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray(value = Const.X.Y.SECOND_CONST, strArr = {Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray(value = {Const.X.Y.FIRST_CONST}, strArr = {Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray(value = {Const.X.Y.SECOND_CONST, Const.X.Y.FIRST_CONST, Const.X.Y.THIRD_CONST}, strArr = {Const.X.Y.FIRST_CONST}) public class B {} """, """ import org.example.Const; import org.example.FooDefaultStringArray; - @FooDefaultStringArray(b = {Const.X.Y.SOME_CONST}) - @FooDefaultStringArray(value = Const.X.Y.OTHER_CONST, b = {Const.X.Y.SOME_CONST}) - @FooDefaultStringArray(b = {Const.X.Y.SOME_CONST}) - @FooDefaultStringArray(value = {Const.X.Y.OTHER_CONST, Const.X.Y.THIRD_CONST}, b = {Const.X.Y.SOME_CONST}) + @FooDefaultStringArray(strArr = {Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray(value = Const.X.Y.SECOND_CONST, strArr = {Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray(strArr = {Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray(value = {Const.X.Y.SECOND_CONST, Const.X.Y.THIRD_CONST}, strArr = {Const.X.Y.FIRST_CONST}) public class B {} """ ) @@ -614,20 +577,20 @@ public class A {} java( """ import org.example.FooDefaultStringArray; - @FooDefaultStringArray(value = "a", b = {"a"}) - @FooDefaultStringArray(value = "b", b = {"a"}) - @FooDefaultStringArray(value = {}, b = {"a"}) - @FooDefaultStringArray(value = {"a"}, b = {"a"}) - @FooDefaultStringArray(value = {"b", "a", "c"}, b = {"a"}) + @FooDefaultStringArray(value = "a", strArr = {"a"}) + @FooDefaultStringArray(value = "b", strArr = {"a"}) + @FooDefaultStringArray(value = {}, strArr = {"a"}) + @FooDefaultStringArray(value = {"a"}, strArr = {"a"}) + @FooDefaultStringArray(value = {"b", "a", "c"}, strArr = {"a"}) public class B {} """, """ import org.example.FooDefaultStringArray; - @FooDefaultStringArray(b = {"a"}) - @FooDefaultStringArray(value = "b", b = {"a"}) - @FooDefaultStringArray(b = {"a"}) - @FooDefaultStringArray(b = {"a"}) - @FooDefaultStringArray(value = {"b", "c"}, b = {"a"}) + @FooDefaultStringArray(strArr = {"a"}) + @FooDefaultStringArray(value = "b", strArr = {"a"}) + @FooDefaultStringArray(strArr = {"a"}) + @FooDefaultStringArray(strArr = {"a"}) + @FooDefaultStringArray(value = {"b", "c"}, strArr = {"a"}) public class B {} """ ) @@ -654,7 +617,7 @@ public class A {} java( """ import org.example.FooDefaultClass; - @FooDefaultClass(c = Integer.class) + @FooDefaultClass(cla = Integer.class) public class B {} """ ) @@ -678,7 +641,7 @@ public class A {} java( """ import org.example.FooDefaultClass; - @FooDefaultClass(value = Integer.class, c = Long.class) + @FooDefaultClass(value = Integer.class, cla = Long.class) public class B {} """ ) @@ -708,12 +671,12 @@ public class A {} java( """ import org.example.FooDefaultClass; - @FooDefaultClass(value = Long.class, c = Byte.class) + @FooDefaultClass(value = Long.class, cla = Byte.class) public class B {} """, """ import org.example.FooDefaultClass; - @FooDefaultClass(c = Byte.class) + @FooDefaultClass(cla = Byte.class) public class B {} """ ) @@ -747,14 +710,14 @@ public class A {} java( """ import org.example.FooDefaultClass; - @FooDefaultClass(value = Integer.class, c = Integer.class) - @FooDefaultClass(value = Long.class, c = Integer.class) + @FooDefaultClass(value = Integer.class, cla = Integer.class) + @FooDefaultClass(value = Long.class, cla = Integer.class) public class B {} """, """ import org.example.FooDefaultClass; - @FooDefaultClass(c = Integer.class) - @FooDefaultClass(value = Long.class, c = Integer.class) + @FooDefaultClass(cla = Integer.class) + @FooDefaultClass(value = Long.class, cla = Integer.class) public class B {} """ ) @@ -781,8 +744,8 @@ public class A {} java( """ import org.example.FooDefaultClassArray; - @FooDefaultClassArray(d = Integer.class) - @FooDefaultClassArray(d = {Long.class}) + @FooDefaultClassArray(claArr = Integer.class) + @FooDefaultClassArray(claArr = {Long.class}) public class B {} """ ) @@ -810,9 +773,9 @@ public class A {} java( """ import org.example.FooDefaultClassArray; - @FooDefaultClassArray(value = Long.class, d = {Byte.class}) - @FooDefaultClassArray(value = {}, d = {Integer.class}) - @FooDefaultClassArray(value = {Long.class}, d = {Integer.class}) + @FooDefaultClassArray(value = Long.class, claArr = {Byte.class}) + @FooDefaultClassArray(value = {}, claArr = {Integer.class}) + @FooDefaultClassArray(value = {Long.class}, claArr = {Integer.class}) public class B {} """ ) @@ -850,16 +813,16 @@ public class A {} java( """ import org.example.FooDefaultClassArray; - @FooDefaultClassArray(value = Long.class, d = {Byte.class}) - @FooDefaultClassArray(value = {}, d = {Float.class}) - @FooDefaultClassArray(value = {Long.class}, d = {Integer.class}) + @FooDefaultClassArray(value = Long.class, claArr = {Byte.class}) + @FooDefaultClassArray(value = {}, claArr = {Float.class}) + @FooDefaultClassArray(value = {Long.class}, claArr = {Integer.class}) public class B {} """, """ import org.example.FooDefaultClassArray; - @FooDefaultClassArray(d = {Byte.class}) - @FooDefaultClassArray(d = {Float.class}) - @FooDefaultClassArray(d = {Integer.class}) + @FooDefaultClassArray(claArr = {Byte.class}) + @FooDefaultClassArray(claArr = {Float.class}) + @FooDefaultClassArray(claArr = {Integer.class}) public class B {} """ ) @@ -905,160 +868,270 @@ public class A {} java( """ import org.example.FooDefaultClassArray; - @FooDefaultClassArray(value = Integer.class, d = {Integer.class}) - @FooDefaultClassArray(value = Long.class, d = {Integer.class}) - @FooDefaultClassArray(value = {}, d = {Integer.class}) - @FooDefaultClassArray(value = {Integer.class}, d = {Integer.class}) - @FooDefaultClassArray(value = {Long.class, Integer.class, Short.class}, d = {Integer.class}) + @FooDefaultClassArray(value = Integer.class, claArr = {Integer.class}) + @FooDefaultClassArray(value = Long.class, claArr = {Integer.class}) + @FooDefaultClassArray(value = {}, claArr = {Integer.class}) + @FooDefaultClassArray(value = {Integer.class}, claArr = {Integer.class}) + @FooDefaultClassArray(value = {Long.class, Integer.class, Short.class}, claArr = {Integer.class}) public class B {} """, """ import org.example.FooDefaultClassArray; - @FooDefaultClassArray(d = {Integer.class}) - @FooDefaultClassArray(value = Long.class, d = {Integer.class}) - @FooDefaultClassArray(d = {Integer.class}) - @FooDefaultClassArray(d = {Integer.class}) - @FooDefaultClassArray(value = {Long.class, Short.class}, d = {Integer.class}) + @FooDefaultClassArray(claArr = {Integer.class}) + @FooDefaultClassArray(value = Long.class, claArr = {Integer.class}) + @FooDefaultClassArray(claArr = {Integer.class}) + @FooDefaultClassArray(claArr = {Integer.class}) + @FooDefaultClassArray(value = {Long.class, Short.class}, claArr = {Integer.class}) public class B {} """ ) ); } } - } - @Nested - class UsingLiteralAttributeValue { @Nested - class WithLiteralTypeAttribute { + class WithEnumTypeAttribute { @Test - void literalAttribute_absent_usingNullOldAttributeValue_addsSafely() { + void enumAttribute_absent_doesNothing() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "a", null, null, null)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultEnum", null, null, null, null, null)), //language=java java( """ - import org.example.FooDefaultString; - @FooDefaultString - @FooDefaultString() - @FooDefaultString(a = "b") + import org.example.FooDefaultEnum; + @FooDefaultEnum + @FooDefaultEnum() public class A {} - """, + """ + ), + //language=java + java( """ - import org.example.FooDefaultString; - @FooDefaultString("a") - @FooDefaultString("a") - @FooDefaultString(value = "a", a = "b") - public class A {} + import org.example.FooDefaultEnum; + import org.example.FooEnum; + @FooDefaultEnum(enu = FooEnum.ONE) + public class B {} """ ) ); } @Test - void literalAttribute_absent_usingLiteralOldAttributeValue_doesNothing() { + void enumAttribute_existing_usingAddOnly_doesNothing() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "a", "b", null, null)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultEnum", null, null, null, true, null)), //language=java java( """ - import org.example.FooDefaultString; - @FooDefaultString - @FooDefaultString() - @FooDefaultString(a = "b") + import org.example.FooDefaultEnum; + import org.example.FooEnum; + @FooDefaultEnum(FooEnum.ONE) + @FooDefaultEnum(FooEnum.TWO) public class A {} """ + ), + //language=java + java( + """ + import org.example.FooDefaultEnum; + import org.example.FooEnum; + @FooDefaultEnum(value = FooEnum.ONE, enu = FooEnum.TWO) + public class B {} + """ ) ); } @Test - void literalAttribute_existing_usingAddOnly_doesNothing() { + void enumAttribute_existing_usingNullOldAttributeValue_removesSafely() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "a", null, true, null)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultEnum", null, null, null, null, null)), //language=java java( """ - import org.example.Const; - import org.example.FooDefaultString; - @FooDefaultString("b") - @FooDefaultString(Const.X.Y.OTHER_CONST) - @FooDefaultString(value = "b") - @FooDefaultString(value = Const.X.Y.OTHER_CONST) + import org.example.FooDefaultEnum; + import org.example.FooEnum; + @FooDefaultEnum(FooEnum.ONE) + @FooDefaultEnum(FooEnum.TWO) + public class A {} + """, + """ + import org.example.FooDefaultEnum; + import org.example.FooEnum; + @FooDefaultEnum + @FooDefaultEnum public class A {} """ + ), + //language=java + java( + """ + import org.example.FooDefaultEnum; + import org.example.FooEnum; + @FooDefaultEnum(value = FooEnum.ONE, enu = FooEnum.TWO) + public class B {} + """, + """ + import org.example.FooDefaultEnum; + import org.example.FooEnum; + @FooDefaultEnum(enu = FooEnum.TWO) + public class B {} + """ ) ); } @Test - void literalAttribute_existing_usingNullOldAttributeValue_updatesSafely() { + void enumAttribute_existing_usingProvidedOldAttributeValue_removesSafelyOnlyMatched() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "a", null, null, null)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultEnum", null, null, "FooEnum.ONE", null, null)), //language=java java( """ - import org.example.Const; - import org.example.FooDefaultString; - @FooDefaultString("b") - @FooDefaultString(Const.X.Y.OTHER_CONST) - @FooDefaultString(value = "c") - @FooDefaultString(value = Const.X.Y.THIRD_CONST) + import org.example.FooDefaultEnum; + import org.example.FooEnum; + @FooDefaultEnum(FooEnum.ONE) + @FooDefaultEnum(FooEnum.TWO) + @FooDefaultEnum(value = FooEnum.ONE) + @FooDefaultEnum(value = FooEnum.TWO) public class A {} """, """ - import org.example.Const; - import org.example.FooDefaultString; - @FooDefaultString("a") - @FooDefaultString("a") - @FooDefaultString("a") - @FooDefaultString("a") + import org.example.FooDefaultEnum; + import org.example.FooEnum; + @FooDefaultEnum + @FooDefaultEnum(FooEnum.TWO) + @FooDefaultEnum + @FooDefaultEnum(FooEnum.TWO) + public class A {} + """ + ), + //language=java + java( + """ + import org.example.FooDefaultEnum; + import org.example.FooEnum; + @FooDefaultEnum(value = FooEnum.ONE, enu = FooEnum.ONE) + @FooDefaultEnum(value = FooEnum.TWO, enu = FooEnum.ONE) + public class B {} + """, + """ + import org.example.FooDefaultEnum; + import org.example.FooEnum; + @FooDefaultEnum(enu = FooEnum.ONE) + @FooDefaultEnum(value = FooEnum.TWO, enu = FooEnum.ONE) + public class B {} + """ + ) + ); + } + } + + @Nested + class WithEnumArrayTypeAttribute { + @Test + void enumArrayAttribute_absent_doesNothing() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultEnumArray", null, null, null, null, null)), + //language=java + java( + """ + import org.example.FooDefaultEnumArray; + @FooDefaultEnumArray + @FooDefaultEnumArray() public class A {} """ + ), + //language=java + java( + """ + import org.example.FooDefaultEnumArray; + import org.example.FooEnum; + @FooDefaultEnumArray(enuArr = FooEnum.ONE) + @FooDefaultEnumArray(enuArr = {FooEnum.TWO}) + public class B {} + """ ) ); } - @Disabled("We can't support this right now, as there is no reference to the actual string literal of the constant") @Test - void literalAttribute_existing_asConst_usingProvidedOldAttributeValue_ofConstValue_updatesSafelyOnlyMatched() { + void enumArrayAttribute_existing_usingAddOnly_doesNothing() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "b", "a", null, null)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultEnumArray", null, null, null, true, null)), //language=java java( """ - import org.example.Const; - import org.example.FooDefaultString; - @FooDefaultString(Const.X.Y.SOME_CONST) - @FooDefaultString(Const.X.Y.OTHER_CONST) - @FooDefaultString(value = Const.X.Y.SOME_CONST) - @FooDefaultString(value = Const.X.Y.OTHER_CONST) + import org.example.FooDefaultEnumArray; + import org.example.FooEnum; + @FooDefaultEnumArray(FooEnum.ONE) + @FooDefaultEnumArray(value = FooEnum.TWO) + @FooDefaultEnumArray({}) + @FooDefaultEnumArray({FooEnum.THREE}) + @FooDefaultEnumArray(value = {}) + @FooDefaultEnumArray(value = {FooEnum.FOUR}) + public class A {} + """ + ), + //language=java + java( + """ + import org.example.FooDefaultEnumArray; + import org.example.FooEnum; + @FooDefaultEnumArray(value = FooEnum.ONE, enuArr = {FooEnum.ONE}) + @FooDefaultEnumArray(value = {}, enuArr = {FooEnum.TWO}) + @FooDefaultEnumArray(value = {FooEnum.THREE}, enuArr = {FooEnum.THREE}) + public class B {} + """ + ) + ); + } + + @Test + void enumArrayAttribute_existing_usingNullOldAttributeValue_removesSafely() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultEnumArray", null, null, null, null, null)), + //language=java + java( + """ + import org.example.FooDefaultEnumArray; + import org.example.FooEnum; + @FooDefaultEnumArray(FooEnum.ONE) + @FooDefaultEnumArray(value = FooEnum.TWO) + @FooDefaultEnumArray({}) + @FooDefaultEnumArray({FooEnum.THREE}) + @FooDefaultEnumArray(value = {}) + @FooDefaultEnumArray(value = {FooEnum.FOUR}) public class A {} """, """ - import org.example.Const; - import org.example.FooDefaultString; - @FooDefaultString("b") - @FooDefaultString(Const.X.Y.OTHER_CONST) - @FooDefaultString("b") - @FooDefaultString(Const.X.Y.OTHER_CONST) + import org.example.FooDefaultEnumArray; + import org.example.FooEnum; + @FooDefaultEnumArray + @FooDefaultEnumArray + @FooDefaultEnumArray + @FooDefaultEnumArray + @FooDefaultEnumArray + @FooDefaultEnumArray public class A {} """ ), //language=java java( """ - import org.example.Const; - import org.example.FooDefaultString; - @FooDefaultString(value = Const.X.Y.SOME_CONST, a = Const.X.Y.SOME_CONST) - @FooDefaultString(value = Const.X.Y.OTHER_CONST, a = Const.X.Y.SOME_CONST) + import org.example.FooDefaultEnumArray; + import org.example.FooEnum; + @FooDefaultEnumArray(value = FooEnum.ONE, enuArr = {FooEnum.ONE}) + @FooDefaultEnumArray(value = {}, enuArr = {FooEnum.TWO}) + @FooDefaultEnumArray(value = {FooEnum.THREE}, enuArr = {FooEnum.THREE}) public class B {} """, """ - import org.example.Const; - import org.example.FooDefaultString; - @FooDefaultString(value = "b", a = Const.X.Y.SOME_CONST) - @FooDefaultString(value = Const.X.Y.OTHER_CONST, a = Const.X.Y.SOME_CONST) + import org.example.FooDefaultEnumArray; + import org.example.FooEnum; + @FooDefaultEnumArray(enuArr = {FooEnum.ONE}) + @FooDefaultEnumArray(enuArr = {FooEnum.TWO}) + @FooDefaultEnumArray(enuArr = {FooEnum.THREE}) public class B {} """ ) @@ -1066,51 +1139,557 @@ public class B {} } @Test - void literalAttribute_existing_asConst_usingProvidedOldAttributeValue_ofConstRef_updatesSafelyOnlyMatched() { + void enumArrayAttribute_existing_usingProvidedOldAttributeValue_removesSafelyOnlyMatched() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "b", "Const.X.Y.SOME_CONST", null, null)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultEnumArray", null, null, "FooEnum.ONE", null, null)), //language=java java( """ - import org.example.Const; - import org.example.FooDefaultString; - @FooDefaultString(Const.X.Y.SOME_CONST) - @FooDefaultString(Const.X.Y.OTHER_CONST) - @FooDefaultString(value = Const.X.Y.SOME_CONST) - @FooDefaultString(value = Const.X.Y.OTHER_CONST) + import org.example.FooDefaultEnumArray; + import org.example.FooEnum; + @FooDefaultEnumArray(FooEnum.ONE) + @FooDefaultEnumArray(FooEnum.TWO) + @FooDefaultEnumArray(value = FooEnum.ONE) + @FooDefaultEnumArray(value = FooEnum.TWO) + @FooDefaultEnumArray({}) + @FooDefaultEnumArray({FooEnum.ONE}) + @FooDefaultEnumArray({FooEnum.TWO, FooEnum.ONE, FooEnum.THREE}) + @FooDefaultEnumArray(value = {}) + @FooDefaultEnumArray(value = {FooEnum.ONE}) + @FooDefaultEnumArray(value = {FooEnum.TWO, FooEnum.ONE, FooEnum.THREE}) public class A {} """, """ - import org.example.Const; - import org.example.FooDefaultString; - @FooDefaultString("b") - @FooDefaultString(Const.X.Y.OTHER_CONST) - @FooDefaultString("b") - @FooDefaultString(Const.X.Y.OTHER_CONST) + import org.example.FooDefaultEnumArray; + import org.example.FooEnum; + @FooDefaultEnumArray + @FooDefaultEnumArray(FooEnum.TWO) + @FooDefaultEnumArray + @FooDefaultEnumArray(FooEnum.TWO) + @FooDefaultEnumArray + @FooDefaultEnumArray + @FooDefaultEnumArray({FooEnum.TWO, FooEnum.THREE}) + @FooDefaultEnumArray + @FooDefaultEnumArray + @FooDefaultEnumArray({FooEnum.TWO, FooEnum.THREE}) public class A {} """ - )/*, + ), //language=java java( """ - import org.example.Const; - import org.example.FooDefaultString; - @FooDefaultString(value = Const.X.Y.SOME_CONST, a = Const.X.Y.SOME_CONST) - @FooDefaultString(value = Const.X.Y.OTHER_CONST, a = Const.X.Y.SOME_CONST) + import org.example.FooDefaultEnumArray; + import org.example.FooEnum; + @FooDefaultEnumArray(value = FooEnum.ONE, enuArr = {FooEnum.ONE}) + @FooDefaultEnumArray(value = FooEnum.TWO, enuArr = {FooEnum.ONE}) + @FooDefaultEnumArray(value = {}, enuArr = {FooEnum.ONE}) + @FooDefaultEnumArray(value = {FooEnum.ONE}, enuArr = {FooEnum.ONE}) + @FooDefaultEnumArray(value = {FooEnum.TWO, FooEnum.ONE, FooEnum.THREE}, enuArr = {FooEnum.ONE}) public class B {} """, """ - import org.example.Const; - import org.example.FooDefaultString; - @FooDefaultString(value = "b", a = Const.X.Y.SOME_CONST) - @FooDefaultString(value = Const.X.Y.OTHER_CONST, a = Const.X.Y.SOME_CONST) + import org.example.FooDefaultEnumArray; + import org.example.FooEnum; + @FooDefaultEnumArray(enuArr = {FooEnum.ONE}) + @FooDefaultEnumArray(value = FooEnum.TWO, enuArr = {FooEnum.ONE}) + @FooDefaultEnumArray(enuArr = {FooEnum.ONE}) + @FooDefaultEnumArray(enuArr = {FooEnum.ONE}) + @FooDefaultEnumArray(value = {FooEnum.TWO, FooEnum.THREE}, enuArr = {FooEnum.ONE}) public class B {} """ - )*/ + ) + ); + } + } + + // TODO: long and boolean versions? + } + + @Nested + class UsingLiteralAttributeValue { + @Nested + class WithLiteralTypeAttribute { + @Test + void literalAttribute_absent_usingNullOldAttributeValue_addsSafely() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "a", null, null, null)), + //language=java + java( + """ + import org.example.FooDefaultString; + @FooDefaultString + @FooDefaultString() + @FooDefaultString(str = "b") + public class A {} + """, + """ + import org.example.FooDefaultString; + @FooDefaultString("a") + @FooDefaultString("a") + @FooDefaultString(value = "a", str = "b") + public class A {} + """ + ) + ); + } + + @Test + void literalAttribute_absent_usingLiteralOldAttributeValue_doesNothing() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "a", "b", null, null)), + //language=java + java( + """ + import org.example.FooDefaultString; + @FooDefaultString + @FooDefaultString() + @FooDefaultString(str = "b") + public class A {} + """ + ) + ); + } + + @Test + void literalAttribute_existing_usingAddOnly_doesNothing() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "a", null, true, null)), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString("b") + @FooDefaultString(Const.X.Y.SECOND_CONST) + @FooDefaultString(value = "b") + @FooDefaultString(value = Const.X.Y.SECOND_CONST) + public class A {} + """ + ) + ); + } + + @Test + void literalAttribute_existing_usingNullOldAttributeValue_updatesSafely() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "a", null, null, null)), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString("b") + @FooDefaultString(Const.X.Y.SECOND_CONST) + @FooDefaultString(value = "c") + @FooDefaultString(value = Const.X.Y.THIRD_CONST) + public class A {} + """, + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString("a") + @FooDefaultString("a") + @FooDefaultString("a") + @FooDefaultString("a") + public class A {} + """ + ), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString(value = "b", str = "b") + @FooDefaultString(value = Const.X.Y.SECOND_CONST, str = Const.X.Y.SECOND_CONST) + public class B {} + """, + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString(value = "a", str = "b") + @FooDefaultString(value = "a", str = Const.X.Y.SECOND_CONST) + public class B {} + """ + ) + ); + } + + @Test + void literalAttribute_existing_asConst_usingProvidedOldAttributeValue_ofConstRef_updatesSafelyOnlyMatched() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "b", "Const.X.Y.FIRST_CONST", null, null)), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString(Const.X.Y.FIRST_CONST) + @FooDefaultString(Const.X.Y.SECOND_CONST) + @FooDefaultString(value = Const.X.Y.FIRST_CONST) + @FooDefaultString(value = Const.X.Y.SECOND_CONST) + public class A {} + """, + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString("b") + @FooDefaultString(Const.X.Y.SECOND_CONST) + @FooDefaultString("b") + @FooDefaultString(Const.X.Y.SECOND_CONST) + public class A {} + """ + ), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString(value = Const.X.Y.FIRST_CONST, str = Const.X.Y.FIRST_CONST) + @FooDefaultString(value = Const.X.Y.SECOND_CONST, str = Const.X.Y.FIRST_CONST) + public class B {} + """, + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString(value = "b", str = Const.X.Y.FIRST_CONST) + @FooDefaultString(value = Const.X.Y.SECOND_CONST, str = Const.X.Y.FIRST_CONST) + public class B {} + """ + ) + ); + } + + @Test + void literalAttribute_existing_usingProvidedOldAttributeValue_updatesSafelyOnlyMatched() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "b", "a", null, null)), + //language=java + java( + """ + import org.example.FooDefaultString; + @FooDefaultString("a") + @FooDefaultString("c") + @FooDefaultString(value = "a") + @FooDefaultString(value = "c") + public class A {} + """, + """ + import org.example.FooDefaultString; + @FooDefaultString("b") + @FooDefaultString("c") + @FooDefaultString("b") + @FooDefaultString("c") + public class A {} + """ + ), + //language=java + java( + """ + import org.example.FooDefaultString; + @FooDefaultString(value = "a", str = "a") + @FooDefaultString(value = "c", str = "a") + public class B {} + """, + """ + import org.example.FooDefaultString; + @FooDefaultString(value = "b", str = "a") + @FooDefaultString(value = "c", str = "a") + public class B {} + """ + ) ); } + } + + @Nested + class WithLiteralArrayTypeAttribute { + @Nested + class UsingSingularValue { + @Test + void literalArrayAttribute_absent_usingNullOldAttributeValue_addsSafely() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", null, "a", null, null, null)), + //language=java + java( + """ + import org.example.FooDefaultStringArray; + @FooDefaultStringArray + @FooDefaultStringArray() + @FooDefaultStringArray(strArr = {"b"}) + public class A {} + """, + """ + import org.example.FooDefaultStringArray; + @FooDefaultStringArray("a") + @FooDefaultStringArray("a") + @FooDefaultStringArray(value = "a", strArr = {"b"}) + public class A {} + """ + ) + ); + } + + @Test + void literalArrayAttribute_absent_usingLiteralOldAttributeValue_doesNothing() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", null, "a", "b", null, null)), + //language=java + java( + """ + import org.example.FooDefaultStringArray; + @FooDefaultStringArray + @FooDefaultStringArray() + @FooDefaultStringArray(strArr = {"b"}) + public class A {} + """ + ) + ); + } + + @Test + void literalArrayAttribute_existing_usingAddOnly_doesNothing() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", null, "a", null, true, null)), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultStringArray; + @FooDefaultStringArray("b") + @FooDefaultStringArray(Const.X.Y.SECOND_CONST) + @FooDefaultStringArray(value = "b") + @FooDefaultStringArray(value = Const.X.Y.SECOND_CONST) + @FooDefaultStringArray({"b"}) + @FooDefaultStringArray({Const.X.Y.SECOND_CONST}) + @FooDefaultStringArray(value = {"b"}) + @FooDefaultStringArray(value = {Const.X.Y.SECOND_CONST}) + public class A {} + """ + ) + ); + } + + // TODO: revisit + @Test + void literalArrayAttribute_existing_usingNullOldAttributeValue_updatesSafely() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", null, "a", null, null, null)), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultStringArray; + @FooDefaultStringArray("b") + @FooDefaultStringArray(Const.X.Y.SECOND_CONST) + @FooDefaultStringArray(value = "c") + @FooDefaultStringArray(value = Const.X.Y.THIRD_CONST) + @FooDefaultStringArray({"b"}) + @FooDefaultStringArray({Const.X.Y.SECOND_CONST}) + // TODO: The two below are curious simplifications + @FooDefaultStringArray(value = {"c"}) + @FooDefaultStringArray(value = {Const.X.Y.THIRD_CONST}) + public class A {} + """, + """ + import org.example.Const; + import org.example.FooDefaultStringArray; + @FooDefaultStringArray("a") + @FooDefaultStringArray("a") + @FooDefaultStringArray("a") + @FooDefaultStringArray("a") + @FooDefaultStringArray({"a"}) + @FooDefaultStringArray({"a"}) + // TODO: The two below are curious simplifications + @FooDefaultStringArray("a") + @FooDefaultStringArray("a") + public class A {} + """ + ), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultStringArray; + @FooDefaultStringArray(value = "b", strArr = {"b"}) + @FooDefaultStringArray(value = Const.X.Y.SECOND_CONST, strArr = {Const.X.Y.SECOND_CONST}) + @FooDefaultStringArray(value = {"c"}, strArr = {"c"}) + @FooDefaultStringArray(value = {Const.X.Y.THIRD_CONST}, strArr = {Const.X.Y.THIRD_CONST}) + public class B {} + """, + """ + import org.example.Const; + import org.example.FooDefaultStringArray; + @FooDefaultStringArray(value = "a", strArr = {"b"}) + @FooDefaultStringArray(value = "a", strArr = {Const.X.Y.SECOND_CONST}) + @FooDefaultStringArray(value = {"a"}, strArr = {"c"}) + @FooDefaultStringArray(value = {"a"}, strArr = {Const.X.Y.THIRD_CONST}) + public class B {} + """ + ) + ); + } + + @Test + void literalArrayAttribute_existing_usingNullOldAttributeValue_andAppendArray_appendsSafely() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", null, "a", null, null, true)), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultStringArray; + @FooDefaultStringArray("b") + @FooDefaultStringArray(Const.X.Y.SECOND_CONST) + @FooDefaultStringArray(value = "c") + @FooDefaultStringArray(value = Const.X.Y.THIRD_CONST) + @FooDefaultStringArray({"b"}) + @FooDefaultStringArray({Const.X.Y.SECOND_CONST}) + @FooDefaultStringArray(value = {"c"}) + @FooDefaultStringArray(value = {Const.X.Y.THIRD_CONST}) + public class A {} + """, + """ + import org.example.Const; + import org.example.FooDefaultStringArray; + @FooDefaultStringArray({"b", "a"}) + @FooDefaultStringArray({Const.X.Y.SECOND_CONST, "a"}) + @FooDefaultStringArray({"c", "a"}) + @FooDefaultStringArray({Const.X.Y.THIRD_CONST, "a"}) + @FooDefaultStringArray({"b", "a"}) + @FooDefaultStringArray({Const.X.Y.SECOND_CONST, "a"}) + @FooDefaultStringArray({"c", "a"}) + @FooDefaultStringArray({Const.X.Y.THIRD_CONST, "a"}) + public class A {} + """ + ), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultStringArray; + @FooDefaultStringArray(value = "b", strArr = {"b"}) + @FooDefaultStringArray(value = Const.X.Y.SECOND_CONST, strArr = {Const.X.Y.SECOND_CONST}) + @FooDefaultStringArray(value = {"c"}, strArr = {"c"}) + @FooDefaultStringArray(value = {Const.X.Y.THIRD_CONST}, strArr = {Const.X.Y.THIRD_CONST}) + public class B {} + """, + """ + import org.example.Const; + import org.example.FooDefaultStringArray; + @FooDefaultStringArray(value = {"b", "a"}, strArr = {"b"}) + @FooDefaultStringArray(value = {Const.X.Y.SECOND_CONST, "a"}, strArr = {Const.X.Y.SECOND_CONST}) + @FooDefaultStringArray(value = {"c", "a"}, strArr = {"c"}) + @FooDefaultStringArray(value = {Const.X.Y.THIRD_CONST, "a"}, strArr = {Const.X.Y.THIRD_CONST}) + public class B {} + """ + ) + ); + } + + @Test + void literalArrayAttribute_existing_asConst_usingProvidedOldAttributeValue_ofConstRef_updatesSafelyOnlyMatched() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", null, "b", "Const.X.Y.FIRST_CONST", null, null)), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultStringArray; + @FooDefaultStringArray(Const.X.Y.FIRST_CONST) + @FooDefaultStringArray(Const.X.Y.SECOND_CONST) + @FooDefaultStringArray(value = Const.X.Y.FIRST_CONST) + @FooDefaultStringArray(value = Const.X.Y.SECOND_CONST) + @FooDefaultStringArray({}) + // TODO: Try to make below consistent + @FooDefaultStringArray({Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray({Const.X.Y.SECOND_CONST, Const.X.Y.FIRST_CONST, Const.X.Y.THIRD_CONST}) + @FooDefaultStringArray(value = {}) + @FooDefaultStringArray(value = {Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray(value = {Const.X.Y.SECOND_CONST, Const.X.Y.FIRST_CONST, Const.X.Y.THIRD_CONST}) + public class A {} + """, + """ + import org.example.Const; + import org.example.FooDefaultStringArray; + @FooDefaultStringArray("b") + @FooDefaultStringArray(Const.X.Y.SECOND_CONST) + @FooDefaultStringArray("b") + @FooDefaultStringArray(Const.X.Y.SECOND_CONST) + @FooDefaultStringArray + // TODO: Try to make below consistent + @FooDefaultStringArray({"b"}) + @FooDefaultStringArray({Const.X.Y.SECOND_CONST, "b", Const.X.Y.THIRD_CONST}) + @FooDefaultStringArray + @FooDefaultStringArray("b") + @FooDefaultStringArray({Const.X.Y.SECOND_CONST, "b", Const.X.Y.THIRD_CONST}) + public class A {} + """ + )/*, + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString(value = Const.X.Y.FIRST_CONST, a = Const.X.Y.FIRST_CONST) + @FooDefaultString(value = Const.X.Y.SECOND_CONST, a = Const.X.Y.FIRST_CONST) + public class B {} + """, + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString(value = "b", a = Const.X.Y.FIRST_CONST) + @FooDefaultString(value = Const.X.Y.SECOND_CONST, a = Const.X.Y.FIRST_CONST) + public class B {} + """ + )*/ + ); + } + } + + + - // TODO: `literalAttribute_existing_usingProvidedOldAttributeValue_updatesSafelyOnlyMatched` + +// @Test +// void literalAttribute_existing_usingProvidedOldAttributeValue_updatesSafelyOnlyMatched() { +// rewriteRun( +// spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "b", "a", null, null)), +// //language=java +// java( +// """ +// import org.example.FooDefaultString; +// @FooDefaultString("a") +// @FooDefaultString("c") +// @FooDefaultString(value = "a") +// @FooDefaultString(value = "c") +// public class A {} +// """, +// """ +// import org.example.FooDefaultString; +// @FooDefaultString("b") +// @FooDefaultString("c") +// @FooDefaultString("b") +// @FooDefaultString("c") +// public class A {} +// """ +// ), +// //language=java +// java( +// """ +// import org.example.FooDefaultString; +// @FooDefaultString(value = "a", a = "a") +// @FooDefaultString(value = "c", a = "a") +// public class B {} +// """, +// """ +// import org.example.FooDefaultString; +// @FooDefaultString(value = "b", a = "a") +// @FooDefaultString(value = "c", a = "a") +// public class B {} +// """ +// ) +// ); +// } } // TODO: nested `WithLiteralArrayTypeAttribute` @@ -1159,28 +1738,19 @@ public class A { @Test void literalToListFromSingleValueUsingAppendArray() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", "bars", "xyz", null, null, true)), - //language=java - java( - """ - package org.example; - public @interface Foo { - String[] bars() default {}; - } - """ - ), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", "strArr", "xyz", null, null, true)), //language=java java( """ - import org.example.Foo; + import org.example.FooDefaultStringArray; - @Foo(bars = "abc") + @FooDefaultStringArray(strArr = "abc") public class A {} """, """ - import org.example.Foo; + import org.example.FooDefaultStringArray; - @Foo(bars = {"abc", "xyz"}) + @FooDefaultStringArray(strArr = {"abc", "xyz"}) public class A {} """ ) @@ -1190,29 +1760,19 @@ public class A {} @Test void addValueAttributeClass() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", null, "Integer.class", null, null, null)), - java( - """ - package org.example; - public @interface Foo { - Class value(); - } - """ - ), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultClass", null, "Integer.class", null, null, null)), java( """ - import org.example.Foo; + import org.example.FooDefaultClass; - @Foo - public class A { - } + @FooDefaultClass + public class A {} """, """ - import org.example.Foo; + import org.example.FooDefaultClass; - @Foo(Integer.class) - public class A { - } + @FooDefaultClass(Integer.class) + public class A {} """ ) ); @@ -1221,31 +1781,21 @@ public class A { @Test void addValueAttributeFullyQualifiedClass() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", null, "java.math.BigDecimal.class", null, null, null)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultClass", null, "java.math.BigDecimal.class", null, null, null)), java( """ - package org.example; - public @interface Foo { - Class value(); - } - """ - ), - java( - """ - import org.example.Foo; + import org.example.FooDefaultClass; - @Foo - public class A { - } + @FooDefaultClass + public class A {} """, """ - import org.example.Foo; + import org.example.FooDefaultClass; import java.math.BigDecimal; - @Foo(BigDecimal.class) - public class A { - } + @FooDefaultClass(BigDecimal.class) + public class A {} """ ) ); @@ -1254,30 +1804,19 @@ public class A { @Test void updateValueAttribute() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", null, "hello", null, null, null)), - java( - """ - package org.example; - public @interface Foo { - String value() default ""; - } - """ - ), - + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "hello", null, null, null)), java( """ - import org.example.Foo; + import org.example.FooDefaultString; - @Foo("goodbye") - public class A { - } + @FooDefaultString("goodbye") + public class A {} """, """ - import org.example.Foo; + import org.example.FooDefaultString; - @Foo("hello") - public class A { - } + @FooDefaultString("hello") + public class A {} """ ) ); @@ -1286,30 +1825,19 @@ public class A { @Test void updateValueAttributeClass() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", null, "Integer.class", null, null, null)), - java( - """ - package org.example; - public @interface Foo { - Class value(); - } - """ - ), - + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultClass", null, "Integer.class", null, null, null)), java( """ - import org.example.Foo; + import org.example.FooDefaultClass; - @Foo(Long.class) - public class A { - } + @FooDefaultClass(Long.class) + public class A {} """, """ - import org.example.Foo; + import org.example.FooDefaultClass; - @Foo(Integer.class) - public class A { - } + @FooDefaultClass(Integer.class) + public class A {} """ ) ); @@ -1318,30 +1846,19 @@ public class A { @Test void removeValueAttribute() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", null, null, null, null, null)), - java( - """ - package org.example; - public @interface Foo { - String value() default ""; - } - """ - ), - + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, null, null, null, null)), java( """ - import org.example.Foo; + import org.example.FooDefaultString; - @Foo("goodbye") - public class A { - } + @FooDefaultString("goodbye") + public class A {} """, """ - import org.example.Foo; + import org.example.FooDefaultString; - @Foo - public class A { - } + @FooDefaultString + public class A {} """ ) ); @@ -1350,30 +1867,19 @@ public class A { @Test void removeValueAttributeClass() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", null, null, null, null, null)), - java( - """ - package org.example; - public @interface Foo { - Class value(); - } - """ - ), - + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultClass", null, null, null, null, null)), java( """ - import org.example.Foo; + import org.example.FooDefaultClass; - @Foo(Long.class) - public class A { - } + @FooDefaultClass(Long.class) + public class A {} """, """ - import org.example.Foo; + import org.example.FooDefaultClass; - @Foo - public class A { - } + @FooDefaultClass + public class A {} """ ) ); @@ -1382,31 +1888,19 @@ public class A { @Test void removeExplicitAttributeNameWhenRemovingValue() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", "name", null, null, null, null)), - java( - """ - package org.example; - public @interface Foo { - String value(); - String name(); - } - """ - ), - + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", "str", null, null, null, null)), java( """ - import org.example.Foo; + import org.example.FooDefaultString; - @Foo(value = "newTest1", name = "newTest2") - public class A { - } + @FooDefaultString(value = "newTest1", str = "newTest2") + public class A {} """, """ - import org.example.Foo; + import org.example.FooDefaultString; - @Foo("newTest1") - public class A { - } + @FooDefaultString("newTest1") + public class A {} """ ) ); @@ -2286,274 +2780,166 @@ public class A { } @Test - void appendMultipleValuesToExistingArrayAttribute() { - rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute( - "org.example.Foo", - "array", - "b,c", - null, - 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 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( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute( - "org.example.Foo", - "array", - "b,c", - null, - 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 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 {}; - } - """ - ), + void appendMultipleValuesToExistingArrayAttribute() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", "strArr", "b,c", null, false, true)), java( """ - import org.example.Foo; + import org.example.FooDefaultStringArray; - @Foo({"a", "b"}) - public class A { - } + @FooDefaultStringArray(b = {"a"}) + public class A {} """, """ - import org.example.Foo; + import org.example.FooDefaultStringArray; - @Foo({"a", "b", "c"}) - public class A { - } + @FooDefaultStringArray(b = {"a", "b", "c"}) + public class A {} """ ) ); } @Test - void appendMultipleValuesToExistingArrayAttributeNonSet() { + void appendMultipleValuesToExistingArrayValueAttribute() { rewriteRun( spec -> spec.recipe(new AddOrUpdateAnnotationAttribute( - "org.example.Foo", - "array", + "org.example.FooDefaultStringArray", + null, "b,c", null, false, - true)), + true + )), java( """ - package org.example; - public @interface Foo { - String[] array() default {}; - } + import org.example.FooDefaultStringArray; - public class A { - } + @FooDefaultStringArray({"a"}) + public class A {} + """, + """ + import org.example.FooDefaultStringArray; + + @FooDefaultStringArray({"a", "b", "c"}) + public class A {} """ - ), + ) + ); + } + + @Test + void appendMultipleValuesToExistingArrayAttributeWithOverlap() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray","strArr", "b,c", null, false, true)), java( """ - import org.example.Foo; + import org.example.FooDefaultStringArray; - @Foo(array = {"a", "b"}) - public class A { - } + @FooDefaultStringArray(strArr = {"a", "b"}) + public class A {} """, """ - import org.example.Foo; + import org.example.FooDefaultStringArray; - @Foo(array = {"a", "b", "c"}) - public class A { - } + @FooDefaultStringArray(strArr = {"a", "b", "c"}) + public class A {} """ ) ); } @Test - void appendMultipleValuesToExistingArrayValueAttributeNonSet() { + void appendMultipleValuesToExistingArrayValueAttributeWithOverlap() { rewriteRun( spec -> spec.recipe(new AddOrUpdateAnnotationAttribute( - "org.example.Foo", + "org.example.FooDefaultStringArray", null, "b,c", null, false, - true)), + true + )), java( """ - package org.example; - public @interface Foo { - String[] value() default {}; - } + import org.example.FooDefaultStringArray; - public class A { - } + @FooDefaultStringArray({"a", "b"}) + public class A {} + """, + """ + import org.example.FooDefaultStringArray; + + @FooDefaultStringArray({"a", "b", "c"}) + public class A {} """ - ), + ) + ); + } + + // literal array attrType, explicit non-default attrName, string literal array attrVal, null oldAttrVal, false addOnly (does not matter) + @Test + void appendMultipleValuesToExistingArrayAttributeNonSet() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", "strArr", "b,c", null, false, true)), java( """ - import org.example.Foo; + import org.example.FooDefaultStringArray; - @Foo({"a", "b"}) - public class A { - } + @FooDefaultStringArray(strArr = {"a", "b"}) + public class A {} """, """ - import org.example.Foo; + import org.example.FooDefaultStringArray; - @Foo({"a", "b", "c"}) - public class A { - } + @FooDefaultStringArray(strArr = {"a", "b", "c"}) + public class A {} """ ) ); } + // literal array attrType, implicit "value" attrName, string literal array attrVal, null oldAttrVal, false addOnly (does not matter), true appendArray @Test - void updateConstantWithValue() { + void appendMultipleValuesToExistingArrayValueAttributeNonSet() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", "value", "hello", null, false, null)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", null, "b,c", null, false, true)), java( """ - package org.example; + import org.example.FooDefaultStringArray; - public class Const { - public class A { - public class B { - public static final String HI = "hi"; - } - } - } - """ - ), - java( + @FooDefaultStringArray({"a", "b"}) + public class A {} + """, """ - package org.example; - public @interface Foo { - String value() default ""; - } + import org.example.FooDefaultStringArray; + + @FooDefaultStringArray({"a", "b", "c"}) + public class A {} """ - ), + ) + ); + } + + // literal attrType, explicit "value" attrName, string literal attrVal, null oldAttrVal, false addOnly (does not matter) + @Test + void updateConstantWithValue() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", "value", "hello", null, false, null)), java( """ - import org.example.Foo; import org.example.Const; + import org.example.FooDefaultString; - @Foo(value = Const.A.B.HI) - public class A { - } + @FooDefaultString(value = Const.X.Y.FIRST_CONST) + public class A {} """, """ - import org.example.Foo; import org.example.Const; + import org.example.FooDefaultString; - @Foo("hello") - public class A { - } + @FooDefaultString("hello") + public class A {} """ ) ); @@ -2659,30 +3045,19 @@ class OnMatch { @Test void matchValue() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", null, "hello", "goodbye", null, null)), - java( - """ - package org.example; - public @interface Foo { - String value() default ""; - } - """, - SourceSpec::skip - ), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "hello", "goodbye", null, null)), java( """ - import org.example.Foo; + import org.example.FooDefaultString; - @Foo("goodbye") - public class A { - } + @FooDefaultString("goodbye") + public class A {} """, """ - import org.example.Foo; + import org.example.FooDefaultString; - @Foo("hello") - public class A { - } + @FooDefaultString("hello") + public class A {} """ ) ); @@ -2691,44 +3066,21 @@ public class A { @Test void matchConstant() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", null, "hi", "Const.A.B.HI", false, null)), - java( - """ - package org.example; - - public class Const { - public class A { - public class B { - public static final String HI = "hi"; - } - } - } - """ - ), - java( - """ - package org.example; - public @interface Foo { - String value() default ""; - } - """ - ), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "a", "Const.X.Y.FIRST_CONST", false, null)), java( """ - import org.example.Foo; import org.example.Const; + import org.example.FooDefaultString; - @Foo(Const.A.B.HI) - public class A { - } + @FooDefaultString(Const.X.Y.FIRST_CONST) + public class A {} """, """ - import org.example.Foo; import org.example.Const; + import org.example.FooDefaultString; - @Foo("hi") - public class A { - } + @FooDefaultString("a") + public class A {} """ ) ); @@ -3055,31 +3407,19 @@ class AsValueAttribute { @Test void implicitWithNullAttributeName() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute( - "org.example.Foo", - null, - "b", - null, - false, - true)), - java( - FOO_ANNOTATION_WITH_STRING_ARRAY_VALUE, - SourceSpec::skip - ), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", null, "b", null, false, true)), java( """ - import org.example.Foo; + import org.example.FooDefaultStringArray; - @Foo({"a"}) - public class A { - } + @FooDefaultStringArray({"a"}) + public class A {} """, """ - import org.example.Foo; + import org.example.FooDefaultStringArray; - @Foo({"a", "b"}) - public class A { - } + @FooDefaultStringArray({"a", "b"}) + public class A {} """ ) ); @@ -3088,31 +3428,19 @@ public class A { @Test void implicitWithAttributeNameValue() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute( - "org.example.Foo", - null, - "b", - null, - false, - true)), - java( - FOO_ANNOTATION_WITH_STRING_ARRAY_VALUE, - SourceSpec::skip - ), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", null, "b", null, false, true)), java( """ - import org.example.Foo; + import org.example.FooDefaultStringArray; - @Foo(value = {"a"}) - public class A { - } + @FooDefaultStringArray({"a"}) + public class A {} """, """ - import org.example.Foo; + import org.example.FooDefaultStringArray; - @Foo({"a", "b"}) - public class A { - } + @FooDefaultStringArray({"a", "b"}) + public class A {} """ ) ); @@ -3121,31 +3449,19 @@ public class A { @Test void explicitWithNullAttributeName() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute( - "org.example.Foo", - null, - "b", - null, - false, - true)), - java( - FOO_ANNOTATION_WITH_STRING_ARRAY_VALUE, - SourceSpec::skip - ), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", null, "b", null, false, true)), java( """ - import org.example.Foo; + import org.example.FooDefaultStringArray; - @Foo(value = {"a"}) - public class A { - } + @FooDefaultStringArray(value = {"a"}) + public class A {} """, """ - import org.example.Foo; + import org.example.FooDefaultStringArray; - @Foo({"a", "b"}) - public class A { - } + @FooDefaultStringArray({"a", "b"}) + public class A {} """ ) ); @@ -3154,31 +3470,19 @@ public class A { @Test void explicitWithAttributeNameValue() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute( - "org.example.Foo", - "value", - "b", - null, - false, - true)), - java( - FOO_ANNOTATION_WITH_STRING_ARRAY_VALUE, - SourceSpec::skip - ), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", "value", "b", null, false, true)), java( """ - import org.example.Foo; + import org.example.FooDefaultStringArray; - @Foo(value = {"a"}) - public class A { - } + @FooDefaultStringArray(value = {"a"}) + public class A {} """, """ - import org.example.Foo; + import org.example.FooDefaultStringArray; - @Foo({"a", "b"}) - public class A { - } + @FooDefaultStringArray({"a", "b"}) + public class A {} """ ) ); @@ -3189,16 +3493,7 @@ public class A { @Test void fieldAccessArgumentDefaultAttribute() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute( - "org.example.Foo", null, "hello", null, false, false)), - java( - """ - package org.example; - public @interface Foo { - String[] value() default {}; - } - """ - ), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", null, "hello", null, false, false)), java( """ package org.example; @@ -3210,19 +3505,17 @@ public interface Bar { java( """ import org.example.Bar; - import org.example.Foo; + import org.example.FooDefaultStringArray; - @Foo({Bar.BAR}) - public class A { - } + @FooDefaultStringArray({Bar.BAR}) + public class A {} """, """ import org.example.Bar; - import org.example.Foo; + import org.example.FooDefaultStringArray; - @Foo({"hello"}) - public class A { - } + @FooDefaultStringArray({"hello"}) + public class A {} """ ) ); @@ -3256,16 +3549,14 @@ public interface Bar { import org.example.Foo; @Foo(foo = {Bar.BAR}) - public class A { - } + public class A {} """, """ import org.example.Bar; import org.example.Foo; @Foo(foo = {"hello"}) - public class A { - } + public class A {} """ ) ); @@ -3274,28 +3565,12 @@ public class A { @Test void doNotMisMatchWhenUsingFieldReferenceOnNamedAttribute() { rewriteRun( - spec -> spec.recipe( - new AddOrUpdateAnnotationAttribute( - "org.example.Foo", - "name", - "newValue", - "oldValue", - null, - null)), - java( - """ - package org.example; - public @interface Foo { - String name() default ""; - } - """, - SourceSpec::skip - ), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", "str", "newValue", "oldValue", null, null)), java( """ import org.example.Foo; - @Foo(name = A.OTHER_VALUE) + @FooDefaultString(str = A.OTHER_VALUE) public class A { public static final String OTHER_VALUE = "otherValue"; } 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 b6730e85ef..b972d26274 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/AddOrUpdateAnnotationAttribute.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/AddOrUpdateAnnotationAttribute.java @@ -175,8 +175,12 @@ public J.Annotation visitAnnotation(J.Annotation original, ExecutionContext ctx) } if (exp instanceof J.NewArray) { List initializerList = requireNonNull(((J.NewArray) exp).getInitializer()); + List updatedList = updateInitializer(annotation, initializerList, getAttributeValues()); + if (updatedList.isEmpty()) { + return null; + } return as.withAssignment(((J.NewArray) exp) - .withInitializer(updateInitializer(annotation, initializerList, getAttributeValues()))); + .withInitializer(updatedList)); } if (exp instanceof J.Literal) { if (!valueMatches(exp, oldAttributeValue) || newAttributeValue.equals(((J.Literal) exp).getValueSource())) { @@ -187,7 +191,14 @@ public J.Annotation visitAnnotation(J.Annotation original, ExecutionContext ctx) Expression flattenedList = createAnnotationLiteralFromString( annotation, wrapValues(updatedList.stream() - .map(e -> ((J.Literal) e).getValueSource()) + .map(e -> { + if (e instanceof J.Literal) { + return ((J.Literal) e).getValueSource(); + } else if (e instanceof J.FieldAccess) { + return getUsefulNameFromFieldAccess(((J.FieldAccess) e)); + } + return ""; + }) .collect(toList()), true) ); return as.withAssignment(flattenedList); @@ -201,6 +212,22 @@ public J.Annotation visitAnnotation(J.Annotation original, ExecutionContext ctx) if (isFullyQualifiedClass() && getFullyQualifiedClass(newAttributeValue).equals(exp.toString())) { return as; } + if (TRUE.equals(appendArray) && attributeIsArray(annotation)) { + List updatedList = updateInitializer(annotation, singletonList(exp), getAttributeValues()); + return as.withAssignment(createAnnotationLiteralFromString( + annotation, + wrapValues(updatedList.stream() + .map(e -> { + if (e instanceof J.Literal) { + return ((J.Literal) e).getValueSource(); + } else if (e instanceof J.FieldAccess) { + return getUsefulNameFromFieldAccess(((J.FieldAccess) e)); + } + return ""; + }) + .collect(toList()), true) + )); + } //noinspection ConstantConditions return JavaTemplate.apply("#{} = #{}", getCursor(), as.getCoordinates().replace(), var_.getSimpleName(), newAttributeValue) .getArguments().get(annotation.getArguments().indexOf(as)); @@ -220,6 +247,22 @@ public J.Annotation visitAnnotation(J.Annotation original, ExecutionContext ctx) if (!valueMatches(literal, oldAttributeValue) || newAttributeValue.equals(literal.getValueSource())) { return literal; } + if (TRUE.equals(appendArray) && attributeIsArray(annotation)) { + List updatedList = updateInitializer(annotation, singletonList(literal), getAttributeValues()); + return createAnnotationLiteralFromString( + annotation, + wrapValues(updatedList.stream() + .map(e -> { + if (e instanceof J.Literal) { + return ((J.Literal) e).getValueSource(); + } else if (e instanceof J.FieldAccess) { + return getUsefulNameFromFieldAccess(((J.FieldAccess) e)); + } + return ""; + }) + .collect(toList()), true) + ); + } return createAnnotationLiteral(annotation, newAttributeValue); } if (oldAttributeValue == null && newAttributeValue != null) { @@ -245,6 +288,23 @@ public J.Annotation visitAnnotation(J.Annotation original, ExecutionContext ctx) if (!valueMatches(fieldAccess, oldAttributeValue) || newAttributeValue.equals(fieldAccess.toString())) { return fieldAccess; } + if (TRUE.equals(appendArray) && attributeIsArray(annotation)) { + List updatedList = updateInitializer(annotation, singletonList(fieldAccess), getAttributeValues()); + Expression flattenedList = createAnnotationLiteralFromString( + annotation, + wrapValues(updatedList.stream() + .map(e -> { + if (e instanceof J.Literal) { + return ((J.Literal) e).getValueSource(); + } else if (e instanceof J.FieldAccess) { + return getUsefulNameFromFieldAccess(((J.FieldAccess) e)); + } + return ""; + }) + .collect(toList()), true) + ); + return flattenedList; + } String attrVal = newAttributeValue.contains(",") && attributeIsArray(annotation) ? getAttributeValues().stream().map(String::valueOf).collect(joining(",", "{", "}")) : newAttributeValue; @@ -268,7 +328,11 @@ public J.Annotation visitAnnotation(J.Annotation original, ExecutionContext ctx) if (attributeName != null && !"value".equals(attributeValue)) { return isAnnotationWithOnlyValueMethod(annotation) ? arrayValue : createAnnotationAssignment(annotation, "value", arrayValue); } - return arrayValue.withInitializer(updateInitializer(annotation, requireNonNull(arrayValue.getInitializer()), getAttributeValues())); + List updatedList = updateInitializer(annotation, requireNonNull(arrayValue.getInitializer()), getAttributeValues()); + if (updatedList.isEmpty()) { + return null; + } + return arrayValue.withInitializer(updatedList); } private Expression createAnnotationLiteral(J.Annotation annotation, String newAttributeValue) { @@ -309,20 +373,13 @@ private List updateInitializer(J.Annotation annotation, List { - if (it instanceof J.Literal && valueMatches(it, oldAttributeValue)) { - List newItemsList = new ArrayList<>(); + List newItemsList = new ArrayList<>(); + if ((it instanceof J.Literal || it instanceof J.FieldAccess) && valueMatches(it, oldAttributeValue)) { for (String attribute : attributeList) { J.Literal newLiteral = new J.Literal(randomId(), SINGLE_SPACE, EMPTY, attribute, maybeQuoteStringArgument(annotation, attribute), null, JavaType.Primitive.String); newItemsList.add(newLiteral); } return newItemsList; - } else if (it instanceof J.FieldAccess && valueMatches(it, oldAttributeValue)) { - List newItemsList = new ArrayList<>(); - for (String attribute : attributeList) { - J.FieldAccess newFieldAccess = TypeTree.build(attribute); - newItemsList.add(newFieldAccess); - } - return newItemsList; } else if (it instanceof J.Empty) { return new ArrayList<>(); } @@ -385,6 +442,13 @@ private static boolean isAnnotationWithOnlyValueMethod(J.Annotation annotation) return getMethods(annotation).size() == 1 && "value".equals(getMethods(annotation).get(0).getName()); } + private static String getUsefulNameFromFieldAccess(J.FieldAccess fa) { + if (!(fa.getTarget() instanceof J.Identifier)) { + return fa.toString(); + } + return ((J.Identifier) fa.getTarget()).getSimpleName() + "." + fa.getSimpleName(); + } + private static boolean valueMatches(@Nullable Expression expression, @Nullable String oldAttributeValue) { if (expression == null) { return oldAttributeValue == null; @@ -394,10 +458,7 @@ private static boolean valueMatches(@Nullable Expression expression, @Nullable S return oldAttributeValue.equals(((J.Literal) expression).getValue()); } else if (expression instanceof J.FieldAccess) { J.FieldAccess fa = (J.FieldAccess) expression; - if (!(fa.getTarget() instanceof J.Identifier)) { - return oldAttributeValue.equals(fa.toString()); - } - String currentValue = ((J.Identifier) fa.getTarget()).getSimpleName() + "." + fa.getSimpleName(); + String currentValue = getUsefulNameFromFieldAccess(fa); return oldAttributeValue.equals(currentValue); } else if (expression instanceof J.Identifier) { // class names, static variables, ... if (oldAttributeValue.endsWith(".class")) { From 4509b2049f22bd665b41dfaaabda04b1e0c1559c Mon Sep 17 00:00:00 2001 From: Steve Elliott Date: Thu, 21 Aug 2025 09:54:32 -0400 Subject: [PATCH 5/8] wip --- .../AddOrUpdateAnnotationAttributeTest.java | 262 +++++++++++++++--- 1 file changed, 216 insertions(+), 46 deletions(-) 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 bf637bb339..573aa7cab4 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 @@ -53,7 +53,11 @@ public class Y { //language=java """ package org.example; - public @interface FooDefaultBase { + import java.lang.annotation.Repeatable; + @interface FdsWrapper { FooDefaultString[] value(); } + @Repeatable(FdsWrapper.class) + public @interface FooDefaultString { + String value() default ""; String str() default ""; String[] strArr() default {}; Class cla() default Number.class; @@ -67,24 +71,23 @@ public class Y { } """, //language=java - """ - package org.example; - import java.lang.annotation.Repeatable; - @interface FdsWrapper { FooDefaultString[] value(); } - @Repeatable(FdsWrapper.class) - public @interface FooDefaultString extends FooDefaultBase { - String value() default ""; - // TODO: need to figure out why it can't see interface inherited methods - } - """, - //language=java """ package org.example; import java.lang.annotation.Repeatable; @interface FdsaWrapper { FooDefaultStringArray[] value(); } @Repeatable(FdsaWrapper.class) - public @interface FooDefaultStringArray extends FooDefaultBase { + public @interface FooDefaultStringArray { String[] value() default {}; + String str() default ""; + String[] strArr() default {}; + Class cla() default Number.class; + Class[] claArr() default {}; + FooEnum enu() default FooEnum.JUNK; + FooEnum[] enuArr() default {}; + long lon() default 0L; + long[] lonArr() default {}; + boolean boo() default false; + boolean[] booArr() default {}; } """, //language=java @@ -93,8 +96,18 @@ public class Y { import java.lang.annotation.Repeatable; @interface FdcWrapper { FooDefaultClass[] value(); } @Repeatable(FdcWrapper.class) - public @interface FooDefaultClass extends FooDefaultBase { + public @interface FooDefaultClass { Class value() default Number.class; + String str() default ""; + String[] strArr() default {}; + Class cla() default Number.class; + Class[] claArr() default {}; + FooEnum enu() default FooEnum.JUNK; + FooEnum[] enuArr() default {}; + long lon() default 0L; + long[] lonArr() default {}; + boolean boo() default false; + boolean[] booArr() default {}; } """, //language=java @@ -103,8 +116,18 @@ public class Y { import java.lang.annotation.Repeatable; @interface FdcaWrapper { FooDefaultClassArray[] value(); } @Repeatable(FdcaWrapper.class) - public @interface FooDefaultClassArray extends FooDefaultBase { + public @interface FooDefaultClassArray { Class[] value() default {}; + String str() default ""; + String[] strArr() default {}; + Class cla() default Number.class; + Class[] claArr() default {}; + FooEnum enu() default FooEnum.JUNK; + FooEnum[] enuArr() default {}; + long lon() default 0L; + long[] lonArr() default {}; + boolean boo() default false; + boolean[] booArr() default {}; } """, //language=java @@ -113,8 +136,18 @@ public class Y { import java.lang.annotation.Repeatable; @interface FdeWrapper { FooDefaultEnum[] value(); } @Repeatable(FdeWrapper.class) - public @interface FooDefaultEnum extends FooDefaultBase { + public @interface FooDefaultEnum { FooEnum value() default FooEnum.JUNK; + String str() default ""; + String[] strArr() default {}; + Class cla() default Number.class; + Class[] claArr() default {}; + FooEnum enu() default FooEnum.JUNK; + FooEnum[] enuArr() default {}; + long lon() default 0L; + long[] lonArr() default {}; + boolean boo() default false; + boolean[] booArr() default {}; } """, //language=java @@ -123,8 +156,18 @@ public class Y { import java.lang.annotation.Repeatable; @interface FdeaWrapper { FooDefaultEnumArray[] value(); } @Repeatable(FdeaWrapper.class) - public @interface FooDefaultEnumArray extends FooDefaultBase { + public @interface FooDefaultEnumArray { FooEnum[] value() default {}; + String str() default ""; + String[] strArr() default {}; + Class cla() default Number.class; + Class[] claArr() default {}; + FooEnum enu() default FooEnum.JUNK; + FooEnum[] enuArr() default {}; + long lon() default 0L; + long[] lonArr() default {}; + boolean boo() default false; + boolean[] booArr() default {}; } """, //language=java @@ -133,8 +176,18 @@ public class Y { import java.lang.annotation.Repeatable; @interface FdlWrapper { FooDefaultLong[] value(); } @Repeatable(FdlWrapper.class) - public @interface FooDefaultLong extends FooDefaultBase { + public @interface FooDefaultLong { long value() default 0L; + String str() default ""; + String[] strArr() default {}; + Class cla() default Number.class; + Class[] claArr() default {}; + FooEnum enu() default FooEnum.JUNK; + FooEnum[] enuArr() default {}; + long lon() default 0L; + long[] lonArr() default {}; + boolean boo() default false; + boolean[] booArr() default {}; } """, //language=java @@ -143,8 +196,18 @@ public class Y { import java.lang.annotation.Repeatable; @interface FdlaWrapper { FooDefaultLongArray[] value(); } @Repeatable(FdlaWrapper.class) - public @interface FooDefaultLongArray extends FooDefaultBase { + public @interface FooDefaultLongArray { long[] value() default {}; + String str() default ""; + String[] strArr() default {}; + Class cla() default Number.class; + Class[] claArr() default {}; + FooEnum enu() default FooEnum.JUNK; + FooEnum[] enuArr() default {}; + long lon() default 0L; + long[] lonArr() default {}; + boolean boo() default false; + boolean[] booArr() default {}; } """, //language=java @@ -153,8 +216,18 @@ public class Y { import java.lang.annotation.Repeatable; @interface FdbWrapper { FooDefaultBoolean[] value(); } @Repeatable(FdbWrapper.class) - public @interface FooDefaultBoolean extends FooDefaultBase { + public @interface FooDefaultBoolean { boolean value() default false; + String str() default ""; + String[] strArr() default {}; + Class cla() default Number.class; + Class[] claArr() default {}; + FooEnum enu() default FooEnum.JUNK; + FooEnum[] enuArr() default {}; + long lon() default 0L; + long[] lonArr() default {}; + boolean boo() default false; + boolean[] booArr() default {}; } """, //language=java @@ -163,8 +236,18 @@ public class Y { import java.lang.annotation.Repeatable; @interface FdbaWrapper { FooDefaultBooleanArray[] value(); } @Repeatable(FdbaWrapper.class) - public @interface FooDefaultBooleanArray extends FooDefaultBase { + public @interface FooDefaultBooleanArray { boolean[] value() default {}; + String str() default ""; + String[] strArr() default {}; + Class cla() default Number.class; + Class[] claArr() default {}; + FooEnum enu() default FooEnum.JUNK; + FooEnum[] enuArr() default {}; + long lon() default 0L; + long[] lonArr() default {}; + boolean boo() default false; + boolean[] booArr() default {}; } """ )); @@ -1546,6 +1629,11 @@ void literalArrayAttribute_existing_usingNullOldAttributeValue_andAppendArray_ap @FooDefaultStringArray({Const.X.Y.SECOND_CONST}) @FooDefaultStringArray(value = {"c"}) @FooDefaultStringArray(value = {Const.X.Y.THIRD_CONST}) + // below already contain the value to append + @FooDefaultStringArray("a") + @FooDefaultStringArray(value = "a") + @FooDefaultStringArray({Const.X.Y.SECOND_CONST, "a", Const.X.Y.THIRD_CONST}) + @FooDefaultStringArray(value = {Const.X.Y.SECOND_CONST, "a", Const.X.Y.THIRD_CONST}) public class A {} """, """ @@ -1559,6 +1647,11 @@ public class A {} @FooDefaultStringArray({Const.X.Y.SECOND_CONST, "a"}) @FooDefaultStringArray({"c", "a"}) @FooDefaultStringArray({Const.X.Y.THIRD_CONST, "a"}) + // below already contain the value to append + @FooDefaultStringArray("a") + @FooDefaultStringArray("a") + @FooDefaultStringArray({Const.X.Y.SECOND_CONST, "a", Const.X.Y.THIRD_CONST}) + @FooDefaultStringArray({Const.X.Y.SECOND_CONST, "a", Const.X.Y.THIRD_CONST}) public class A {} """ ), @@ -1571,6 +1664,10 @@ public class A {} @FooDefaultStringArray(value = Const.X.Y.SECOND_CONST, strArr = {Const.X.Y.SECOND_CONST}) @FooDefaultStringArray(value = {"c"}, strArr = {"c"}) @FooDefaultStringArray(value = {Const.X.Y.THIRD_CONST}, strArr = {Const.X.Y.THIRD_CONST}) + // below already contain the value to append + @FooDefaultStringArray(value = "a", strArr = {"b"}) + @FooDefaultStringArray(value = {"a"}, strArr = {"c"}) + @FooDefaultStringArray(value = {Const.X.Y.SECOND_CONST, "a", Const.X.Y.THIRD_CONST}, strArr = {"d"}) public class B {} """, """ @@ -1580,12 +1677,17 @@ public class B {} @FooDefaultStringArray(value = {Const.X.Y.SECOND_CONST, "a"}, strArr = {Const.X.Y.SECOND_CONST}) @FooDefaultStringArray(value = {"c", "a"}, strArr = {"c"}) @FooDefaultStringArray(value = {Const.X.Y.THIRD_CONST, "a"}, strArr = {Const.X.Y.THIRD_CONST}) + // below already contain the value to append + @FooDefaultStringArray(value = "a", strArr = {"b"}) + @FooDefaultStringArray(value = {"a"}, strArr = {"c"}) + @FooDefaultStringArray(value = {Const.X.Y.SECOND_CONST, "a", Const.X.Y.THIRD_CONST}, strArr = {"d"}) public class B {} """ ) ); } + // TODO: revisit @Test void literalArrayAttribute_existing_asConst_usingProvidedOldAttributeValue_ofConstRef_updatesSafelyOnlyMatched() { rewriteRun( @@ -1624,24 +1726,102 @@ public class A {} @FooDefaultStringArray({Const.X.Y.SECOND_CONST, "b", Const.X.Y.THIRD_CONST}) public class A {} """ - )/*, + ), //language=java java( """ import org.example.Const; - import org.example.FooDefaultString; - @FooDefaultString(value = Const.X.Y.FIRST_CONST, a = Const.X.Y.FIRST_CONST) - @FooDefaultString(value = Const.X.Y.SECOND_CONST, a = Const.X.Y.FIRST_CONST) + import org.example.FooDefaultStringArray; + @FooDefaultStringArray(value = Const.X.Y.FIRST_CONST, strArr = {Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray(value = Const.X.Y.SECOND_CONST, strArr = {Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray(value = {}, strArr = {Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray(value = {Const.X.Y.FIRST_CONST}, strArr = {Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray(value = {Const.X.Y.SECOND_CONST, Const.X.Y.FIRST_CONST, Const.X.Y.THIRD_CONST}, strArr = {Const.X.Y.FIRST_CONST}) public class B {} """, """ import org.example.Const; - import org.example.FooDefaultString; - @FooDefaultString(value = "b", a = Const.X.Y.FIRST_CONST) - @FooDefaultString(value = Const.X.Y.SECOND_CONST, a = Const.X.Y.FIRST_CONST) + import org.example.FooDefaultStringArray; + @FooDefaultStringArray(value = "b", strArr = {Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray(value = Const.X.Y.SECOND_CONST, strArr = {Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray(strArr = {Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray(value = {"b"}, strArr = {Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray(value = {Const.X.Y.SECOND_CONST, "b", Const.X.Y.THIRD_CONST}, strArr = {Const.X.Y.FIRST_CONST}) public class B {} """ - )*/ + ) + ); + } + + // TODO: revisit + @Test + void literalArrayAttribute_existing_asConst_usingProvidedOldAttributeValue_ofConstRef_andAppendArray_appendsSafelyOnlyForMatched() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", null, "b", "Const.X.Y.FIRST_CONST", null, true)), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultStringArray; + @FooDefaultStringArray(Const.X.Y.FIRST_CONST) + @FooDefaultStringArray(Const.X.Y.SECOND_CONST) + @FooDefaultStringArray(value = Const.X.Y.FIRST_CONST) + @FooDefaultStringArray(value = Const.X.Y.SECOND_CONST) + @FooDefaultStringArray({}) + // TODO: Try to make below consistent + @FooDefaultStringArray({Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray({Const.X.Y.SECOND_CONST, Const.X.Y.FIRST_CONST, Const.X.Y.THIRD_CONST}) + @FooDefaultStringArray(value = {}) + @FooDefaultStringArray(value = {Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray(value = {Const.X.Y.SECOND_CONST, Const.X.Y.FIRST_CONST, Const.X.Y.THIRD_CONST}) + // below already contain the value to append + @FooDefaultStringArray({Const.X.Y.FIRST_CONST, "b", Const.X.Y.SECOND_CONST}) + @FooDefaultStringArray(value = {Const.X.Y.FIRST_CONST, "b", Const.X.Y.SECOND_CONST}) + public class A {} + """, + """ + import org.example.Const; + import org.example.FooDefaultStringArray; + @FooDefaultStringArray(Const.X.Y.FIRST_CONST, "b") + @FooDefaultStringArray(Const.X.Y.SECOND_CONST) + @FooDefaultStringArray({Const.X.Y.FIRST_CONST, "b"}) + @FooDefaultStringArray(Const.X.Y.SECOND_CONST) + @FooDefaultStringArray + // TODO: Try to make below consistent + @FooDefaultStringArray({Const.X.Y.FIRST_CONST, "b"}) + @FooDefaultStringArray({Const.X.Y.SECOND_CONST, Const.X.Y.FIRST_CONST, Const.X.Y.THIRD_CONST, "b"}) + @FooDefaultStringArray + @FooDefaultStringArray({Const.X.Y.FIRST_CONST, "b"}) + @FooDefaultStringArray({Const.X.Y.SECOND_CONST, Const.X.Y.FIRST_CONST, Const.X.Y.THIRD_CONST, "b"}) + // below already contain the value to append + @FooDefaultStringArray({Const.X.Y.FIRST_CONST, "b", Const.X.Y.SECOND_CONST}) + @FooDefaultStringArray({Const.X.Y.FIRST_CONST, "b", Const.X.Y.SECOND_CONST}) + public class A {} + """ + ), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultStringArray; + @FooDefaultStringArray(value = Const.X.Y.FIRST_CONST, strArr = {Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray(value = Const.X.Y.SECOND_CONST, strArr = {Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray(value = {}, strArr = {Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray(value = {Const.X.Y.FIRST_CONST}, strArr = {Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray(value = {Const.X.Y.SECOND_CONST, Const.X.Y.FIRST_CONST, Const.X.Y.THIRD_CONST}, strArr = {Const.X.Y.FIRST_CONST}) + public class B {} + """, + """ + import org.example.Const; + import org.example.FooDefaultStringArray; + @FooDefaultStringArray(value = "b", strArr = {Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray(value = Const.X.Y.SECOND_CONST, strArr = {Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray(strArr = {Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray(value = {"b"}, strArr = {Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray(value = {Const.X.Y.SECOND_CONST, "b", Const.X.Y.THIRD_CONST}, strArr = {Const.X.Y.FIRST_CONST}) + public class B {} + """ + ) ); } } @@ -2787,13 +2967,13 @@ void appendMultipleValuesToExistingArrayAttribute() { """ import org.example.FooDefaultStringArray; - @FooDefaultStringArray(b = {"a"}) + @FooDefaultStringArray(strArr = {"a"}) public class A {} """, """ import org.example.FooDefaultStringArray; - @FooDefaultStringArray(b = {"a", "b", "c"}) + @FooDefaultStringArray(strArr = {"a", "b", "c"}) public class A {} """ ) @@ -3370,23 +3550,13 @@ public class A { @Test void nomatchClass() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", null, "Integer.class", "Double.class", null, null)), - java( - """ - package org.example; - public @interface Foo { - Class value(); - } - """, - SourceSpec::skip - ), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultClass", null, "Integer.class", "Double.class", null, null)), java( """ - import org.example.Foo; + import org.example.FooDefaultClass; - @Foo(Long.class) - public class A { - } + @FooDefaultClass(Long.class) + public class A {} """ ) ); @@ -3568,7 +3738,7 @@ void doNotMisMatchWhenUsingFieldReferenceOnNamedAttribute() { spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", "str", "newValue", "oldValue", null, null)), java( """ - import org.example.Foo; + import org.example.FooDefaultString; @FooDefaultString(str = A.OTHER_VALUE) public class A { From 438146dc0ef4e4d293d19c93b209f2e403916756 Mon Sep 17 00:00:00 2001 From: Steve Elliott Date: Tue, 26 Aug 2025 17:34:42 -0400 Subject: [PATCH 6/8] WiP --- .../AddOrUpdateAnnotationAttributeTest.java | 2 +- .../java/AddOrUpdateAnnotationAttribute.java | 242 +++++++++++++++++- 2 files changed, 242 insertions(+), 2 deletions(-) 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 573aa7cab4..6cdeb1d05b 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 @@ -1782,7 +1782,7 @@ public class A {} """ import org.example.Const; import org.example.FooDefaultStringArray; - @FooDefaultStringArray(Const.X.Y.FIRST_CONST, "b") + @FooDefaultStringArray({Const.X.Y.FIRST_CONST, "b"}) @FooDefaultStringArray(Const.X.Y.SECOND_CONST) @FooDefaultStringArray({Const.X.Y.FIRST_CONST, "b"}) @FooDefaultStringArray(Const.X.Y.SECOND_CONST) 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 b972d26274..93163f8856 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/AddOrUpdateAnnotationAttribute.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/AddOrUpdateAnnotationAttribute.java @@ -92,6 +92,220 @@ public String getDescription() { @Override public TreeVisitor getVisitor() { + TreeVisitor v = Preconditions.check(new UsesType<>(annotationType, false), new JavaIsoVisitor() { + private String attributeNameOrDefault() { + return attributeName == null ? "value" : attributeName; + } + + private List getMethods(J.Annotation annotation) { + return ((JavaType.FullyQualified) requireNonNull(annotation.getAnnotationType().getType())).getMethods(); + } + + private Optional findMethod(J.Annotation annotation, String methodName) { + for (JavaType.Method it : getMethods(annotation)) { + if (methodName.equals(it.getName())) { + return Optional.of(it); + } + } + return Optional.empty(); + } + + private String getUsefulNameFromFieldAccess(J.FieldAccess fa) { + if (!(fa.getTarget() instanceof J.Identifier)) { + return fa.toString(); + } + return ((J.Identifier) fa.getTarget()).getSimpleName() + "." + fa.getSimpleName(); + } + + private void addPossibleClassImports(@Nullable String value) { + if (value == null) { + return; + } + for (String singleVal : value.split(",")) { + if (singleVal.endsWith(".class") && StringUtils.countOccurrences(singleVal, ".") > 1) { + maybeAddImport(singleVal.substring(0, singleVal.length() - 6)); + } + } + } + + private boolean attributeMatchesName(Expression e, String name) { + if (e instanceof J.Assignment) { + J.Assignment as = (J.Assignment) e; + if (as.getVariable() instanceof J.Identifier) { + return ((J.Identifier) as.getVariable()).getSimpleName().equals(name); + } + } + return name.equals("value"); + } + + private boolean alreadyContainsAttributeOfName(J.Annotation annotation, String name) { + List existingArguments = annotation.getArguments(); + if (existingArguments == null) { + return false; + } + for (Expression e : annotation.getArguments()) { + if (attributeMatchesName(e, name)) { + return true; + } + } + return false; + } + + private boolean valueMatches(Expression expression, String oldAttributeValue) { + if (expression instanceof J.Literal) { + return oldAttributeValue.equals(((J.Literal) expression).getValue()); + } else if (expression instanceof J.FieldAccess) { + J.FieldAccess fa = (J.FieldAccess) expression; + String currentValue = getUsefulNameFromFieldAccess(fa); + return oldAttributeValue.equals(currentValue); + } else if (expression instanceof J.Identifier) { // class names, static variables, ... + if (oldAttributeValue.endsWith(".class")) { + String className = TypeUtils.toString(requireNonNull(expression.getType())) + ".class"; + return className.endsWith(oldAttributeValue); + } + return oldAttributeValue.equals(((J.Identifier) expression).getSimpleName()); + } + throw new IllegalArgumentException("Unexpected expression type: " + expression.getClass()); + } + + private J.Empty newEmpty() { + return new J.Empty(randomId(), SINGLE_SPACE, EMPTY); + } + + private List updateInitializerDroppingMatched(@Nullable List initializer, String searchValue) { + List updatedInitializer = ListUtils.filter(ListUtils.map(initializer, e -> { + if (valueMatches(e, searchValue)) { + return newEmpty(); + } + return e; + }), e -> !(e instanceof J.Empty)); + return updatedInitializer == null ? emptyList() : updatedInitializer; + } + + private List updateInitializerChangingMatched(@Nullable List initializer, String searchValue, String newValue) { + List updatedInitializer = ListUtils.map(initializer, e -> { + if (valueMatches(e, searchValue)) { + // TODO - Change from this to specific setup based on newValue and appendArray + return e; + } + return e; + }); + return updatedInitializer == null ? emptyList() : updatedInitializer; + } + + // attributeValue == null + private J.Annotation tryRemoveAnnotationAttribute(J.Annotation annotation, String searchAttribute, @Nullable String searchValue) { + List updatedArgs = ListUtils.map(annotation.getArguments(), it -> { + if (attributeMatchesName(it, searchAttribute)) { + if (searchValue == null) { + return newEmpty(); + } + if (it instanceof J.Assignment) { + J.Assignment as = (J.Assignment) it; + Expression asValue = as.getAssignment(); + if (asValue instanceof J.NewArray) { + J.NewArray asArray = (J.NewArray) asValue; + List updatedInitializer = updateInitializerDroppingMatched(asArray.getInitializer(), searchValue); + return as.withAssignment(asArray.withInitializer(updatedInitializer)); + } + if (valueMatches(asValue, searchValue)) { + return newEmpty(); + } + } else if (it instanceof J.NewArray) { + J.NewArray itArray = (J.NewArray) it; + List updatedInitializer = updateInitializerDroppingMatched(itArray.getInitializer(), searchValue); + return itArray.withInitializer(updatedInitializer); + } else if (valueMatches(it, searchValue)) { + return newEmpty(); + } + } + return it; + }); + return annotation.withArguments(ListUtils.filter(updatedArgs, it -> !(it instanceof J.Empty))); + } + + private J.Annotation tryAddAnnotationAttribute(J.Annotation annotation, String newAttribute, String newValue) { + // TODO + return annotation; + } + + private J.Annotation tryUpdateAnnotationAttribute(J.Annotation annotation, String searchAttribute, @Nullable String searchValue, String newValue) { + List updatedArgs = ListUtils.map(annotation.getArguments(), it -> { + if (attributeMatchesName(it, searchAttribute)) { + if (searchValue == null) { + if (it instanceof J.Assignment) { + J.Assignment as = (J.Assignment) it; + // TODO - overwriting using as.withAssignment(...), but differs by newValue typing and appendArray + } + // TODO - overwriting using new, but differs by newValue typing and appendArray + } else { + if (it instanceof J.Assignment) { + J.Assignment as = (J.Assignment) it; + Expression asValue = as.getAssignment(); + if (asValue instanceof J.NewArray) { + J.NewArray asArray = (J.NewArray) asValue; + List updatedInitializer = updateInitializerChangingMatched(asArray.getInitializer(), searchValue, newValue); + return as.withAssignment(asArray.withInitializer(updatedInitializer)); + } + if (valueMatches(asValue, searchValue)) { + // TODO instantiate the correct typing + } + } + // TODO: else + } + } + return it; + }); + return annotation.withArguments(updatedArgs); + } + + @Override + public J.Annotation visitAnnotation(J.Annotation original, ExecutionContext ctx) { + J.Annotation a = super.visitAnnotation(original, ctx); + String searchAttribute = attributeNameOrDefault(); + String searchValue = oldAttributeValue; + // if not the right type of annotation or cannot find the method for a non-shallow class + if ( + !TypeUtils.isOfClassType(a.getType(), annotationType) || + !(a.getType() instanceof JavaType.ShallowClass || findMethod(a, searchAttribute).isPresent()) + ) { + return a; + } + boolean existingAttribute = alreadyContainsAttributeOfName(a, searchAttribute); + // if only want to add, but it already has attribute, ignores new attributeValue + if (TRUE.equals(addOnly) && existingAttribute) { + return a; + } + + // if you want to remove + if (attributeValue == null) { + // if you can't update anything + if (!existingAttribute || TRUE.equals(addOnly)) { + return a; + } + a = tryRemoveAnnotationAttribute(a, searchAttribute, searchValue); + } else { + // if you can't update anything + if (existingAttribute && TRUE.equals(addOnly)) { + return a; + } + if (existingAttribute) { + a = tryUpdateAnnotationAttribute(a, searchAttribute, searchValue, attributeValue); + } else { + a = tryAddAnnotationAttribute(a, searchAttribute, attributeValue); + } + } + addPossibleClassImports(attributeValue); + + // TODO: double check this (and also simplification in general) + if (original != a) { + doAfterVisit(new SimplifySingleElementAnnotation().getVisitor()); + } + return maybeAutoFormat(original, a, ctx); + } + }); + + return Preconditions.check(new UsesType<>(annotationType, false), new JavaIsoVisitor() { @Override public J.Annotation visitAnnotation(J.Annotation original, ExecutionContext ctx) { @@ -370,6 +584,26 @@ private String attributeName() { } private List updateInitializer(J.Annotation annotation, List initializerList, List attributeList) { + if (TRUE.equals(appendArray)) { + if (oldAttributeValue != null) { + // if initializer contains old attribute value + // append new values (de-duped) to end of attribute's existing value + // else + // do not append + } else { + // append new values (de-duped) to end of attribute's existing value + } + } else { + if (oldAttributeValue != null) { + // if initializer contains old attribute value + // replace existing old attribute value in initializer with new values + // else + // do not replace + } else { + // replace initializer with new values + } + } + // If `oldAttributeValue` is defined, replace the old value with the new value(s). Ignore the `appendArray` option in this case. if (oldAttributeValue != null) { return ListUtils.flatMap(initializerList, it -> { @@ -379,10 +613,16 @@ private List updateInitializer(J.Annotation annotation, List(); } + if (TRUE.equals(appendArray)) { + newItemsList.add(0, it); + return newItemsList; + } return it; }); } From 00f95b04e02ee966bf4cf922c25a598b1c97ca3a Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Wed, 27 Aug 2025 11:27:26 +0200 Subject: [PATCH 7/8] Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../org/openrewrite/java/AddOrUpdateAnnotationAttribute.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 93163f8856..cc7d24363e 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/AddOrUpdateAnnotationAttribute.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/AddOrUpdateAnnotationAttribute.java @@ -135,7 +135,7 @@ private boolean attributeMatchesName(Expression e, String name) { return ((J.Identifier) as.getVariable()).getSimpleName().equals(name); } } - return name.equals("value"); + return "value".equals(name); } private boolean alreadyContainsAttributeOfName(J.Annotation annotation, String name) { @@ -782,7 +782,7 @@ private boolean attributeNameAlreadyPresent(J.Annotation a) { return true; } } - } else if (attributeName().equals("value")) { + } else if ("value".equals(attributeName())) { return true; } } From 3172f18010a9dddeb7e04b3511db8ac2de873b57 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Wed, 27 Aug 2025 11:41:06 +0200 Subject: [PATCH 8/8] Inline return --- .../org/openrewrite/java/AddOrUpdateAnnotationAttribute.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 cc7d24363e..4da44318aa 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/AddOrUpdateAnnotationAttribute.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/AddOrUpdateAnnotationAttribute.java @@ -504,7 +504,7 @@ public J.Annotation visitAnnotation(J.Annotation original, ExecutionContext ctx) } if (TRUE.equals(appendArray) && attributeIsArray(annotation)) { List updatedList = updateInitializer(annotation, singletonList(fieldAccess), getAttributeValues()); - Expression flattenedList = createAnnotationLiteralFromString( + return createAnnotationLiteralFromString( annotation, wrapValues(updatedList.stream() .map(e -> { @@ -517,7 +517,6 @@ public J.Annotation visitAnnotation(J.Annotation original, ExecutionContext ctx) }) .collect(toList()), true) ); - return flattenedList; } String attrVal = newAttributeValue.contains(",") && attributeIsArray(annotation) ? getAttributeValues().stream().map(String::valueOf).collect(joining(",", "{", "}")) :