From 1102ae995759dab9f2948e2e0933c5db46782ccd Mon Sep 17 00:00:00 2001 From: Peter Gafert Date: Sun, 19 Jan 2025 17:07:48 +0100 Subject: [PATCH] extend CI to run tests with JDK 21 This was previously blocked by our Gradle version being too old. Now that we can run the tests with JDK 21 it shows though, that the `Annotation` `toString()` behavior has changed again. Unfortunately, this always either causes complexity on the production or test side. If we want to test the `AnnotationProxy.toString()` behavior, then either we have to add a workaround to the test code, or we have to adjust the production code to produce the equivalent `toString()` value. I opted for the latter in hopes that they will finally stop changing the `toString()` style every couple of versions. Signed-off-by: Peter Gafert --- .github/workflows/build.yml | 4 +- .../core/domain/Java9DomainPlugin.java | 14 +- .../tngtech/archunit/core/PluginLoader.java | 3 +- .../core/domain/AnnotationFormatter.java | 157 ++++++++++++++++++ .../domain/AnnotationPropertiesFormatter.java | 124 -------------- .../archunit/core/domain/AnnotationProxy.java | 11 +- .../archunit/core/domain/DomainPlugin.java | 16 +- .../core/domain/Java14DomainPlugin.java | 17 +- .../core/domain/Java21DomainPlugin.java | 38 +++++ .../core/domain/AnnotationFormatterTest.java | 35 ++++ .../AnnotationPropertiesFormatterTest.java | 1 + .../core/domain/AnnotationProxyTest.java | 89 ++++++---- .../syntheticimport/ClassWithSynthetics.java | 6 +- .../testutil/ReflectionTestUtils.java | 11 ++ build.gradle | 2 +- .../archunit.java-version-conventions.gradle | 3 +- 16 files changed, 340 insertions(+), 191 deletions(-) create mode 100644 archunit/src/main/java/com/tngtech/archunit/core/domain/AnnotationFormatter.java delete mode 100644 archunit/src/main/java/com/tngtech/archunit/core/domain/AnnotationPropertiesFormatter.java create mode 100644 archunit/src/main/java/com/tngtech/archunit/core/domain/Java21DomainPlugin.java create mode 100644 archunit/src/test/java/com/tngtech/archunit/core/domain/AnnotationFormatterTest.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c0ab6d6927..86adf1d694 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ on: pull_request: env: - build_java_version: 17 + build_java_version: 21 jobs: build: @@ -47,6 +47,7 @@ jobs: - 8 - 11 - 17 + - 21 runs-on: ${{ matrix.os }} steps: - name: Checkout @@ -96,6 +97,7 @@ jobs: - 8 - 11 - 17 + - 21 runs-on: ${{ matrix.os }} steps: - name: Checkout diff --git a/archunit/src/jdk9main/java/com/tngtech/archunit/core/domain/Java9DomainPlugin.java b/archunit/src/jdk9main/java/com/tngtech/archunit/core/domain/Java9DomainPlugin.java index 229bedbba5..61bb71f848 100644 --- a/archunit/src/jdk9main/java/com/tngtech/archunit/core/domain/Java9DomainPlugin.java +++ b/archunit/src/jdk9main/java/com/tngtech/archunit/core/domain/Java9DomainPlugin.java @@ -24,11 +24,13 @@ @SuppressWarnings("unused") class Java9DomainPlugin implements DomainPlugin { @Override - public void plugInAnnotationPropertiesFormatter(InitialConfiguration propertiesFormatter) { - propertiesFormatter.set(AnnotationPropertiesFormatter.configure() - .formattingArraysWithCurlyBrackets() - .formattingTypesAsClassNames() - .quotingStrings() - .build()); + public void plugInAnnotationFormatter(InitialConfiguration propertiesFormatter) { + propertiesFormatter.set( + AnnotationFormatter.formatAnnotationType(JavaClass::getName) + .formatProperties(config -> config + .formattingArraysWithCurlyBrackets() + .formattingTypesAsClassNames() + .quotingStrings() + )); } } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/PluginLoader.java b/archunit/src/main/java/com/tngtech/archunit/core/PluginLoader.java index 8ca11fe862..64216c8279 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/PluginLoader.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/PluginLoader.java @@ -139,7 +139,8 @@ public Creator load(String pluginClassName) { public enum JavaVersion { JAVA_9(9), - JAVA_14(14); + JAVA_14(14), + JAVA_21(21); private final int releaseVersion; diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/AnnotationFormatter.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/AnnotationFormatter.java new file mode 100644 index 0000000000..5fd20c13f7 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/AnnotationFormatter.java @@ -0,0 +1,157 @@ +/* + * Copyright 2014-2025 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.core.domain; + +import java.lang.reflect.Array; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.IntStream; + +import com.google.common.base.Joiner; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; + +class AnnotationFormatter { + private final Function annotationTypeFormatter; + private final AnnotationPropertiesFormatter propertiesFormatter; + + AnnotationFormatter(Function annotationTypeFormatter, AnnotationPropertiesFormatter propertiesFormatter) { + this.annotationTypeFormatter = annotationTypeFormatter; + this.propertiesFormatter = propertiesFormatter; + } + + String format(JavaClass annotationType, Map annotationProperties) { + return String.format("@%s(%s)", annotationTypeFormatter.apply(annotationType), propertiesFormatter.formatProperties(annotationProperties)); + } + + static Builder formatAnnotationType(Function annotationTypeFormatter) { + return new Builder(annotationTypeFormatter); + } + + static class Builder { + private final Function annotationTypeFormatter; + + private Builder(Function annotationTypeFormatter) { + this.annotationTypeFormatter = annotationTypeFormatter; + } + + AnnotationFormatter formatProperties(Consumer config) { + AnnotationPropertiesFormatter.Builder propertiesFormatterBuilder = AnnotationPropertiesFormatter.configure(); + config.accept(propertiesFormatterBuilder); + return new AnnotationFormatter(annotationTypeFormatter, propertiesFormatterBuilder.build()); + } + } + + static class AnnotationPropertiesFormatter { + private final Function, String> arrayFormatter; + private final Function, String> typeFormatter; + private final Function stringFormatter; + private final boolean omitOptionalIdentifierForSingleElementAnnotations; + + private AnnotationPropertiesFormatter(Builder builder) { + this.arrayFormatter = checkNotNull(builder.arrayFormatter); + this.typeFormatter = checkNotNull(builder.typeFormatter); + this.stringFormatter = checkNotNull(builder.stringFormatter); + this.omitOptionalIdentifierForSingleElementAnnotations = builder.omitOptionalIdentifierForSingleElementAnnotations; + } + + String formatProperties(Map properties) { + // see Builder#omitOptionalIdentifierForSingleElementAnnotations() for documentation + if (properties.size() == 1 && properties.containsKey("value") && omitOptionalIdentifierForSingleElementAnnotations) { + return formatValue(properties.get("value")); + } + + return properties.entrySet().stream() + .map(entry -> entry.getKey() + "=" + formatValue(entry.getValue())) + .collect(joining(", ")); + } + + String formatValue(Object input) { + if (input instanceof Class) { + return typeFormatter.apply((Class) input); + } + if (input instanceof String) { + return stringFormatter.apply((String) input); + } + if (!input.getClass().isArray()) { + return String.valueOf(input); + } + + List elemToString = IntStream.range(0, Array.getLength(input)) + .mapToObj(i -> formatValue(Array.get(input, i))) + .collect(toList()); + return arrayFormatter.apply(elemToString); + } + + static Builder configure() { + return new Builder(); + } + + static class Builder { + private Function, String> arrayFormatter; + private Function, String> typeFormatter; + private Function stringFormatter = identity(); + private boolean omitOptionalIdentifierForSingleElementAnnotations = false; + + Builder formattingArraysWithSquareBrackets() { + arrayFormatter = input -> "[" + Joiner.on(", ").join(input) + "]"; + return this; + } + + Builder formattingArraysWithCurlyBrackets() { + arrayFormatter = input -> "{" + Joiner.on(", ").join(input) + "}"; + return this; + } + + Builder formattingTypesToString() { + typeFormatter = String::valueOf; + return this; + } + + Builder formattingTypesAsClassNames() { + typeFormatter = input -> input.getName() + ".class"; + return this; + } + + Builder quotingStrings() { + stringFormatter = input -> "\"" + input + "\""; + return this; + } + + /** + * Configures that the identifier is omitted if the annotation is a + * single-element annotation + * and the identifier of the only element is "value". + * + *
  • Example with this configuration: {@code @Copyright("2020 Acme Corporation")}
  • + *
  • Example without this configuration: {@code @Copyright(value="2020 Acme Corporation")}
+ */ + Builder omitOptionalIdentifierForSingleElementAnnotations() { + omitOptionalIdentifierForSingleElementAnnotations = true; + return this; + } + + AnnotationPropertiesFormatter build() { + return new AnnotationPropertiesFormatter(this); + } + } + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/AnnotationPropertiesFormatter.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/AnnotationPropertiesFormatter.java deleted file mode 100644 index c7eaa24cb1..0000000000 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/AnnotationPropertiesFormatter.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2014-2025 TNG Technology Consulting GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.tngtech.archunit.core.domain; - -import java.lang.reflect.Array; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.IntStream; - -import com.google.common.base.Joiner; - -import static com.google.common.base.Preconditions.checkNotNull; -import static java.util.function.Function.identity; -import static java.util.stream.Collectors.joining; -import static java.util.stream.Collectors.toList; - -class AnnotationPropertiesFormatter { - private final Function, String> arrayFormatter; - private final Function, String> typeFormatter; - private final Function stringFormatter; - private final boolean omitOptionalIdentifierForSingleElementAnnotations; - - private AnnotationPropertiesFormatter(Builder builder) { - this.arrayFormatter = checkNotNull(builder.arrayFormatter); - this.typeFormatter = checkNotNull(builder.typeFormatter); - this.stringFormatter = checkNotNull(builder.stringFormatter); - this.omitOptionalIdentifierForSingleElementAnnotations = builder.omitOptionalIdentifierForSingleElementAnnotations; - } - - String formatProperties(Map properties) { - // see Builder#omitOptionalIdentifierForSingleElementAnnotations() for documentation - if (properties.size() == 1 && properties.containsKey("value") && omitOptionalIdentifierForSingleElementAnnotations) { - return formatValue(properties.get("value")); - } - - return properties.entrySet().stream() - .map(entry -> entry.getKey() + "=" + formatValue(entry.getValue())) - .collect(joining(", ")); - } - - String formatValue(Object input) { - if (input instanceof Class) { - return typeFormatter.apply((Class) input); - } - if (input instanceof String) { - return stringFormatter.apply((String) input); - } - if (!input.getClass().isArray()) { - return String.valueOf(input); - } - - List elemToString = IntStream.range(0, Array.getLength(input)) - .mapToObj(i -> formatValue(Array.get(input, i))) - .collect(toList()); - return arrayFormatter.apply(elemToString); - } - - static Builder configure() { - return new Builder(); - } - - static class Builder { - private Function, String> arrayFormatter; - private Function, String> typeFormatter; - private Function stringFormatter = identity(); - private boolean omitOptionalIdentifierForSingleElementAnnotations = false; - - Builder formattingArraysWithSquareBrackets() { - arrayFormatter = input -> "[" + Joiner.on(", ").join(input) + "]"; - return this; - } - - Builder formattingArraysWithCurlyBrackets() { - arrayFormatter = input -> "{" + Joiner.on(", ").join(input) + "}"; - return this; - } - - Builder formattingTypesToString() { - typeFormatter = String::valueOf; - return this; - } - - Builder formattingTypesAsClassNames() { - typeFormatter = input -> input.getName() + ".class"; - return this; - } - - Builder quotingStrings() { - stringFormatter = input -> "\"" + input + "\""; - return this; - } - - /** - * Configures that the identifier is omitted if the annotation is a - * single-element annotation - * and the identifier of the only element is "value". - * - *
  • Example with this configuration: {@code @Copyright("2020 Acme Corporation")}
  • - *
  • Example without this configuration: {@code @Copyright(value="2020 Acme Corporation")}
- */ - Builder omitOptionalIdentifierForSingleElementAnnotations() { - omitOptionalIdentifierForSingleElementAnnotations = true; - return this; - } - - AnnotationPropertiesFormatter build() { - return new AnnotationPropertiesFormatter(this); - } - } -} diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/AnnotationProxy.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/AnnotationProxy.java index dd02d62574..8426ce08b9 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/AnnotationProxy.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/AnnotationProxy.java @@ -37,10 +37,10 @@ @MayResolveTypesViaReflection(reason = "We depend on the classpath, if we proxy an annotation type") class AnnotationProxy { - private static final InitialConfiguration propertiesFormatter = new InitialConfiguration<>(); + private static final InitialConfiguration annotationFormatter = new InitialConfiguration<>(); static { - DomainPlugin.Loader.loadForCurrentPlatform().plugInAnnotationPropertiesFormatter(propertiesFormatter); + DomainPlugin.Loader.loadForCurrentPlatform().plugInAnnotationFormatter(annotationFormatter); } public static A of(Class annotationType, JavaAnnotation toProxy) { @@ -277,12 +277,7 @@ private ToStringHandler(Class annotationType, JavaAnnotation toProxy, Conv @Override public Object handle(Object proxy, Method method, Object[] args) { - return String.format("@%s(%s)", toProxy.getRawType().getName(), propertiesString()); - } - - private String propertiesString() { - Map unwrappedProperties = unwrapProxiedProperties(); - return propertiesFormatter.get().formatProperties(unwrappedProperties); + return annotationFormatter.get().format(toProxy.getRawType(), unwrapProxiedProperties()); } private Map unwrapProxiedProperties() { diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/DomainPlugin.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/DomainPlugin.java index 940f9ec395..d488226b32 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/DomainPlugin.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/DomainPlugin.java @@ -20,10 +20,11 @@ import com.tngtech.archunit.core.PluginLoader; import static com.tngtech.archunit.core.PluginLoader.JavaVersion.JAVA_14; +import static com.tngtech.archunit.core.PluginLoader.JavaVersion.JAVA_21; import static com.tngtech.archunit.core.PluginLoader.JavaVersion.JAVA_9; interface DomainPlugin { - void plugInAnnotationPropertiesFormatter(InitialConfiguration valueFormatter); + void plugInAnnotationFormatter(InitialConfiguration annotationFormatter); @Internal class Loader { @@ -31,6 +32,7 @@ class Loader { .forType(DomainPlugin.class) .ifVersionGreaterOrEqualTo(JAVA_9).load("com.tngtech.archunit.core.domain.Java9DomainPlugin") .ifVersionGreaterOrEqualTo(JAVA_14).load("com.tngtech.archunit.core.domain.Java14DomainPlugin") + .ifVersionGreaterOrEqualTo(JAVA_21).load("com.tngtech.archunit.core.domain.Java21DomainPlugin") .fallback(new LegacyDomainPlugin()); static DomainPlugin loadForCurrentPlatform() { @@ -39,11 +41,13 @@ static DomainPlugin loadForCurrentPlatform() { private static class LegacyDomainPlugin implements DomainPlugin { @Override - public void plugInAnnotationPropertiesFormatter(InitialConfiguration valueFormatter) { - valueFormatter.set(AnnotationPropertiesFormatter.configure() - .formattingArraysWithSquareBrackets() - .formattingTypesToString() - .build()); + public void plugInAnnotationFormatter(InitialConfiguration annotationFormatter) { + annotationFormatter.set( + AnnotationFormatter + .formatAnnotationType(JavaClass::getName) + .formatProperties(config -> config + .formattingArraysWithSquareBrackets() + .formattingTypesToString())); } } } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/Java14DomainPlugin.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/Java14DomainPlugin.java index 6e071ef71b..b094b31da8 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/Java14DomainPlugin.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/Java14DomainPlugin.java @@ -24,12 +24,15 @@ @SuppressWarnings("unused") class Java14DomainPlugin implements DomainPlugin { @Override - public void plugInAnnotationPropertiesFormatter(InitialConfiguration propertiesFormatter) { - propertiesFormatter.set(AnnotationPropertiesFormatter.configure() - .formattingArraysWithCurlyBrackets() - .formattingTypesAsClassNames() - .quotingStrings() - .omitOptionalIdentifierForSingleElementAnnotations() - .build()); + public void plugInAnnotationFormatter(InitialConfiguration propertiesFormatter) { + propertiesFormatter.set( + AnnotationFormatter + .formatAnnotationType(JavaClass::getName) + .formatProperties(config -> config + .formattingArraysWithCurlyBrackets() + .formattingTypesAsClassNames() + .quotingStrings() + .omitOptionalIdentifierForSingleElementAnnotations() + )); } } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/Java21DomainPlugin.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/Java21DomainPlugin.java new file mode 100644 index 0000000000..279b33ef66 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/Java21DomainPlugin.java @@ -0,0 +1,38 @@ +/* + * Copyright 2014-2025 TNG Technology Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tngtech.archunit.core.domain; + +import com.tngtech.archunit.core.InitialConfiguration; +import com.tngtech.archunit.core.PluginLoader; + +/** + * Resolved via {@link PluginLoader} + */ +@SuppressWarnings("unused") +class Java21DomainPlugin implements DomainPlugin { + @Override + public void plugInAnnotationFormatter(InitialConfiguration propertiesFormatter) { + propertiesFormatter.set( + AnnotationFormatter + .formatAnnotationType(javaClass -> javaClass.getName().replace("$", ".")) + .formatProperties(config -> config + .formattingArraysWithCurlyBrackets() + .formattingTypesAsClassNames() + .quotingStrings() + .omitOptionalIdentifierForSingleElementAnnotations() + )); + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/core/domain/AnnotationFormatterTest.java b/archunit/src/test/java/com/tngtech/archunit/core/domain/AnnotationFormatterTest.java new file mode 100644 index 0000000000..eea33d9a2a --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/core/domain/AnnotationFormatterTest.java @@ -0,0 +1,35 @@ +package com.tngtech.archunit.core.domain; + +import com.google.common.collect.ImmutableMap; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import org.junit.jupiter.api.Test; + +import static com.tngtech.archunit.testutil.Assertions.assertThat; +import static java.util.Collections.emptyMap; + +class AnnotationFormatterTest { + @Test + void should_format_annotation_type_as_configured() { + JavaClass annotationType = new ClassFileImporter().importClass(getClass()); + AnnotationFormatter formatter = AnnotationFormatter + .formatAnnotationType(javaClass -> "changed." + javaClass.getSimpleName()) + .formatProperties(config -> config.formattingArraysWithSquareBrackets().formattingTypesToString()); + + assertThat(formatter.format(annotationType, emptyMap())) + .isEqualTo("@changed." + getClass().getSimpleName() + "()"); + } + + @Test + void should_use_property_formatter_to_format_properties() { + JavaClass annotationType = new ClassFileImporter().importClass(getClass()); + AnnotationFormatter formatter = AnnotationFormatter + .formatAnnotationType(JavaClass::getName) + .formatProperties(config -> config + .formattingArraysWithCurlyBrackets() + .formattingTypesToString() + .quotingStrings()); + + assertThat(formatter.format(annotationType, ImmutableMap.of("test", new String[]{"one", "two"}))) + .contains("{\"one\", \"two\"}"); + } +} \ No newline at end of file diff --git a/archunit/src/test/java/com/tngtech/archunit/core/domain/AnnotationPropertiesFormatterTest.java b/archunit/src/test/java/com/tngtech/archunit/core/domain/AnnotationPropertiesFormatterTest.java index 69f42383d4..4e07fb2e8c 100644 --- a/archunit/src/test/java/com/tngtech/archunit/core/domain/AnnotationPropertiesFormatterTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/domain/AnnotationPropertiesFormatterTest.java @@ -3,6 +3,7 @@ import java.util.List; import com.google.common.collect.ImmutableList; +import com.tngtech.archunit.core.domain.AnnotationFormatter.AnnotationPropertiesFormatter; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; diff --git a/archunit/src/test/java/com/tngtech/archunit/core/domain/AnnotationProxyTest.java b/archunit/src/test/java/com/tngtech/archunit/core/domain/AnnotationProxyTest.java index 74ecbd4d4e..00896d61e4 100644 --- a/archunit/src/test/java/com/tngtech/archunit/core/domain/AnnotationProxyTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/domain/AnnotationProxyTest.java @@ -9,13 +9,18 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; import com.google.common.collect.ImmutableMap; +import com.tngtech.archunit.base.Suppliers; import com.tngtech.archunit.core.InitialConfiguration; +import com.tngtech.archunit.core.domain.AnnotationFormatter.AnnotationPropertiesFormatter; import com.tngtech.archunit.core.importer.ClassFileImporter; import org.assertj.core.api.Condition; import org.junit.Test; +import static com.tngtech.archunit.testutil.ReflectionTestUtils.getFieldValue; import static java.util.Arrays.stream; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; @@ -23,6 +28,17 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; public class AnnotationProxyTest { + private final Supplier annotationFormatterForCurrentPlatform = + Suppliers.memoize(() -> { + DomainPlugin domainPlugin = DomainPlugin.Loader.loadForCurrentPlatform(); + InitialConfiguration formatter = new InitialConfiguration<>(); + domainPlugin.plugInAnnotationFormatter(formatter); + return formatter.get(); + }); + private final Supplier> annotationTypeFormatter = + Suppliers.memoize(() -> getFieldValue(annotationFormatterForCurrentPlatform.get(), "annotationTypeFormatter")); + private final Supplier annotationPropertiesFormatter = + Suppliers.memoize(() -> getFieldValue(annotationFormatterForCurrentPlatform.get(), "propertiesFormatter")); @Test public void annotation_type_is_returned() { @@ -215,8 +231,10 @@ public void subAnnotations_default_are_returned() { // NOTE: For now we'll just implement reference equality and hashcode of the proxy object public void equals_hashcode_and_toString() { TestAnnotation annotation = importAnnotation(ClassWithTestAnnotation.class, TestAnnotation.class); + TestAnnotation other = importAnnotation(ClassWithTestAnnotation.class, TestAnnotation.class); assertThat(annotation).isEqualTo(annotation); + assertThat(annotation).isNotEqualTo(other); assertThat(annotation.hashCode()).isEqualTo(annotation.hashCode()); assertThat(annotation.toString()).is(matching(TestAnnotation.class, propertiesOf(TestAnnotation.class))); } @@ -246,59 +264,58 @@ public void array_is_converted_to_the_correct_type() { } private ImmutableMap propertiesOf(Class type) { - AnnotationPropertiesFormatter formatter = getAnnotationPropertiesFormatterForCurrentPlatform(); + AnnotationPropertiesFormatter propertiesFormatter = annotationPropertiesFormatter.get(); ImmutableMap result = ImmutableMap.builder() .put("primitive", "77") .put("primitiveWithDefault", "1") - .put("primitives", formatter.formatValue(new int[]{77, 88})) - .put("primitivesWithDefault", formatter.formatValue(new int[]{1, 2})) - .put("string", formatter.formatValue("foo")) - .put("stringWithDefault", formatter.formatValue("something")) - .put("strings", formatter.formatValue(new String[]{"one", "two"})) - .put("stringsWithDefault", formatter.formatValue(new String[]{"something", "more"})) - .put("type", formatter.formatValue(String.class)) - .put("typeWithDefault", formatter.formatValue(Serializable.class)) - .put("types", formatter.formatValue(new Class[]{Map.class, List.class})) - .put("typesWithDefault", formatter.formatValue(new Class[]{Serializable.class, String.class})) + .put("primitives", propertiesFormatter.formatValue(new int[]{77, 88})) + .put("primitivesWithDefault", propertiesFormatter.formatValue(new int[]{1, 2})) + .put("string", propertiesFormatter.formatValue("foo")) + .put("stringWithDefault", propertiesFormatter.formatValue("something")) + .put("strings", propertiesFormatter.formatValue(new String[]{"one", "two"})) + .put("stringsWithDefault", propertiesFormatter.formatValue(new String[]{"something", "more"})) + .put("type", propertiesFormatter.formatValue(String.class)) + .put("typeWithDefault", propertiesFormatter.formatValue(Serializable.class)) + .put("types", propertiesFormatter.formatValue(new Class[]{Map.class, List.class})) + .put("typesWithDefault", propertiesFormatter.formatValue(new Class[]{Serializable.class, String.class})) .put("enumConstant", String.valueOf(TestEnum.SECOND)) .put("enumConstantWithDefault", String.valueOf(TestEnum.FIRST)) - .put("enumConstants", formatter.formatValue(new TestEnum[]{TestEnum.SECOND, TestEnum.THIRD})) - .put("enumConstantsWithDefault", formatter.formatValue(new TestEnum[]{TestEnum.FIRST, TestEnum.SECOND})) - .put("subAnnotation", - formatSubAnnotation(formatter, "custom")) - .put("subAnnotationWithDefault", - formatSubAnnotation(formatter, "default")) + .put("enumConstants", propertiesFormatter.formatValue(new TestEnum[]{TestEnum.SECOND, TestEnum.THIRD})) + .put("enumConstantsWithDefault", propertiesFormatter.formatValue(new TestEnum[]{TestEnum.FIRST, TestEnum.SECOND})) + .put("subAnnotation", formatSubAnnotation("custom")) + .put("subAnnotationWithDefault", formatSubAnnotation("default")) .put("subAnnotations", - formatter.formatValue(new Object[]{ - subAnnotationFormatter(formatter, "customOne"), - subAnnotationFormatter(formatter, "customTwo")})) + propertiesFormatter.formatValue(new Object[]{ + subAnnotationFormatter("customOne"), + subAnnotationFormatter("customTwo")})) .put("subAnnotationsWithDefault", - formatter.formatValue(new Object[]{ - subAnnotationFormatter(formatter, "defaultOne"), - subAnnotationFormatter(formatter, "defaultTwo")})) + propertiesFormatter.formatValue(new Object[]{ + subAnnotationFormatter("defaultOne"), + subAnnotationFormatter("defaultTwo")})) .build(); ensureInSync(ClassWithTestAnnotation.class.getAnnotation(type), result); return result; } - private AnnotationPropertiesFormatter getAnnotationPropertiesFormatterForCurrentPlatform() { - DomainPlugin domainPlugin = DomainPlugin.Loader.loadForCurrentPlatform(); - InitialConfiguration formatter = new InitialConfiguration<>(); - domainPlugin.plugInAnnotationPropertiesFormatter(formatter); - return formatter.get(); + private String formatSubAnnotation(String value) { + Map properties = Collections.singletonMap("value", value); + return "@" + formatAnnotationType(SubAnnotation.class) + "(" + formatAnnotationProperties(properties) + ")"; } - private String formatSubAnnotation(AnnotationPropertiesFormatter formatter, String value) { - Map properties = Collections.singletonMap("value", value); - return "@com.tngtech.archunit.core.domain.AnnotationProxyTest$SubAnnotation(" + formatter.formatProperties(properties) + ")"; + private String formatAnnotationType(Class annotationType) { + return annotationTypeFormatter.get().apply(new ClassFileImporter().importClass(annotationType)); + } + + private String formatAnnotationProperties(Map properties) { + return annotationPropertiesFormatter.get().formatProperties(properties); } // NOTE: We do not want this value to be treated as a string by the formatter, and e.g. quoted -> Object - private Object subAnnotationFormatter(AnnotationPropertiesFormatter formatter, String value) { + private Object subAnnotationFormatter(String value) { return new Object() { @Override public String toString() { - return formatSubAnnotation(formatter, value); + return formatSubAnnotation(value); } }; } @@ -387,8 +404,10 @@ private void ensureInSync(TestAnnotation annotation, Map result) .map(Method::getName) .collect(toSet()); assertThat(result.keySet()).as("Specified expected keys").isEqualTo(necessaryKeysAsSanityCheck); + + String expectedAnnotationString = annotation.toString(); for (String v : result.values()) { - assertThat(annotation.toString()).contains(v); + assertThat(v).isSubstringOf(expectedAnnotationString); } } @@ -403,7 +422,7 @@ public boolean matches(String value) { if (value == null) { return false; } - String expectedPart = "@" + annotationType.getName(); + String expectedPart = "@" + formatAnnotationType(annotationType); if (!value.contains(expectedPart)) { return mismatch(expectedPart); } diff --git a/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/syntheticimport/ClassWithSynthetics.java b/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/syntheticimport/ClassWithSynthetics.java index 22846ed03c..1aa96aa5a0 100644 --- a/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/syntheticimport/ClassWithSynthetics.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/syntheticimport/ClassWithSynthetics.java @@ -4,8 +4,12 @@ @SuppressWarnings({"unused", "InnerClassMayBeStatic"}) public class ClassWithSynthetics implements Comparator { - // for (non-static) inner classes the compiler must create a synthetic field, holding a reference to the outer class + // for (non-static) inner classes the compiler must create a synthetic field, holding a reference to the outer class, + // if it's used from somewhere outside the constructor public class ClassWithSyntheticField { + String causeSyntheticOuterClassFieldToBeCreated() { + return ClassWithSynthetics.this.toString(); + } } abstract class Parent { diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/ReflectionTestUtils.java b/archunit/src/test/java/com/tngtech/archunit/testutil/ReflectionTestUtils.java index 2d506a63ee..45352f1639 100644 --- a/archunit/src/test/java/com/tngtech/archunit/testutil/ReflectionTestUtils.java +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/ReflectionTestUtils.java @@ -42,4 +42,15 @@ public static Set> getHierarchy(Class clazz) { } return result; } + + @SuppressWarnings("unchecked") + public static T getFieldValue(Object object, String fieldName) { + try { + Field field = field(object.getClass(), fieldName); + field.setAccessible(true); + return (T) field.get(object); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } } diff --git a/build.gradle b/build.gradle index 62f20df244..1f476f898f 100644 --- a/build.gradle +++ b/build.gradle @@ -80,7 +80,7 @@ ext { ] minSupportedJavaVersion = JavaVersion.VERSION_1_8 - maxSupportedJavaVersion = JavaVersion.VERSION_17 + maxSupportedJavaVersion = JavaVersion.VERSION_21 isTestBuild = project.hasProperty('testJavaVersion') configuredTestJavaVersion = project.findProperty('testJavaVersion')?.toString()?.with { JavaVersion.toVersion(it) } assert configuredTestJavaVersion <= maxSupportedJavaVersion: diff --git a/buildSrc/src/main/groovy/archunit.java-version-conventions.gradle b/buildSrc/src/main/groovy/archunit.java-version-conventions.gradle index fbc3dd12ed..4caec4a8d7 100644 --- a/buildSrc/src/main/groovy/archunit.java-version-conventions.gradle +++ b/buildSrc/src/main/groovy/archunit.java-version-conventions.gradle @@ -2,7 +2,8 @@ class VersionSpec { private static final SortedSet LTS_VERSIONS_SORTED_ASCENDING = [ JavaVersion.VERSION_1_8, JavaVersion.VERSION_11, - JavaVersion.VERSION_17 + JavaVersion.VERSION_17, + JavaVersion.VERSION_21 ] as SortedSet JavaVersion desiredJdkVersion