diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/controller/OtherController.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/controller/OtherController.java new file mode 100644 index 0000000000..b1f09313f1 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/controller/OtherController.java @@ -0,0 +1,9 @@ +package com.tngtech.archunit.example.layers.controller; + +import com.tngtech.archunit.example.layers.controller.marshaller.StringUnmarshaller; + +@SuppressWarnings("unused") +public class OtherController { + void receive(@UnmarshalTransport(StringUnmarshaller.class) Object param) { + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/controller/UnmarshalTransport.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/controller/UnmarshalTransport.java new file mode 100644 index 0000000000..36e79c7313 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/controller/UnmarshalTransport.java @@ -0,0 +1,12 @@ +package com.tngtech.archunit.example.layers.controller; + +import java.lang.annotation.Retention; + +import com.tngtech.archunit.example.layers.controller.marshaller.Unmarshaller; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +public @interface UnmarshalTransport { + Class>[] value(); +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/controller/marshaller/ByteUnmarshaller.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/controller/marshaller/ByteUnmarshaller.java new file mode 100644 index 0000000000..b5ec5f22e7 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/controller/marshaller/ByteUnmarshaller.java @@ -0,0 +1,8 @@ +package com.tngtech.archunit.example.layers.controller.marshaller; + +public class ByteUnmarshaller implements Unmarshaller { + @Override + public T unmarschal(Byte from) { + return null; + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/controller/marshaller/StringUnmarshaller.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/controller/marshaller/StringUnmarshaller.java new file mode 100644 index 0000000000..05cb22fbb3 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/controller/marshaller/StringUnmarshaller.java @@ -0,0 +1,8 @@ +package com.tngtech.archunit.example.layers.controller.marshaller; + +public class StringUnmarshaller implements Unmarshaller { + @Override + public T unmarschal(String from) { + return null; + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/controller/marshaller/Unmarshaller.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/controller/marshaller/Unmarshaller.java new file mode 100644 index 0000000000..dca0ace6ff --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/controller/marshaller/Unmarshaller.java @@ -0,0 +1,6 @@ +package com.tngtech.archunit.example.layers.controller.marshaller; + +public interface Unmarshaller { + @SuppressWarnings("unused") + T unmarschal(F from); +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/service/OtherServiceViolatingLayerRules.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/service/OtherServiceViolatingLayerRules.java new file mode 100644 index 0000000000..d9732e4c2a --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/service/OtherServiceViolatingLayerRules.java @@ -0,0 +1,11 @@ +package com.tngtech.archunit.example.layers.service; + +import com.tngtech.archunit.example.layers.controller.UnmarshalTransport; +import com.tngtech.archunit.example.layers.controller.marshaller.ByteUnmarshaller; +import com.tngtech.archunit.example.layers.controller.marshaller.StringUnmarshaller; + +public class OtherServiceViolatingLayerRules { + @SuppressWarnings("unused") + public void dependentOnParameterAnnotation(@UnmarshalTransport({StringUnmarshaller.class, ByteUnmarshaller.class}) Object param) { + } +} diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/PublicAPIRules.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/PublicAPIRules.java index 19cbe590e9..0c52ffdf90 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/PublicAPIRules.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/PublicAPIRules.java @@ -73,7 +73,7 @@ public class PublicAPIRules { .and(doNot(inheritPublicAPI())) .and(are(relevantArchUnitMembers())) - .should(notBePublic()) + .should().notBePublic() .because("users of ArchUnit should only access intended members, to preserve maintainability"); diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java index 62b592f046..176b9a9303 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java @@ -80,7 +80,11 @@ import com.tngtech.archunit.example.layers.controller.SomeController; import com.tngtech.archunit.example.layers.controller.SomeGuiController; import com.tngtech.archunit.example.layers.controller.SomeUtility; +import com.tngtech.archunit.example.layers.controller.UnmarshalTransport; import com.tngtech.archunit.example.layers.controller.WronglyAnnotated; +import com.tngtech.archunit.example.layers.controller.marshaller.ByteUnmarshaller; +import com.tngtech.archunit.example.layers.controller.marshaller.StringUnmarshaller; +import com.tngtech.archunit.example.layers.controller.marshaller.Unmarshaller; import com.tngtech.archunit.example.layers.controller.one.SomeEnum; import com.tngtech.archunit.example.layers.controller.one.UseCaseOneThreeController; import com.tngtech.archunit.example.layers.controller.one.UseCaseOneTwoController; @@ -100,6 +104,7 @@ import com.tngtech.archunit.example.layers.security.Secured; import com.tngtech.archunit.example.layers.service.Async; import com.tngtech.archunit.example.layers.service.ComplexServiceAnnotation; +import com.tngtech.archunit.example.layers.service.OtherServiceViolatingLayerRules; import com.tngtech.archunit.example.layers.service.ProxiedConnection; import com.tngtech.archunit.example.layers.service.ServiceHelper; import com.tngtech.archunit.example.layers.service.ServiceInterface; @@ -174,6 +179,7 @@ import static com.tngtech.archunit.testutils.ExpectedAccess.callFromMethod; import static com.tngtech.archunit.testutils.ExpectedAccess.callFromStaticInitializer; import static com.tngtech.archunit.testutils.ExpectedDependency.annotatedClass; +import static com.tngtech.archunit.testutils.ExpectedDependency.annotatedParameter; import static com.tngtech.archunit.testutils.ExpectedDependency.constructor; import static com.tngtech.archunit.testutils.ExpectedDependency.field; import static com.tngtech.archunit.testutils.ExpectedDependency.genericFieldType; @@ -233,7 +239,7 @@ Stream CodingRulesTest() { .inLine(9)); expectFailures.ofRule("fields that have raw type java.util.logging.Logger should be private " + - "and should be static and should be final, because we agreed on this convention") + "and should be static and should be final, because we agreed on this convention") .by(ExpectedField.of(ClassViolatingCodingRules.class, "log").doesNotHaveModifier(JavaModifier.PRIVATE)) .by(ExpectedField.of(ClassViolatingCodingRules.class, "log").doesNotHaveModifier(JavaModifier.FINAL)); @@ -245,8 +251,8 @@ Stream CodingRulesTest() { .by(method(ClassViolatingCodingRules.class, "jodaTimeIsBad") .withReturnType(org.joda.time.DateTime.class)); - expectFailures.ofRule("no classes should use field injection, because field injection is considered harmful; " - + "use constructor injection or setter injection instead; see https://stackoverflow.com/q/39890849 for detailed explanations") + expectFailures.ofRule("no classes should use field injection, because field injection is considered harmful; " + + "use constructor injection or setter injection instead; see https://stackoverflow.com/q/39890849 for detailed explanations") .by(ExpectedField.of(ClassViolatingInjectionRules.class, "badBecauseAutowiredField").beingAnnotatedWith(Autowired.class)) .by(ExpectedField.of(ClassViolatingInjectionRules.class, "badBecauseValueField").beingAnnotatedWith(Value.class)) .by(ExpectedField.of(ClassViolatingInjectionRules.class, "badBecauseJavaxInjectField").beingAnnotatedWith(javax.inject.Inject.class)) @@ -815,6 +821,15 @@ Stream LayerDependencyRulesTest() { .by(annotatedClass(ServiceViolatingLayerRules.class).withAnnotationParameterType(ComplexControllerAnnotation.class)) .by(annotatedClass(ServiceViolatingLayerRules.class).withAnnotationParameterType(SimpleControllerAnnotation.class)) .by(annotatedClass(ServiceViolatingLayerRules.class).withAnnotationParameterType(SomeEnum.class)) + .by(annotatedParameter(Object.class) + .ofMethod(OtherServiceViolatingLayerRules.class, "dependentOnParameterAnnotation", Object.class) + .annotatedWith(UnmarshalTransport.class)) + .by(annotatedParameter(Object.class) + .ofMethod(OtherServiceViolatingLayerRules.class, "dependentOnParameterAnnotation", Object.class) + .withAnnotationParameterType(StringUnmarshaller.class)) + .by(annotatedParameter(Object.class) + .ofMethod(OtherServiceViolatingLayerRules.class, "dependentOnParameterAnnotation", Object.class) + .withAnnotationParameterType(ByteUnmarshaller.class)) .by(method(ComplexServiceAnnotation.class, "controllerAnnotation").withReturnType(ComplexControllerAnnotation.class)) .by(method(ComplexServiceAnnotation.class, "controllerEnum").withReturnType(SomeEnum.class)) @@ -898,6 +913,15 @@ Stream LayerDependencyRulesTest() { .by(annotatedClass(ServiceViolatingLayerRules.class).withAnnotationParameterType(ComplexControllerAnnotation.class)) .by(annotatedClass(ServiceViolatingLayerRules.class).withAnnotationParameterType(SimpleControllerAnnotation.class)) .by(annotatedClass(ServiceViolatingLayerRules.class).withAnnotationParameterType(SomeEnum.class)) + .by(annotatedParameter(Object.class) + .ofMethod(OtherServiceViolatingLayerRules.class, "dependentOnParameterAnnotation", Object.class) + .annotatedWith(UnmarshalTransport.class)) + .by(annotatedParameter(Object.class) + .ofMethod(OtherServiceViolatingLayerRules.class, "dependentOnParameterAnnotation", Object.class) + .withAnnotationParameterType(StringUnmarshaller.class)) + .by(annotatedParameter(Object.class) + .ofMethod(OtherServiceViolatingLayerRules.class, "dependentOnParameterAnnotation", Object.class) + .withAnnotationParameterType(ByteUnmarshaller.class)) .by(method(ComplexServiceAnnotation.class, "controllerAnnotation").withReturnType(ComplexControllerAnnotation.class)) .by(method(ComplexServiceAnnotation.class, "controllerEnum").withReturnType(SomeEnum.class)) @@ -977,6 +1001,15 @@ Stream LayeredArchitectureTest() { .by(annotatedClass(ServiceViolatingLayerRules.class).withAnnotationParameterType(ComplexControllerAnnotation.class)) .by(annotatedClass(ServiceViolatingLayerRules.class).withAnnotationParameterType(SimpleControllerAnnotation.class)) .by(annotatedClass(ServiceViolatingLayerRules.class).withAnnotationParameterType(SomeEnum.class)) + .by(annotatedParameter(Object.class) + .ofMethod(OtherServiceViolatingLayerRules.class, "dependentOnParameterAnnotation", Object.class) + .annotatedWith(UnmarshalTransport.class)) + .by(annotatedParameter(Object.class) + .ofMethod(OtherServiceViolatingLayerRules.class, "dependentOnParameterAnnotation", Object.class) + .withAnnotationParameterType(StringUnmarshaller.class)) + .by(annotatedParameter(Object.class) + .ofMethod(OtherServiceViolatingLayerRules.class, "dependentOnParameterAnnotation", Object.class) + .withAnnotationParameterType(ByteUnmarshaller.class)) .by(method(ComplexServiceAnnotation.class, "controllerAnnotation").withReturnType(ComplexControllerAnnotation.class)) .by(method(ComplexServiceAnnotation.class, "controllerEnum").withReturnType(SomeEnum.class)) .by(method(OtherJpa.class, "testConnection") @@ -1117,6 +1150,10 @@ Stream NamingConventionTest() { .by(simpleNameOf(InheritedControllerImpl.class).notEndingWith("Controller")) .by(simpleNameOf(ComplexControllerAnnotation.class).notEndingWith("Controller")) .by(simpleNameOf(SimpleControllerAnnotation.class).notEndingWith("Controller")) + .by(simpleNameOf(UnmarshalTransport.class).notEndingWith("Controller")) + .by(simpleNameOf(Unmarshaller.class).notEndingWith("Controller")) + .by(simpleNameOf(StringUnmarshaller.class).notEndingWith("Controller")) + .by(simpleNameOf(ByteUnmarshaller.class).notEndingWith("Controller")) .by(simpleNameOf(SomeUtility.class).notEndingWith("Controller")) .by(simpleNameOf(WronglyAnnotated.class).notEndingWith("Controller")) .by(simpleNameOf(SomeEnum.class).notEndingWith("Controller")) @@ -1398,14 +1435,15 @@ Stream SlicesIsolationTest() { // controllers_should_only_use_their_own_slice addExpectedCommonFailureFor_controllers_should_only_use_their_own_slice .accept("controllers_should_only_use_their_own_slice", expectedTestFailures); - expectedTestFailures.by(sliceDependency() - .described("Controller one depends on Controller two") - .by(callFromMethod(UseCaseOneTwoController.class, doSomethingOne) - .toConstructor(UseCaseTwoController.class) - .inLine(10)) - .by(callFromMethod(UseCaseOneTwoController.class, doSomethingOne) - .toMethod(UseCaseTwoController.class, doSomethingTwo) - .inLine(10))) + expectedTestFailures + .by(sliceDependency() + .described("Controller one depends on Controller two") + .by(callFromMethod(UseCaseOneTwoController.class, doSomethingOne) + .toConstructor(UseCaseTwoController.class) + .inLine(10)) + .by(callFromMethod(UseCaseOneTwoController.class, doSomethingOne) + .toMethod(UseCaseTwoController.class, doSomethingTwo) + .inLine(10))) .by(sliceDependency() .described("Controller three depends on Controller one") .by(callFromMethod(UseCaseThreeController.class, doSomethingThree) diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedDependency.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedDependency.java index 8bde882c19..13172db16c 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedDependency.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedDependency.java @@ -11,6 +11,8 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.Iterables.getOnlyElement; +import static com.tngtech.archunit.core.domain.Formatters.formatMethod; +import static com.tngtech.archunit.core.domain.Formatters.formatNamesOf; import static com.tngtech.archunit.core.domain.JavaConstructor.CONSTRUCTOR_NAME; import static java.util.regex.Pattern.quote; @@ -82,6 +84,10 @@ public static AnnotationDependencyCreator annotatedClass(Class clazz) { return new AnnotationDependencyCreator(clazz); } + public static AnnotationParameterDependencyCreator annotatedParameter(Class parameterType) { + return new AnnotationParameterDependencyCreator(parameterType); + } + public static AccessCreator accessFrom(Class clazz) { return new AccessCreator(clazz); } @@ -317,21 +323,40 @@ public ExpectedDependency inLine(int lineNumber) { } } + public static class AnnotationParameterDependencyCreator { + private final Class parameterType; + + private AnnotationParameterDependencyCreator(Class parameterType) { + this.parameterType = parameterType; + } + + public AnnotationDependencyCreator ofMethod(Class originClass, String methodName, Class... paramTypes) { + return new AnnotationDependencyCreator(originClass, "Parameter <" + parameterType.getName() + "> of method <" + + formatMethod(originClass.getName(), methodName, formatNamesOf(paramTypes)) + ">"); + } + } + public static class AnnotationDependencyCreator { - private final Class owner; + private final Class originClass; + private final String originDescription; - AnnotationDependencyCreator(Class owner) { - this.owner = owner; + AnnotationDependencyCreator(Class originClass) { + this(originClass, originClass.getName()); + } + + AnnotationDependencyCreator(Class originClass, String originDescription) { + this.originClass = originClass; + this.originDescription = originDescription; } public ExpectedDependency annotatedWith(Class annotationType) { - String dependencyPattern = getDependencyPattern(owner.getName(), "is annotated with", annotationType.getName(), 0); - return new ExpectedDependency(owner, annotationType, dependencyPattern); + String dependencyPattern = getDependencyPattern(originDescription, "is annotated with", annotationType.getName(), 0); + return new ExpectedDependency(originClass, annotationType, dependencyPattern); } public ExpectedDependency withAnnotationParameterType(Class type) { - String dependencyPattern = getDependencyPattern(owner.getName(), "has annotation member of type", type.getName(), 0); - return new ExpectedDependency(owner, type, dependencyPattern); + String dependencyPattern = getDependencyPattern(originDescription, "has annotation member of type", type.getName(), 0); + return new ExpectedDependency(originClass, type, dependencyPattern); } } } diff --git a/archunit/src/main/java/com/tngtech/archunit/base/DescribedPredicate.java b/archunit/src/main/java/com/tngtech/archunit/base/DescribedPredicate.java index 0e66297c6e..2116c0a8ee 100644 --- a/archunit/src/main/java/com/tngtech/archunit/base/DescribedPredicate.java +++ b/archunit/src/main/java/com/tngtech/archunit/base/DescribedPredicate.java @@ -141,7 +141,7 @@ public static DescribedPredicate> empty() { return new EmptyPredicate(); } - public static DescribedPredicate> anyElementThat(final DescribedPredicate predicate) { + public static DescribedPredicate> anyElementThat(final DescribedPredicate predicate) { return new AnyElementPredicate<>(predicate); } @@ -320,7 +320,7 @@ public boolean apply(Iterable input) { } } - private static class AnyElementPredicate extends DescribedPredicate> { + private static class AnyElementPredicate extends DescribedPredicate> { private final DescribedPredicate predicate; AnyElementPredicate(DescribedPredicate predicate) { @@ -329,7 +329,7 @@ private static class AnyElementPredicate extends DescribedPredicate iterable) { + public boolean apply(Iterable iterable) { for (T javaClass : iterable) { if (predicate.apply(javaClass)) { return true; diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/Dependency.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/Dependency.java index b857131cd1..bde3fb6006 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/Dependency.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/Dependency.java @@ -177,6 +177,10 @@ private static Origin findSuitableOrigin(Object dependencyCause, Object originCa JavaClass clazz = (JavaClass) originCandidate; return new Origin(clazz, clazz.getDescription()); } + if (originCandidate instanceof JavaCodeUnit.Parameter) { + JavaCodeUnit.Parameter parameter = (JavaCodeUnit.Parameter) originCandidate; + return new Origin(parameter.getOwner().getOwner(), parameter.getDescription()); + } throw new IllegalStateException("Could not find suitable dependency origin for " + dependencyCause); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/ImportContext.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/ImportContext.java index 6a9de36649..8ef5c44a59 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/ImportContext.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/ImportContext.java @@ -57,6 +57,4 @@ public interface ImportContext { Set createConstructorCallsFor(JavaCodeUnit codeUnit); JavaClass resolveClass(String fullyQualifiedClassName); - - Optional getMethodReturnType(String declaringClassName, String methodName); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaAnnotation.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaAnnotation.java index f2fb4f2b6a..cb833c70ce 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaAnnotation.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaAnnotation.java @@ -100,7 +100,7 @@ private JavaAnnotation(JavaClass type, OWNER owner, CanBeAnnotated annotatedElem private static CanBeAnnotated getAnnotatedElement(Object owner) { Object candiate = owner; - while (!(candiate instanceof JavaClass) && !(candiate instanceof JavaMember) && (candiate instanceof HasOwner)) { + while (!(candiate instanceof CanBeAnnotated) && (candiate instanceof HasOwner)) { candiate = ((HasOwner) candiate).getOwner(); } if (!(candiate instanceof CanBeAnnotated)) { diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java index d550b9f080..db2d00c9cc 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java @@ -289,10 +289,10 @@ public JavaClass getComponentType() { /** * Returns the component type of this class, if this class is an array, otherwise - * {@link Optional#absent()}. The component type is the type of the elements of an array type. + * {@link Optional#empty()}. The component type is the type of the elements of an array type. * Consider {@code String[]}, then the component type would be {@code String}. * Likewise for {@code String[][]} the component type would be {@code String[]}. - * @return The component type, if this type is an array, otherwise {@link Optional#absent()} + * @return The component type, if this type is an array, otherwise {@link Optional#empty()} */ @PublicAPI(usage = ACCESS) public Optional tryGetComponentType() { @@ -754,7 +754,7 @@ public Set getAllClassesSelfIsAssignableTo() { /** * Returns the enclosing class if this class is nested within another class. - * Otherwise {@link Optional#absent()}.

+ * Otherwise {@link Optional#empty()}.

* Take for example *

      * class OuterClass {
@@ -763,7 +763,7 @@ public Set getAllClassesSelfIsAssignableTo() {
      * }
      * 
* Then {@code InnerClass.}{@link #getEnclosingClass()} would return {@code Optional.of(OuterClass)}. - * While {@code OuterClass.}{@link #getEnclosingClass()} would return {@link Optional#absent()}. + * While {@code OuterClass.}{@link #getEnclosingClass()} would return {@link Optional#empty()}. */ @PublicAPI(usage = ACCESS) public Optional getEnclosingClass() { @@ -772,7 +772,7 @@ public Optional getEnclosingClass() { /** * Returns the enclosing {@link JavaCodeUnit} if this class is declared within the context of a {@link JavaCodeUnit}, - * e.g. a method or a constructor. Otherwise {@link Optional#absent()}.

+ * e.g. a method or a constructor. Otherwise {@link Optional#empty()}.

* Take for example *

      * class OuterClass {
@@ -793,7 +793,7 @@ public Optional getEnclosingClass() {
      * {@code OuterClass()} and {@code LocalClass.}{@link #getEnclosingCodeUnit()} would return the {@link JavaMethod}
      * {@code void someMethod()}.
* On the other hand {@code OuterClass.}{@link #getEnclosingCodeUnit()} or - * {@code InnerClass.}{@link #getEnclosingCodeUnit()} would return {@link Optional#absent()}, since they are + * {@code InnerClass.}{@link #getEnclosingCodeUnit()} would return {@link Optional#empty()}, since they are * not defined within the context of a {@link JavaCodeUnit}. */ @PublicAPI(usage = ACCESS) @@ -845,7 +845,7 @@ public JavaField getField(String name) { } /** - * @return The field with the given name, if this class has such a field, otherwise {@link Optional#absent()}. + * @return The field with the given name, if this class has such a field, otherwise {@link Optional#empty()}. */ @PublicAPI(usage = ACCESS) public Optional tryGetField(String name) { @@ -886,7 +886,7 @@ public JavaCodeUnit getCodeUnitWithParameterTypes(String name, List> pa } /** - * Same as {@link #getCodeUnitWithParameterTypes(String, List)}, but will return {@link Optional#absent()} + * Same as {@link #getCodeUnitWithParameterTypes(String, List)}, but will return {@link Optional#empty()} * if there is no such {@link JavaCodeUnit}. */ @PublicAPI(usage = ACCESS) @@ -903,7 +903,7 @@ public JavaCodeUnit getCodeUnitWithParameterTypeNames(String name, List } /** - * Same as {@link #getCodeUnitWithParameterTypeNames(String, List)}, but will return {@link Optional#absent()} + * Same as {@link #getCodeUnitWithParameterTypeNames(String, List)}, but will return {@link Optional#empty()} * if there is no such {@link JavaCodeUnit}. */ @PublicAPI(usage = ACCESS) @@ -921,7 +921,7 @@ public JavaMethod getMethod(String name) { } /** - * @return The method with the given name and the given parameter types. + * @return The method with the given name and the given raw parameter types. * @throws IllegalArgumentException If this class does not have such a method. */ @PublicAPI(usage = ACCESS) @@ -939,7 +939,7 @@ public JavaMethod getMethod(String name, String... parameters) { /** * @return The method with the given name and with zero parameters, - * if this class has such a method, otherwise {@link Optional#absent()}. + * if this class has such a method, otherwise {@link Optional#empty()}. */ @PublicAPI(usage = ACCESS) public Optional tryGetMethod(String name) { @@ -948,7 +948,7 @@ public Optional tryGetMethod(String name) { /** * @return The method with the given name and the given parameter types, - * if this class has such a method, otherwise {@link Optional#absent()}. + * if this class has such a method, otherwise {@link Optional#empty()}. */ @PublicAPI(usage = ACCESS) public Optional tryGetMethod(String name, Class... parameters) { @@ -983,7 +983,12 @@ public JavaConstructor getConstructor() { } /** - * @return The constructor with the given parameter types. + * @return The constructor with the given parameter types. Note that these are the raw parameter types, which can + * include synthetic parameter types under certain circumstances. For example inner classes have the outer + * class as synthetic first parameter type and enums have a {@code String} name and {@code int} ordinal as + * prepended synthetic parameters. Unfortunately there is no fixed rule, e.g. local class constructors + * can have multiple synthetic parameters appended to the parameters derived from the source code. + * * @throws IllegalArgumentException If this class does not have a constructor with the given parameter types. */ @PublicAPI(usage = ACCESS) @@ -1001,7 +1006,7 @@ public JavaConstructor getConstructor(String... parameters) { /** * @return The constructor with zero parameters, - * if this class has such a constructor, otherwise {@link Optional#absent()}. + * if this class has such a constructor, otherwise {@link Optional#empty()}. */ @PublicAPI(usage = ACCESS) public Optional tryGetConstructor() { @@ -1009,8 +1014,8 @@ public Optional tryGetConstructor() { } /** - * @return The constructor with the given parameter types, - * if this class has such a constructor, otherwise {@link Optional#absent()}. + * @return The constructor with the given parameter types (compare {@link #getConstructor(Class[])}), + * if this class has such a constructor, otherwise {@link Optional#empty()}. */ @PublicAPI(usage = ACCESS) public Optional tryGetConstructor(Class... parameters) { diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDependencies.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDependencies.java index e3794bf5e7..115f6d0a37 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDependencies.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDependencies.java @@ -15,6 +15,7 @@ */ package com.tngtech.archunit.core.domain; +import java.util.Collection; import java.util.List; import java.util.Set; @@ -197,7 +198,9 @@ private Set annotationDependenciesFromSelf() { .addAll(annotationDependencies(javaClass)) .addAll(annotationDependencies(javaClass.getFields())) .addAll(annotationDependencies(javaClass.getMethods())) + .addAll(parameterAnnotationDependencies(javaClass.getMethods())) .addAll(annotationDependencies(javaClass.getConstructors())) + .addAll(parameterAnnotationDependencies(javaClass.getConstructors())) .build(); } @@ -291,7 +294,15 @@ private static Set dependenciesOfWildcardType(JavaWildcardType javaTy return result.build(); } - private > Set annotationDependencies(Set annotatedObjects) { + private Set parameterAnnotationDependencies(Set codeUnits) { + ImmutableSet.Builder result = ImmutableSet.builder(); + for (JavaCodeUnit codeUnit : codeUnits) { + result.addAll(annotationDependencies(codeUnit.getParameters())); + } + return result.build(); + } + + private > Set annotationDependencies(Collection annotatedObjects) { ImmutableSet.Builder result = ImmutableSet.builder(); for (T annotated : annotatedObjects) { result.addAll(annotationDependencies(annotated)); diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaCodeUnit.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaCodeUnit.java index fedcd3c960..4a06b4e942 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaCodeUnit.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaCodeUnit.java @@ -15,28 +15,46 @@ */ package com.tngtech.archunit.core.domain; +import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Set; +import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.base.ChainableFunction; import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.base.ForwardingList; import com.tngtech.archunit.base.Optional; import com.tngtech.archunit.core.MayResolveTypesViaReflection; import com.tngtech.archunit.core.ResolvesTypesViaReflection; +import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; +import com.tngtech.archunit.core.domain.properties.HasAnnotations; +import com.tngtech.archunit.core.domain.properties.HasOwner; import com.tngtech.archunit.core.domain.properties.HasParameterTypes; import com.tngtech.archunit.core.domain.properties.HasReturnType; import com.tngtech.archunit.core.domain.properties.HasThrowsClause; +import com.tngtech.archunit.core.domain.properties.HasType; import com.tngtech.archunit.core.domain.properties.HasTypeParameters; import com.tngtech.archunit.core.importer.DomainBuilders.JavaCodeUnitBuilder; +import com.tngtech.archunit.core.importer.DomainBuilders.JavaCodeUnitBuilder.ParameterAnnotationsBuilder; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +import static com.tngtech.archunit.base.DescribedPredicate.anyElementThat; +import static com.tngtech.archunit.base.DescribedPredicate.equalTo; +import static com.tngtech.archunit.base.Guava.toGuava; import static com.tngtech.archunit.core.domain.Formatters.formatMethod; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.equivalentTo; +import static com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Utils.toAnnotationOfType; +import static com.tngtech.archunit.core.domain.properties.HasName.Functions.GET_NAME; import static com.tngtech.archunit.core.domain.properties.HasName.Utils.namesOf; +import static com.tngtech.archunit.core.domain.properties.HasType.Functions.GET_RAW_TYPE; /** * Represents a unit of code containing accesses to other units of code. A unit of code can be @@ -53,8 +71,7 @@ public abstract class JavaCodeUnit implements HasParameterTypes, HasReturnType, HasTypeParameters, HasThrowsClause { private final JavaType returnType; - private final List rawParameterTypes; - private final List parameterTypes; + private final Parameters parameters; private final String fullName; private final List> typeParameters; private final Set referencedClassObjects; @@ -68,19 +85,12 @@ public abstract class JavaCodeUnit super(builder); typeParameters = builder.getTypeParameters(this); returnType = builder.getReturnType(this); - rawParameterTypes = builder.getRawParameterTypes(); - parameterTypes = getParameterTypes(builder); + parameters = new Parameters(this, builder); fullName = formatMethod(getOwner().getName(), getName(), namesOf(getRawParameterTypes())); referencedClassObjects = ImmutableSet.copyOf(builder.getReferencedClassObjects(this)); instanceofChecks = ImmutableSet.copyOf(builder.getInstanceofChecks(this)); } - @SuppressWarnings({"unchecked", "rawtypes"}) // the cast is safe because the list is immutable, thus used in a covariant way - private List getParameterTypes(JavaCodeUnitBuilder builder) { - List genericParameterTypes = builder.getGenericParameterTypes(this); - return genericParameterTypes.isEmpty() ? (List) rawParameterTypes : genericParameterTypes; - } - /** * @return The full name of this {@link JavaCodeUnit}, i.e. a string containing {@code ${declaringClass}.${name}(${parameterTypes})} */ @@ -90,16 +100,59 @@ public String getFullName() { return fullName; } + /** + * @return the raw parameter types of this {@link JavaCodeUnit}. On the contrary to {@link #getParameterTypes()} + * these will always be {@link JavaClass} and thus not containing any parameterization/generic + * information. Note that the raw parameter types can contain synthetic parameters added by the compiler. + * E.g. for inner class constructors that receive the outer class as synthetic parameter or enum constructors + * that receive enum name and ordinal as synthetic parameters. There is no guarantee about the number + * of synthetic parameters, nor if they are appended or prepended, as e.g. local classes will append + * all local variables from the outer scope that are referenced as additional synthetic constructor + * parameters. + * + * @see #getParameterTypes() + * @see #getParameters() + */ @Override @PublicAPI(usage = ACCESS) public List getRawParameterTypes() { - return rawParameterTypes; + return parameters.getRawParameterTypes(); } + /** + * @return the (possibly generic) parameter types of this {@link JavaCodeUnit}. This could for example be a + * {@link JavaTypeVariable} or a {@link JavaParameterizedType}, but also simply a {@link JavaClass}.
+ * Note that if the method has a generic signature (e.g. declaring a parameterized type) then + * on the contrary to {@link #getRawParameterTypes()} these types will be parsed from the + * signature encoded in the bytecode, i.e. they will not contain synthetic parameters added by the + * compiler. However, if there is no generic signature, then this information can also not be parsed + * from the signature. In this case the parameter types will be equal to the raw parameter types + * and by that can also contain synthetic parameters. + * + * @see #getRawParameterTypes() + * @see #getParameters() + */ @Override @PublicAPI(usage = ACCESS) public List getParameterTypes() { - return parameterTypes; + return parameters.getParameterTypes(); + } + + /** + * @return the {@link Parameter parameters} of this {@link JavaCodeUnit}. On the contrary to the Reflection API this will only contain + * the parameters from the signature and not synthetic parameters, if the signature is generic. In these cases + * {@link #getParameters()}{@code .size()} will always be equal to {@link #getParameterTypes()}{@code .size()}, + * but not necessarily to {@link #getRawParameterTypes()}{@code .size()} in case the compiler adds synthetic parameters.
+ * Note that for non-generic method signatures {@link #getParameters()} actually contains the raw parameter types and thus + * can also contain synthetic parameters. Unfortunately there is no way at the moment to distinguish synthetic + * parameters from non-synthetic parameters in these cases. + * + * @see #getRawParameterTypes() + * @see #getParameterTypes() + */ + @PublicAPI(usage = ACCESS) + public List getParameters() { + return parameters; } @Override @@ -195,6 +248,11 @@ public List> getTypeParameter return typeParameters; } + @PublicAPI(usage = ACCESS) + public List>> getParameterAnnotations() { + return parameters.getAnnotations(); + } + void completeAccessesFrom(ImportContext context) { fieldAccesses = context.createFieldAccessesFor(this); methodCalls = context.createMethodCallsFor(this); @@ -211,6 +269,184 @@ static Class[] reflect(List parameters) { return result.toArray(new Class[0]); } + /** + * A parameter of a {@link JavaCodeUnit}, i.e. encapsulates the raw parameter type, the (possibly) generic + * parameter type and any annotations this parameter has. + */ + @PublicAPI(usage = ACCESS) + public static final class Parameter implements HasType, HasOwner, HasAnnotations { + private static final ChainableFunction GET_ANNOTATION_TYPE_NAME = GET_RAW_TYPE.then(GET_NAME); + private static final Function GUAVA_GET_ANNOTATION_TYPE_NAME = toGuava(GET_ANNOTATION_TYPE_NAME); + + private final JavaCodeUnit owner; + private final int index; + private final JavaType type; + private final JavaClass rawType; + private final Map> annotations; + + private Parameter(JavaCodeUnit owner, ParameterAnnotationsBuilder builder, int index, JavaType type) { + this.owner = owner; + this.index = index; + this.type = type; + this.rawType = type.toErasure(); + this.annotations = buildIndexedByTypeName(builder); + } + + private Map> buildIndexedByTypeName(ParameterAnnotationsBuilder builder) { + Set> annotations = builder.build(this); + return Maps.uniqueIndex(annotations, GUAVA_GET_ANNOTATION_TYPE_NAME); + } + + @Override + @PublicAPI(usage = ACCESS) + public JavaCodeUnit getOwner() { + return owner; + } + + @PublicAPI(usage = ACCESS) + public int getIndex() { + return index; + } + + @Override + @PublicAPI(usage = ACCESS) + public JavaType getType() { + return type; + } + + @Override + @PublicAPI(usage = ACCESS) + public JavaClass getRawType() { + return rawType; + } + + @Override + public boolean isAnnotatedWith(Class annotationType) { + return annotations.containsKey(annotationType.getName()); + } + + @Override + public boolean isAnnotatedWith(String annotationTypeName) { + return annotations.containsKey(annotationTypeName); + } + + @Override + public boolean isAnnotatedWith(DescribedPredicate> predicate) { + return anyElementThat(predicate).apply(annotations.values()); + } + + @Override + public boolean isMetaAnnotatedWith(Class annotationType) { + return isMetaAnnotatedWith(GET_RAW_TYPE.is(equivalentTo(annotationType))); + } + + @Override + public boolean isMetaAnnotatedWith(String annotationTypeName) { + return isMetaAnnotatedWith(GET_ANNOTATION_TYPE_NAME.is(equalTo(annotationTypeName))); + } + + @Override + public boolean isMetaAnnotatedWith(DescribedPredicate> predicate) { + return CanBeAnnotated.Utils.isMetaAnnotatedWith(annotations.values(), predicate); + } + + @Override + public Set> getAnnotations() { + return ImmutableSet.copyOf(annotations.values()); + } + + @Override + public A getAnnotationOfType(Class type) { + return getAnnotationOfType(type.getName()).as(type); + } + + @Override + public JavaAnnotation getAnnotationOfType(String typeName) { + Optional> annotation = tryGetAnnotationOfType(typeName); + if (!annotation.isPresent()) { + throw new IllegalArgumentException(String.format("%s is not annotated with @%s", getDescription(), typeName)); + } + return annotation.get(); + } + + @Override + public Optional tryGetAnnotationOfType(Class type) { + return tryGetAnnotationOfType(type.getName()).map(toAnnotationOfType(type)); + } + + @Override + public Optional> tryGetAnnotationOfType(String typeName) { + return Optional.ofNullable(annotations.get(typeName)); + } + + @Override + @PublicAPI(usage = ACCESS) + public String getDescription() { + return "Parameter <" + type.getName() + "> of " + startWithLowercase(owner.getDescription()); + } + + @Override + public String toString() { + return "JavaParameter{owner='" + owner.getFullName() + "', index='" + index + "', type='" + type.getName() + "'}"; + } + + static String startWithLowercase(String string) { + return Character.toLowerCase(string.charAt(0)) + string.substring(1); + } + } + + private static class Parameters extends ForwardingList { + private final List rawParameterTypes; + private final List parameterTypes; + private final List>> parameterAnnotations; + private final List parameters; + + Parameters(JavaCodeUnit owner, JavaCodeUnitBuilder builder) { + rawParameterTypes = builder.getRawParameterTypes(); + parameterTypes = getParameterTypes(builder.getGenericParameterTypes(owner)); + parameters = createParameters(owner, builder, parameterTypes); + parameterAnnotations = annotationsOf(parameters); + } + + private List>> annotationsOf(List parameters) { + ImmutableList.Builder>> result = ImmutableList.builder(); + for (Parameter parameter : parameters) { + result.add(parameter.getAnnotations()); + } + return result.build(); + } + + private static List createParameters(JavaCodeUnit owner, JavaCodeUnitBuilder builder, List parameterTypes) { + ImmutableList.Builder result = ImmutableList.builder(); + for (int i = 0; i < parameterTypes.size(); i++) { + result.add(new Parameter(owner, builder.getParameterAnnotationsBuilder(i), i, parameterTypes.get(i))); + } + return result.build(); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) // the cast is safe because the list is immutable, thus used in a covariant way + private List getParameterTypes(List genericParameterTypes) { + return genericParameterTypes.isEmpty() ? (List) rawParameterTypes : genericParameterTypes; + } + + List getRawParameterTypes() { + return rawParameterTypes; + } + + List getParameterTypes() { + return parameterTypes; + } + + List>> getAnnotations() { + return parameterAnnotations; + } + + @Override + protected List delegate() { + return parameters; + } + } + public static final class Predicates { private Predicates() { } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileImportRecord.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileImportRecord.java index 7e8af92cd0..b725c55a8c 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileImportRecord.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileImportRecord.java @@ -31,6 +31,8 @@ import com.tngtech.archunit.core.domain.JavaMember; import com.tngtech.archunit.core.domain.JavaMethod; import com.tngtech.archunit.core.importer.DomainBuilders.JavaClassTypeParametersBuilder; +import com.tngtech.archunit.core.importer.DomainBuilders.JavaCodeUnitBuilder; +import com.tngtech.archunit.core.importer.DomainBuilders.JavaMemberBuilder; import com.tngtech.archunit.core.importer.DomainBuilders.JavaParameterizedTypeBuilder; import com.tngtech.archunit.core.importer.DomainBuilders.JavaTypeParameterBuilder; import com.tngtech.archunit.core.importer.RawAccessRecord.CodeUnit; @@ -194,14 +196,14 @@ Set getAnnotationsFor(JavaClass owner) { } Set getMemberAnnotationTypeNamesFor(JavaClass owner) { - Iterable> memberBuilders = Iterables.concat( + Iterable> memberBuilders = Iterables.concat( fieldBuildersByOwner.get(owner.getName()), methodBuildersByOwner.get(owner.getName()), constructorBuildersByOwner.get(owner.getName()), nullToEmpty(staticInitializerBuildersByOwner.get(owner.getName()))); ImmutableSet.Builder result = ImmutableSet.builder(); - for (DomainBuilders.JavaMemberBuilder memberBuilder : memberBuilders) { + for (JavaMemberBuilder memberBuilder : memberBuilders) { for (DomainBuilders.JavaAnnotationBuilder annotationBuilder : annotationsByOwner.get(getMemberKey(owner.getName(), memberBuilder.getName(), memberBuilder.getDescriptor()))) { result.add(annotationBuilder.getFullyQualifiedClassName()); } @@ -209,10 +211,24 @@ Set getMemberAnnotationTypeNamesFor(JavaClass owner) { return result.build(); } - private Iterable> nullToEmpty(DomainBuilders.JavaStaticInitializerBuilder staticInitializerBuilder) { + Set getParameterAnnotationTypeNamesFor(JavaClass owner) { + Iterable> codeUnitBuilders = Iterables.>concat( + methodBuildersByOwner.get(owner.getName()), + constructorBuildersByOwner.get(owner.getName())); + + ImmutableSet.Builder result = ImmutableSet.builder(); + for (JavaCodeUnitBuilder codeUnitBuilder : codeUnitBuilders) { + for (DomainBuilders.JavaAnnotationBuilder annotationBuilder : codeUnitBuilder.getParameterAnnotationBuilders()) { + result.add(annotationBuilder.getFullyQualifiedClassName()); + } + } + return result.build(); + } + + private Iterable> nullToEmpty(DomainBuilders.JavaStaticInitializerBuilder staticInitializerBuilder) { return staticInitializerBuilder != null - ? Collections.>singleton(staticInitializerBuilder) - : Collections.>emptySet(); + ? Collections.>singleton(staticInitializerBuilder) + : Collections.>emptySet(); } Set getAnnotationsFor(JavaMember owner) { diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassGraphCreator.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassGraphCreator.java index 9db909f2fe..d774beb98f 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassGraphCreator.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassGraphCreator.java @@ -52,6 +52,7 @@ import com.tngtech.archunit.core.importer.DomainBuilders.JavaFieldAccessBuilder; import com.tngtech.archunit.core.importer.DomainBuilders.JavaMethodCallBuilder; import com.tngtech.archunit.core.importer.DomainBuilders.JavaParameterizedTypeBuilder; +import com.tngtech.archunit.core.importer.ImportedClasses.MethodReturnTypeGetter; import com.tngtech.archunit.core.importer.RawAccessRecord.CodeUnit; import com.tngtech.archunit.core.importer.resolvers.ClassResolver; @@ -79,7 +80,12 @@ class ClassGraphCreator implements ImportContext { ClassGraphCreator(ClassFileImportRecord importRecord, ClassResolver classResolver) { this.importRecord = importRecord; - classes = new ImportedClasses(importRecord.getClasses(), classResolver); + classes = new ImportedClasses(importRecord.getClasses(), classResolver, new MethodReturnTypeGetter() { + @Override + public Optional getReturnType(String declaringClassName, String methodName) { + return getMethodReturnType(declaringClassName, methodName); + } + }); superclassStrategy = createSuperclassStrategy(); interfaceStrategy = createInterfaceStrategy(); } @@ -185,6 +191,7 @@ private Set getAnnotationTypeNamesToResolveFor(JavaClass javaClass) { return ImmutableSet.builder() .addAll(importRecord.getAnnotationTypeNamesFor(javaClass)) .addAll(importRecord.getMemberAnnotationTypeNamesFor(javaClass)) + .addAll(importRecord.getParameterAnnotationTypeNamesFor(javaClass)) .build(); } @@ -246,7 +253,7 @@ public Optional createSuperclass(JavaClass owner) { public Optional createGenericSuperclass(JavaClass owner) { Optional> genericSuperclassBuilder = importRecord.getGenericSuperclassFor(owner); return genericSuperclassBuilder.isPresent() - ? Optional.of(genericSuperclassBuilder.get().build(owner, getTypeParametersInContextOf(owner), classes.byTypeName())) + ? Optional.of(genericSuperclassBuilder.get().build(owner, getTypeParametersInContextOf(owner), classes)) : Optional.empty(); } @@ -259,7 +266,7 @@ public Optional> createGenericInterfaces(JavaClass owner) { ImmutableSet.Builder result = ImmutableSet.builder(); for (JavaParameterizedTypeBuilder builder : genericInterfaceBuilders.get()) { - result.add(builder.build(owner, getTypeParametersInContextOf(owner), classes.byTypeName())); + result.add(builder.build(owner, getTypeParametersInContextOf(owner), classes)); } return Optional.>of(result.build()); } @@ -285,12 +292,12 @@ public Set createInterfaces(JavaClass owner) { @Override public List> createTypeParameters(JavaClass owner) { JavaClassTypeParametersBuilder typeParametersBuilder = importRecord.getTypeParameterBuildersFor(owner.getName()); - return typeParametersBuilder.build(owner, classes.byTypeName()); + return typeParametersBuilder.build(owner, classes); } @Override public Set createFields(JavaClass owner) { - return build(importRecord.getFieldBuildersFor(owner.getName()), owner, classes.byTypeName()); + return build(importRecord.getFieldBuildersFor(owner.getName()), owner, classes); } @Override @@ -302,17 +309,17 @@ public Set createMethods(JavaClass owner) { @Override public Optional apply(JavaMethod method) { Optional defaultValueBuilder = importRecord.getAnnotationDefaultValueBuilderFor(method); - return defaultValueBuilder.isPresent() ? defaultValueBuilder.get().build(method, ClassGraphCreator.this) : Optional.empty(); + return defaultValueBuilder.isPresent() ? defaultValueBuilder.get().build(method, classes) : Optional.empty(); } }); } } - return build(methodBuilders, owner, classes.byTypeName()); + return build(methodBuilders, owner, classes); } @Override public Set createConstructors(JavaClass owner) { - return build(importRecord.getConstructorBuildersFor(owner.getName()), owner, classes.byTypeName()); + return build(importRecord.getConstructorBuildersFor(owner.getName()), owner, classes); } @Override @@ -321,7 +328,7 @@ public Optional createStaticInitializer(JavaClass owner) if (!builder.isPresent()) { return Optional.empty(); } - JavaStaticInitializer staticInitializer = builder.get().build(owner, classes.byTypeName()); + JavaStaticInitializer staticInitializer = builder.get().build(owner, classes); return Optional.of(staticInitializer); } @@ -336,7 +343,7 @@ public Map> createAnnotations(JavaMember owne } private Map> createAnnotations(OWNER owner, Set annotationBuilders) { - return buildAnnotations(owner, annotationBuilders, this); + return buildAnnotations(owner, annotationBuilders, classes); } @Override @@ -364,8 +371,7 @@ public JavaClass resolveClass(String fullyQualifiedClassName) { return classes.getOrResolve(fullyQualifiedClassName); } - @Override - public Optional getMethodReturnType(String declaringClassName, String methodName) { + private Optional getMethodReturnType(String declaringClassName, String methodName) { for (DomainBuilders.JavaMethodBuilder methodBuilder : importRecord.getMethodBuildersFor(declaringClassName)) { if (methodBuilder.getName().equals(methodName) && methodBuilder.hasNoParameters()) { return Optional.of(classes.getOrResolve(methodBuilder.getReturnTypeName())); diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassesByTypeName.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassesByTypeName.java deleted file mode 100644 index f2911b58da..0000000000 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassesByTypeName.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2014-2021 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.importer; - -import com.tngtech.archunit.core.domain.JavaClass; - -interface ClassesByTypeName { - JavaClass get(String typeName); -} diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/DomainBuilders.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/DomainBuilders.java index cfd101d27f..b3c5de5b94 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/DomainBuilders.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/DomainBuilders.java @@ -29,6 +29,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.SetMultimap; import com.tngtech.archunit.Internal; import com.tngtech.archunit.base.Function; import com.tngtech.archunit.base.HasDescription; @@ -39,7 +40,6 @@ import com.tngtech.archunit.core.domain.AccessTarget.MethodCallTarget; import com.tngtech.archunit.core.domain.DomainObjectCreationContext; import com.tngtech.archunit.core.domain.Formatters; -import com.tngtech.archunit.core.domain.ImportContext; import com.tngtech.archunit.core.domain.InstanceofCheck; import com.tngtech.archunit.core.domain.JavaAnnotation; import com.tngtech.archunit.core.domain.JavaClass; @@ -84,10 +84,10 @@ public final class DomainBuilders { private DomainBuilders() { } - static Map> buildAnnotations(T owner, Set annotations, ImportContext importContext) { + static Map> buildAnnotations(T owner, Set annotations, ImportedClasses importedClasses) { ImmutableMap.Builder> result = ImmutableMap.builder(); for (JavaAnnotationBuilder annotationBuilder : annotations) { - JavaAnnotation javaAnnotation = annotationBuilder.build(owner, importContext); + JavaAnnotation javaAnnotation = annotationBuilder.build(owner, importedClasses); result.put(javaAnnotation.getRawType().getName(), javaAnnotation); } return result.build(); @@ -132,7 +132,7 @@ public abstract static class JavaMemberBuilder modifiers; private JavaClass owner; - ClassesByTypeName importedClasses; + ImportedClasses importedClasses; private int firstLineNumber; private JavaMemberBuilder() { @@ -162,10 +162,10 @@ SELF self() { return (SELF) this; } - abstract OUTPUT construct(SELF self, ClassesByTypeName importedClasses); + abstract OUTPUT construct(SELF self, ImportedClasses importedClasses); JavaClass get(String typeName) { - return importedClasses.get(typeName); + return importedClasses.getOrResolve(typeName); } public String getName() { @@ -189,7 +189,7 @@ public int getFirstLineNumber() { } @Override - public final OUTPUT build(JavaClass owner, ClassesByTypeName importedClasses) { + public final OUTPUT build(JavaClass owner, ImportedClasses importedClasses) { this.owner = owner; this.importedClasses = importedClasses; return construct(self(), importedClasses); @@ -217,7 +217,7 @@ String getTypeName() { public JavaType getType(JavaField field) { return genericType.isPresent() ? genericType.get().finish(field, allTypeParametersInContextOf(field.getOwner()), importedClasses) - : importedClasses.get(rawType.getFullyQualifiedClassName()); + : importedClasses.getOrResolve(rawType.getFullyQualifiedClassName()); } private static Iterable> allTypeParametersInContextOf(JavaClass javaClass) { @@ -225,7 +225,7 @@ private static Iterable> allTypeParametersInContextOf(JavaCl } @Override - JavaField construct(JavaFieldBuilder builder, ClassesByTypeName importedClasses) { + JavaField construct(JavaFieldBuilder builder, ImportedClasses importedClasses) { return DomainObjectCreationContext.createJavaField(builder); } } @@ -236,6 +236,7 @@ public abstract static class JavaCodeUnitBuilder> genericParameterTypes; private List rawParameterTypes; + private SetMultimap parameterAnnotationsByIndex; private JavaCodeUnitTypeParametersBuilder typeParametersBuilder; private List throwsDeclarations; private final Set rawReferencedClassObjects = new HashSet<>(); @@ -256,6 +257,11 @@ SELF withParameterTypes(List> genericParam return self(); } + SELF withParameterAnnotations(SetMultimap parameterAnnotationsByIndex) { + this.parameterAnnotationsByIndex = parameterAnnotationsByIndex; + return self(); + } + SELF withTypeParameters(List> typeParameterBuilders) { this.typeParametersBuilder = new JavaCodeUnitTypeParametersBuilder(typeParameterBuilders); return self(); @@ -318,6 +324,10 @@ private List build(List> generic return result.build(); } + public Set getParameterAnnotations(int index) { + return parameterAnnotationsByIndex.get(index); + } + public List> getTypeParameters(JavaCodeUnit owner) { return typeParametersBuilder.build(owner, importedClasses); } @@ -349,6 +359,33 @@ private List asJavaClasses(List descriptors) { } return result.build(); } + + public ParameterAnnotationsBuilder getParameterAnnotationsBuilder(int index) { + return new ParameterAnnotationsBuilder(parameterAnnotationsByIndex.get(index), importedClasses); + } + + public Iterable getParameterAnnotationBuilders() { + return parameterAnnotationsByIndex.values(); + } + + @Internal + public static class ParameterAnnotationsBuilder { + private final Iterable annotationBuilders; + private final ImportedClasses importedClasses; + + private ParameterAnnotationsBuilder(Iterable annotationBuilders, ImportedClasses importedClasses) { + this.annotationBuilders = annotationBuilders; + this.importedClasses = importedClasses; + } + + public Set> build(JavaCodeUnit.Parameter owner) { + ImmutableSet.Builder> result = ImmutableSet.builder(); + for (DomainBuilders.JavaAnnotationBuilder annotationBuilder : annotationBuilders) { + result.add(annotationBuilder.build(owner, importedClasses)); + } + return result.build(); + } + } } @Internal @@ -369,7 +406,7 @@ void withAnnotationDefaultValue(Function> createAnn } @Override - JavaMethod construct(JavaMethodBuilder builder, final ClassesByTypeName importedClasses) { + JavaMethod construct(JavaMethodBuilder builder, final ImportedClasses importedClasses) { return DomainObjectCreationContext.createJavaMethod(builder, createAnnotationDefaultValue); } } @@ -380,7 +417,7 @@ public static final class JavaConstructorBuilder extends JavaCodeUnitBuilder getModifiers() { public static final class JavaAnnotationBuilder { private JavaClassDescriptor type; private final Map values = new LinkedHashMap<>(); - private ImportContext importContext; + private ImportedClasses importedClasses; JavaAnnotationBuilder() { } @@ -523,13 +560,13 @@ JavaAnnotationBuilder addProperty(String key, ValueBuilder valueBuilder) { } public JavaClass getType() { - return importContext.resolveClass(type.getFullyQualifiedClassName()); + return importedClasses.getOrResolve(type.getFullyQualifiedClassName()); } public Map getValues(T owner) { ImmutableMap.Builder result = ImmutableMap.builder(); for (Map.Entry entry : values.entrySet()) { - Optional value = entry.getValue().build(owner, importContext); + Optional value = entry.getValue().build(owner, importedClasses); if (value.isPresent()) { result.put(entry.getKey(), value.get()); } @@ -537,18 +574,18 @@ public Map getValues(T owner) { return result.build(); } - public JavaAnnotation build(T owner, ImportContext importContext) { - this.importContext = importContext; + public JavaAnnotation build(T owner, ImportedClasses importedClasses) { + this.importedClasses = importedClasses; return DomainObjectCreationContext.createJavaAnnotation(owner, this); } abstract static class ValueBuilder { - abstract Optional build(T owner, ImportContext importContext); + abstract Optional build(T owner, ImportedClasses importedClasses); static ValueBuilder ofFinished(final Object value) { return new ValueBuilder() { @Override - Optional build(T owner, ImportContext unused) { + Optional build(T owner, ImportedClasses unused) { return Optional.of(value); } }; @@ -557,8 +594,8 @@ Optional build(T owner, ImportContext unused) static ValueBuilder from(final JavaAnnotationBuilder builder) { return new ValueBuilder() { @Override - Optional build(T owner, ImportContext importContext) { - return Optional.of(builder.build(owner, importContext)); + Optional build(T owner, ImportedClasses importedClasses) { + return Optional.of(builder.build(owner, importedClasses)); } }; } @@ -577,26 +614,26 @@ public static final class JavaStaticInitializerBuilder extends JavaCodeUnitBuild } @Override - JavaStaticInitializer construct(JavaStaticInitializerBuilder builder, ClassesByTypeName importedClasses) { + JavaStaticInitializer construct(JavaStaticInitializerBuilder builder, ImportedClasses importedClasses) { return DomainObjectCreationContext.createJavaStaticInitializer(builder); } } interface JavaTypeCreationProcess { - JavaType finish(OWNER owner, Iterable> allTypeParametersInContext, ClassesByTypeName classes); + JavaType finish(OWNER owner, Iterable> allTypeParametersInContext, ImportedClasses classes); abstract class JavaTypeFinisher { private JavaTypeFinisher() { } - abstract JavaType finish(JavaType input, ClassesByTypeName classes); + abstract JavaType finish(JavaType input, ImportedClasses classes); abstract String getFinishedName(String name); JavaTypeFinisher after(final JavaTypeFinisher other) { return new JavaTypeFinisher() { @Override - JavaType finish(JavaType input, ClassesByTypeName classes) { + JavaType finish(JavaType input, ImportedClasses classes) { return JavaTypeFinisher.this.finish(other.finish(input, classes), classes); } @@ -609,7 +646,7 @@ String getFinishedName(String name) { static JavaTypeFinisher IDENTITY = new JavaTypeFinisher() { @Override - JavaType finish(JavaType input, ClassesByTypeName classes) { + JavaType finish(JavaType input, ImportedClasses classes) { return input; } @@ -621,13 +658,13 @@ String getFinishedName(String name) { static final JavaTypeFinisher ARRAY_CREATOR = new JavaTypeFinisher() { @Override - public JavaType finish(JavaType componentType, ClassesByTypeName classes) { + public JavaType finish(JavaType componentType, ImportedClasses classes) { JavaClassDescriptor erasureType = JavaClassDescriptor.From.javaClass(componentType.toErasure()).toArrayDescriptor(); if (componentType instanceof JavaClass) { - return classes.get(erasureType.getFullyQualifiedClassName()); + return classes.getOrResolve(erasureType.getFullyQualifiedClassName()); } - JavaClass erasure = classes.get(erasureType.getFullyQualifiedClassName()); + JavaClass erasure = classes.getOrResolve(erasureType.getFullyQualifiedClassName()); return createGenericArrayType(componentType, erasure); } @@ -644,7 +681,7 @@ public static final class JavaTypeParameterBuilder private final String name; private final List> upperBounds = new ArrayList<>(); private OWNER owner; - private ClassesByTypeName importedClasses; + private ImportedClasses importedClasses; JavaTypeParameterBuilder(String name) { this.name = checkNotNull(name); @@ -654,10 +691,10 @@ void addBound(JavaTypeCreationProcess bound) { upperBounds.add(bound); } - public JavaTypeVariable build(OWNER owner, ClassesByTypeName importedClasses) { + public JavaTypeVariable build(OWNER owner, ImportedClasses importedClasses) { this.owner = owner; this.importedClasses = importedClasses; - return createTypeVariable(name, owner, this.importedClasses.get(Object.class.getName())); + return createTypeVariable(name, owner, this.importedClasses.getOrResolve(Object.class.getName())); } String getName() { @@ -677,14 +714,14 @@ private static abstract class AbstractTypeParametersBuilder> build(OWNER owner, ClassesByTypeName classesByTypeName) { + final List> build(OWNER owner, ImportedClasses ImportedClasses) { if (typeParameterBuilders.isEmpty()) { return Collections.emptyList(); } Map, JavaTypeParameterBuilder> typeArgumentsToBuilders = new LinkedHashMap<>(); for (JavaTypeParameterBuilder builder : typeParameterBuilders) { - typeArgumentsToBuilders.put(builder.build(owner, classesByTypeName), builder); + typeArgumentsToBuilders.put(builder.build(owner, ImportedClasses), builder); } Set> allGenericParametersInContext = union(typeParametersFromEnclosingContextOf(owner), typeArgumentsToBuilders.keySet()); for (Map.Entry, JavaTypeParameterBuilder> typeParameterToBuilder : typeArgumentsToBuilders.entrySet()) { @@ -747,7 +784,7 @@ private static Set> allTypeParametersInEnclosingContextOf(Ja } interface JavaTypeBuilder { - JavaType build(OWNER owner, Iterable> allTypeParametersInContext, ClassesByTypeName importedClasses); + JavaType build(OWNER owner, Iterable> allTypeParametersInContext, ImportedClasses importedClasses); } @Internal @@ -756,7 +793,7 @@ public static final class JavaWildcardTypeBuilder private final List> upperBoundCreationProcesses = new ArrayList<>(); private OWNER owner; private Iterable> allTypeParametersInContext; - private ClassesByTypeName importedClasses; + private ImportedClasses importedClasses; JavaWildcardTypeBuilder() { } @@ -772,7 +809,7 @@ public JavaWildcardTypeBuilder addUpperBound(JavaTypeCreationProcess> allTypeParametersInContext, ClassesByTypeName importedClasses) { + public JavaWildcardType build(OWNER owner, Iterable> allTypeParametersInContext, ImportedClasses importedClasses) { this.owner = owner; this.allTypeParametersInContext = allTypeParametersInContext; this.importedClasses = importedClasses; @@ -805,11 +842,11 @@ void addTypeArgument(JavaTypeCreationProcess typeCreationProcess) { } @Override - public JavaType build(OWNER owner, Iterable> allTypeParametersInContext, ClassesByTypeName classes) { + public JavaType build(OWNER owner, Iterable> allTypeParametersInContext, ImportedClasses classes) { List typeArguments = buildJavaTypes(typeArgumentCreationProcesses, owner, allTypeParametersInContext, classes); return typeArguments.isEmpty() - ? classes.get(type.getFullyQualifiedClassName()) - : new ImportedParameterizedType(classes.get(type.getFullyQualifiedClassName()), typeArguments); + ? classes.getOrResolve(type.getFullyQualifiedClassName()) + : new ImportedParameterizedType(classes.getOrResolve(type.getFullyQualifiedClassName()), typeArguments); } String getTypeName() { @@ -822,7 +859,7 @@ JavaParameterizedTypeBuilder forInnerClass(String simpleInnerClassName) { } } - private static List buildJavaTypes(List> typeCreationProcesses, OWNER owner, Iterable> allGenericParametersInContext, ClassesByTypeName classes) { + private static List buildJavaTypes(List> typeCreationProcesses, OWNER owner, Iterable> allGenericParametersInContext, ImportedClasses classes) { ImmutableList.Builder result = ImmutableList.builder(); for (JavaTypeCreationProcess typeCreationProcess : typeCreationProcesses) { result.add(typeCreationProcess.finish(owner, allGenericParametersInContext, classes)); @@ -830,22 +867,22 @@ private static List buildJavaTypes(List upperBounds, ClassesByTypeName importedClasses) { + private static JavaClass getUnboundErasureType(List upperBounds, ImportedClasses importedClasses) { return upperBounds.size() > 0 ? upperBounds.get(0).toErasure() - : importedClasses.get(Object.class.getName()); + : importedClasses.getOrResolve(Object.class.getName()); } @Internal interface BuilderWithBuildParameter { - VALUE build(PARAMETER parameter, ClassesByTypeName importedClasses); + VALUE build(PARAMETER parameter, ImportedClasses importedClasses); @Internal class BuildFinisher { static Set build( Set> builders, PARAMETER parameter, - ClassesByTypeName importedClasses) { + ImportedClasses importedClasses) { checkNotNull(builders); checkNotNull(parameter); diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/ImportedClasses.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/ImportedClasses.java index ee093722cf..d8cb2f62ac 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/ImportedClasses.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/ImportedClasses.java @@ -40,11 +40,13 @@ class ImportedClasses { private final ImmutableMap directlyImported; private final Map allClasses = new HashMap<>(); private final ClassResolver resolver; + private final MethodReturnTypeGetter getMethodReturnType; - ImportedClasses(Map directlyImported, ClassResolver resolver) { + ImportedClasses(Map directlyImported, ClassResolver resolver, MethodReturnTypeGetter methodReturnTypeGetter) { this.directlyImported = ImmutableMap.copyOf(directlyImported); allClasses.putAll(directlyImported); this.resolver = resolver; + this.getMethodReturnType = methodReturnTypeGetter; } Map getDirectlyImported() { @@ -73,15 +75,6 @@ Collection getAllWithOuterClassesSortedBeforeInnerClasses() { return ImmutableSortedMap.copyOf(allClasses).values(); } - ClassesByTypeName byTypeName() { - return new ClassesByTypeName() { - @Override - public JavaClass get(String typeName) { - return ImportedClasses.this.getOrResolve(typeName); - } - }; - } - private static JavaClass simpleClassOf(String typeName) { JavaClassDescriptor descriptor = JavaClassDescriptor.From.name(typeName); DomainBuilders.JavaClassBuilder builder = new DomainBuilders.JavaClassBuilder().withDescriptor(descriptor); @@ -94,4 +87,12 @@ private static void addModifiersIfPossible(DomainBuilders.JavaClassBuilder build builder.withModifiers(PRIMITIVE_AND_ARRAY_TYPE_MODIFIERS); } } + + public Optional getMethodReturnType(String declaringClassName, String methodName) { + return getMethodReturnType.getReturnType(declaringClassName, methodName); + } + + interface MethodReturnTypeGetter { + Optional getReturnType(String declaringClassName, String methodName); + } } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaClassProcessor.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaClassProcessor.java index 9a78696bd5..051964c104 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaClassProcessor.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaClassProcessor.java @@ -22,8 +22,10 @@ import java.util.List; import java.util.Set; +import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.SetMultimap; import com.google.common.primitives.Booleans; import com.google.common.primitives.Bytes; import com.google.common.primitives.Chars; @@ -36,7 +38,6 @@ import com.tngtech.archunit.base.HasDescription; import com.tngtech.archunit.base.Optional; import com.tngtech.archunit.core.MayResolveTypesViaReflection; -import com.tngtech.archunit.core.domain.ImportContext; import com.tngtech.archunit.core.domain.JavaAnnotation; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaClassDescriptor; @@ -325,6 +326,7 @@ private static class MethodProcessor extends MethodVisitor { private final DomainBuilders.JavaCodeUnitBuilder codeUnitBuilder; private final DeclarationHandler declarationHandler; private final Set annotations = new HashSet<>(); + private final SetMultimap parameterAnnotationsByIndex = HashMultimap.create(); private int actualLineNumber; MethodProcessor(String declaringClassName, AccessHandler accessHandler, DomainBuilders.JavaCodeUnitBuilder codeUnitBuilder, DeclarationHandler declarationHandler) { @@ -333,6 +335,7 @@ private static class MethodProcessor extends MethodVisitor { this.accessHandler = accessHandler; this.codeUnitBuilder = codeUnitBuilder; this.declarationHandler = declarationHandler; + codeUnitBuilder.withParameterAnnotations(parameterAnnotationsByIndex); } @Override @@ -340,6 +343,11 @@ public void visitCode() { actualLineNumber = 0; } + @Override + public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) { + return new AnnotationProcessor(addAnnotationAtIndex(parameterAnnotationsByIndex, parameter), annotationBuilderFor(desc)); + } + // NOTE: ASM does not reliably visit this method, so if this method is skipped, line number 0 is recorded @Override public void visitLineNumber(int line, Label start) { @@ -606,6 +614,15 @@ public void add(DomainBuilders.JavaAnnotationBuilder annotation) { }; } + private static TakesAnnotationBuilder addAnnotationAtIndex(final SetMultimap annotations, final int index) { + return new TakesAnnotationBuilder() { + @Override + public void add(DomainBuilders.JavaAnnotationBuilder annotation) { + annotations.put(index, annotation); + } + }; + } + private static TakesAnnotationBuilder addAnnotationAsProperty(final String name, final DomainBuilders.JavaAnnotationBuilder annotationBuilder) { return new TakesAnnotationBuilder() { @Override @@ -674,7 +691,7 @@ public void visitEnd() { private class ArrayValueBuilder extends ValueBuilder { @Override - public Optional build(T owner, ImportContext importContext) { + public Optional build(T owner, ImportedClasses importContext) { Optional> componentType = determineComponentType(importContext); if (!componentType.isPresent()) { return Optional.empty(); @@ -705,7 +722,7 @@ private Object toArray(Class componentType, List values) { return values.toArray((Object[]) Array.newInstance(componentType, values.size())); } - private List buildValues(T owner, ImportContext importContext) { + private List buildValues(T owner, ImportedClasses importContext) { List result = new ArrayList<>(); for (ValueBuilder value : values) { result.addAll(value.build(owner, importContext).asSet()); @@ -713,7 +730,7 @@ private List buildValues(T owner, ImportConte return result; } - private Optional> determineComponentType(ImportContext importContext) { + private Optional> determineComponentType(ImportedClasses importContext) { if (derivedComponentType != null) { return Optional.>of(derivedComponentType); } @@ -776,10 +793,10 @@ private interface AnnotationArrayContext { private static ValueBuilder javaEnumBuilder(final String desc, final String value) { return new ValueBuilder() { @Override - public Optional build(T owner, ImportContext importContext) { + public Optional build(T owner, ImportedClasses importContext) { return Optional.of( new DomainBuilders.JavaEnumConstantBuilder() - .withDeclaringClass(importContext.resolveClass(JavaClassDescriptorImporter.importAsmTypeFromDescriptor(desc).getFullyQualifiedClassName())) + .withDeclaringClass(importContext.getOrResolve(JavaClassDescriptorImporter.importAsmTypeFromDescriptor(desc).getFullyQualifiedClassName())) .withName(value) .build()); } @@ -792,8 +809,8 @@ static ValueBuilder convert(Object input) { if (value instanceof JavaClassDescriptor) { return new ValueBuilder() { @Override - public Optional build(T owner, ImportContext importContext) { - return Optional.of(importContext.resolveClass(((JavaClassDescriptor) value).getFullyQualifiedClassName())); + public Optional build(T owner, ImportedClasses importContext) { + return Optional.of(importContext.getOrResolve(((JavaClassDescriptor) value).getFullyQualifiedClassName())); } }; } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/SignatureTypeArgumentProcessor.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/SignatureTypeArgumentProcessor.java index fb44fc60bd..4aeb67f712 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/SignatureTypeArgumentProcessor.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/SignatureTypeArgumentProcessor.java @@ -161,7 +161,7 @@ static class NewJavaTypeCreationProcess implements } @Override - public JavaType finish(OWNER owner, Iterable> allTypeParametersInContext, ClassesByTypeName classes) { + public JavaType finish(OWNER owner, Iterable> allTypeParametersInContext, ImportedClasses classes) { JavaType type = builder.build(owner, allTypeParametersInContext, classes); return typeFinisher.finish(type, classes); } @@ -181,11 +181,11 @@ static class ReferenceCreationProcess implements J } @Override - public JavaType finish(OWNER owner, Iterable> allTypeParametersInContext, ClassesByTypeName classes) { + public JavaType finish(OWNER owner, Iterable> allTypeParametersInContext, ImportedClasses classes) { return finisher.finish(createTypeVariable(owner, allTypeParametersInContext, classes), classes); } - private JavaType createTypeVariable(OWNER owner, Iterable> allTypeParametersInContext, ClassesByTypeName classes) { + private JavaType createTypeVariable(OWNER owner, Iterable> allTypeParametersInContext, ImportedClasses classes) { for (JavaTypeVariable existingTypeVariable : allTypeParametersInContext) { if (existingTypeVariable.getName().equals(typeVariableName)) { return existingTypeVariable; diff --git a/archunit/src/test/java/com/tngtech/archunit/core/domain/DependencyTest.java b/archunit/src/test/java/com/tngtech/archunit/core/domain/DependencyTest.java index a0c17a11eb..6169dba6a3 100644 --- a/archunit/src/test/java/com/tngtech/archunit/core/domain/DependencyTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/domain/DependencyTest.java @@ -228,6 +228,27 @@ public void Dependency_from_annotation_on_member(JavaMember annotatedMember) { .contains(annotatedMember.getDescription() + " is annotated with <" + annotationClass.getName() + ">"); } + @Test + public void Dependency_from_parameter_annotation() { + @SuppressWarnings("unused") + class SomeClass { + void method(@SomeAnnotation(String.class) Object param) { + } + } + + JavaCodeUnit.Parameter parameter = getOnlyElement( + new ClassFileImporter().importClass(SomeClass.class) + .getMethod("method", Object.class).getParameters()); + + Dependency dependency = getOnlyElement(Dependency.tryCreateFromAnnotation(getOnlyElement(parameter.getAnnotations()))); + + assertThatType(dependency.getOriginClass()).matches(SomeClass.class); + assertThatType(dependency.getTargetClass()).matches(SomeAnnotation.class); + assertThat(dependency.getDescription()).as("description") + .contains("Parameter <" + Object.class.getName() + "> of method <" + SomeClass.class.getName() + "." + "method(" + Object.class.getName() + ")> " + + "is annotated with <" + SomeAnnotation.class.getName() + ">"); + } + @Test @UseDataProvider("annotated_classes") public void Dependency_from_class_annotation_member(JavaClass annotatedClass) { diff --git a/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaClassTest.java b/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaClassTest.java index a14bc70f03..22f5bc53b5 100644 --- a/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaClassTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaClassTest.java @@ -762,6 +762,10 @@ public void direct_dependencies_from_self_by_annotation() { .from(ClassWithAnnotationDependencies.class) .to(OnMethod.class) .inLineNumber(0)) + .areAtLeastOne(annotationTypeDependency() + .from(ClassWithAnnotationDependencies.class) + .to(OnMethodParam.class) + .inLineNumber(0)) .areAtLeastOne(annotationMemberOfTypeDependency() .from(ClassWithAnnotationDependencies.class) .to(WithType.class) @@ -920,7 +924,7 @@ public void direct_dependencies_from_self_finds_correct_set_of_target_types() { assertThatTypes(targets).matchInAnyOrder( B.class, AhavingMembersOfTypeB.class, Object.class, String.class, List.class, Serializable.class, SomeSuperclass.class, - WithType.class, WithNestedAnnotations.class, OnClass.class, OnMethod.class, + WithType.class, WithNestedAnnotations.class, OnClass.class, OnMethod.class, OnMethodParam.class, OnConstructor.class, OnField.class, MetaAnnotated.class, WithEnum.class, WithPrimitive.class, WithOtherEnum.class, WithOtherType.class, SomeEnumAsAnnotationParameter.class, SomeEnumAsAnnotationArrayParameter.class, @@ -1247,6 +1251,12 @@ public void direct_dependencies_to_self_by_annotation() { .to(OnMethod.class) .inLineNumber(0)); + assertThat(javaClasses.get(OnMethodParam.class).getDirectDependenciesToSelf()) + .areAtLeastOne(annotationTypeDependency() + .from(ClassWithAnnotationDependencies.class) + .to(OnMethodParam.class) + .inLineNumber(0)); + assertThat(javaClasses.get(OnConstructor.class).getDirectDependenciesToSelf()) .areAtLeastOne(annotationTypeDependency() .from(ClassWithAnnotationDependencies.class) diff --git a/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaCodeUnitTest.java b/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaCodeUnitTest.java index 5be95a7ff6..74d459f0f0 100644 --- a/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaCodeUnitTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaCodeUnitTest.java @@ -1,11 +1,30 @@ package com.tngtech.archunit.core.domain; +import java.io.File; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ImmutableList; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import org.assertj.core.api.ThrowableAssert.ThrowingCallable; import org.junit.Test; +import org.junit.runner.RunWith; +import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Sets.union; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.equivalentTo; +import static com.tngtech.archunit.core.domain.JavaCodeUnit.Parameter.startWithLowercase; import static com.tngtech.archunit.core.domain.TestUtils.importClassWithContext; -import static org.assertj.core.api.Assertions.assertThat; +import static com.tngtech.archunit.core.domain.properties.HasType.Functions.GET_RAW_TYPE; +import static com.tngtech.archunit.testutil.Assertions.assertThat; +import static com.tngtech.archunit.testutil.Assertions.assertThatAnnotation; +import static com.tngtech.java.junit.dataprovider.DataProviders.testForEach; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +@RunWith(DataProviderRunner.class) public class JavaCodeUnitTest { @Test @@ -23,10 +42,282 @@ public void offers_all_accesses_from_Self() { assertThat(method.getAccessesFromSelf()) .hasSize(6) - .containsOnlyElementsOf(union(union( - method.getConstructorCallsFromSelf(), - method.getMethodCallsFromSelf()), - method.getFieldAccesses())); + .containsOnlyElementsOf(ImmutableList.>builder() + .addAll(method.getConstructorCallsFromSelf()) + .addAll(method.getMethodCallsFromSelf()) + .addAll(method.getFieldAccesses()) + .build()); + } + + @DataProvider + public static Object[][] code_units_with_parameters() { + JavaClass javaClass = new ClassFileImporter().importClass(ClassWithVariousCodeUnitParameters.class); + return testForEach( + javaClass.getConstructor(Object.class, String.class), + javaClass.getConstructor(List.class, Map.class), + javaClass.getMethod("method", Object.class, String.class), + javaClass.getMethod("method", List.class, Map.class) + ); + } + + @Test + @UseDataProvider("code_units_with_parameters") + public void creates_parameters(JavaCodeUnit codeUnit) { + for (int i = 0; i < codeUnit.getParameters().size(); i++) { + assertThat(codeUnit.getParameters().get(i).getRawType()).isEqualTo(codeUnit.getRawParameterTypes().get(i)); + assertThat(codeUnit.getParameters().get(i).getType()).isEqualTo(codeUnit.getParameterTypes().get(i)); + } + } + + @Test + public void creates_parameters_when_raw_parameter_types_do_not_match_generic_parameter_types_for_inner_class() { + // For inner classes the raw parameter types from the bytecode do not match the generic signature from the bytecode. The Reflection API mirrors this, + // so the generic parameter types do not contain the synthetic owner parameter for inner classes, while the raw parameter types do. + // This complicates things, since the indexes of raw and generic parameter types don't need to match by that. + // Note that we deviate from the Reflection API and do not mirror the raw parameter types as parameters, but the generic ones + + @SuppressWarnings({"InnerClassMayBeStatic", "unused"}) + class InnerClassCausingConstructorDeviationBetweenRawParameterTypesAndGenericParameterTypes { + private InnerClassCausingConstructorDeviationBetweenRawParameterTypesAndGenericParameterTypes(List first, List second) { + } + } + + JavaConstructor constructor = new ClassFileImporter() + .importClass(InnerClassCausingConstructorDeviationBetweenRawParameterTypesAndGenericParameterTypes.class) + .getConstructor(getClass(), List.class, List.class); + + assertThat(constructor.getParameters()).hasSize(2); + + for (int i = 0; i < constructor.getParameters().size(); i++) { + assertThat(constructor.getParameters().get(i).getIndex()).as("parameter index").isEqualTo(i); + assertThat(constructor.getParameters().get(i).getType()).isEqualTo(constructor.getParameterTypes().get(i)); + assertThat(constructor.getParameters().get(i).getRawType()).isEqualTo(constructor.getRawParameterTypes().get(i + 1)); + } + } + + @Test + public void creates_parameters_when_raw_parameter_types_do_not_match_generic_parameter_types_for_enum() { + // For enums the raw parameter types from the bytecode do not match the generic signature from the bytecode. The Reflection API mirrors this, + // so the generic parameter types do not contain the synthetic parameters for name and ordinal, while the raw parameter types do. + // This complicates things, since the indexes of raw and generic parameter types don't need to match by that. + // Note that we deviate from the Reflection API and do not mirror the raw parameter types as parameters, but the generic ones + + JavaConstructor constructor = new ClassFileImporter() + .importClass(NonTrivialEnum.class) + .getConstructor(String.class, int.class, File.class, double.class); + + assertThat(constructor.getParameters()).hasSize(2); + + for (int i = 0; i < constructor.getParameters().size(); i++) { + assertThat(constructor.getParameters().get(i).getIndex()).as("parameter index").isEqualTo(i); + assertThat(constructor.getParameters().get(i).getType()).isEqualTo(constructor.getParameterTypes().get(i)); + assertThat(constructor.getParameters().get(i).getRawType()).isEqualTo(constructor.getRawParameterTypes().get(i + 2)); + } + } + + @Test + public void falls_back_to_creating_parameters_with_only_generic_types_if_match_between_raw_types_and_generic_types_cannot_be_made() { + final List nonConstant = newArrayList(getClass().getName()); + class LocalClassReferencingNonConstantFromOuterScope { + @SuppressWarnings("unused") + LocalClassReferencingNonConstantFromOuterScope(List someParameterizedType) { + System.out.println("Using " + nonConstant + " which causes another synthetic List parameter to be appended to the constructor"); + } + } + + JavaConstructor constructor = new ClassFileImporter() + .importClass(LocalClassReferencingNonConstantFromOuterScope.class) + .getConstructor(getClass(), List.class, List.class); + + assertThat(constructor.getParameters()).hasSameSizeAs(constructor.getParameterTypes()); + + for (int i = 0; i < constructor.getParameters().size(); i++) { + assertThat(constructor.getParameters().get(i).getIndex()).as("parameter index").isEqualTo(i); + assertThat(constructor.getParameters().get(i).getType()).isEqualTo(constructor.getParameterTypes().get(i)); + assertThat(constructor.getParameters().get(i).getRawType()).isEqualTo(constructor.getParameters().get(i).getType().toErasure()); + } + } + + @DataProvider + public static Object[][] code_units_with_four_different_parameters() { + @SuppressWarnings("unused") + class SomeClass { + SomeClass(List first, T second, String third, int fourth) { + } + + void method(List first, T second, String third, int fourth) { + } + } + JavaClass javaClass = new ClassFileImporter().importClass(SomeClass.class); + return testForEach( + javaClass.getConstructor(List.class, Object.class, String.class, int.class), + javaClass.getMethod("method", List.class, Object.class, String.class, int.class) + ); + } + + @Test + @UseDataProvider("code_units_with_four_different_parameters") + public void adds_description_to_parameters_of_code_unit(JavaCodeUnit codeUnit) { + List parameters = codeUnit.getParameters(); + + assertThat(parameters.get(0).getDescription()).isEqualTo("Parameter <" + List.class.getName() + "<" + String.class.getName() + ">> of " + startWithLowercase(codeUnit.getDescription())); + assertThat(parameters.get(1).getDescription()).isEqualTo("Parameter of " + startWithLowercase(codeUnit.getDescription())); + assertThat(parameters.get(2).getDescription()).isEqualTo("Parameter <" + String.class.getName() + "> of " + startWithLowercase(codeUnit.getDescription())); + assertThat(parameters.get(3).getDescription()).isEqualTo("Parameter <" + int.class.getName() + "> of " + startWithLowercase(codeUnit.getDescription())); + } + + @Test + @UseDataProvider("code_units_with_four_different_parameters") + public void adds_index_to_parameters_of_code_unit(JavaCodeUnit codeUnit) { + List parameters = codeUnit.getParameters(); + + assertThat(parameters.get(0).getIndex()).isEqualTo(0); + assertThat(parameters.get(1).getIndex()).isEqualTo(1); + assertThat(parameters.get(2).getIndex()).isEqualTo(2); + assertThat(parameters.get(3).getIndex()).isEqualTo(3); + } + + @DataProvider + public static Object[][] data_adds_owner_to_parameters_of_code_unit() { + @SuppressWarnings("unused") + class SomeClass { + SomeClass(String param) { + } + + void method(String param) { + } + } + JavaClass javaClass = new ClassFileImporter().importClass(SomeClass.class); + return testForEach( + javaClass.getConstructor(String.class), + javaClass.getMethod("method", String.class)); + } + + @Test + @UseDataProvider + public void test_adds_owner_to_parameters_of_code_unit(JavaCodeUnit codeUnit) { + for (JavaCodeUnit.Parameter parameter : codeUnit.getParameters()) { + assertThat(parameter.getOwner()).isEqualTo(codeUnit); + } + } + + @Test + public void parameter_isAnnotatedWith() { + @SuppressWarnings("unused") + class SomeClass { + void method(@SomeParameterAnnotation("test") String param) { + } + } + + JavaCodeUnit.Parameter parameter = new ClassFileImporter().importClass(SomeClass.class) + .getMethod("method", String.class).getParameters().get(0); + + assertThat(parameter.isAnnotatedWith(SomeParameterAnnotation.class)) + .as("parameter is annotated with @" + SomeParameterAnnotation.class.getSimpleName()).isTrue(); + assertThat(parameter.isAnnotatedWith(Deprecated.class)) + .as("parameter is annotated with @" + Deprecated.class.getSimpleName()).isFalse(); + + assertThat(parameter.isAnnotatedWith(SomeParameterAnnotation.class.getName())) + .as("parameter is annotated with @" + SomeParameterAnnotation.class.getSimpleName()).isTrue(); + assertThat(parameter.isAnnotatedWith(Deprecated.class.getName())) + .as("parameter is annotated with @" + Deprecated.class.getSimpleName()).isFalse(); + + assertThat(parameter.isAnnotatedWith(GET_RAW_TYPE.is(equivalentTo(SomeParameterAnnotation.class)))) + .as("parameter is annotated with @" + SomeParameterAnnotation.class.getSimpleName()).isTrue(); + assertThat(parameter.isAnnotatedWith(GET_RAW_TYPE.is(equivalentTo(Deprecated.class)))) + .as("parameter is annotated with @" + Deprecated.class.getSimpleName()).isFalse(); + } + + @Test + public void parameter_isMetaAnnotatedWith() { + @SuppressWarnings("unused") + class SomeClass { + void method(@SomeParameterAnnotation("test") String param) { + } + } + + JavaCodeUnit.Parameter parameter = new ClassFileImporter().importClass(SomeClass.class) + .getMethod("method", String.class).getParameters().get(0); + + assertThat(parameter.isMetaAnnotatedWith(SomeMetaAnnotation.class)) + .as("parameter is meta-annotated with @" + SomeMetaAnnotation.class.getSimpleName()).isTrue(); + assertThat(parameter.isMetaAnnotatedWith(Deprecated.class)) + .as("parameter is meta-annotated with @" + Deprecated.class.getSimpleName()).isFalse(); + + assertThat(parameter.isMetaAnnotatedWith(SomeMetaAnnotation.class.getName())) + .as("parameter is meta-annotated with @" + SomeMetaAnnotation.class.getSimpleName()).isTrue(); + assertThat(parameter.isMetaAnnotatedWith(Deprecated.class.getName())) + .as("parameter is meta-annotated with @" + Deprecated.class.getSimpleName()).isFalse(); + + assertThat(parameter.isMetaAnnotatedWith(GET_RAW_TYPE.is(equivalentTo(SomeMetaAnnotation.class)))) + .as("parameter is meta-annotated with @" + SomeMetaAnnotation.class.getSimpleName()).isTrue(); + assertThat(parameter.isMetaAnnotatedWith(GET_RAW_TYPE.is(equivalentTo(Deprecated.class)))) + .as("parameter is meta-annotated with @" + Deprecated.class.getSimpleName()).isFalse(); + } + + @Test + public void parameter_getAnnotationOfType() { + @SuppressWarnings("unused") + class SomeClass { + void method(@SomeParameterAnnotation("test") String param) { + } + } + + final JavaCodeUnit.Parameter parameter = new ClassFileImporter().importClass(SomeClass.class) + .getMethod("method", String.class).getParameters().get(0); + + SomeParameterAnnotation annotation = parameter.getAnnotationOfType(SomeParameterAnnotation.class); + assertThat(annotation).isInstanceOf(SomeParameterAnnotation.class); + assertThat(annotation.value()).isEqualTo("test"); + assertThatThrownBy(new ThrowingCallable() { + @Override + public void call() { + parameter.getAnnotationOfType(Deprecated.class); + } + }).isInstanceOf(IllegalArgumentException.class); + + JavaAnnotation javaAnnotation = parameter.getAnnotationOfType(SomeParameterAnnotation.class.getName()); + assertThatAnnotation(javaAnnotation).hasType(SomeParameterAnnotation.class); + assertThat(javaAnnotation.get("value")).contains("test"); + assertThatThrownBy(new ThrowingCallable() { + @Override + public void call() { + parameter.getAnnotationOfType(Deprecated.class.getName()); + } + }).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void parameter_tryGetAnnotationOfType() { + @SuppressWarnings("unused") + class SomeClass { + void method(@SomeParameterAnnotation("test") String param) { + } + } + + final JavaCodeUnit.Parameter parameter = new ClassFileImporter().importClass(SomeClass.class) + .getMethod("method", String.class).getParameters().get(0); + + assertThat(parameter.tryGetAnnotationOfType(SomeParameterAnnotation.class).get()).isInstanceOf(SomeParameterAnnotation.class); + assertThat(parameter.tryGetAnnotationOfType(Deprecated.class)).isAbsent(); + + assertThatAnnotation(parameter.tryGetAnnotationOfType(SomeParameterAnnotation.class.getName()).get()).hasType(SomeParameterAnnotation.class); + assertThat(parameter.tryGetAnnotationOfType(Deprecated.class.getName())).isAbsent(); + } + + @SuppressWarnings("unused") + private static class ClassWithVariousCodeUnitParameters { + ClassWithVariousCodeUnitParameters(Object simple, String noParameterizedTypes) { + } + + ClassWithVariousCodeUnitParameters(List complex, Map withParameterizedTypes) { + } + + void method(Object simple, String noParameterizedTypes) { + } + + void method(List complex, Map withParameterizedTypes) { + } } @SuppressWarnings("unused") @@ -58,4 +349,20 @@ void method1() { void method2() { } } + + @SuppressWarnings("unused") + enum NonTrivialEnum { + VALUE(new File("irrelevant"), 0); + + NonTrivialEnum(File file, double primitive) { + } + } + + @interface SomeMetaAnnotation { + } + + @SomeMetaAnnotation + @interface SomeParameterAnnotation { + String value(); + } } \ No newline at end of file diff --git a/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterAnnotationsTest.java b/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterAnnotationsTest.java index a5a3749b6d..419c443023 100644 --- a/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterAnnotationsTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterAnnotationsTest.java @@ -3,6 +3,7 @@ import java.io.Serializable; import java.lang.annotation.Annotation; import java.lang.reflect.Array; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -12,6 +13,7 @@ import com.tngtech.archunit.core.domain.JavaAnnotation; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.domain.JavaCodeUnit; import com.tngtech.archunit.core.domain.JavaConstructor; import com.tngtech.archunit.core.domain.JavaEnumConstant; import com.tngtech.archunit.core.domain.JavaField; @@ -25,6 +27,7 @@ import com.tngtech.archunit.core.importer.testexamples.annotatedclassimport.ClassWithUnimportedAnnotation; import com.tngtech.archunit.core.importer.testexamples.annotatedclassimport.SimpleAnnotation; import com.tngtech.archunit.core.importer.testexamples.annotatedclassimport.TypeAnnotationWithEnumAndArrayValue; +import com.tngtech.archunit.core.importer.testexamples.annotatedparameters.ClassWithMethodWithAnnotatedParameters; import com.tngtech.archunit.core.importer.testexamples.annotationfieldimport.ClassWithAnnotatedFields; import com.tngtech.archunit.core.importer.testexamples.annotationfieldimport.FieldAnnotationWithArrays; import com.tngtech.archunit.core.importer.testexamples.annotationmethodimport.ClassWithAnnotatedMethods; @@ -35,7 +38,6 @@ import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; import com.tngtech.java.junit.dataprovider.UseDataProvider; -import org.assertj.core.api.Assertions; import org.junit.Test; import org.junit.runner.RunWith; @@ -49,6 +51,7 @@ import static com.tngtech.archunit.core.importer.testexamples.annotationmethodimport.ClassWithAnnotatedMethods.stringAnnotatedMethod; import static com.tngtech.archunit.testutil.Assertions.assertThat; import static com.tngtech.archunit.testutil.Assertions.assertThatAnnotation; +import static com.tngtech.archunit.testutil.Assertions.assertThatAnnotations; import static com.tngtech.archunit.testutil.Assertions.assertThatType; import static com.tngtech.archunit.testutil.assertion.JavaAnnotationAssertion.annotationProperty; import static com.tngtech.java.junit.dataprovider.DataProviders.testForEach; @@ -194,11 +197,11 @@ public void imports_class_with_annotation_with_empty_array() { assertThat(Array.getLength(annotation.get("annotations").get())).isZero(); ClassAnnotationWithArrays reflected = clazz.getAnnotationOfType(ClassAnnotationWithArrays.class); - Assertions.assertThat(reflected.primitives()).isEmpty(); - Assertions.assertThat(reflected.objects()).isEmpty(); - Assertions.assertThat(reflected.enums()).isEmpty(); - Assertions.assertThat(reflected.classes()).isEmpty(); - Assertions.assertThat(reflected.annotations()).isEmpty(); + assertThat(reflected.primitives()).isEmpty(); + assertThat(reflected.objects()).isEmpty(); + assertThat(reflected.enums()).isEmpty(); + assertThat(reflected.classes()).isEmpty(); + assertThat(reflected.annotations()).isEmpty(); } @Test @@ -214,10 +217,10 @@ public void imports_class_annotated_with_unimported_annotation() { assertThat((JavaEnumConstant) annotation.get("optionalEnum").get()).isEquivalentTo(OTHER_VALUE); SomeAnnotation reflected = clazz.getAnnotationOfType(SomeAnnotation.class); - Assertions.assertThat(reflected.mandatory()).isEqualTo("mandatory"); - Assertions.assertThat(reflected.optional()).isEqualTo("optional"); - Assertions.assertThat(reflected.mandatoryEnum()).isEqualTo(SOME_VALUE); - Assertions.assertThat(reflected.optionalEnum()).isEqualTo(OTHER_VALUE); + assertThat(reflected.mandatory()).isEqualTo("mandatory"); + assertThat(reflected.optional()).isEqualTo("optional"); + assertThat(reflected.mandatoryEnum()).isEqualTo(SOME_VALUE); + assertThat(reflected.optionalEnum()).isEqualTo(OTHER_VALUE); } @Test @@ -422,11 +425,11 @@ public void imports_method_with_annotation_with_empty_array() { assertThat(Array.getLength(annotation.get("annotations").get())).isZero(); MethodAnnotationWithArrays reflected = annotation.as(MethodAnnotationWithArrays.class); - Assertions.assertThat(reflected.primitives()).isEmpty(); - Assertions.assertThat(reflected.objects()).isEmpty(); - Assertions.assertThat(reflected.enums()).isEmpty(); - Assertions.assertThat(reflected.classes()).isEmpty(); - Assertions.assertThat(reflected.annotations()).isEmpty(); + assertThat(reflected.primitives()).isEmpty(); + assertThat(reflected.objects()).isEmpty(); + assertThat(reflected.enums()).isEmpty(); + assertThat(reflected.classes()).isEmpty(); + assertThat(reflected.annotations()).isEmpty(); } @Test @@ -440,8 +443,134 @@ public void imports_methods_annotated_with_unimported_annotation() { assertThat(annotation.get("optional")).contains("optional"); SomeAnnotation reflected = annotation.as(SomeAnnotation.class); - Assertions.assertThat(reflected.mandatory()).isEqualTo("mandatory"); - Assertions.assertThat(reflected.optional()).isEqualTo("optional"); + assertThat(reflected.mandatory()).isEqualTo("mandatory"); + assertThat(reflected.optional()).isEqualTo("optional"); + } + + @Test + public void imports_empty_parameter_annotations_for_method_without_annotated_parameters() { + JavaMethod method = new ClassFileImporter().importPackagesOf(ClassWithMethodWithAnnotatedParameters.class) + .get(ClassWithMethodWithAnnotatedParameters.class) + .getMethod(ClassWithMethodWithAnnotatedParameters.methodWithTwoUnannotatedParameters, String.class, int.class); + + for (JavaCodeUnit.Parameter parameter : method.getParameters()) { + assertThat(parameter.getAnnotations()).isEmpty(); + } + assertThat(method.getParameterAnnotations()).containsExactly( + Collections.>emptySet(), Collections.>emptySet()); + } + + @Test + public void imports_method_with_one_parameter_with_one_annotation() { + JavaMethod method = new ClassFileImporter().importPackagesOf(ClassWithMethodWithAnnotatedParameters.class) + .get(ClassWithMethodWithAnnotatedParameters.class) + .getMethod(ClassWithMethodWithAnnotatedParameters.methodWithOneAnnotatedParameterWithOneAnnotation, String.class); + + List>> parameterAnnotations = method.getParameterAnnotations(); + + JavaCodeUnit.Parameter parameter = getOnlyElement(method.getParameters()); + JavaAnnotation annotation = getOnlyElement(getOnlyElement(parameterAnnotations)); + + assertThat((JavaEnumConstant) annotation.get("value").get()).isEquivalentTo(OTHER_VALUE); + assertThat((JavaEnumConstant) annotation.get("valueWithDefault").get()).isEquivalentTo(SOME_VALUE); + assertThat(((JavaEnumConstant[]) annotation.get("enumArray").get())).matches(SOME_VALUE, OTHER_VALUE); + assertThat(((JavaEnumConstant[]) annotation.get("enumArrayWithDefault").get())).matches(OTHER_VALUE); + JavaAnnotation subAnnotation = (JavaAnnotation) annotation.get("subAnnotation").get(); + assertThat(subAnnotation.get("value").get()).isEqualTo("changed"); + assertThat(subAnnotation.getOwner()).isEqualTo(annotation); + assertThat(subAnnotation.getAnnotatedElement()).isEqualTo(parameter); + assertThat(((JavaAnnotation) annotation.get("subAnnotationWithDefault").get()).get("value").get()) + .isEqualTo("default"); + JavaAnnotation[] subAnnotationArray = (JavaAnnotation[]) annotation.get("subAnnotationArray").get(); + assertThat(subAnnotationArray[0].get("value").get()).isEqualTo("one"); + assertThat(subAnnotationArray[0].getOwner()).isEqualTo(annotation); + assertThat(subAnnotationArray[0].getAnnotatedElement()).isEqualTo(parameter); + assertThat(subAnnotationArray[1].get("value").get()).isEqualTo("two"); + assertThat(subAnnotationArray[1].getOwner()).isEqualTo(annotation); + assertThat(subAnnotationArray[1].getAnnotatedElement()).isEqualTo(parameter); + assertThat(((JavaAnnotation[]) annotation.get("subAnnotationArrayWithDefault").get())[0].get("value").get()) + .isEqualTo("first"); + assertThatType((JavaClass) annotation.get("clazz").get()).matches(Map.class); + assertThatType((JavaClass) annotation.get("clazzWithDefault").get()).matches(String.class); + assertThat((JavaClass[]) annotation.get("classes").get()).matchExactly(Object.class, Serializable.class); + assertThat((JavaClass[]) annotation.get("classesWithDefault").get()).matchExactly(Serializable.class, List.class); + + assertThatAnnotation(getOnlyElement(parameter.getAnnotations())).matches(method.reflect().getParameterAnnotations()[0][0]); + assertThatAnnotation(annotation).matches(method.reflect().getParameterAnnotations()[0][0]); + } + + @Test + public void imports_method_with_one_parameter_with_two_annotations() { + JavaMethod method = new ClassFileImporter().importPackagesOf(ClassWithMethodWithAnnotatedParameters.class) + .get(ClassWithMethodWithAnnotatedParameters.class) + .getMethod(ClassWithMethodWithAnnotatedParameters.methodWithOneAnnotatedParameterWithTwoAnnotations, String.class); + + List>> parameterAnnotations = method.getParameterAnnotations(); + + Set> annotations = getOnlyElement(parameterAnnotations); + + assertThat(annotations).isEqualTo(getOnlyElement(method.getParameters()).getAnnotations()); + assertThatAnnotations(annotations).match(ImmutableSet.copyOf(method.reflect().getParameterAnnotations()[0])); + } + + @Test + public void imports_methods_with_multiple_parameters_with_annotations() { + JavaMethod method = new ClassFileImporter().importPackagesOf(ClassWithMethodWithAnnotatedParameters.class) + .get(ClassWithMethodWithAnnotatedParameters.class) + .getMethod(ClassWithMethodWithAnnotatedParameters.methodWithTwoAnnotatedParameters, String.class, int.class); + + List>> parameterAnnotations = method.getParameterAnnotations(); + + List parameters = method.getParameters(); + for (int i = 0; i < 2; i++) { + assertThat(parameterAnnotations.get(i)).isEqualTo(parameters.get(i).getAnnotations()); + assertThatAnnotations(parameterAnnotations.get(i)).match(ImmutableSet.copyOf(method.reflect().getParameterAnnotations()[i])); + } + } + + @Test + public void imports_methods_with_multiple_parameters_with_annotations_but_unannotated_parameters_in_between() { + JavaMethod method = new ClassFileImporter().importPackagesOf(ClassWithMethodWithAnnotatedParameters.class) + .get(ClassWithMethodWithAnnotatedParameters.class) + .getMethod(ClassWithMethodWithAnnotatedParameters.methodWithAnnotatedParametersGap, String.class, int.class, Object.class, List.class); + + List>> parameterAnnotations = method.getParameterAnnotations(); + + assertThatAnnotations(parameterAnnotations.get(0)).isNotEmpty(); + assertThatAnnotations(parameterAnnotations.get(1)).isEmpty(); + assertThatAnnotations(parameterAnnotations.get(2)).isEmpty(); + assertThatAnnotations(parameterAnnotations.get(3)).isNotEmpty(); + + List parameters = method.getParameters(); + for (int i = 0; i < 4; i++) { + assertThat(parameterAnnotations.get(i)).isEqualTo(parameters.get(i).getAnnotations()); + assertThatAnnotations(parameterAnnotations.get(i)).match(ImmutableSet.copyOf(method.reflect().getParameterAnnotations()[i])); + } + } + + @Test + public void imports_parameter_annotations_correctly_for_synthetic_constructor_parameter() { + @SuppressWarnings("unused") + class LocalClassThusSyntheticFirstConstructorParameter { + LocalClassThusSyntheticFirstConstructorParameter(Object notAnnotated, @SimpleAnnotation("test") List annotated) { + } + } + + JavaConstructor constructor = getOnlyElement(new ClassFileImporter().importClasses(LocalClassThusSyntheticFirstConstructorParameter.class, String.class) + .get(LocalClassThusSyntheticFirstConstructorParameter.class) + .getConstructors()); + + List>> parameterAnnotations = constructor.getParameterAnnotations(); + + assertThat(parameterAnnotations).as("parameter annotations").hasSize(2); + assertThatAnnotations(parameterAnnotations.get(0)).isEmpty(); + assertThatAnnotations(parameterAnnotations.get(1)).isNotEmpty(); + + List parameters = constructor.getParameters(); + for (int i = 0; i < parameterAnnotations.size(); i++) { + assertThat(parameterAnnotations.get(i)).isEqualTo(parameters.get(i).getAnnotations()); + assertThatAnnotations(parameterAnnotations.get(i)).match(ImmutableSet.copyOf(constructor.reflect().getParameterAnnotations()[i])); + } } @Test @@ -531,7 +660,9 @@ public static Object[][] elementsAnnotatedWithSomeAnnotation() { new ClassFileImporter().importClass(MetaAnnotatedClass.class), new ClassFileImporter().importClass(ClassWithMetaAnnotatedField.class).getField("metaAnnotatedField"), new ClassFileImporter().importClass(ClassWithMetaAnnotatedMethod.class).getMethod("metaAnnotatedMethod"), - new ClassFileImporter().importClass(ClassWithMetaAnnotatedConstructor.class).getConstructor() + new ClassFileImporter().importClass(ClassWithMetaAnnotatedConstructor.class).getConstructor(), + getOnlyElement(new ClassFileImporter().importClass(ClassWithMetaAnnotatedConstructorParameter.class).getConstructor(String.class).getParameters()), + getOnlyElement(new ClassFileImporter().importClass(ClassWithMetaAnnotatedMethodParameter.class).getMethod("method", String.class).getParameters()) ); } @@ -672,6 +803,18 @@ private static class ClassWithMetaAnnotatedConstructor { } } + @SuppressWarnings("unused") + private static class ClassWithMetaAnnotatedConstructorParameter { + ClassWithMetaAnnotatedConstructorParameter(@MetaAnnotatedAnnotation String param) { + } + } + + @SuppressWarnings("unused") + private static class ClassWithMetaAnnotatedMethodParameter { + void method(@MetaAnnotatedAnnotation String param) { + } + } + private static class SomeMetaMetaMetaAnnotationClassParameter { } diff --git a/archunit/src/test/java/com/tngtech/archunit/core/importer/ImportTestUtils.java b/archunit/src/test/java/com/tngtech/archunit/core/importer/ImportTestUtils.java index 8d748a6abd..3ee9ac055c 100644 --- a/archunit/src/test/java/com/tngtech/archunit/core/importer/ImportTestUtils.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/importer/ImportTestUtils.java @@ -42,21 +42,22 @@ import com.tngtech.archunit.core.domain.JavaTypeVariable; import com.tngtech.archunit.core.importer.DomainBuilders.JavaMethodCallBuilder; import com.tngtech.archunit.core.importer.DomainBuilders.JavaTypeCreationProcess; +import com.tngtech.archunit.core.importer.resolvers.ClassResolver; import org.objectweb.asm.Type; import static com.tngtech.archunit.core.domain.JavaConstructor.CONSTRUCTOR_NAME; public class ImportTestUtils { - private static Set createConstructors(JavaClass owner, Class inputClass, ClassesByTypeName importedClasses) { + private static Set createConstructors(JavaClass owner, Class inputClass, ImportedClasses importedClasses) { return finish(constructorBuildersFor(inputClass), owner, importedClasses); } - private static Set createMethods(JavaClass owner, Class inputClass, ClassesByTypeName importedClasses) { + private static Set createMethods(JavaClass owner, Class inputClass, ImportedClasses importedClasses) { return finish(methodBuildersFor(inputClass), owner, importedClasses); } - private static Set createFields(JavaClass owner, Class inputClass, ClassesByTypeName importedClasses) { + private static Set createFields(JavaClass owner, Class inputClass, ImportedClasses importedClasses) { return finish(fieldBuildersFor(inputClass), owner, importedClasses); } @@ -105,7 +106,7 @@ private static Set Set finish(Set> builders, JavaClass owner, - ClassesByTypeName importedClasses) { + ImportedClasses importedClasses) { ImmutableSet.Builder result = ImmutableSet.builder(); for (DomainBuilders.BuilderWithBuildParameter builder : builders) { result.add(builder.build(owner, importedClasses)); @@ -255,7 +256,8 @@ public static JavaAnnotation javaAnnotationFrom(Annotation annotation } private static JavaAnnotation javaAnnotationFrom(Annotation annotation, Class annotatedClass, ImportContext importContext) { - return javaAnnotationBuilderFrom(annotation, annotatedClass, importContext).build(importContext.resolveClass(annotatedClass.getName()), importContext); + return javaAnnotationBuilderFrom(annotation, annotatedClass, importContext) + .build(importContext.resolveClass(annotatedClass.getName()), ImportTestUtils.simpleImportedClasses()); } private static DomainBuilders.JavaAnnotationBuilder javaAnnotationBuilderFrom(Annotation annotation, Class annotatedClass, @@ -314,15 +316,36 @@ public String apply(JavaAnnotation input) { }); } - public static class ImportedTestClasses implements ClassesByTypeName { + public static class ImportedTestClasses extends ImportedClasses { private final Map imported = new HashMap<>(); + ImportedTestClasses() { + super( + Collections.emptyMap(), + new ClassResolver() { + @Override + public void setClassUriImporter(ClassUriImporter classUriImporter) { + } + + @Override + public Optional tryResolve(String typeName) { + return Optional.empty(); + } + }, + new MethodReturnTypeGetter() { + @Override + public Optional getReturnType(String declaringClassName, String methodName) { + return Optional.empty(); + } + }); + } + void register(JavaClass clazz) { imported.put(clazz.getName(), clazz); } @Override - public JavaClass get(String typeName) { + public JavaClass getOrResolve(String typeName) { return imported.containsKey(typeName) ? imported.get(typeName) : importNew(JavaClassDescriptor.From.name(typeName).resolveClass()); @@ -421,10 +444,5 @@ public Set createConstructorCallsFor(JavaCodeUnit codeUnit) public JavaClass resolveClass(String fullyQualifiedClassName) { throw new UnsupportedOperationException("Override me where necessary"); } - - @Override - public Optional getMethodReturnType(String declaringClassName, String methodName) { - return Optional.empty(); - } } } diff --git a/archunit/src/test/java/com/tngtech/archunit/core/importer/JavaAnnotationTestBuilder.java b/archunit/src/test/java/com/tngtech/archunit/core/importer/JavaAnnotationTestBuilder.java deleted file mode 100644 index ee4896d1a4..0000000000 --- a/archunit/src/test/java/com/tngtech/archunit/core/importer/JavaAnnotationTestBuilder.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.tngtech.archunit.core.importer; - -import com.tngtech.archunit.core.domain.ImportContext; -import com.tngtech.archunit.core.domain.JavaAnnotation; -import com.tngtech.archunit.core.domain.JavaClass; -import com.tngtech.archunit.core.domain.JavaClassDescriptor; -import com.tngtech.archunit.core.importer.DomainBuilders.JavaAnnotationBuilder; -import com.tngtech.archunit.core.importer.DomainBuilders.JavaAnnotationBuilder.ValueBuilder; - -public class JavaAnnotationTestBuilder { - private final JavaAnnotationBuilder delegate = new JavaAnnotationBuilder(); - - public JavaAnnotationTestBuilder withType(JavaClassDescriptor type) { - delegate.withType(type); - return this; - } - - public JavaAnnotationTestBuilder addProperty(String key, Object value) { - delegate.addProperty(key, ValueBuilder.ofFinished(value)); - return this; - } - - public JavaAnnotation build(JavaClass owner, ImportContext importContext) { - return delegate.build(owner, importContext); - } -} diff --git a/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/annotatedparameters/ClassWithMethodWithAnnotatedParameters.java b/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/annotatedparameters/ClassWithMethodWithAnnotatedParameters.java new file mode 100644 index 0000000000..23f5e76f90 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/annotatedparameters/ClassWithMethodWithAnnotatedParameters.java @@ -0,0 +1,116 @@ +package com.tngtech.archunit.core.importer.testexamples.annotatedparameters; + +import java.io.Serializable; +import java.lang.annotation.Retention; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.tngtech.archunit.core.importer.testexamples.SomeEnum; + +import static com.tngtech.archunit.core.importer.testexamples.SomeEnum.OTHER_VALUE; +import static com.tngtech.archunit.core.importer.testexamples.SomeEnum.SOME_VALUE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@SuppressWarnings("unused") +public class ClassWithMethodWithAnnotatedParameters { + public static String methodWithTwoUnannotatedParameters = "methodWithTwoUnannotatedParameters"; + public static String methodWithOneAnnotatedParameterWithOneAnnotation = "methodWithOneAnnotatedParameterWithOneAnnotation"; + public static String methodWithOneAnnotatedParameterWithTwoAnnotations = "methodWithOneAnnotatedParameterWithTwoAnnotations"; + public static String methodWithTwoAnnotatedParameters = "methodWithTwoAnnotatedParameters"; + public static String methodWithAnnotatedParametersGap = "methodWithAnnotatedParametersGap"; + + void methodWithTwoUnannotatedParameters(String one, int two) { + } + + void methodWithOneAnnotatedParameterWithOneAnnotation( + @SomeParameterAnnotation( + value = OTHER_VALUE, + enumArray = {SOME_VALUE, OTHER_VALUE}, + subAnnotation = @SimpleAnnotation("changed"), + subAnnotationArray = {@SimpleAnnotation("one"), @SimpleAnnotation("two")}, + clazz = Map.class, + classes = {Object.class, Serializable.class} + ) String param + ) { + } + + void methodWithOneAnnotatedParameterWithTwoAnnotations( + @OtherParameterAnnotation(String.class) + @SomeParameterAnnotation( + value = OTHER_VALUE, + enumArray = {SOME_VALUE, OTHER_VALUE}, + subAnnotation = @SimpleAnnotation("changed"), + subAnnotationArray = {@SimpleAnnotation("one"), @SimpleAnnotation("two")}, + clazz = Map.class, + classes = {Object.class, Serializable.class} + ) String param + ) { + } + + void methodWithTwoAnnotatedParameters( + @SomeParameterAnnotation( + value = OTHER_VALUE, + enumArray = {SOME_VALUE, OTHER_VALUE}, + subAnnotation = @SimpleAnnotation("first"), + subAnnotationArray = {@SimpleAnnotation("first_one"), @SimpleAnnotation("first_two")}, + clazz = String.class, + classes = {String.class, Serializable.class} + ) String first, + @OtherParameterAnnotation(String.class) + @SomeParameterAnnotation( + value = SOME_VALUE, + enumArray = {OTHER_VALUE, SOME_VALUE}, + subAnnotation = @SimpleAnnotation("second"), + subAnnotationArray = {@SimpleAnnotation("second_one"), @SimpleAnnotation("second_two")}, + clazz = List.class, + classes = {Set.class, Map.class} + ) int second + ) { + } + + void methodWithAnnotatedParametersGap( + @SimpleAnnotation("first") String first, + int second, + T third, + @SimpleAnnotation("fourth") List fourth + ) { + } + + @Retention(RUNTIME) + @interface SomeParameterAnnotation { + SomeEnum value(); + + SomeEnum valueWithDefault() default SOME_VALUE; + + SomeEnum[] enumArray(); + + SomeEnum[] enumArrayWithDefault() default {OTHER_VALUE}; + + SimpleAnnotation subAnnotation(); + + SimpleAnnotation subAnnotationWithDefault() default @SimpleAnnotation("default"); + + SimpleAnnotation[] subAnnotationArray(); + + SimpleAnnotation[] subAnnotationArrayWithDefault() default {@SimpleAnnotation("first"), @SimpleAnnotation("second")}; + + Class clazz(); + + Class clazzWithDefault() default String.class; + + Class[] classes(); + + Class[] classesWithDefault() default {Serializable.class, List.class}; + } + + @Retention(RUNTIME) + public @interface SimpleAnnotation { + String value(); + } + + @Retention(RUNTIME) + public @interface OtherParameterAnnotation { + Class value(); + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/Assertions.java b/archunit/src/test/java/com/tngtech/archunit/testutil/Assertions.java index 6e3ab7741f..b60d92ff95 100644 --- a/archunit/src/test/java/com/tngtech/archunit/testutil/Assertions.java +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/Assertions.java @@ -46,6 +46,7 @@ import com.tngtech.archunit.testutil.assertion.DependencyAssertion; import com.tngtech.archunit.testutil.assertion.DescribedPredicateAssertion; import com.tngtech.archunit.testutil.assertion.JavaAnnotationAssertion; +import com.tngtech.archunit.testutil.assertion.JavaAnnotationsAssertion; import com.tngtech.archunit.testutil.assertion.JavaClassAssertion; import com.tngtech.archunit.testutil.assertion.JavaClassDescriptorAssertion; import com.tngtech.archunit.testutil.assertion.JavaCodeUnitAssertion; @@ -195,6 +196,10 @@ public static JavaAnnotationAssertion assertThatAnnotation(JavaAnnotation ann return new JavaAnnotationAssertion(annotation); } + public static JavaAnnotationsAssertion assertThatAnnotations(Set> annotation) { + return new JavaAnnotationsAssertion(annotation); + } + @SuppressWarnings("unchecked") // covariant public static AccessesAssertion assertThatAccesses(Collection> accesses) { return new AccessesAssertion((Collection>) accesses); diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/JavaAnnotationAssertion.java b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/JavaAnnotationAssertion.java index 2b37c37cd1..2deb6421b5 100644 --- a/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/JavaAnnotationAssertion.java +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/JavaAnnotationAssertion.java @@ -26,6 +26,7 @@ import static com.tngtech.archunit.testutil.Assertions.assertThatType; import static com.tngtech.archunit.testutil.TestUtils.invoke; import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static java.util.Collections.singleton; import static org.assertj.core.api.Assertions.assertThatThrownBy; public class JavaAnnotationAssertion extends AbstractObjectAssert> { @@ -33,6 +34,13 @@ public JavaAnnotationAssertion(JavaAnnotation actual) { super(actual, JavaAnnotationAssertion.class); } + public JavaAnnotationAssertion matches(Annotation annotation) { + assertThat(runtimePropertiesOf(singleton(actual))) + .as("runtime properties of " + actual) + .containsExactly(propertiesOf(annotation)); + return this; + } + public JavaAnnotationAssertion hasType(Class annotationType) { assertThatType(actual.getRawType()) .as("annotation type of " + descriptionText()) diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/JavaAnnotationsAssertion.java b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/JavaAnnotationsAssertion.java new file mode 100644 index 0000000000..5beb09a7b5 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/JavaAnnotationsAssertion.java @@ -0,0 +1,58 @@ +package com.tngtech.archunit.testutil.assertion; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.ImmutableMap; +import com.tngtech.archunit.core.domain.JavaAnnotation; +import org.assertj.core.api.AbstractIterableAssert; + +import static com.tngtech.archunit.testutil.Assertions.assertThat; +import static com.tngtech.archunit.testutil.Assertions.assertThatAnnotation; + +public class JavaAnnotationsAssertion extends AbstractIterableAssert>, JavaAnnotation, JavaAnnotationAssertion> { + @SuppressWarnings({"unchecked", "rawtypes"}) + public JavaAnnotationsAssertion(Set> javaAnnotations) { + super((Set) javaAnnotations, JavaAnnotationsAssertion.class); + } + + public JavaAnnotationsAssertion match(Collection annotations) { + assertThat(actual).hasSameSizeAs(annotations); + + Map> actualByClassName = annotationsByClassName(actual); + Map reflectionByClassName = reflectionByClassName(annotations); + assertThat(actualByClassName.keySet()) + .as("annotation type names") + .containsExactlyInAnyOrderElementsOf(reflectionByClassName.keySet()); + + for (Map.Entry> entry : actualByClassName.entrySet()) { + Annotation reflectionAnnotation = reflectionByClassName.get(entry.getKey()); + assertThatAnnotation(entry.getValue()).matches(reflectionAnnotation); + } + + return myself; + } + + private Map> annotationsByClassName(Iterable> annotations) { + ImmutableMap.Builder> result = ImmutableMap.builder(); + for (JavaAnnotation annotation : annotations) { + result.put(annotation.getRawType().getName(), annotation); + } + return result.build(); + } + + private Map reflectionByClassName(Iterable annotations) { + ImmutableMap.Builder result = ImmutableMap.builder(); + for (Annotation annotation : annotations) { + result.put(annotation.annotationType().getName(), annotation); + } + return result.build(); + } + + @Override + protected JavaAnnotationAssertion toAssert(JavaAnnotation value, String description) { + return assertThatAnnotation(value).as(description); + } +} diff --git a/archunit/src/test/resources/log4j2.xml b/archunit/src/test/resources/log4j2.xml index 0fff9802f6..99048df8cf 100644 --- a/archunit/src/test/resources/log4j2.xml +++ b/archunit/src/test/resources/log4j2.xml @@ -6,6 +6,9 @@ + + +