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 784c10c111..4dbede2a88 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,1867 @@ 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 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; + 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; + 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 FdsaWrapper { FooDefaultStringArray[] value(); } + @Repeatable(FdsaWrapper.class) + 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 + """ + package org.example; + import java.lang.annotation.Repeatable; + @interface FdcWrapper { FooDefaultClass[] value(); } + @Repeatable(FdcWrapper.class) + 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 + """ + package org.example; + import java.lang.annotation.Repeatable; + @interface FdcaWrapper { FooDefaultClassArray[] value(); } + @Repeatable(FdcaWrapper.class) + 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 + """ + package org.example; + import java.lang.annotation.Repeatable; + @interface FdeWrapper { FooDefaultEnum[] value(); } + @Repeatable(FdeWrapper.class) + 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 + """ + package org.example; + import java.lang.annotation.Repeatable; + @interface FdeaWrapper { FooDefaultEnumArray[] value(); } + @Repeatable(FdeaWrapper.class) + 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 + """ + package org.example; + import java.lang.annotation.Repeatable; + @interface FdlWrapper { FooDefaultLong[] value(); } + @Repeatable(FdlWrapper.class) + 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 + """ + package org.example; + import java.lang.annotation.Repeatable; + @interface FdlaWrapper { FooDefaultLongArray[] value(); } + @Repeatable(FdlaWrapper.class) + 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 + """ + package org.example; + import java.lang.annotation.Repeatable; + @interface FdbWrapper { FooDefaultBoolean[] value(); } + @Repeatable(FdbWrapper.class) + 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 + """ + package org.example; + import java.lang.annotation.Repeatable; + @interface FdbaWrapper { FooDefaultBooleanArray[] value(); } + @Repeatable(FdbaWrapper.class) + 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 {}; + } + """ + )); + } + + @Nested + class UsingImplicitAttributeName { + @Nested + class UsingNullAttributeValue { + @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(str = "a") + 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.FIRST_CONST) + @FooDefaultString(value = "b") + @FooDefaultString(value = Const.X.Y.FIRST_CONST) + public class A {} + """ + ), + //language=java + java( + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString(value = "a", str = "z") + @FooDefaultString(value = Const.X.Y.FIRST_CONST, str = "z") + 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.Const; + import org.example.FooDefaultString; + @FooDefaultString("a") + @FooDefaultString(Const.X.Y.FIRST_CONST) + @FooDefaultString(value = "b") + @FooDefaultString(value = Const.X.Y.FIRST_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", str = "z1") + @FooDefaultString(value = Const.X.Y.FIRST_CONST, str = "z2") + public class B {} + """, + """ + import org.example.Const; + import org.example.FooDefaultString; + @FooDefaultString(str = "z1") + @FooDefaultString(str = "z2") + 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.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 + @FooDefaultString(Const.X.Y.SECOND_CONST) + @FooDefaultString + @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(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_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(value = "a", str = "a") + @FooDefaultString(value = "b", str = "a") + public class B {} + """, + """ + import org.example.FooDefaultString; + @FooDefaultString(str = "a") + @FooDefaultString(value = "b", str = "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(strArr = "a") + @FooDefaultStringArray(strArr = {"b"}) + 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", strArr = {"z"}) + @FooDefaultStringArray(value = {}, strArr = {"y"}) + @FooDefaultStringArray(value = {"d"}, strArr = {"x"}) + 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.Const; + import org.example.FooDefaultStringArray; + @FooDefaultStringArray("a") + @FooDefaultStringArray(Const.X.Y.FIRST_CONST) + @FooDefaultStringArray(value = "b") + @FooDefaultStringArray(Const.X.Y.FIRST_CONST) + @FooDefaultStringArray({}) + @FooDefaultStringArray({"c"}) + @FooDefaultStringArray({Const.X.Y.FIRST_CONST}) + @FooDefaultStringArray(value = {}) + @FooDefaultStringArray(value = {"d"}) + @FooDefaultStringArray(value = {Const.X.Y.FIRST_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", 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(strArr = {"z1"}) + @FooDefaultStringArray(strArr = {"z2"}) + @FooDefaultStringArray(strArr = {"y1"}) + @FooDefaultStringArray(strArr = {"y2"}) + @FooDefaultStringArray(strArr = {"y3"}) + 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.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({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.SECOND_CONST) + @FooDefaultStringArray + @FooDefaultStringArray(Const.X.Y.SECOND_CONST) + @FooDefaultStringArray + @FooDefaultStringArray({Const.X.Y.SECOND_CONST, Const.X.Y.THIRD_CONST}) + @FooDefaultStringArray + @FooDefaultStringArray({Const.X.Y.SECOND_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.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(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 {} + """ + ) + ); + } + + @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({}) + @FooDefaultStringArray({"a"}) + @FooDefaultStringArray({"b", "a", "c"}) + @FooDefaultStringArray(value = {}) + @FooDefaultStringArray(value = {"a"}) + @FooDefaultStringArray(value = {"b", "a", "c"}) + public class A {} + """, + """ + import org.example.FooDefaultStringArray; + @FooDefaultStringArray + @FooDefaultStringArray("b") + @FooDefaultStringArray + @FooDefaultStringArray("b") + @FooDefaultStringArray + @FooDefaultStringArray + @FooDefaultStringArray({"b", "c"}) + @FooDefaultStringArray + @FooDefaultStringArray + @FooDefaultStringArray({"b", "c"}) + public class A {} + """ + ), + //language=java + java( + """ + import org.example.FooDefaultStringArray; + @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(strArr = {"a"}) + @FooDefaultStringArray(value = "b", strArr = {"a"}) + @FooDefaultStringArray(strArr = {"a"}) + @FooDefaultStringArray(strArr = {"a"}) + @FooDefaultStringArray(value = {"b", "c"}, strArr = {"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(cla = Integer.class) + 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, cla = Long.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(value = Long.class, cla = Byte.class) + public class B {} + """, + """ + import org.example.FooDefaultClass; + @FooDefaultClass(cla = Byte.class) + 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, cla = Integer.class) + @FooDefaultClass(value = Long.class, cla = Integer.class) + public class B {} + """, + """ + import org.example.FooDefaultClass; + @FooDefaultClass(cla = Integer.class) + @FooDefaultClass(value = Long.class, cla = 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(claArr = Integer.class) + @FooDefaultClassArray(claArr = {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, claArr = {Byte.class}) + @FooDefaultClassArray(value = {}, claArr = {Integer.class}) + @FooDefaultClassArray(value = {Long.class}, claArr = {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, claArr = {Byte.class}) + @FooDefaultClassArray(value = {}, claArr = {Float.class}) + @FooDefaultClassArray(value = {Long.class}, claArr = {Integer.class}) + public class B {} + """, + """ + import org.example.FooDefaultClassArray; + @FooDefaultClassArray(claArr = {Byte.class}) + @FooDefaultClassArray(claArr = {Float.class}) + @FooDefaultClassArray(claArr = {Integer.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({}) + @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 {} + """, + """ + import org.example.FooDefaultClassArray; + @FooDefaultClassArray + @FooDefaultClassArray(Long.class) + @FooDefaultClassArray + @FooDefaultClassArray(Long.class) + @FooDefaultClassArray + @FooDefaultClassArray + @FooDefaultClassArray({Long.class, Short.class}) + @FooDefaultClassArray + @FooDefaultClassArray + @FooDefaultClassArray({Long.class, Short.class}) + public class A {} + """ + ), + //language=java + java( + """ + import org.example.FooDefaultClassArray; + @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(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 WithEnumTypeAttribute { + @Test + void enumAttribute_absent_doesNothing() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultEnum", null, null, null, null, null)), + //language=java + java( + """ + import org.example.FooDefaultEnum; + @FooDefaultEnum + @FooDefaultEnum() + public class A {} + """ + ), + //language=java + java( + """ + import org.example.FooDefaultEnum; + import org.example.FooEnum; + @FooDefaultEnum(enu = FooEnum.ONE) + public class B {} + """ + ) + ); + } + + @Test + void enumAttribute_existing_usingAddOnly_doesNothing() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultEnum", null, null, null, true, null)), + //language=java + java( + """ + 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 enumAttribute_existing_usingNullOldAttributeValue_removesSafely() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultEnum", null, null, null, null, null)), + //language=java + java( + """ + 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 enumAttribute_existing_usingProvidedOldAttributeValue_removesSafelyOnlyMatched() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultEnum", null, null, "FooEnum.ONE", null, null)), + //language=java + java( + """ + 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.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 {} + """ + ) + ); + } + + @Test + void enumArrayAttribute_existing_usingAddOnly_doesNothing() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultEnumArray", null, null, null, true, 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 {} + """ + ), + //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.FooDefaultEnumArray; + import org.example.FooEnum; + @FooDefaultEnumArray + @FooDefaultEnumArray + @FooDefaultEnumArray + @FooDefaultEnumArray + @FooDefaultEnumArray + @FooDefaultEnumArray + 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 {} + """, + """ + import org.example.FooDefaultEnumArray; + import org.example.FooEnum; + @FooDefaultEnumArray(enuArr = {FooEnum.ONE}) + @FooDefaultEnumArray(enuArr = {FooEnum.TWO}) + @FooDefaultEnumArray(enuArr = {FooEnum.THREE}) + public class B {} + """ + ) + ); + } + + @Test + void enumArrayAttribute_existing_usingProvidedOldAttributeValue_removesSafelyOnlyMatched() { + rewriteRun( + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultEnumArray", null, null, "FooEnum.ONE", null, null)), + //language=java + java( + """ + 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.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.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.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}) + // 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 {} + """, + """ + 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"}) + // 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 {} + """ + ), + //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}) + // 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 {} + """, + """ + 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}) + // 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( + 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.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 {} + """ + ) + ); + } + + // 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 {} + """ + ) + ); + } + } + + + + + +// @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` + // 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 @@ -61,31 +1916,43 @@ public class A { } @Test - void addValueAttributeClass() { + void literalToListFromSingleValueUsingAppendArray() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", null, "Integer.class", null, null, null)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", "strArr", "xyz", null, null, true)), + //language=java java( """ - package org.example; - public @interface Foo { - Class value(); - } + import org.example.FooDefaultStringArray; + + @FooDefaultStringArray(strArr = "abc") + public class A {} + """, + """ + import org.example.FooDefaultStringArray; + + @FooDefaultStringArray(strArr = {"abc", "xyz"}) + public class A {} """ - ), + ) + ); + } + + @Test + void addValueAttributeClass() { + rewriteRun( + 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 {} """ ) ); @@ -94,31 +1961,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 {} """ ) ); @@ -127,30 +1984,19 @@ public class A { @Test void updateValueAttribute() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", null, "hello", null, null, null)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, "hello", null, null, null)), java( """ - package org.example; - public @interface Foo { - String value() default ""; - } - """ - ), - - 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 {} """ ) ); @@ -159,30 +2005,19 @@ public class A { @Test void updateValueAttributeClass() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", null, "Integer.class", null, null, null)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultClass", null, "Integer.class", null, null, null)), java( """ - package org.example; - public @interface Foo { - Class value(); - } - """ - ), - - 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 {} """ ) ); @@ -191,30 +2026,19 @@ public class A { @Test void removeValueAttribute() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.Foo", null, null, null, null, null)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", null, null, null, null, null)), java( """ - package org.example; - public @interface Foo { - String value() default ""; - } - """ - ), + import org.example.FooDefaultString; - java( - """ - import org.example.Foo; - - @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 {} """ ) ); @@ -223,30 +2047,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 {} """ ) ); @@ -255,31 +2068,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 {} """ ) ); @@ -748,7 +2549,7 @@ public class A { """ import org.example.Foo; - @Foo(array = {"newTest1", "newTest2"}) + @Foo(array = {"oldTest", "newTest1", "newTest2"}) public class A { } """ @@ -1161,35 +2962,19 @@ public class A { @Test void appendMultipleValuesToExistingArrayAttribute() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute( - "org.example.Foo", - "array", - "b,c", - null, - false, - true)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", "strArr", "b,c", null, false, true)), java( """ - package org.example; - public @interface Foo { - String[] array() default {}; - } - """ - ), - java( - """ - import org.example.Foo; + import org.example.FooDefaultStringArray; - @Foo(array = {"a"}) - public class A { - } + @FooDefaultStringArray(strArr = {"a"}) + 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 {} """ ) ); @@ -1199,34 +2984,25 @@ public class A { void appendMultipleValuesToExistingArrayValueAttribute() { 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 {}; - } - """ - ), - 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", "c"}) - public class A { - } + @FooDefaultStringArray({"a", "b", "c"}) + public class A {} """ ) ); @@ -1235,35 +3011,19 @@ 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 {}; - } - """ - ), + 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 {} """ ) ); @@ -1273,160 +3033,93 @@ public class A { 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 {}; - } - """ - ), - java( - """ - import org.example.Foo; + import org.example.FooDefaultStringArray; - @Foo({"a", "b"}) - public class A { - } + @FooDefaultStringArray({"a", "b"}) + public class A {} """, """ - import org.example.Foo; + import org.example.FooDefaultStringArray; - @Foo({"a", "b", "c"}) - public class A { - } + @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.Foo", - "array", - "b,c", - null, - false, - true)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", "strArr", "b,c", null, false, true)), java( """ - package org.example; - public @interface Foo { - String[] array() default {}; - } - - public class A { - } - """ - ), - 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 {} """ ) ); } + // literal array attrType, implicit "value" attrName, string literal array attrVal, null oldAttrVal, false addOnly (does not matter), true appendArray @Test void appendMultipleValuesToExistingArrayValueAttributeNonSet() { rewriteRun( - spec -> spec.recipe(new AddOrUpdateAnnotationAttribute( - "org.example.Foo", - null, - "b,c", - null, - false, - true)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultStringArray", null, "b,c", null, false, true)), java( """ - package org.example; - public @interface Foo { - String[] value() default {}; - } + import org.example.FooDefaultStringArray; - public class A { - } - """ - ), - java( - """ - import org.example.Foo; - - @Foo({"a", "b"}) - public class A { - } + @FooDefaultStringArray({"a", "b"}) + public class A {} """, """ - import org.example.Foo; + import org.example.FooDefaultStringArray; - @Foo({"a", "b", "c"}) - public class A { - } + @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.Foo", "value", "hello", null, false, null)), + spec -> spec.recipe(new AddOrUpdateAnnotationAttribute("org.example.FooDefaultString", "value", "hello", null, 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 ""; - } - """ - ), - 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 {} """ ) ); @@ -1532,30 +3225,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 {} """ ) ); @@ -1564,44 +3246,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 {} """ ) ); @@ -1891,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 {} """ ) ); @@ -1928,31 +3577,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 {} """ ) ); @@ -1961,31 +3598,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 {} """ ) ); @@ -1994,31 +3619,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 {} """ ) ); @@ -2027,31 +3640,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 {} """ ) ); @@ -2062,16 +3663,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; @@ -2083,19 +3675,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 {} """ ) ); @@ -2129,16 +3719,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 {} """ ) ); @@ -2147,28 +3735,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; + import org.example.FooDefaultString; - @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 dec7d133af..a147561c93 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.*; @@ -34,6 +31,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; @@ -94,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 "value".equals(name); + } + + 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) { @@ -142,7 +354,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); List args = a.getArguments(); // Case for existing attribute: `@Foo("q")` -> @Foo(value = "q") @@ -164,28 +376,77 @@ 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()); + 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())) { 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 -> { + 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); + } 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())) { 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)); @@ -197,11 +458,30 @@ 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(literal, oldAttributeValue)) { + return null; + } + return literal; } 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) { @@ -216,7 +496,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; @@ -224,6 +507,22 @@ 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()); + 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) + ); + } String attrVal = newAttributeValue.contains(",") && attributeIsArray(annotation) ? getAttributeValues().stream().map(String::valueOf).collect(joining(",", "{", "}")) : newAttributeValue; @@ -237,12 +536,21 @@ 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); } - 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) { @@ -252,6 +560,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) @@ -274,15 +588,43 @@ 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 -> { - if (it instanceof J.Literal && oldAttributeValue.equals(((J.Literal) it).getValue())) { - 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); } + if (!TRUE.equals(appendArray)) { + return newItemsList; + } + } else if (it instanceof J.Empty) { + return new ArrayList<>(); + } + if (TRUE.equals(appendArray)) { + newItemsList.add(0, it); return newItemsList; } return it; @@ -330,13 +672,27 @@ 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) { 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; @@ -346,10 +702,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")) { @@ -419,4 +772,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 ("value".equals(attributeName())) { + return true; + } + } + return false; + } }