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 20a625012d..418b474a23 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 @@ -56,4 +56,6 @@ public interface ImportContext { Set getConstructorsWithParameterOfType(JavaClass javaClass); Set> getConstructorThrowsDeclarationsOfType(JavaClass javaClass); + + JavaClass resolveClass(String fullyQualifiedClassName); } 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 e11cfac8af..bad96fb931 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 @@ -81,6 +81,7 @@ public class JavaClass implements HasName.AndFullName, HasAnnotations, HasModifi private final Set interfaces = new HashSet<>(); private final Set subClasses = new HashSet<>(); private Optional enclosingClass = Optional.absent(); + private Optional componentType = Optional.absent(); private Supplier> annotations = Suppliers.ofInstance(Collections.emptyMap()); private Supplier> allMethods; @@ -186,6 +187,30 @@ public boolean isArray() { return javaType.isArray(); } + /** + * This is a convenience method for {@link #tryGetComponentType()} in cases where + * clients know that this type is certainly an array type and thus the component type present. + * @throws IllegalArgumentException if this class is no array + * @return The result of {@link #tryGetComponentType()} + */ + @PublicAPI(usage = ACCESS) + public JavaClass getComponentType() { + return tryGetComponentType().getOrThrow(new IllegalArgumentException( + String.format("Type %s is no array", getSimpleName()))); + } + + /** + * 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. + * 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()} + */ + @PublicAPI(usage = ACCESS) + Optional tryGetComponentType() { + return componentType; + } + /** * @return Returns true if this class is declared within another class. * Returns false for top-level classes. @@ -841,6 +866,7 @@ public Map get() { } CompletionProcess completeFrom(ImportContext context) { + completeComponentType(context); enclosingClass = context.createEnclosingClass(this); memberDependenciesOnClass = new MemberDependenciesOnClass( context.getFieldsOfType(this), @@ -852,6 +878,15 @@ CompletionProcess completeFrom(ImportContext context) { return new CompletionProcess(); } + private void completeComponentType(ImportContext context) { + JavaClass current = this; + while (current.isArray() && !current.componentType.isPresent()) { + JavaClass componentType = context.resolveClass(current.javaType.tryGetComponentType().get().getName()); + current.componentType = Optional.of(componentType); + current = componentType; + } + } + @Override public String toString() { return "JavaClass{name='" + javaType.getName() + "\'}"; 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 f26f84428e..f9438ca636 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 @@ -275,6 +275,11 @@ public Optional createEnclosingClass(JavaClass owner) { Optional.absent(); } + @Override + public JavaClass resolveClass(String fullyQualifiedClassName) { + return classes.getOrResolve(fullyQualifiedClassName); + } + private static class MemberDependenciesByTarget { private final SetMultimap fieldTypeDependencies = HashMultimap.create(); private final SetMultimap methodParameterTypeDependencies = HashMultimap.create(); @@ -336,4 +341,4 @@ Set> getConstructorThrowsDeclarationsOfType(J return constructorThrowsDeclarationDependencies.get(javaClass); } } -} \ No newline at end of file +} 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 ebe85f8805..5c3faa9e8d 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 @@ -23,6 +23,9 @@ import com.tngtech.archunit.core.domain.testobjects.IsArrayTestClass; import com.tngtech.archunit.core.domain.testobjects.SuperA; import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.core.importer.testexamples.arrays.ClassAccessingOneDimensionalArray; +import com.tngtech.archunit.core.importer.testexamples.arrays.ClassAccessingTwoDimensionalArray; +import com.tngtech.archunit.core.importer.testexamples.arrays.ClassUsedInArray; import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; import com.tngtech.java.junit.dataprovider.UseDataProvider; @@ -80,6 +83,7 @@ public void finds_array_type() { JavaMethod method = importClassWithContext(IsArrayTestClass.class).getMethod("anArray"); assertThat(method.getRawReturnType().isArray()).isTrue(); + assertThat(method.getRawReturnType().tryGetComponentType().get()).matches(Object.class); } @Test @@ -87,6 +91,41 @@ public void finds_non_array_type() { JavaMethod method = importClassWithContext(IsArrayTestClass.class).getMethod("notAnArray"); assertThat(method.getRawReturnType().isArray()).isFalse(); + assertThat(method.getRawReturnType().tryGetComponentType()).isAbsent(); + } + + @Test + public void finds_multidimensional_array_type() { + JavaClasses classes = importClassesWithContext(ClassUsedInArray.class, ClassAccessingOneDimensionalArray.class, ClassAccessingTwoDimensionalArray.class); + JavaClass type = classes.get(ClassUsedInArray.class); + JavaClass oneDimArray = classes.get(ClassAccessingOneDimensionalArray.class).getField("array").getRawType(); + JavaClass twoDimArray = classes.get(ClassAccessingTwoDimensionalArray.class).getField("array").getRawType(); + + assertThat(oneDimArray.isArray()).isTrue(); + assertThat(oneDimArray.tryGetComponentType().get()).isEqualTo(type); + assertThat(twoDimArray.isArray()).isTrue(); + assertThat(twoDimArray.tryGetComponentType().get()).isEqualTo(oneDimArray); + assertThat(twoDimArray.tryGetComponentType().get().tryGetComponentType().get()).isEqualTo(type); + } + + @Test + public void finds_component_type_chain_of_otherwise_unreferenced_component_type() { + class OnlyReferencingMultiDimArray { + OnlyReferencingMultiDimArray[][][] field; + } + + JavaClass javaClass = importClassesWithContext(OnlyReferencingMultiDimArray.class) + .get(OnlyReferencingMultiDimArray.class); + + JavaClass arrayType = javaClass.getField("field").getRawType(); + JavaClass twoDim = arrayType.getComponentType(); + assertThat(twoDim.getName()).isEqualTo(OnlyReferencingMultiDimArray[][].class.getName()); + + JavaClass oneDim = twoDim.getComponentType(); + assertThat(oneDim.getName()).isEqualTo(OnlyReferencingMultiDimArray[].class.getName()); + + JavaClass original = oneDim.getComponentType(); + assertThat(original).isEqualTo(javaClass); } @Test @@ -1134,4 +1173,4 @@ private class NestedNamedInnerClass { } } } -} \ No newline at end of file +} 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 a8a6c71e93..c48fbd4657 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 @@ -428,5 +428,10 @@ public Set getConstructorsWithParameterOfType(JavaClass javaCla public Set> getConstructorThrowsDeclarationsOfType(JavaClass javaClass) { return Collections.emptySet(); } + + @Override + public JavaClass resolveClass(String fullyQualifiedClassName) { + throw new UnsupportedOperationException("Override me where necessary"); + } } -} \ No newline at end of file +}