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/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/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/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/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)