From 956475b789b7e4245ad5ebbba4db8f438fd9edff Mon Sep 17 00:00:00 2001 From: Johannes Wengert Date: Fri, 25 Sep 2020 17:00:10 +0200 Subject: [PATCH] Add component type of arrays to JavaClass dependencies from self This will apply recursively for multi-dimensional arrays, e.g. for a class `Bar` depending on `Foo[][][]` we would consider `Bar` to have dependencies to `Foo[][][]`, `Foo[][]`, `Foo[]` and `Foo`. Issue: #255 Signed-off-by: Johannes Wengert Signed-off-by: Peter Gafert --- .../a81a2b54-5a18-4145-b544-7a580aba0425 | 3 + .../archunit/core/domain/Dependency.java | 60 +++++--- .../core/domain/JavaClassDependencies.java | 38 +++--- .../archunit/core/domain/DependencyTest.java | 98 +++++++++++++- .../archunit/core/domain/JavaClassTest.java | 81 ++++++++++- .../archunit/core/domain/TestUtils.java | 2 +- .../assertion/DependenciesAssertion.java | 128 +++++++++++++++--- 7 files changed, 346 insertions(+), 64 deletions(-) diff --git a/archunit-example/example-plain/src/test/resources/frozen/a81a2b54-5a18-4145-b544-7a580aba0425 b/archunit-example/example-plain/src/test/resources/frozen/a81a2b54-5a18-4145-b544-7a580aba0425 index 6420a7657c..8c6177734c 100644 --- a/archunit-example/example-plain/src/test/resources/frozen/a81a2b54-5a18-4145-b544-7a580aba0425 +++ b/archunit-example/example-plain/src/test/resources/frozen/a81a2b54-5a18-4145-b544-7a580aba0425 @@ -9,6 +9,7 @@ Field has type in (SomeController.java:0) Field has type in (DaoCallingService.java:0) Field has type <[Lcom.tngtech.archunit.example.layers.service.ServiceType;> in (ServiceType.java:0) +Field depends on component type in (ServiceType.java:0) Field has type in (ServiceViolatingDaoRules.java:0) Method calls method in (SomeMediator.java:15) Method calls constructor ()> in (SomeGuiController.java:7) @@ -20,4 +21,6 @@ Method has return type in (ComplexServiceAnnotation.java:0) Method calls method <[Lcom.tngtech.archunit.example.layers.service.ServiceType;.clone()> in (ServiceType.java:3) Method has return type <[Lcom.tngtech.archunit.example.layers.service.ServiceType;> in (ServiceType.java:0) +Method depends on component type in (ServiceType.java:0) +Method depends on component type in (ServiceType.java:3) Method calls method in (ServiceViolatingDaoRules.java:27) \ No newline at end of file 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 5b76288367..5d6e496689 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 @@ -15,17 +15,18 @@ */ package com.tngtech.archunit.core.domain; +import java.util.Collections; import java.util.HashSet; import java.util.Objects; import java.util.Set; import com.google.common.base.MoreObjects; import com.google.common.collect.ComparisonChain; +import com.google.common.collect.ImmutableSet; import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.base.ChainableFunction; import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.base.HasDescription; -import com.tngtech.archunit.base.Optional; import com.tngtech.archunit.core.domain.properties.HasName; import com.tngtech.archunit.core.domain.properties.HasSourceCodeLocation; @@ -62,11 +63,17 @@ private Dependency(JavaClass originClass, JavaClass targetClass, int lineNumber, this.sourceCodeLocation = SourceCodeLocation.of(originClass, lineNumber); } - static Optional tryCreateFromAccess(JavaAccess access) { - if (access.getOriginOwner().equals(access.getTargetOwner()) || access.getTargetOwner().isPrimitive()) { - return Optional.absent(); + static Set tryCreateFromAccess(JavaAccess access) { + JavaClass originOwner = access.getOriginOwner(); + JavaClass targetOwner = access.getTargetOwner(); + if (originOwner.equals(targetOwner) || targetOwner.isPrimitive()) { + return Collections.emptySet(); } - return Optional.of(new Dependency(access.getOriginOwner(), access.getTargetOwner(), access.getLineNumber(), access.getDescription())); + + ImmutableSet.Builder dependencies = ImmutableSet.builder() + .addAll(createComponentTypeDependencies(originOwner, access.getOrigin().getDescription(), targetOwner, access.getSourceCodeLocation())); + dependencies.add(new Dependency(originOwner, targetOwner, access.getLineNumber(), access.getDescription())); + return dependencies.build(); } static Dependency fromInheritance(JavaClass origin, JavaClass targetSuperType) { @@ -87,32 +94,32 @@ static Dependency fromInheritance(JavaClass origin, JavaClass targetSuperType) { return new Dependency(origin, targetSuperType, 0, description); } - static Optional tryCreateFromField(JavaField field) { + static Set tryCreateFromField(JavaField field) { return tryCreateDependencyFromJavaMember(field, "has type", field.getRawType()); } - static Optional tryCreateFromReturnType(JavaMethod method) { + static Set tryCreateFromReturnType(JavaMethod method) { return tryCreateDependencyFromJavaMember(method, "has return type", method.getRawReturnType()); } - static Optional tryCreateFromParameter(JavaCodeUnit codeUnit, JavaClass parameter) { + static Set tryCreateFromParameter(JavaCodeUnit codeUnit, JavaClass parameter) { return tryCreateDependencyFromJavaMember(codeUnit, "has parameter of type", parameter); } - static Optional tryCreateFromThrowsDeclaration(ThrowsDeclaration declaration) { + static Set tryCreateFromThrowsDeclaration(ThrowsDeclaration declaration) { return tryCreateDependencyFromJavaMember(declaration.getLocation(), "throws type", declaration.getRawType()); } - static Optional tryCreateFromInstanceofCheck(InstanceofCheck instanceofCheck) { + static Set tryCreateFromInstanceofCheck(InstanceofCheck instanceofCheck) { return tryCreateDependencyFromJavaMemberWithLocation(instanceofCheck.getOwner(), "checks instanceof", instanceofCheck.getRawType(), instanceofCheck.getLineNumber()); } - static Optional tryCreateFromAnnotation(JavaAnnotation target) { + static Set tryCreateFromAnnotation(JavaAnnotation target) { Origin origin = findSuitableOrigin(target); return tryCreateDependency(origin.originClass, origin.originDescription, "is annotated with", target.getRawType()); } - static Optional tryCreateFromAnnotationMember(JavaAnnotation annotation, JavaClass memberType) { + static Set tryCreateFromAnnotationMember(JavaAnnotation annotation, JavaClass memberType) { Origin origin = findSuitableOrigin(annotation); return tryCreateDependency(origin.originClass, origin.originDescription, "has annotation member of type", memberType); } @@ -130,32 +137,47 @@ private static Origin findSuitableOrigin(JavaAnnotation annotation) { throw new IllegalStateException("Could not find suitable dependency origin for " + annotation); } - private static Optional tryCreateDependencyFromJavaMember(JavaMember origin, String dependencyType, JavaClass target) { + private static Set tryCreateDependencyFromJavaMember(JavaMember origin, String dependencyType, JavaClass target) { return tryCreateDependency(origin.getOwner(), origin.getDescription(), dependencyType, target); } - private static Optional tryCreateDependencyFromJavaMemberWithLocation(JavaMember origin, String dependencyType, JavaClass target, int lineNumber) { + private static Set tryCreateDependencyFromJavaMemberWithLocation(JavaMember origin, String dependencyType, JavaClass target, int lineNumber) { return tryCreateDependency(origin.getOwner(), origin.getDescription(), dependencyType, target, SourceCodeLocation.of(origin.getOwner(), lineNumber)); } - private static Optional tryCreateDependency( + private static Set tryCreateDependency( JavaClass originClass, String originDescription, String dependencyType, JavaClass targetClass) { return tryCreateDependency(originClass, originDescription, dependencyType, targetClass, originClass.getSourceCodeLocation()); } - private static Optional tryCreateDependency( + private static Set tryCreateDependency( JavaClass originClass, String originDescription, String dependencyType, JavaClass targetClass, SourceCodeLocation sourceCodeLocation) { if (originClass.equals(targetClass) || targetClass.isPrimitive()) { - return Optional.absent(); + return Collections.emptySet(); } + ImmutableSet.Builder dependencies = ImmutableSet.builder() + .addAll(createComponentTypeDependencies(originClass, originDescription, targetClass, sourceCodeLocation)); String targetDescription = bracketFormat(targetClass.getName()); String dependencyDescription = originDescription + " " + dependencyType + " " + targetDescription; String description = dependencyDescription + " in " + sourceCodeLocation; - int lineNumber = sourceCodeLocation.getLineNumber(); - return Optional.of(new Dependency(originClass, targetClass, lineNumber, description)); + dependencies.add(new Dependency(originClass, targetClass, sourceCodeLocation.getLineNumber(), description)); + return dependencies.build(); + } + + private static Set createComponentTypeDependencies(JavaClass originClass, String originDescription, JavaClass targetClass, SourceCodeLocation sourceCodeLocation) { + ImmutableSet.Builder result = ImmutableSet.builder(); + JavaClass componentType = targetClass; + while (componentType.isArray()) { + componentType = componentType.getComponentType(); + String componentTypeTargetDescription = bracketFormat(componentType.getName()); + String componentTypeDependencyDescription = originDescription + " depends on component type " + componentTypeTargetDescription; + String componentTypeDescription = componentTypeDependencyDescription + " in " + sourceCodeLocation; + result.add(new Dependency(originClass, componentType, sourceCodeLocation.getLineNumber(), componentTypeDescription)); + } + return result.build(); } private static String bracketFormat(String name) { 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 a7d17a79d7..9819d871d7 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 @@ -142,7 +142,7 @@ Set getInstanceofChecksWithTypeOfClass() { private Set dependenciesFromAccesses(Set> accesses) { ImmutableSet.Builder result = ImmutableSet.builder(); for (JavaAccess access : accesses) { - result.addAll(Dependency.tryCreateFromAccess(access).asSet()); + result.addAll(Dependency.tryCreateFromAccess(access)); } return result.build(); } @@ -158,7 +158,7 @@ private Set inheritanceDependenciesFromSelf() { private Set fieldDependenciesFromSelf() { ImmutableSet.Builder result = ImmutableSet.builder(); for (JavaField field : javaClass.getFields()) { - result.addAll(Dependency.tryCreateFromField(field).asSet()); + result.addAll(Dependency.tryCreateFromField(field)); } return result.build(); } @@ -166,7 +166,7 @@ private Set fieldDependenciesFromSelf() { private Set returnTypeDependenciesFromSelf() { ImmutableSet.Builder result = ImmutableSet.builder(); for (JavaMethod method : javaClass.getMethods()) { - result.addAll(Dependency.tryCreateFromReturnType(method).asSet()); + result.addAll(Dependency.tryCreateFromReturnType(method)); } return result.build(); } @@ -175,7 +175,7 @@ private Set methodParameterDependenciesFromSelf() { ImmutableSet.Builder result = ImmutableSet.builder(); for (JavaMethod method : javaClass.getMethods()) { for (JavaClass parameter : method.getRawParameterTypes()) { - result.addAll(Dependency.tryCreateFromParameter(method, parameter).asSet()); + result.addAll(Dependency.tryCreateFromParameter(method, parameter)); } } return result.build(); @@ -185,7 +185,7 @@ private Set throwsDeclarationDependenciesFromSelf() { ImmutableSet.Builder result = ImmutableSet.builder(); for (JavaCodeUnit codeUnit : javaClass.getCodeUnits()) { for (ThrowsDeclaration throwsDeclaration : codeUnit.getThrowsClause()) { - result.addAll(Dependency.tryCreateFromThrowsDeclaration(throwsDeclaration).asSet()); + result.addAll(Dependency.tryCreateFromThrowsDeclaration(throwsDeclaration)); } } return result.build(); @@ -195,7 +195,7 @@ private Set constructorParameterDependenciesFromSelf() { ImmutableSet.Builder result = ImmutableSet.builder(); for (JavaConstructor constructor : javaClass.getConstructors()) { for (JavaClass parameter : constructor.getRawParameterTypes()) { - result.addAll(Dependency.tryCreateFromParameter(constructor, parameter).asSet()); + result.addAll(Dependency.tryCreateFromParameter(constructor, parameter)); } } return result.build(); @@ -214,7 +214,7 @@ private Set instanceofCheckDependenciesFromSelf() { ImmutableSet.Builder result = ImmutableSet.builder(); for (JavaCodeUnit codeUnit : javaClass.getCodeUnits()) { for (InstanceofCheck instanceofCheck : codeUnit.getInstanceofChecks()) { - result.addAll(Dependency.tryCreateFromInstanceofCheck(instanceofCheck).asSet()); + result.addAll(Dependency.tryCreateFromInstanceofCheck(instanceofCheck)); } } return result.build(); @@ -231,21 +231,21 @@ private > Set annotatio private > Set annotationDependencies(T annotated) { final ImmutableSet.Builder result = ImmutableSet.builder(); for (final JavaAnnotation annotation : annotated.getAnnotations()) { - result.addAll(Dependency.tryCreateFromAnnotation(annotation).asSet()); + result.addAll(Dependency.tryCreateFromAnnotation(annotation)); annotation.accept(new DefaultParameterVisitor() { @Override public void visitClass(String propertyName, JavaClass javaClass) { - result.addAll(Dependency.tryCreateFromAnnotationMember(annotation, javaClass).asSet()); + result.addAll(Dependency.tryCreateFromAnnotationMember(annotation, javaClass)); } @Override public void visitEnumConstant(String propertyName, JavaEnumConstant enumConstant) { - result.addAll(Dependency.tryCreateFromAnnotationMember(annotation, enumConstant.getDeclaringClass()).asSet()); + result.addAll(Dependency.tryCreateFromAnnotationMember(annotation, enumConstant.getDeclaringClass())); } @Override public void visitAnnotation(String propertyName, JavaAnnotation memberAnnotation) { - result.addAll(Dependency.tryCreateFromAnnotationMember(annotation, memberAnnotation.getRawType()).asSet()); + result.addAll(Dependency.tryCreateFromAnnotationMember(annotation, memberAnnotation.getRawType())); memberAnnotation.accept(this); } }); @@ -264,7 +264,7 @@ private Set inheritanceDependenciesToSelf() { private Set fieldDependenciesToSelf() { Set result = new HashSet<>(); for (JavaField field : javaClass.getFieldsWithTypeOfSelf()) { - result.addAll(Dependency.tryCreateFromField(field).asSet()); + result.addAll(Dependency.tryCreateFromField(field)); } return result; } @@ -272,7 +272,7 @@ private Set fieldDependenciesToSelf() { private Set returnTypeDependenciesToSelf() { Set result = new HashSet<>(); for (JavaMethod method : javaClass.getMethodsWithReturnTypeOfSelf()) { - result.addAll(Dependency.tryCreateFromReturnType(method).asSet()); + result.addAll(Dependency.tryCreateFromReturnType(method)); } return result; } @@ -280,7 +280,7 @@ private Set returnTypeDependenciesToSelf() { private Set methodParameterDependenciesToSelf() { Set result = new HashSet<>(); for (JavaMethod method : javaClass.getMethodsWithParameterTypeOfSelf()) { - result.addAll(Dependency.tryCreateFromParameter(method, javaClass).asSet()); + result.addAll(Dependency.tryCreateFromParameter(method, javaClass)); } return result; } @@ -288,7 +288,7 @@ private Set methodParameterDependenciesToSelf() { private Set throwsDeclarationDependenciesToSelf() { Set result = new HashSet<>(); for (ThrowsDeclaration throwsDeclaration : getThrowsDeclarationsWithTypeOfClass()) { - result.addAll(Dependency.tryCreateFromThrowsDeclaration(throwsDeclaration).asSet()); + result.addAll(Dependency.tryCreateFromThrowsDeclaration(throwsDeclaration)); } return result; } @@ -296,7 +296,7 @@ private Set throwsDeclarationDependenciesToSelf() { private Set constructorParameterDependenciesToSelf() { Set result = new HashSet<>(); for (JavaConstructor constructor : javaClass.getConstructorsWithParameterTypeOfSelf()) { - result.addAll(Dependency.tryCreateFromParameter(constructor, javaClass).asSet()); + result.addAll(Dependency.tryCreateFromParameter(constructor, javaClass)); } return result; } @@ -304,10 +304,10 @@ private Set constructorParameterDependenciesToSelf() { private Iterable annotationDependenciesToSelf() { Set result = new HashSet<>(); for (JavaAnnotation annotation : annotationsWithTypeOfClass) { - result.addAll(Dependency.tryCreateFromAnnotation(annotation).asSet()); + result.addAll(Dependency.tryCreateFromAnnotation(annotation)); } for (JavaAnnotation annotation : annotationsWithParameterTypeOfClass) { - result.addAll(Dependency.tryCreateFromAnnotationMember(annotation, javaClass).asSet()); + result.addAll(Dependency.tryCreateFromAnnotationMember(annotation, javaClass)); } return result; } @@ -315,7 +315,7 @@ private Iterable annotationDependenciesToSelf() { private Set instanceofCheckDependenciesToSelf() { Set result = new HashSet<>(); for (InstanceofCheck instanceofCheck : getInstanceofChecksWithTypeOfClass()) { - result.addAll(Dependency.tryCreateFromInstanceofCheck(instanceofCheck).asSet()); + result.addAll(Dependency.tryCreateFromInstanceofCheck(instanceofCheck)); } return result; } 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 469f9ab7df..0dd11a0225 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 @@ -3,12 +3,17 @@ import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Set; import com.google.common.base.MoreObjects; import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.core.domain.testobjects.ClassWithDependencyOnInstanceofCheck; import com.tngtech.archunit.core.domain.testobjects.ClassWithDependencyOnInstanceofCheck.InstanceOfCheckTarget; +import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.testutil.Assertions; +import com.tngtech.archunit.testutil.assertion.DependenciesAssertion; import com.tngtech.archunit.testutil.assertion.DependencyAssertion; import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; @@ -26,23 +31,102 @@ import static com.tngtech.archunit.core.domain.TestUtils.importClassesWithContext; import static com.tngtech.archunit.core.domain.TestUtils.simulateCall; import static com.tngtech.archunit.testutil.Assertions.assertThat; +import static com.tngtech.archunit.testutil.Assertions.assertThatDependencies; import static com.tngtech.archunit.testutil.Assertions.assertThatType; +import static com.tngtech.archunit.testutil.assertion.DependenciesAssertion.from; import static com.tngtech.java.junit.dataprovider.DataProviders.$; import static com.tngtech.java.junit.dataprovider.DataProviders.$$; import static com.tngtech.java.junit.dataprovider.DataProviders.testForEach; @RunWith(DataProviderRunner.class) public class DependencyTest { + + @DataProvider + public static Object[][] field_array_types() throws NoSuchFieldException { + @SuppressWarnings("unused") + class ClassWithArrayDependencies { + private String[] oneDimArray; + private String[][] multiDimArray; + } + return testForEach( + ClassWithArrayDependencies.class.getDeclaredField("oneDimArray"), + ClassWithArrayDependencies.class.getDeclaredField("multiDimArray")); + } + + @Test + @UseDataProvider("field_array_types") + public void Dependencies_from_field_with_component_type(Field reflectionArrayField) { + Class reflectionDeclaringClass = reflectionArrayField.getDeclaringClass(); + JavaField field = new ClassFileImporter().importClasses(reflectionDeclaringClass).get(reflectionDeclaringClass).getField(reflectionArrayField.getName()); + + Set dependencies = Dependency.tryCreateFromField(field); + + DependenciesAssertion.ExpectedDependencies expectedDependencies = from(reflectionDeclaringClass).to(reflectionArrayField.getType()) + .withDescriptionContaining("Field <%s> has type <%s>", field.getFullName(), reflectionArrayField.getType().getName()) + .inLocation(DependencyTest.class, 0); + Class expectedComponentType = reflectionArrayField.getType().getComponentType(); + while (expectedComponentType != null) { + expectedDependencies.from(reflectionDeclaringClass).to(expectedComponentType) + .withDescriptionContaining("Field <%s> depends on component type <%s>", field.getFullName(), expectedComponentType.getName()) + .inLocation(DependencyTest.class, 0); + expectedComponentType = expectedComponentType.getComponentType(); + } + + assertThatDependencies(dependencies).containOnly(expectedDependencies); + } + @Test public void Dependency_from_access() { JavaMethodCall call = simulateCall().from(getClass(), "toString").to(Object.class, "toString"); - Dependency dependency = Dependency.tryCreateFromAccess(call).get(); + Dependency dependency = getOnlyElement(Dependency.tryCreateFromAccess(call)); assertThatType(dependency.getTargetClass()).as("target class").isEqualTo(call.getTargetOwner()); assertThat(dependency.getDescription()) .as("description").isEqualTo(call.getDescription()); } + @DataProvider + public static Object[][] method_calls_to_array_types() throws NoSuchMethodException { + @SuppressWarnings("unused") + class ClassWithArrayDependencies { + private void oneDimArray() { + new String[0].clone(); + } + + private void multiDimArray() { + new String[0][0].clone(); + } + } + return $$( + $(ClassWithArrayDependencies.class.getDeclaredMethod("oneDimArray"), String[].class, 93), + $(ClassWithArrayDependencies.class.getDeclaredMethod("multiDimArray"), String[][].class, 97) + ); + } + + @Test + @UseDataProvider("method_calls_to_array_types") + public void Dependency_from_access_with_component_type(Method reflectionMethodWithArrayMethodCall, Class arrayType, int expectedLineNumber) { + Class reflectionDeclaringClass = reflectionMethodWithArrayMethodCall.getDeclaringClass(); + JavaMethod method = new ClassFileImporter().importClasses(reflectionDeclaringClass) + .get(reflectionDeclaringClass).getMethod(reflectionMethodWithArrayMethodCall.getName()); + JavaMethodCall call = getOnlyElement(method.getMethodCallsFromSelf()); + + Set dependencies = Dependency.tryCreateFromAccess(call); + + DependenciesAssertion.ExpectedDependencies expectedDependencies = from(reflectionDeclaringClass).to(arrayType) + .withDescriptionContaining("Method <%s> calls method <%s>", method.getFullName(), arrayType.getName() + ".clone()") + .inLocation(DependencyTest.class, expectedLineNumber); + Class expectedComponentType = arrayType.getComponentType(); + while (expectedComponentType != null) { + expectedDependencies.from(reflectionDeclaringClass).to(expectedComponentType) + .withDescriptionContaining("Method <%s> depends on component type <%s>", method.getFullName(), expectedComponentType.getName()) + .inLocation(DependencyTest.class, expectedLineNumber); + expectedComponentType = expectedComponentType.getComponentType(); + } + + assertThatDependencies(dependencies).containOnly(expectedDependencies); + } + @Test public void Dependency_from_origin_and_target() { JavaClass origin = importClassWithContext(getClass()); @@ -68,7 +152,7 @@ public void Dependency_from_throws_declaration() { .get(ClassWithDependencyOnThrowable.class).getMethod("method"); ThrowsDeclaration throwsDeclaration = getOnlyElement(origin.getThrowsClause()); - Dependency dependency = Dependency.tryCreateFromThrowsDeclaration(throwsDeclaration).get(); + Dependency dependency = getOnlyElement(Dependency.tryCreateFromThrowsDeclaration(throwsDeclaration)); assertThatType(dependency.getOriginClass()).matches(ClassWithDependencyOnThrowable.class); assertThatType(dependency.getTargetClass()).matches(IOException.class); @@ -92,7 +176,7 @@ public static Object[][] with_instanceof_check_members() { public void Dependency_from_instanceof_check_in_code_unit(JavaCodeUnit memberWithInstanceofCheck, int expectedLineNumber) { InstanceofCheck instanceofCheck = getOnlyElement(memberWithInstanceofCheck.getInstanceofChecks()); - Dependency dependency = Dependency.tryCreateFromInstanceofCheck(instanceofCheck).get(); + Dependency dependency = getOnlyElement(Dependency.tryCreateFromInstanceofCheck(instanceofCheck)); Assertions.assertThatDependency(dependency) .matches(ClassWithDependencyOnInstanceofCheck.class, InstanceOfCheckTarget.class) @@ -116,7 +200,7 @@ public void Dependency_from_annotation_on_class(JavaClass annotatedClass) { JavaAnnotation annotation = annotatedClass.getAnnotations().iterator().next(); Class annotationClass = annotation.getRawType().reflect(); - Dependency dependency = Dependency.tryCreateFromAnnotation(annotation).get(); + Dependency dependency = getOnlyElement(Dependency.tryCreateFromAnnotation(annotation)); assertThatType(dependency.getOriginClass()).isEqualTo(annotatedClass); assertThatType(dependency.getTargetClass()).matches(annotationClass); assertThat(dependency.getDescription()).as("description") @@ -140,7 +224,7 @@ public void Dependency_from_annotation_on_member(JavaMember annotatedMember) { JavaAnnotation annotation = annotatedMember.getAnnotations().iterator().next(); Class annotationClass = annotation.getRawType().reflect(); - Dependency dependency = Dependency.tryCreateFromAnnotation(annotation).get(); + Dependency dependency = getOnlyElement(Dependency.tryCreateFromAnnotation(annotation)); assertThatType(dependency.getOriginClass()).matches(ClassWithAnnotatedMembers.class); assertThatType(dependency.getTargetClass()).matches(annotationClass); assertThat(dependency.getDescription()).as("description") @@ -153,7 +237,7 @@ public void Dependency_from_class_annotation_member(JavaClass annotatedClass) { JavaAnnotation annotation = annotatedClass.getAnnotationOfType(SomeAnnotation.class.getName()); JavaClass memberType = ((JavaClass) annotation.get("value").get()); - Dependency dependency = Dependency.tryCreateFromAnnotationMember(annotation, memberType).get(); + Dependency dependency = getOnlyElement(Dependency.tryCreateFromAnnotationMember(annotation, memberType)); assertThatType(dependency.getOriginClass()).isEqualTo(annotatedClass); assertThatType(dependency.getTargetClass()).isEqualTo(memberType); assertThat(dependency.getDescription()).as("description") @@ -166,7 +250,7 @@ public void Dependency_from_member_annotation_member(JavaMember annotatedMember) JavaAnnotation annotation = annotatedMember.getAnnotationOfType(SomeAnnotation.class.getName()); JavaClass memberType = ((JavaClass) annotation.get("value").get()); - Dependency dependency = Dependency.tryCreateFromAnnotationMember(annotation, memberType).get(); + Dependency dependency = getOnlyElement(Dependency.tryCreateFromAnnotationMember(annotation, memberType)); assertThatType(dependency.getOriginClass()).isEqualTo(annotatedMember.getOwner()); assertThatType(dependency.getTargetClass()).isEqualTo(memberType); assertThat(dependency.getDescription()).as("description") 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 0ab0f0b3e2..ea7231ca3a 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 @@ -1,5 +1,6 @@ package com.tngtech.archunit.core.domain; +import java.io.File; import java.io.Serializable; import java.lang.annotation.Retention; import java.util.AbstractList; @@ -77,6 +78,7 @@ import static com.tngtech.archunit.testutil.ReflectionTestUtils.getHierarchy; import static com.tngtech.java.junit.dataprovider.DataProviders.testForEach; import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static java.util.regex.Pattern.quote; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -86,6 +88,77 @@ public class JavaClassTest { @Rule public ExpectedException thrown = ExpectedException.none(); + @Test + public void finds_array_component_types_as_dependencies_from_self() { + @SuppressWarnings("unused") + class ArrayComponentTypeDependencies { + private String[] strings; + + public ArrayComponentTypeDependencies(Integer[] ints) { + } + + private void internal() { + new File[0].clone(); + } + + public Object[] objects() { + return null; + } + + public void foo(Float[] floats) { + } + } + + JavaClass javaClass = importClassWithContext(ArrayComponentTypeDependencies.class); + + // Assert the presence of both the array type and the component type of the array for all dependencies + assertThat(javaClass.getDirectDependenciesFromSelf()) + .areAtLeastOne(callDependency() + .from(ArrayComponentTypeDependencies.class) + .to(File[].class) + .inLineNumber(101)) + .areAtLeastOne(componentTypeDependency() + .from(ArrayComponentTypeDependencies.class) + .to(File.class) + .inLineNumber(101)) + + .areAtLeastOne(methodReturnTypeDependency() + .from(ArrayComponentTypeDependencies.class) + .to(Object[].class) + .inLineNumber(0)) + .areAtLeastOne(componentTypeDependency() + .from(ArrayComponentTypeDependencies.class) + .to(Object.class) + .inLineNumber(0)) + + .areAtLeastOne(fieldTypeDependency() + .from(ArrayComponentTypeDependencies.class) + .to(String[].class) + .inLineNumber(0)) + .areAtLeastOne(componentTypeDependency() + .from(ArrayComponentTypeDependencies.class) + .to(String.class) + .inLineNumber(0)) + + .areAtLeastOne(parameterTypeDependency() + .from(ArrayComponentTypeDependencies.class) + .to(Integer[].class) + .inLineNumber(0)) + .areAtLeastOne(componentTypeDependency() + .from(ArrayComponentTypeDependencies.class) + .to(Integer.class) + .inLineNumber(0)) + + .areAtLeastOne(parameterTypeDependency() + .from(ArrayComponentTypeDependencies.class) + .to(Float[].class) + .inLineNumber(0)) + .areAtLeastOne(componentTypeDependency() + .from(ArrayComponentTypeDependencies.class) + .to(Float.class) + .inLineNumber(0)); + } + @Test public void finds_array_type() { @SuppressWarnings("unused") @@ -1061,6 +1134,10 @@ private static DependencyConditionCreation annotationMemberOfTypeDependency() { return new DependencyConditionCreation("has annotation member of type"); } + private static DependencyConditionCreation componentTypeDependency() { + return new DependencyConditionCreation("depends on component type"); + } + private static AnyDependencyConditionCreation anyDependency() { return new AnyDependencyConditionCreation(); } @@ -1128,7 +1205,7 @@ private class Step3 { Step3(Class target) { this.target = target; - targetDescription = target.getSimpleName(); + targetDescription = target.getName(); } Step3(Class target, String targetName) { @@ -1144,7 +1221,7 @@ public boolean matches(Dependency value) { return value.getOriginClass().isEquivalentTo(origin) && value.getTargetClass().isEquivalentTo(target) && value.getDescription().matches(String.format(".*%s.*%s.*%s.*:%d.*", - origin.getSimpleName(), descriptionPart, targetDescription, lineNumber)); + quote(origin.getSimpleName()), quote(descriptionPart), quote(targetDescription), lineNumber)); } }; } diff --git a/archunit/src/test/java/com/tngtech/archunit/core/domain/TestUtils.java b/archunit/src/test/java/com/tngtech/archunit/core/domain/TestUtils.java index 256b9d011c..ca2691376c 100644 --- a/archunit/src/test/java/com/tngtech/archunit/core/domain/TestUtils.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/domain/TestUtils.java @@ -140,7 +140,7 @@ public static ConstructorCallTarget targetFrom(JavaConstructor constructor) { } public static Dependency dependencyFrom(JavaAccess access) { - return Dependency.tryCreateFromAccess(access).get(); + return getOnlyElement(Dependency.tryCreateFromAccess(access)); } public static class AccessesSimulator { diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/DependenciesAssertion.java b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/DependenciesAssertion.java index 91504a16cf..1ee77dd3de 100644 --- a/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/DependenciesAssertion.java +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/DependenciesAssertion.java @@ -3,17 +3,24 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.regex.Pattern; +import com.google.common.base.Joiner; +import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.collect.FluentIterable; -import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; import com.tngtech.archunit.core.domain.Dependency; import org.assertj.core.api.AbstractIterableAssert; +import static com.google.common.base.Predicates.not; +import static com.google.common.collect.Iterables.getLast; +import static java.lang.System.lineSeparator; +import static java.util.regex.Pattern.quote; import static org.assertj.core.api.Assertions.assertThat; public class DependenciesAssertion extends AbstractIterableAssert< - DependenciesAssertion, Iterable, Dependency, DependencyAssertion> { + DependenciesAssertion, Iterable, Dependency, DependencyAssertion> { public DependenciesAssertion(Iterable dependencies) { super(dependencies, DependenciesAssertion.class); @@ -53,20 +60,40 @@ public DependenciesAssertion doesNotContain(Class expectedOrigin, Class ex return this; } + public DependenciesAssertion contain(final ExpectedDependencies expectedDependencies) { + matchExpectedDependencies(expectedDependencies) + .assertNoMissingDependencies(); + return this; + } + public DependenciesAssertion containOnly(final ExpectedDependencies expectedDependencies) { - FluentIterable rest = FluentIterable.from(actual); - for (final List> expectedDependency : expectedDependencies) { - rest = rest.filter(new Predicate() { - @Override - public boolean apply(Dependency input) { - return !matches(input, expectedDependency.get(0), expectedDependency.get(1)); - } - }); - } - assertThat(rest.toSet()).as("unexpected elements").isEmpty(); + ExpectedDependenciesMatchResult result = matchExpectedDependencies(expectedDependencies); + result.assertNoMissingDependencies(); + result.assertAllDependenciesMatched(); return this; } + private ExpectedDependenciesMatchResult matchExpectedDependencies(ExpectedDependencies expectedDependencies) { + FluentIterable rest = FluentIterable.from(actual); + List missingDependencies = new ArrayList<>(); + for (final ExpectedDependency expectedDependency : expectedDependencies) { + if (!rest.anyMatch(matches(expectedDependency))) { + missingDependencies.add(expectedDependency); + } + rest = rest.filter(not(matches(expectedDependency))); + } + return new ExpectedDependenciesMatchResult(missingDependencies, rest.toList()); + } + + private Predicate matches(final ExpectedDependency expectedDependency) { + return new Predicate() { + @Override + public boolean apply(Dependency input) { + return expectedDependency.matches(input); + } + }; + } + public DependenciesAssertion containOnly(Class expectedOrigin, Class expectedTarget) { for (Dependency dependency : actual) { toAssert(dependency, dependency.getDescription()).matches(expectedOrigin, expectedTarget); @@ -96,14 +123,14 @@ public ExpectedDependencies to(Class target) { } } - public static class ExpectedDependencies implements Iterable>> { - List>> expectedDependencies = new ArrayList<>(); + public static class ExpectedDependencies implements Iterable { + List expectedDependencies = new ArrayList<>(); private ExpectedDependencies() { } @Override - public Iterator>> iterator() { + public Iterator iterator() { return expectedDependencies.iterator(); } @@ -112,8 +139,77 @@ public ExpectedDependenciesCreator from(Class origin) { } ExpectedDependencies add(Class origin, Class target) { - expectedDependencies.add(ImmutableList.of(origin, target)); + expectedDependencies.add(new ExpectedDependency(origin, target)); return this; } + + public ExpectedDependencies withDescriptionContaining(String descriptionTemplate, Object... args) { + getLast(expectedDependencies).descriptionContaining(descriptionTemplate, args); + return this; + } + + public ExpectedDependencies inLocation(Class location, int lineNumber) { + getLast(expectedDependencies).location(location, lineNumber); + return this; + } + } + + private static class ExpectedDependency { + private final Class origin; + private final Class target; + private Optional descriptionPattern = Optional.absent(); + private Optional locationPart = Optional.absent(); + + ExpectedDependency(Class origin, Class target) { + this.origin = origin; + this.target = target; + } + + boolean matches(Dependency dependency) { + if (!dependency.getOriginClass().isEquivalentTo(origin) || !dependency.getTargetClass().isEquivalentTo(target)) { + return false; + } + if (descriptionPattern.isPresent() && !descriptionPattern.get().matcher(dependency.getDescription()).matches()) { + return false; + } + return !locationPart.isPresent() || dependency.getDescription().endsWith(locationPart.get()); + } + + public void descriptionContaining(String descriptionTemplate, Object[] args) { + String descriptionPart = String.format(descriptionTemplate, args); + descriptionPattern = Optional.of(Pattern.compile(".*" + quote(descriptionPart) + ".*")); + } + + public void location(Class location, int lineNumber) { + locationPart = Optional.of(String.format("in (%s.java:%d)", location.getSimpleName(), lineNumber)); + } + + @Override + public String toString() { + String dependency = origin.getName() + " -> " + target.getName(); + String location = locationPart.isPresent() ? " " + locationPart.get() : ""; + String description = descriptionPattern.isPresent() ? " with description matching " + descriptionPattern.get() : ""; + return dependency + location + description; + } + } + + private static class ExpectedDependenciesMatchResult { + private final Iterable missingDependencies; + private final Iterable unexpectedDependencies; + + private ExpectedDependenciesMatchResult(Iterable missingDependencies, Iterable unexpectedDependencies) { + this.missingDependencies = missingDependencies; + this.unexpectedDependencies = unexpectedDependencies; + } + + void assertNoMissingDependencies() { + if (!Iterables.isEmpty(missingDependencies)) { + throw new AssertionError("Could not find expected dependencies:" + lineSeparator() + Joiner.on(lineSeparator()).join(missingDependencies)); + } + } + + public void assertAllDependenciesMatched() { + assertThat(unexpectedDependencies).as("unexpected dependencies").isEmpty(); + } } }