From fe72d9211a761b5f869e04b5828746becec12c85 Mon Sep 17 00:00:00 2001 From: Peter Gafert Date: Sun, 24 Jan 2021 22:32:37 +0100 Subject: [PATCH] add referenced class objects to JavaClass To detect usages of class objects we need to look which classes are referenced in the constant pool. So far the following usage of `SomeClass` would not have been detected by ArchUnit: ``` class Example { private Map, Value> map = Map.of(SomeClass.class, someValue); } ``` In other words, pure usages of `Foo.class` could not be detected, because there was no "access" to `Foo` in the bytecode. However, for each such occurrence the class would actually be present in the constant pool of the referring class. While ASM does not allow direct access to the constant pool, it does allow us to hook into `ldc` instructions, i.e. load constant instructions in the bytecode, that will in turn reference the respective class. The way to detect this is principally an `instanceof` check for `org.objectweb.asm.Type` in `void visitLdcInsn(Object value)`. As far as I could see, any reference of another class as a constant would cause this method to be called with the respective ASM `Type` object. It would actually be possible to import a lot more constants here. I have looked a little into adding all supported constant types, so it would e.g. be possible to have assertions on `String` values, but then decided to let it go for now. Strings would still be simple, but as soon as `Integer` comes into play (which could also be imported), there are a lot of internal optimizations by the JVM (e.g. `iconst_1`, ...), which makes it hard to do this in a consistent way. I think the most valuable feature by far is to detect constant loads of classes (since those cause dependencies), so I decided to keep it simple for now. Signed-off-by: Peter Gafert --- .../domain/DomainObjectCreationContext.java | 4 + .../archunit/core/domain/JavaClass.java | 9 ++ .../archunit/core/domain/JavaCodeUnit.java | 9 +- .../core/domain/ReferencedClassObject.java | 102 ++++++++++++++++++ .../core/importer/DomainBuilders.java | 16 +++ .../importer/JavaClassDescriptorImporter.java | 10 +- .../core/importer/JavaClassProcessor.java | 8 ++ .../importer/RawReferencedClassObject.java | 51 +++++++++ .../domain/ReferencedClassObjectTest.java | 30 ++++++ .../core/importer/ClassFileImporterTest.java | 34 ++++++ .../ReferencingClassObjects.java | 24 +++++ .../tngtech/archunit/testutil/Assertions.java | 6 ++ .../ReferencedClassObjectsAssertion.java | 69 ++++++++++++ 13 files changed, 368 insertions(+), 4 deletions(-) create mode 100644 archunit/src/main/java/com/tngtech/archunit/core/domain/ReferencedClassObject.java create mode 100644 archunit/src/main/java/com/tngtech/archunit/core/importer/RawReferencedClassObject.java create mode 100644 archunit/src/test/java/com/tngtech/archunit/core/domain/ReferencedClassObjectTest.java create mode 100644 archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/referencedclassobjects/ReferencingClassObjects.java create mode 100644 archunit/src/test/java/com/tngtech/archunit/testutil/assertion/ReferencedClassObjectsAssertion.java diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/DomainObjectCreationContext.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/DomainObjectCreationContext.java index 9ba42f6ec7..939266fcc3 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/DomainObjectCreationContext.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/DomainObjectCreationContext.java @@ -146,6 +146,10 @@ public static Source createSource(URI uri, Optional sourceFileName, bool return new Source(uri, sourceFileName, md5InClassSourcesEnabled); } + public static ReferencedClassObject createReferencedClassObject(JavaCodeUnit codeUnit, JavaClass javaClass, int lineNumber) { + return ReferencedClassObject.from(codeUnit, javaClass, lineNumber); + } + public static ThrowsClause createThrowsClause(CODE_UNIT codeUnit, List types) { return ThrowsClause.from(codeUnit, types); } 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 d48fed6bd7..8297ea54a7 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 @@ -648,6 +648,15 @@ public List> getTypeParameters() { return typeParameters; } + @PublicAPI(usage = ACCESS) + public Set getReferencedClassObjects() { + ImmutableSet.Builder result = ImmutableSet.builder(); + for (JavaCodeUnit codeUnit : codeUnits) { + result.addAll(codeUnit.getReferencedClassObjects()); + } + return result.build(); + } + @Override @PublicAPI(usage = ACCESS) public JavaClass toErasure() { 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 f46b457033..3b059f7cb0 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 @@ -50,6 +50,7 @@ public abstract class JavaCodeUnit extends JavaMember implements HasParameterTyp private final JavaClass returnType; private final JavaClassList parameters; private final String fullName; + private final Set referencedClassObjects; private final Set instanceofChecks; private Set fieldAccesses = Collections.emptySet(); @@ -61,7 +62,8 @@ public abstract class JavaCodeUnit extends JavaMember implements HasParameterTyp this.returnType = builder.getReturnType(); this.parameters = builder.getParameters(); fullName = formatMethod(getOwner().getName(), getName(), getRawParameterTypes()); - instanceofChecks = builder.getInstanceofChecks(this); + referencedClassObjects = ImmutableSet.copyOf(builder.getReferencedClassObjects(this)); + instanceofChecks = ImmutableSet.copyOf(builder.getInstanceofChecks(this)); } /** @@ -112,6 +114,11 @@ public Set getConstructorCallsFromSelf() { return constructorCalls; } + @PublicAPI(usage = ACCESS) + public Set getReferencedClassObjects() { + return referencedClassObjects; + } + @PublicAPI(usage = ACCESS) public Set getInstanceofChecks() { return instanceofChecks; diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/ReferencedClassObject.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/ReferencedClassObject.java new file mode 100644 index 0000000000..1489f8e62b --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/ReferencedClassObject.java @@ -0,0 +1,102 @@ +/* + * 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.domain; + +import com.tngtech.archunit.PublicAPI; +import com.tngtech.archunit.base.ChainableFunction; +import com.tngtech.archunit.core.domain.properties.HasOwner; +import com.tngtech.archunit.core.domain.properties.HasSourceCodeLocation; +import com.tngtech.archunit.core.domain.properties.HasType; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; + +@PublicAPI(usage = ACCESS) +public final class ReferencedClassObject implements HasType, HasOwner, HasSourceCodeLocation { + private final JavaCodeUnit owner; + private final JavaClass value; + private final int lineNumber; + private final SourceCodeLocation sourceCodeLocation; + + private ReferencedClassObject(JavaCodeUnit owner, JavaClass value, int lineNumber) { + this.owner = checkNotNull(owner); + this.value = checkNotNull(value); + this.lineNumber = lineNumber; + sourceCodeLocation = SourceCodeLocation.of(owner.getOwner(), lineNumber); + } + + @Override + @PublicAPI(usage = ACCESS) + public JavaType getType() { + return getRawType(); + } + + @Override + @PublicAPI(usage = ACCESS) + public JavaClass getRawType() { + return value; + } + + @Override + @PublicAPI(usage = ACCESS) + public JavaCodeUnit getOwner() { + return owner; + } + + @PublicAPI(usage = ACCESS) + public JavaClass getValue() { + return value; + } + + @PublicAPI(usage = ACCESS) + public int getLineNumber() { + return lineNumber; + } + + @Override + @PublicAPI(usage = ACCESS) + public SourceCodeLocation getSourceCodeLocation() { + return sourceCodeLocation; + } + + @Override + public String toString() { + return toStringHelper(this) + .add("owner", owner) + .add("value", value) + .add("sourceCodeLocation", sourceCodeLocation) + .toString(); + } + + static ReferencedClassObject from(JavaCodeUnit owner, JavaClass javaClass, int lineNumber) { + return new ReferencedClassObject(owner, javaClass, lineNumber); + } + + @PublicAPI(usage = ACCESS) + public static final class Functions { + private Functions() { + } + + @PublicAPI(usage = ACCESS) + public static final ChainableFunction GET_VALUE = new ChainableFunction() { + @Override + public JavaClass apply(ReferencedClassObject input) { + return input.getValue(); + } + }; + } +} 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 a50f751d12..edddcc2a7c 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 @@ -59,6 +59,7 @@ import com.tngtech.archunit.core.domain.JavaType; import com.tngtech.archunit.core.domain.JavaTypeVariable; import com.tngtech.archunit.core.domain.JavaWildcardType; +import com.tngtech.archunit.core.domain.ReferencedClassObject; import com.tngtech.archunit.core.domain.Source; import com.tngtech.archunit.core.domain.ThrowsClause; @@ -67,6 +68,7 @@ import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.completeTypeVariable; import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createInstanceofCheck; import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createJavaClassList; +import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createReferencedClassObject; import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createSource; import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createThrowsClause; import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createTypeVariable; @@ -217,6 +219,7 @@ public abstract static class JavaCodeUnitBuilder parameters; private List throwsDeclarations; + private final Set rawReferencedClassObjects = new HashSet<>(); private final List instanceOfChecks = new ArrayList<>(); private JavaCodeUnitBuilder() { @@ -237,6 +240,11 @@ SELF withThrowsClause(List throwsDeclarations) { return self(); } + SELF addReferencedClassObject(RawReferencedClassObject rawReferencedClassObject) { + rawReferencedClassObjects.add(rawReferencedClassObject); + return self(); + } + SELF addInstanceOfCheck(RawInstanceofCheck rawInstanceOfChecks) { this.instanceOfChecks.add(rawInstanceOfChecks); return self(); @@ -262,6 +270,14 @@ public ThrowsClause getThrowsClause( return createThrowsClause(codeUnit, asJavaClasses(this.throwsDeclarations)); } + public Set getReferencedClassObjects(JavaCodeUnit codeUnit) { + ImmutableSet.Builder result = ImmutableSet.builder(); + for (RawReferencedClassObject rawReferencedClassObject : this.rawReferencedClassObjects) { + result.add(createReferencedClassObject(codeUnit, get(rawReferencedClassObject.getClassName()), rawReferencedClassObject.getLineNumber())); + } + return result.build(); + } + public Set getInstanceofChecks(JavaCodeUnit codeUnit) { ImmutableSet.Builder result = ImmutableSet.builder(); for (RawInstanceofCheck instanceOfCheck : this.instanceOfChecks) { diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaClassDescriptorImporter.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaClassDescriptorImporter.java index 5d317da71f..eb5bebb9d6 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaClassDescriptorImporter.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaClassDescriptorImporter.java @@ -27,10 +27,14 @@ class JavaClassDescriptorImporter { * i.e. java/lang/Object (note that this is not a descriptor like Ljava/lang/Object;) */ static JavaClassDescriptor createFromAsmObjectTypeName(String objectTypeName) { - return importAsmType(Type.getObjectType(objectTypeName)); + return JavaClassDescriptor.From.name(Type.getObjectType(objectTypeName).getClassName()); } - static JavaClassDescriptor importAsmType(Type type) { + static JavaClassDescriptor importAsmType(Object type) { + return importAsmType((Type) type); + } + + private static JavaClassDescriptor importAsmType(Type type) { return JavaClassDescriptor.From.name(type.getClassName()); } @@ -39,7 +43,7 @@ static boolean isAsmType(Object value) { } static Object importAsmTypeIfPossible(Object value) { - return isAsmType(value) ? importAsmType((Type) value) : value; + return isAsmType(value) ? importAsmType(value) : value; } static JavaClassDescriptor importAsmTypeFromDescriptor(String typeDescriptor) { 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 4c3ffc3b7d..749d6b7328 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 @@ -318,6 +318,14 @@ public void visitLineNumber(int line, Label start) { accessHandler.setLineNumber(actualLineNumber); } + @Override + public void visitLdcInsn(Object value) { + if (JavaClassDescriptorImporter.isAsmType(value)) { + codeUnitBuilder.addReferencedClassObject( + RawReferencedClassObject.from(JavaClassDescriptorImporter.importAsmType(value), actualLineNumber)); + } + } + @Override public void visitFieldInsn(int opcode, String owner, String name, String desc) { accessHandler.handleFieldInstruction(opcode, owner, name, desc); diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/RawReferencedClassObject.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/RawReferencedClassObject.java new file mode 100644 index 0000000000..9d5b6a3b02 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/RawReferencedClassObject.java @@ -0,0 +1,51 @@ +/* + * 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.JavaClassDescriptor; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +class RawReferencedClassObject { + private final JavaClassDescriptor type; + private final int lineNumber; + + private RawReferencedClassObject(JavaClassDescriptor type, int lineNumber) { + this.type = checkNotNull(type); + this.lineNumber = lineNumber; + } + + static RawReferencedClassObject from(JavaClassDescriptor target, int lineNumber) { + return new RawReferencedClassObject(target, lineNumber); + } + + String getClassName() { + return type.getFullyQualifiedClassName(); + } + + int getLineNumber() { + return lineNumber; + } + + @Override + public String toString() { + return toStringHelper(this) + .add("type", type) + .add("lineNumber", lineNumber) + .toString(); + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/core/domain/ReferencedClassObjectTest.java b/archunit/src/test/java/com/tngtech/archunit/core/domain/ReferencedClassObjectTest.java new file mode 100644 index 0000000000..0e8cad2339 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/core/domain/ReferencedClassObjectTest.java @@ -0,0 +1,30 @@ +package com.tngtech.archunit.core.domain; + +import java.io.File; + +import com.tngtech.archunit.core.importer.ClassFileImporter; +import org.junit.Test; + +import static com.google.common.collect.Iterables.getOnlyElement; +import static com.tngtech.archunit.core.domain.ReferencedClassObject.Functions.GET_VALUE; +import static org.assertj.core.api.Assertions.assertThat; + +public class ReferencedClassObjectTest { + + @Test + public void function_getValue() { + class SomeClass { + @SuppressWarnings("unused") + Class call() { + return File.class; + } + } + + JavaClasses classes = new ClassFileImporter().importClasses(SomeClass.class, File.class); + JavaMethod owner = classes.get(SomeClass.class).getMethod("call"); + + ReferencedClassObject referencedClassObject = getOnlyElement(owner.getReferencedClassObjects()); + + assertThat(GET_VALUE.apply(referencedClassObject)).isEqualTo(classes.get(File.class)); + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterTest.java b/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterTest.java index de795b987f..f4f22fb5f6 100644 --- a/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterTest.java @@ -1,6 +1,7 @@ package com.tngtech.archunit.core.importer; import java.io.File; +import java.io.FilterInputStream; import java.io.IOException; import java.io.PrintStream; import java.io.Serializable; @@ -12,6 +13,9 @@ import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; +import java.nio.Buffer; +import java.nio.charset.Charset; +import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -147,6 +151,7 @@ import com.tngtech.archunit.core.importer.testexamples.pathone.Class12; import com.tngtech.archunit.core.importer.testexamples.pathtwo.Class21; import com.tngtech.archunit.core.importer.testexamples.pathtwo.Class22; +import com.tngtech.archunit.core.importer.testexamples.referencedclassobjects.ReferencingClassObjects; import com.tngtech.archunit.core.importer.testexamples.simpleimport.AnnotationParameter; import com.tngtech.archunit.core.importer.testexamples.simpleimport.AnnotationToImport; import com.tngtech.archunit.core.importer.testexamples.simpleimport.ClassToImportOne; @@ -159,6 +164,7 @@ import com.tngtech.archunit.testutil.ArchConfigurationRule; import com.tngtech.archunit.testutil.LogTestRule; import com.tngtech.archunit.testutil.OutsideOfClassPathRule; +import com.tngtech.archunit.testutil.assertion.ReferencedClassObjectsAssertion.ExpectedReferencedClassObject; import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; import com.tngtech.java.junit.dataprovider.UseDataProvider; @@ -174,6 +180,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Predicates.containsPattern; import static com.google.common.base.Predicates.not; +import static com.google.common.collect.Iterables.concat; import static com.google.common.collect.Iterables.getFirst; import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.collect.Sets.newHashSet; @@ -200,12 +207,14 @@ import static com.tngtech.archunit.testutil.Assertions.assertThat; import static com.tngtech.archunit.testutil.Assertions.assertThatAccess; import static com.tngtech.archunit.testutil.Assertions.assertThatCall; +import static com.tngtech.archunit.testutil.Assertions.assertThatReferencedClassObjects; import static com.tngtech.archunit.testutil.Assertions.assertThatType; import static com.tngtech.archunit.testutil.Assertions.assertThatTypes; import static com.tngtech.archunit.testutil.ReflectionTestUtils.constructor; import static com.tngtech.archunit.testutil.ReflectionTestUtils.field; import static com.tngtech.archunit.testutil.ReflectionTestUtils.method; import static com.tngtech.archunit.testutil.TestUtils.namesOf; +import static com.tngtech.archunit.testutil.assertion.ReferencedClassObjectsAssertion.referencedClassObject; 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; @@ -836,6 +845,31 @@ public void imports_overridden_methods_correctly() throws Exception { assertThat(subclass.getCodeUnitWithParameterTypes("getSomeField").getModifiers()).containsOnly(PUBLIC); } + @Test + public void imports_referenced_class_objects() { + JavaClass javaClass = new ClassFileImporter().importClass(ReferencingClassObjects.class); + + Set expectedInConstructor = + ImmutableSet.of(referencedClassObject(File.class, 19), referencedClassObject(Path.class, 19)); + Set expectedInMethod = + ImmutableSet.of(referencedClassObject(FileSystem.class, 22), referencedClassObject(Charset.class, 22)); + Set expectedInStaticInitializer = + ImmutableSet.of(referencedClassObject(FilterInputStream.class, 16), referencedClassObject(Buffer.class, 16)); + + assertThatReferencedClassObjects(javaClass.getConstructor().getReferencedClassObjects()) + .hasSize(2) + .containReferencedClassObjects(expectedInConstructor); + assertThatReferencedClassObjects(javaClass.getMethod("referencedClassObjectsInMethod").getReferencedClassObjects()) + .hasSize(2) + .containReferencedClassObjects(expectedInMethod); + assertThatReferencedClassObjects(javaClass.getStaticInitializer().get().getReferencedClassObjects()) + .hasSize(2) + .containReferencedClassObjects(expectedInStaticInitializer); + assertThatReferencedClassObjects(javaClass.getReferencedClassObjects()) + .hasSize(6) + .containReferencedClassObjects(concat(expectedInConstructor, expectedInMethod, expectedInStaticInitializer)); + } + @Test public void imports_own_get_field_access() throws Exception { JavaClass classWithOwnFieldAccess = classesIn("testexamples/fieldaccessimport").get(OwnFieldAccess.class); diff --git a/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/referencedclassobjects/ReferencingClassObjects.java b/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/referencedclassobjects/ReferencingClassObjects.java new file mode 100644 index 0000000000..fd643bfad3 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/referencedclassobjects/ReferencingClassObjects.java @@ -0,0 +1,24 @@ +package com.tngtech.archunit.core.importer.testexamples.referencedclassobjects; + +import java.io.File; +import java.io.FilterInputStream; +import java.nio.Buffer; +import java.nio.charset.Charset; +import java.nio.file.FileSystem; +import java.nio.file.Path; +import java.util.List; + +import com.google.common.collect.ImmutableList; + +@SuppressWarnings({"FieldMayBeFinal", "unused"}) +public class ReferencingClassObjects { + static { + List> referencedClassObjectsInStaticInitializer = ImmutableList.of(FilterInputStream.class, Buffer.class); + } + + List> referencedClassObjectsInConstructor = ImmutableList.>of(File.class, Path.class); + + List> referencedClassObjectsInMethod() { + return ImmutableList.of(FileSystem.class, Charset.class); + } +} 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 adee9f10c9..162bc4d55c 100644 --- a/archunit/src/test/java/com/tngtech/archunit/testutil/Assertions.java +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/Assertions.java @@ -33,6 +33,7 @@ import com.tngtech.archunit.core.domain.JavaPackage; import com.tngtech.archunit.core.domain.JavaType; import com.tngtech.archunit.core.domain.JavaTypeVariable; +import com.tngtech.archunit.core.domain.ReferencedClassObject; import com.tngtech.archunit.core.domain.ThrowsClause; import com.tngtech.archunit.core.domain.ThrowsDeclaration; import com.tngtech.archunit.lang.ArchCondition; @@ -58,6 +59,7 @@ import com.tngtech.archunit.testutil.assertion.JavaTypeAssertion; import com.tngtech.archunit.testutil.assertion.JavaTypeVariableAssertion; import com.tngtech.archunit.testutil.assertion.JavaTypesAssertion; +import com.tngtech.archunit.testutil.assertion.ReferencedClassObjectsAssertion; import org.assertj.core.api.AbstractIterableAssert; import org.assertj.core.api.AbstractListAssert; import org.assertj.core.api.AbstractObjectAssert; @@ -161,6 +163,10 @@ public static JavaFieldAssertion assertThat(JavaField field) { return new JavaFieldAssertion(field); } + public static ReferencedClassObjectsAssertion assertThatReferencedClassObjects(Set referencedClassObjects) { + return new ReferencedClassObjectsAssertion(referencedClassObjects); + } + public static JavaEnumConstantAssertion assertThat(JavaEnumConstant enumConstant) { return new JavaEnumConstantAssertion(enumConstant); } diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/ReferencedClassObjectsAssertion.java b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/ReferencedClassObjectsAssertion.java new file mode 100644 index 0000000000..ae7392b9c6 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/ReferencedClassObjectsAssertion.java @@ -0,0 +1,69 @@ +package com.tngtech.archunit.testutil.assertion; + +import java.util.Set; + +import com.google.common.base.Predicate; +import com.google.common.collect.FluentIterable; +import com.tngtech.archunit.core.domain.ReferencedClassObject; +import org.assertj.core.api.AbstractIterableAssert; +import org.assertj.core.api.AbstractObjectAssert; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static org.assertj.core.api.Assertions.assertThat; + +public class ReferencedClassObjectsAssertion extends AbstractIterableAssert, ReferencedClassObject, ReferencedClassObjectsAssertion.ReferencedClassObjectAssertion> { + public ReferencedClassObjectsAssertion(Set referencedClassObjects) { + super(referencedClassObjects, ReferencedClassObjectsAssertion.class); + } + + @Override + protected ReferencedClassObjectAssertion toAssert(ReferencedClassObject value, String description) { + return new ReferencedClassObjectAssertion(value).as(description); + } + + public void containReferencedClassObjects(Iterable expectedReferencedClassObjects) { + final FluentIterable actualReferencedClassObjects = FluentIterable.from(actual); + Set unmatchedClassObjects = FluentIterable.from(expectedReferencedClassObjects) + .filter(new Predicate() { + @Override + public boolean apply(ExpectedReferencedClassObject expectedReferencedClassObject) { + return !actualReferencedClassObjects.anyMatch(expectedReferencedClassObject); + } + }).toSet(); + assertThat(unmatchedClassObjects).as("Referenced class objects not contained in " + actual).isEmpty(); + } + + static class ReferencedClassObjectAssertion extends AbstractObjectAssert { + public ReferencedClassObjectAssertion(ReferencedClassObject referencedClassObject) { + super(referencedClassObject, ReferencedClassObjectAssertion.class); + } + } + + public static ExpectedReferencedClassObject referencedClassObject(Class type, int lineNumber) { + return new ExpectedReferencedClassObject(type, lineNumber); + } + + public static class ExpectedReferencedClassObject implements Predicate { + private final Class type; + private final int lineNumber; + + private ExpectedReferencedClassObject(Class type, int lineNumber) { + this.type = type; + this.lineNumber = lineNumber; + } + + @Override + @SuppressWarnings("ConstantConditions") + public boolean apply(ReferencedClassObject input) { + return input.getValue().isEquivalentTo(type) && input.getLineNumber() == lineNumber; + } + + @Override + public String toString() { + return toStringHelper(this) + .add("type", type) + .add("lineNumber", lineNumber) + .toString(); + } + } +}