From 5dcccc7042c71fc186438de184ff747412d7744a Mon Sep 17 00:00:00 2001 From: Peter Gafert Date: Tue, 26 Jan 2021 20:53:50 +0100 Subject: [PATCH 1/5] add `toString` to `(Raw)InstanceofCheck` While `toString()` is not really intended as API we should always make sure there is some reasonable string output for debugging purposes. Signed-off-by: Peter Gafert --- .../archunit/core/domain/InstanceofCheck.java | 10 ++++++++++ .../core/importer/RawInstanceofCheck.java | 15 ++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/InstanceofCheck.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/InstanceofCheck.java index 83a8f06694..182b6a1256 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/InstanceofCheck.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/InstanceofCheck.java @@ -19,6 +19,7 @@ import com.tngtech.archunit.core.domain.properties.HasOwner; 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; @@ -57,6 +58,15 @@ public int getLineNumber() { return lineNumber; } + @Override + public String toString() { + return toStringHelper(this) + .add("owner", owner) + .add("target", target) + .add("lineNumber", lineNumber) + .toString(); + } + static InstanceofCheck from(JavaCodeUnit owner, JavaClass target, int lineNumber) { return new InstanceofCheck(owner, target, lineNumber); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/RawInstanceofCheck.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/RawInstanceofCheck.java index 56bb777a51..43afbc19b6 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/RawInstanceofCheck.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/RawInstanceofCheck.java @@ -17,6 +17,7 @@ import com.tngtech.archunit.core.domain.JavaClassDescriptor; +import static com.google.common.base.MoreObjects.toStringHelper; import static com.google.common.base.Preconditions.checkNotNull; class RawInstanceofCheck { @@ -28,15 +29,23 @@ private RawInstanceofCheck(JavaClassDescriptor target, int lineNumber) { this.lineNumber = lineNumber; } - public static RawInstanceofCheck from(JavaClassDescriptor target, int lineNumber) { + static RawInstanceofCheck from(JavaClassDescriptor target, int lineNumber) { return new RawInstanceofCheck(target, lineNumber); } - public JavaClassDescriptor getTarget() { + JavaClassDescriptor getTarget() { return target; } - public int getLineNumber() { + int getLineNumber() { return lineNumber; } + + @Override + public String toString() { + return toStringHelper(this) + .add("target", target) + .add("lineNumber", lineNumber) + .toString(); + } } From 21a074a6a818d40b6373a769146671201dd57bfb Mon Sep 17 00:00:00 2001 From: Peter Gafert Date: Tue, 26 Jan 2021 21:42:45 +0100 Subject: [PATCH 2/5] add `SourceCodeLocation` to `InstanceofCheck` We should unify this and let all domain objects that have a designated source code location implement `HasSourceCodeLocation`. Signed-off-by: Peter Gafert --- .../archunit/core/domain/Dependency.java | 53 ++++++++++++------- .../archunit/core/domain/InstanceofCheck.java | 10 +++- 2 files changed, 42 insertions(+), 21 deletions(-) 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 120c2c8c55..1065485389 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 @@ -28,6 +28,7 @@ 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.HasOwner; import com.tngtech.archunit.core.domain.properties.HasSourceCodeLocation; import static com.google.common.base.Preconditions.checkArgument; @@ -100,44 +101,46 @@ static Dependency fromInheritance(JavaClass origin, JavaClass targetSupertype) { } static Set tryCreateFromField(JavaField field) { - return tryCreateDependencyFromJavaMember(field, "has type", field.getRawType()); + return tryCreateDependency(field, "has type", field.getRawType()); } static Set tryCreateFromReturnType(JavaMethod method) { - return tryCreateDependencyFromJavaMember(method, "has return type", method.getRawReturnType()); + return tryCreateDependency(method, "has return type", method.getRawReturnType()); } static Set tryCreateFromParameter(JavaCodeUnit codeUnit, JavaClass parameter) { - return tryCreateDependencyFromJavaMember(codeUnit, "has parameter of type", parameter); + return tryCreateDependency(codeUnit, "has parameter of type", parameter); } static Set tryCreateFromThrowsDeclaration(ThrowsDeclaration declaration) { - return tryCreateDependencyFromJavaMember(declaration.getLocation(), "throws type", declaration.getRawType()); + return tryCreateDependency(declaration.getLocation(), "throws type", declaration.getRawType()); } static Set tryCreateFromInstanceofCheck(InstanceofCheck instanceofCheck) { - return tryCreateDependencyFromJavaMemberWithLocation(instanceofCheck.getOwner(), "checks instanceof", instanceofCheck.getRawType(), instanceofCheck.getLineNumber()); + return tryCreateDependency( + instanceofCheck.getOwner(), "checks instanceof", + instanceofCheck.getRawType(), instanceofCheck.getSourceCodeLocation()); } static Set tryCreateFromAnnotation(JavaAnnotation target) { Origin origin = findSuitableOrigin(target, target.getAnnotatedElement()); - return tryCreateDependency(origin.originClass, origin.originDescription, "is annotated with", target.getRawType()); + return tryCreateDependency(origin, "is annotated with", target.getRawType()); } static Set tryCreateFromAnnotationMember(JavaAnnotation annotation, JavaClass memberType) { Origin origin = findSuitableOrigin(annotation, annotation.getAnnotatedElement()); - return tryCreateDependency(origin.originClass, origin.originDescription, "has annotation member of type", memberType); + return tryCreateDependency(origin, "has annotation member of type", memberType); } static Set tryCreateFromTypeParameter(JavaTypeVariable typeParameter, JavaClass typeParameterDependency) { String dependencyType = "has type parameter '" + typeParameter.getName() + "' depending on"; Origin origin = findSuitableOrigin(typeParameter, typeParameter.getOwner()); - return tryCreateDependency(origin.originClass, origin.originDescription, dependencyType, typeParameterDependency); + return tryCreateDependency(origin, dependencyType, typeParameterDependency); } static Set tryCreateFromGenericSuperclassTypeArguments(JavaClass originClass, JavaType superclass, JavaClass typeArgumentDependency) { String dependencyType = "has generic superclass " + bracketFormat(superclass.getName()) + " with type argument depending on"; - return tryCreateDependency(originClass, originClass.getDescription(), dependencyType, typeArgumentDependency); + return tryCreateDependency(originClass, originClass.getDescription(), dependencyType, typeArgumentDependency, originClass.getSourceCodeLocation()); } private static Origin findSuitableOrigin(Object dependencyCause, Object originCandidate) { @@ -152,22 +155,20 @@ private static Origin findSuitableOrigin(Object dependencyCause, Object originCa throw new IllegalStateException("Could not find suitable dependency origin for " + dependencyCause); } - private static Set tryCreateDependencyFromJavaMember(JavaMember origin, String dependencyType, JavaClass target) { - return tryCreateDependency(origin.getOwner(), origin.getDescription(), dependencyType, target); + private static & HasDescription> Set tryCreateDependency( + T origin, String dependencyType, JavaClass targetClass) { + return tryCreateDependency(origin, dependencyType, targetClass, origin.getOwner().getSourceCodeLocation()); } - 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 Set tryCreateDependency( - JavaClass originClass, String originDescription, String dependencyType, JavaClass targetClass) { + private static & HasDescription> Set tryCreateDependency( + T origin, String dependencyType, JavaClass targetClass, SourceCodeLocation sourceCodeLocation) { - return tryCreateDependency(originClass, originDescription, dependencyType, targetClass, originClass.getSourceCodeLocation()); + return tryCreateDependency(origin.getOwner(), origin.getDescription(), dependencyType, targetClass, sourceCodeLocation); } private static Set tryCreateDependency( JavaClass originClass, String originDescription, String dependencyType, JavaClass targetClass, SourceCodeLocation sourceCodeLocation) { + ImmutableSet.Builder dependencies = ImmutableSet.builder() .addAll(createComponentTypeDependencies(originClass, originDescription, targetClass, sourceCodeLocation)); String targetDescription = bracketFormat(targetClass.getName()); @@ -177,7 +178,9 @@ private static Set tryCreateDependency( return dependencies.build(); } - private static Set createComponentTypeDependencies(JavaClass originClass, String originDescription, JavaClass targetClass, SourceCodeLocation sourceCodeLocation) { + private static Set createComponentTypeDependencies( + JavaClass originClass, String originDescription, JavaClass targetClass, SourceCodeLocation sourceCodeLocation) { + ImmutableSet.Builder result = ImmutableSet.builder(); Optional componentType = targetClass.tryGetComponentType(); while (componentType.isPresent()) { @@ -271,7 +274,7 @@ public static JavaClasses toTargetClasses(Iterable dependencies) { return JavaClasses.of(classes); } - private static class Origin { + private static class Origin implements HasOwner, HasDescription { private final JavaClass originClass; private final String originDescription; @@ -279,6 +282,16 @@ private Origin(JavaClass originClass, String originDescription) { this.originClass = originClass; this.originDescription = originDescription; } + + @Override + public JavaClass getOwner() { + return originClass; + } + + @Override + public String getDescription() { + return originDescription; + } } public static final class Predicates { diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/InstanceofCheck.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/InstanceofCheck.java index 182b6a1256..d02400f648 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/InstanceofCheck.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/InstanceofCheck.java @@ -17,22 +17,25 @@ import com.tngtech.archunit.PublicAPI; 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; -public final class InstanceofCheck implements HasType, HasOwner { +public final class InstanceofCheck implements HasType, HasOwner, HasSourceCodeLocation { private final JavaCodeUnit owner; private final JavaClass target; private final int lineNumber; + private final SourceCodeLocation sourceCodeLocation; private InstanceofCheck(JavaCodeUnit owner, JavaClass target, int lineNumber) { this.owner = checkNotNull(owner); this.target = checkNotNull(target); this.lineNumber = lineNumber; + sourceCodeLocation = SourceCodeLocation.of(owner.getOwner(), lineNumber); } @Override @@ -58,6 +61,11 @@ public int getLineNumber() { return lineNumber; } + @Override + public SourceCodeLocation getSourceCodeLocation() { + return sourceCodeLocation; + } + @Override public String toString() { return toStringHelper(this) From 1b0098693c81176861963331d9be5bde0e4d39c3 Mon Sep 17 00:00:00 2001 From: Peter Gafert Date: Sun, 24 Jan 2021 20:31:53 +0100 Subject: [PATCH 3/5] rename `importAsmType` -> `importAsmTypeFromDescriptor` A method taking a `String` as parameter should clearly state what format the string has. A `String` is not an "ASM type". Signed-off-by: Peter Gafert --- .../com/tngtech/archunit/core/importer/AccessRecord.java | 2 +- .../archunit/core/importer/JavaClassDescriptorImporter.java | 2 +- .../tngtech/archunit/core/importer/JavaClassProcessor.java | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/AccessRecord.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/AccessRecord.java index 8a0dc24e99..79c2c679fe 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/AccessRecord.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/AccessRecord.java @@ -233,7 +233,7 @@ public JavaCodeUnit getCaller() { @Override public FieldAccessTarget getTarget() { Supplier> fieldSupplier = new FieldTargetSupplier(targetOwner.getAllFields(), record.target); - JavaClass fieldType = classes.getOrResolve(JavaClassDescriptorImporter.importAsmType(record.target.desc).getFullyQualifiedClassName()); + JavaClass fieldType = classes.getOrResolve(JavaClassDescriptorImporter.importAsmTypeFromDescriptor(record.target.desc).getFullyQualifiedClassName()); return new FieldAccessTargetBuilder() .withOwner(targetOwner) .withName(record.target.name) 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 fbaa371d83..5d317da71f 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 @@ -42,7 +42,7 @@ static Object importAsmTypeIfPossible(Object value) { return isAsmType(value) ? importAsmType((Type) value) : value; } - static JavaClassDescriptor importAsmType(String typeDescriptor) { + static JavaClassDescriptor importAsmTypeFromDescriptor(String typeDescriptor) { return importAsmType(Type.getType(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 a578d7b568..4c3ffc3b7d 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 @@ -206,7 +206,7 @@ public FieldVisitor visitField(int access, String name, String desc, String sign DomainBuilders.JavaFieldBuilder fieldBuilder = new DomainBuilders.JavaFieldBuilder() .withName(name) - .withType(JavaClassDescriptorImporter.importAsmType(desc)) + .withType(JavaClassDescriptorImporter.importAsmTypeFromDescriptor(desc)) .withModifiers(JavaModifier.getModifiersForField(access)) .withDescriptor(desc); declarationHandler.onDeclaredField(fieldBuilder); @@ -500,7 +500,7 @@ public void visitEnd() { } private static DomainBuilders.JavaAnnotationBuilder annotationBuilderFor(String desc) { - return new DomainBuilders.JavaAnnotationBuilder().withType(JavaClassDescriptorImporter.importAsmType(desc)); + return new DomainBuilders.JavaAnnotationBuilder().withType(JavaClassDescriptorImporter.importAsmTypeFromDescriptor(desc)); } private static class AnnotationProcessor extends AnnotationVisitor { @@ -736,7 +736,7 @@ private static ValueBuilder javaEnumBuilder(final String desc, final String valu public Optional build(T owner, ImportContext importContext) { return Optional.of( new DomainBuilders.JavaEnumConstantBuilder() - .withDeclaringClass(importContext.resolveClass(JavaClassDescriptorImporter.importAsmType(desc).getFullyQualifiedClassName())) + .withDeclaringClass(importContext.resolveClass(JavaClassDescriptorImporter.importAsmTypeFromDescriptor(desc).getFullyQualifiedClassName())) .withName(value) .build()); } From fe72d9211a761b5f869e04b5828746becec12c85 Mon Sep 17 00:00:00 2001 From: Peter Gafert Date: Sun, 24 Jan 2021 22:32:37 +0100 Subject: [PATCH 4/5] 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(); + } + } +} From 1263ae53e3f72aaeb262e5c05af634a55ea6f474 Mon Sep 17 00:00:00 2001 From: Peter Gafert Date: Tue, 26 Jan 2021 22:39:02 +0100 Subject: [PATCH 5/5] add referenced class object dependencies from and to self Signed-off-by: Peter Gafert --- .../integration/ExamplesIntegrationTest.java | 3 ++ .../testutils/ExpectedDependency.java | 12 +++-- .../archunit/core/domain/Dependency.java | 6 +++ .../core/domain/JavaClassDependencies.java | 9 ++++ .../archunit/core/domain/DependencyTest.java | 23 ++++++++ .../archunit/core/domain/JavaClassTest.java | 52 +++++++++++++++++-- .../DependenciesOnClassObjects.java | 30 +++++++++++ .../elements/ShouldOnlyByClassesThatTest.java | 31 ++++++++--- 8 files changed, 152 insertions(+), 14 deletions(-) create mode 100644 archunit/src/test/java/com/tngtech/archunit/core/domain/testobjects/DependenciesOnClassObjects.java 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 9a672db5f9..6c844ddbfe 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 @@ -972,6 +972,9 @@ Stream OnionArchitectureTest() { .by(field(ShoppingService.class, "productRepository").ofType(ProductRepository.class)) .by(field(ShoppingService.class, "shoppingCartRepository").ofType(ShoppingCartRepository.class)) + .by(method(AdministrationCLI.class, "handle") + .referencingClassObject(ProductRepository.class) + .inLine(16)) .by(callFromMethod(AdministrationCLI.class, "handle", String[].class, AdministrationPort.class) .toMethod(ProductRepository.class, "getTotalCount") .inLine(17).asDependency()) 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 4a2a19b5dd..ecf6fa2f66 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 @@ -190,7 +190,11 @@ public ExpectedDependency withAnnotationType(Class annotationType) { } public AddsLineNumber checkingInstanceOf(Class target) { - return new AddsLineNumber(owner, getOriginName(), target); + return new AddsLineNumber(owner, getOriginName(), "checks instanceof", target); + } + + public AddsLineNumber referencingClassObject(Class target) { + return new AddsLineNumber(owner, getOriginName(), "references class object", target); } private String getOriginName() { @@ -201,15 +205,17 @@ public static class AddsLineNumber { private final Class owner; private final String origin; private final Class target; + private final String dependencyType; - private AddsLineNumber(Class owner, String origin, Class target) { + private AddsLineNumber(Class owner, String origin, String dependencyType, Class target) { this.owner = owner; this.origin = origin; this.target = target; + this.dependencyType = dependencyType; } public ExpectedDependency inLine(int lineNumber) { - String dependencyPattern = getDependencyPattern(origin, "checks instanceof", target.getName(), lineNumber); + String dependencyPattern = getDependencyPattern(origin, dependencyType, target.getName(), lineNumber); return new ExpectedDependency(owner, target, 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 1065485389..af2850301b 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 @@ -122,6 +122,12 @@ static Set tryCreateFromInstanceofCheck(InstanceofCheck instanceofCh instanceofCheck.getRawType(), instanceofCheck.getSourceCodeLocation()); } + static Set tryCreateFromReferencedClassObject(ReferencedClassObject referencedClassObject) { + return tryCreateDependency( + referencedClassObject.getOwner(), "references class object", + referencedClassObject.getRawType(), referencedClassObject.getSourceCodeLocation()); + } + static Set tryCreateFromAnnotation(JavaAnnotation target) { Origin origin = findSuitableOrigin(target, target.getAnnotatedElement()); return tryCreateDependency(origin, "is annotated with", target.getRawType()); 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 c08dfd60e4..3d8d957884 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 @@ -52,6 +52,7 @@ public Set get() { result.addAll(constructorParameterDependenciesFromSelf()); result.addAll(annotationDependenciesFromSelf()); result.addAll(instanceofCheckDependenciesFromSelf()); + result.addAll(referencedClassObjectDependenciesFromSelf()); result.addAll(typeParameterDependenciesFromSelf()); return result.build(); } @@ -158,6 +159,14 @@ private Set instanceofCheckDependenciesFromSelf() { return result.build(); } + private Set referencedClassObjectDependenciesFromSelf() { + ImmutableSet.Builder result = ImmutableSet.builder(); + for (ReferencedClassObject referencedClassObject : javaClass.getReferencedClassObjects()) { + result.addAll(Dependency.tryCreateFromReferencedClassObject(referencedClassObject)); + } + return result.build(); + } + private Set typeParameterDependenciesFromSelf() { ImmutableSet.Builder result = ImmutableSet.builder(); for (JavaTypeVariable typeVariable : javaClass.getTypeParameters()) { 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 3d7aed0736..da3786e878 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 @@ -6,13 +6,16 @@ import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.nio.file.FileSystem; import java.util.Set; import com.google.common.base.MoreObjects; +import com.google.common.collect.FluentIterable; import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.core.domain.testobjects.ClassWithArrayDependencies; import com.tngtech.archunit.core.domain.testobjects.ClassWithDependencyOnInstanceofCheck; import com.tngtech.archunit.core.domain.testobjects.ClassWithDependencyOnInstanceofCheck.InstanceOfCheckTarget; +import com.tngtech.archunit.core.domain.testobjects.DependenciesOnClassObjects; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.testutil.Assertions; import com.tngtech.archunit.testutil.assertion.DependenciesAssertion; @@ -24,6 +27,7 @@ import org.junit.runner.RunWith; import static com.google.common.collect.Iterables.getOnlyElement; +import static com.tngtech.archunit.base.Guava.toGuava; import static com.tngtech.archunit.core.domain.Dependency.Functions.GET_ORIGIN_CLASS; import static com.tngtech.archunit.core.domain.Dependency.Functions.GET_TARGET_CLASS; import static com.tngtech.archunit.core.domain.Dependency.Predicates.dependency; @@ -32,6 +36,7 @@ import static com.tngtech.archunit.core.domain.TestUtils.importClassWithContext; import static com.tngtech.archunit.core.domain.TestUtils.importClassesWithContext; import static com.tngtech.archunit.core.domain.TestUtils.simulateCall; +import static com.tngtech.archunit.core.domain.properties.HasType.Predicates.rawType; import static com.tngtech.archunit.testutil.Assertions.assertThat; import static com.tngtech.archunit.testutil.Assertions.assertThatDependencies; import static com.tngtech.archunit.testutil.Assertions.assertThatType; @@ -289,6 +294,24 @@ class ClassWithTypeParameters extends Base { ClassWithTypeParameters.class.getName(), Base.class.getName(), String.class.getName(), getClass().getSimpleName())); } + @Test + public void Dependency_from_referenced_class_object() { + JavaMethod origin = new ClassFileImporter() + .importClass(DependenciesOnClassObjects.class) + .getMethod("referencedClassObjectsInMethod"); + ReferencedClassObject referencedClassObject = getOnlyElement(FluentIterable.from(origin.getReferencedClassObjects()) + .filter(toGuava(rawType(FileSystem.class))) + .toSet()); + + Dependency dependency = getOnlyElement(Dependency.tryCreateFromReferencedClassObject(referencedClassObject)); + + assertThatType(dependency.getOriginClass()).matches(DependenciesOnClassObjects.class); + assertThatType(dependency.getTargetClass()).matches(FileSystem.class); + assertThat(dependency.getDescription()).as("description") + .contains(String.format("Method <%s> references class object <%s> in (%s.java:%d)", + origin.getFullName(), FileSystem.class.getName(), DependenciesOnClassObjects.class.getSimpleName(), 22)); + } + @Test public void origin_predicates_match() { assertThatDependency(Origin.class, Target.class) 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 ca7c62f975..b764a45f62 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 @@ -2,8 +2,13 @@ import java.io.BufferedInputStream; import java.io.File; +import java.io.FilterInputStream; import java.io.Serializable; import java.lang.annotation.Retention; +import java.nio.Buffer; +import java.nio.charset.Charset; +import java.nio.file.FileSystem; +import java.nio.file.Path; import java.util.AbstractList; import java.util.ArrayList; import java.util.Collection; @@ -25,6 +30,7 @@ import com.tngtech.archunit.core.domain.testobjects.ArrayComponentTypeDependencies; import com.tngtech.archunit.core.domain.testobjects.B; import com.tngtech.archunit.core.domain.testobjects.ComponentTypeDependency; +import com.tngtech.archunit.core.domain.testobjects.DependenciesOnClassObjects; import com.tngtech.archunit.core.domain.testobjects.InterfaceForA; import com.tngtech.archunit.core.domain.testobjects.SuperA; import com.tngtech.archunit.core.importer.ClassFileImporter; @@ -639,6 +645,31 @@ class ClassWithTypeParameters< ); } + @Test + public void direct_dependencies_from_self_by_referenced_class_objects() { + JavaClass javaClass = new ClassFileImporter().importClass(DependenciesOnClassObjects.class); + + assertThatDependencies(javaClass.getDirectDependenciesFromSelf()) + .contain(from(DependenciesOnClassObjects.class).to(FilterInputStream.class).inLocation(DependenciesOnClassObjects.class, 16) + .withDescriptionContaining("references class object") + + .from(DependenciesOnClassObjects.class).to(Buffer.class).inLocation(DependenciesOnClassObjects.class, 16) + .withDescriptionContaining("references class object") + + .from(DependenciesOnClassObjects.class).to(File.class).inLocation(DependenciesOnClassObjects.class, 19) + .withDescriptionContaining("references class object") + + .from(DependenciesOnClassObjects.class).to(Path.class).inLocation(DependenciesOnClassObjects.class, 19) + .withDescriptionContaining("references class object") + + .from(DependenciesOnClassObjects.class).to(FileSystem.class).inLocation(DependenciesOnClassObjects.class, 22) + .withDescriptionContaining("references class object") + + .from(DependenciesOnClassObjects.class).to(Charset.class).inLocation(DependenciesOnClassObjects.class, 22) + .withDescriptionContaining("references class object") + ); + } + @Test public void direct_dependencies_from_self_finds_correct_set_of_target_types() { JavaClass javaClass = importPackagesOf(getClass()).get(ClassWithAnnotationDependencies.class); @@ -872,6 +903,21 @@ class SecondDependingOnOtherThroughTypeParameter< ); } + @Test + public void direct_dependencies_to_self_by_referenced_class_objects() { + JavaClass javaClass = new ClassFileImporter() + .importClasses(DependenciesOnClassObjects.class, DependenciesOnClassObjects.MoreDependencies.class, Charset.class) + .get(Charset.class); + + assertThatDependencies(javaClass.getDirectDependenciesToSelf()) + .contain(from(DependenciesOnClassObjects.class).to(Charset.class).inLocation(DependenciesOnClassObjects.class, 22) + .withDescriptionContaining("references class object") + + .from(DependenciesOnClassObjects.MoreDependencies.class).to(Charset.class).inLocation(DependenciesOnClassObjects.class, 27) + .withDescriptionContaining("references class object") + ); + } + @Test public void direct_dependencies_to_self_finds_correct_set_of_origin_types() { JavaClasses classes = importPackagesOf(getClass()); @@ -879,17 +925,17 @@ public void direct_dependencies_to_self_finds_correct_set_of_origin_types() { Set origins = getOriginsOfDependenciesTo(classes.get(WithType.class)); assertThatTypes(origins).matchInAnyOrder( - ClassWithAnnotationDependencies.class, ClassWithSelfReferences.class, WithNestedAnnotations.class, OnMethodParam.class); + getClass(), ClassWithAnnotationDependencies.class, ClassWithSelfReferences.class, WithNestedAnnotations.class, OnMethodParam.class); origins = getOriginsOfDependenciesTo(classes.get(B.class)); assertThatTypes(origins).matchInAnyOrder( - ClassWithAnnotationDependencies.class, OnMethodParam.class, AAccessingB.class, AhavingMembersOfTypeB.class); + getClass(), ClassWithAnnotationDependencies.class, OnMethodParam.class, AAccessingB.class, AhavingMembersOfTypeB.class); origins = getOriginsOfDependenciesTo(classes.get(SomeEnumAsNestedAnnotationParameter.class)); assertThatTypes(origins).matchInAnyOrder( - ClassWithAnnotationDependencies.class, WithEnum.class); + getClass(), ClassWithAnnotationDependencies.class, WithEnum.class); } private Set getOriginsOfDependenciesTo(JavaClass withType) { diff --git a/archunit/src/test/java/com/tngtech/archunit/core/domain/testobjects/DependenciesOnClassObjects.java b/archunit/src/test/java/com/tngtech/archunit/core/domain/testobjects/DependenciesOnClassObjects.java new file mode 100644 index 0000000000..06a3bb0531 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/core/domain/testobjects/DependenciesOnClassObjects.java @@ -0,0 +1,30 @@ +package com.tngtech.archunit.core.domain.testobjects; + +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 DependenciesOnClassObjects { + 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); + } + + public static class MoreDependencies { + Class moreReferencedClassObjectsInMethod() { + return Charset.class; + } + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ShouldOnlyByClassesThatTest.java b/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ShouldOnlyByClassesThatTest.java index 40d9c6d476..61283ee958 100644 --- a/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ShouldOnlyByClassesThatTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/lang/syntax/elements/ShouldOnlyByClassesThatTest.java @@ -934,7 +934,7 @@ public void areNotInnerClasses_predicate(ClassesThat c public void areAnonymousClasses_predicate(ClassesThat classesShouldOnlyBeBy) { Set classes = filterClassesAppearingInFailureReport( classesShouldOnlyBeBy.areAnonymousClasses()) - .on(ClassAccessingAnonymousClass.class, anonymousClassBeingAccessed.getClass(), + .on(ClassAccessingAnonymousClass_Reference, anonymousClassBeingAccessed.getClass(), ClassAccessingOtherClass.class, ClassBeingAccessedByOtherClass.class); assertThatTypes(classes).matchInAnyOrder(ClassAccessingOtherClass.class, ClassBeingAccessedByOtherClass.class); @@ -945,10 +945,10 @@ public void areAnonymousClasses_predicate(ClassesThat public void areNotAnonymousClasses_predicate(ClassesThat classesShouldOnlyBeBy) { Set classes = filterClassesAppearingInFailureReport( classesShouldOnlyBeBy.areNotAnonymousClasses()) - .on(ClassAccessingAnonymousClass.class, anonymousClassBeingAccessed.getClass(), + .on(ClassAccessingAnonymousClass_Reference, anonymousClassBeingAccessed.getClass(), ClassAccessingOtherClass.class, ClassBeingAccessedByOtherClass.class); - assertThatTypes(classes).matchInAnyOrder(ClassAccessingAnonymousClass.class, anonymousClassBeingAccessed.getClass()); + assertThatTypes(classes).matchInAnyOrder(ClassAccessingAnonymousClass_Reference, anonymousClassBeingAccessed.getClass()); } @Test @@ -956,7 +956,7 @@ public void areNotAnonymousClasses_predicate(ClassesThat classesShouldOnlyBeBy) { Set classes = filterClassesAppearingInFailureReport( classesShouldOnlyBeBy.areLocalClasses()) - .on(ClassBeingAccessedByLocalClass.class, ClassBeingAccessedByLocalClass.getLocalClass(), + .on(ClassAccessingLocalClass_Reference, ClassBeingAccessedByLocalClass.getLocalClass(), ClassAccessingOtherClass.class, ClassBeingAccessedByOtherClass.class); assertThatTypes(classes).matchInAnyOrder(ClassAccessingOtherClass.class, ClassBeingAccessedByOtherClass.class); @@ -967,10 +967,10 @@ public void areLocalClasses_predicate(ClassesThat clas public void areNotLocalClasses_predicate(ClassesThat classesShouldOnlyBeBy) { Set classes = filterClassesAppearingInFailureReport( classesShouldOnlyBeBy.areNotLocalClasses()) - .on(ClassBeingAccessedByLocalClass.class, ClassBeingAccessedByLocalClass.getLocalClass(), + .on(ClassAccessingLocalClass_Reference, ClassBeingAccessedByLocalClass.getLocalClass(), ClassAccessingOtherClass.class, ClassBeingAccessedByOtherClass.class); - assertThatTypes(classes).matchInAnyOrder(ClassBeingAccessedByLocalClass.class, ClassBeingAccessedByLocalClass.getLocalClass()); + assertThatTypes(classes).matchInAnyOrder(ClassAccessingLocalClass_Reference, ClassBeingAccessedByLocalClass.getLocalClass()); } @Test @@ -1050,6 +1050,14 @@ private static DescribedPredicate classWithNameOf(Class type) { return GET_NAME.is(equalTo(type.getName())); } + private static Class classForName(String className) { + try { + return Class.forName(className); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + private static class ClassAccessedByFoo { void method() { } @@ -1306,6 +1314,8 @@ void access() { private class InnerMemberClassBeingAccessed { } + // This must be loaded via Reflection, otherwise the test will be tainted by the dependency on the class object + private static final Class ClassAccessingAnonymousClass_Reference = classForName("com.tngtech.archunit.lang.syntax.elements.ShouldOnlyByClassesThatTest$ClassAccessingAnonymousClass"); private static class ClassAccessingAnonymousClass { @SuppressWarnings("unused") void access() { @@ -1320,15 +1330,20 @@ public void run() { } }; + // This must be loaded via Reflection, otherwise the test will be tainted by the dependency on the class object + private static final Class ClassAccessingLocalClass_Reference = classForName("com.tngtech.archunit.lang.syntax.elements.ShouldOnlyByClassesThatTest$ClassBeingAccessedByLocalClass"); + private static class ClassBeingAccessedByLocalClass { static Class getLocalClass() { + @SuppressWarnings({"unused", "InstantiationOfUtilityClass"}) class LocalClass { - @SuppressWarnings("unused") void access() { new ClassBeingAccessedByLocalClass(); } } - return LocalClass.class; + + // This must be loaded via Reflection, otherwise the test will be tainted by the dependency on the class object + return classForName("com.tngtech.archunit.lang.syntax.elements.ShouldOnlyByClassesThatTest$ClassBeingAccessedByLocalClass$1LocalClass"); } } }