From 4d25f063b5a8b785a1230e93cd65f6c5d6058f67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Siersze=C5=84?= Date: Fri, 25 Feb 2022 13:04:16 +0100 Subject: [PATCH] Add support to test caught exceptions (fixes #591) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Krzysztof Sierszeń --- .../archunit/core/domain/JavaCall.java | 23 +- .../core/domain/JavaConstructorCall.java | 1 + .../archunit/core/domain/JavaMethodCall.java | 1 + .../archunit/core/importer/AccessRecord.java | 70 +++- .../core/importer/CaughtThrowable.java | 31 ++ .../core/importer/ClassFileImportRecord.java | 12 +- .../core/importer/ClassFileProcessor.java | 73 +--- .../core/importer/ClassGraphCreator.java | 76 ++-- .../core/importer/DomainBuilders.java | 69 ++-- .../core/importer/JavaClassProcessor.java | 27 ++ .../core/importer/RawAccessRecord.java | 50 ++- .../core/importer/RecordAccessHandler.java | 106 ++++++ .../core/importer/TryCatchTracker.java | 146 ++++++++ .../ClassFileImporterAccessesTest.java | 82 ++++- .../core/importer/ClassFileImporterTest.java | 18 +- .../trycatch/ClassHoldingMethods.java | 37 ++ .../trycatch/ClassWithTryCatchBlocks.java | 57 +++ .../tngtech/archunit/testutil/Assertions.java | 335 +----------------- .../testutil/ExpectedAccessCreation.java | 67 ++++ .../archunit/testutil/JavaCallQuery.java | 88 +++++ .../assertion/AccessToFieldAssertion.java | 37 ++ .../testutil/assertion/AccessesAssertion.java | 37 ++ .../assertion/BaseAccessAssertion.java | 51 +++ .../BaseCodeUnitAccessAssertion.java | 53 +++ .../testutil/assertion/CallAssertion.java | 46 +++ .../assertion/CodeUnitAccessAssertion.java | 19 + .../assertion/JavaEnumConstantAssertion.java | 28 ++ .../assertion/JavaEnumConstantsAssertion.java | 18 + .../assertion/ThrowsClauseAssertion.java | 28 ++ .../assertion/ThrowsDeclarationAssertion.java | 16 + 30 files changed, 1167 insertions(+), 535 deletions(-) create mode 100644 archunit/src/main/java/com/tngtech/archunit/core/importer/CaughtThrowable.java create mode 100644 archunit/src/main/java/com/tngtech/archunit/core/importer/RecordAccessHandler.java create mode 100644 archunit/src/main/java/com/tngtech/archunit/core/importer/TryCatchTracker.java create mode 100644 archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/trycatch/ClassHoldingMethods.java create mode 100644 archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/trycatch/ClassWithTryCatchBlocks.java create mode 100644 archunit/src/test/java/com/tngtech/archunit/testutil/ExpectedAccessCreation.java create mode 100644 archunit/src/test/java/com/tngtech/archunit/testutil/JavaCallQuery.java create mode 100644 archunit/src/test/java/com/tngtech/archunit/testutil/assertion/AccessToFieldAssertion.java create mode 100644 archunit/src/test/java/com/tngtech/archunit/testutil/assertion/AccessesAssertion.java create mode 100644 archunit/src/test/java/com/tngtech/archunit/testutil/assertion/BaseAccessAssertion.java create mode 100644 archunit/src/test/java/com/tngtech/archunit/testutil/assertion/BaseCodeUnitAccessAssertion.java create mode 100644 archunit/src/test/java/com/tngtech/archunit/testutil/assertion/CallAssertion.java create mode 100644 archunit/src/test/java/com/tngtech/archunit/testutil/assertion/CodeUnitAccessAssertion.java create mode 100644 archunit/src/test/java/com/tngtech/archunit/testutil/assertion/JavaEnumConstantAssertion.java create mode 100644 archunit/src/test/java/com/tngtech/archunit/testutil/assertion/JavaEnumConstantsAssertion.java create mode 100644 archunit/src/test/java/com/tngtech/archunit/testutil/assertion/ThrowsClauseAssertion.java create mode 100644 archunit/src/test/java/com/tngtech/archunit/testutil/assertion/ThrowsDeclarationAssertion.java diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaCall.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaCall.java index ed2a87f188..94ecea3f5f 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaCall.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaCall.java @@ -15,17 +15,38 @@ */ package com.tngtech.archunit.core.domain; +import com.google.common.base.Function; +import com.google.common.collect.Iterables; +import com.google.common.collect.Sets; import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.core.domain.AccessTarget.CodeUnitCallTarget; import com.tngtech.archunit.core.domain.JavaAccess.Predicates.TargetPredicate; +import com.tngtech.archunit.core.importer.CaughtThrowable; +import com.tngtech.archunit.core.importer.DomainBuilders; import com.tngtech.archunit.core.importer.DomainBuilders.JavaAccessBuilder; +import java.util.Set; + import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; public abstract class JavaCall extends JavaCodeUnitAccess { - JavaCall(JavaAccessBuilder builder) { + private static final Function TO_CAUGHT_THROWABLE_CONVERTER = new Function() { + + @Override + public CaughtThrowable apply(JavaClassDescriptor input) { + return new CaughtThrowable(input); + } + }; + private Set caughtThrowables; + + JavaCall(DomainBuilders.JavaCallBuilder, ?> builder) { super(builder); + caughtThrowables = Sets.newHashSet(Iterables.transform(builder.getCaughtThrowables(), TO_CAUGHT_THROWABLE_CONVERTER)); + } + + public Set getCaughtThrowables() { + return caughtThrowables; } public static final class Predicates { diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaConstructorCall.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaConstructorCall.java index 10425af259..bdb5e6a37f 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaConstructorCall.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaConstructorCall.java @@ -19,6 +19,7 @@ import com.tngtech.archunit.core.importer.DomainBuilders.JavaConstructorCallBuilder; public class JavaConstructorCall extends JavaCall { + JavaConstructorCall(JavaConstructorCallBuilder builder) { super(builder); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaMethodCall.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaMethodCall.java index cadb5c635c..7267c1f1ec 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaMethodCall.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaMethodCall.java @@ -19,6 +19,7 @@ import com.tngtech.archunit.core.importer.DomainBuilders.JavaMethodCallBuilder; public class JavaMethodCall extends JavaCall { + JavaMethodCall(JavaMethodCallBuilder builder) { super(builder); } 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 562a93d8a1..67be0f56da 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 @@ -15,10 +15,7 @@ */ package com.tngtech.archunit.core.importer; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; @@ -27,12 +24,7 @@ import com.tngtech.archunit.Internal; import com.tngtech.archunit.base.Optional; import com.tngtech.archunit.core.domain.AccessTarget; -import com.tngtech.archunit.core.domain.AccessTarget.CodeUnitAccessTarget; -import com.tngtech.archunit.core.domain.AccessTarget.ConstructorCallTarget; -import com.tngtech.archunit.core.domain.AccessTarget.ConstructorReferenceTarget; -import com.tngtech.archunit.core.domain.AccessTarget.FieldAccessTarget; -import com.tngtech.archunit.core.domain.AccessTarget.MethodCallTarget; -import com.tngtech.archunit.core.domain.AccessTarget.MethodReferenceTarget; +import com.tngtech.archunit.core.domain.AccessTarget.*; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaClassDescriptor; import com.tngtech.archunit.core.domain.JavaCodeUnit; @@ -63,16 +55,31 @@ interface FieldAccessRecord extends AccessRecord { AccessType getAccessType(); } + @Internal + interface CallRecord extends AccessRecord { + Set getCaughtThrowables(); + } + + @Internal + interface MethodCallRecord extends CallRecord { + + } + + @Internal + interface ConstructorCallRecord extends CallRecord { + + } + @Internal abstract class Factory { abstract PROCESSED_RECORD create(RAW_RECORD record, ImportedClasses classes); - static Factory> forConstructorCallRecord() { - return new Factory>() { + static Factory forConstructorCallRecord() { + return new Factory() { @Override - AccessRecord create(RawAccessRecord record, ImportedClasses classes) { - return new RawAccessRecordProcessed<>(record, classes, CONSTRUCTOR_CALL_TARGET_FACTORY); + ConstructorCallRecord create(RawAccessRecord.ForMethodCall record, ImportedClasses classes) { + return new RawConstructorCallRecordProcessed(record, classes); } }; } @@ -86,11 +93,11 @@ AccessRecord create(RawAccessRecord record, Imported }; } - static Factory> forMethodCallRecord() { - return new Factory>() { + static Factory forMethodCallRecord() { + return new Factory() { @Override - AccessRecord create(RawAccessRecord record, ImportedClasses classes) { - return new RawAccessRecordProcessed<>(record, classes, METHOD_CALL_TARGET_FACTORY); + MethodCallRecord create(RawAccessRecord.ForMethodCall record, ImportedClasses classes) { + return new RawMethodCallRecordProcessed(record, classes); } }; } @@ -307,6 +314,33 @@ public AccessType getAccessType() { } } + private abstract static class RawCodeUnitCallRecordProcessed extends RawAccessRecordProcessed { + private final Set caughtThrowables; + + RawCodeUnitCallRecordProcessed(RawAccessRecord.ForMethodCall record, ImportedClasses classes, AccessTargetFactory accessTargetFactory) { + super(record, classes, accessTargetFactory); + caughtThrowables = record.caughtThrowables; + } + + public Set getCaughtThrowables() { + return caughtThrowables; + } + } + + private static class RawConstructorCallRecordProcessed extends RawCodeUnitCallRecordProcessed implements ConstructorCallRecord { + + RawConstructorCallRecordProcessed(RawAccessRecord.ForMethodCall record, ImportedClasses classes) { + super(record, classes, CONSTRUCTOR_CALL_TARGET_FACTORY); + } + } + + private static class RawMethodCallRecordProcessed extends RawCodeUnitCallRecordProcessed implements MethodCallRecord { + + RawMethodCallRecordProcessed(RawAccessRecord.ForMethodCall record, ImportedClasses classes) { + super(record, classes, METHOD_CALL_TARGET_FACTORY); + } + } + private static Supplier createOriginSupplier(final CodeUnit origin, final ImportedClasses classes) { return Suppliers.memoize(new Supplier() { @Override diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/CaughtThrowable.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/CaughtThrowable.java new file mode 100644 index 0000000000..127db7d7b6 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/CaughtThrowable.java @@ -0,0 +1,31 @@ +/* + * Copyright 2014-2022 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; + +public class CaughtThrowable { + + private final JavaClassDescriptor throwableType; + + public CaughtThrowable(JavaClassDescriptor throwableType) { + this.throwableType = throwableType; + } + + public JavaClassDescriptor getThrowableType() { + return throwableType; + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileImportRecord.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileImportRecord.java index 05b963ce5e..8e0124a2bb 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileImportRecord.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileImportRecord.java @@ -68,8 +68,8 @@ class ClassFileImportRecord { private final EnclosingDeclarationsByInnerClasses enclosingDeclarationsByOwner = new EnclosingDeclarationsByInnerClasses(); private final Set rawFieldAccessRecords = new HashSet<>(); - private final Set rawMethodCallRecords = new HashSet<>(); - private final Set rawConstructorCallRecords = new HashSet<>(); + private final Set rawMethodCallRecords = new HashSet<>(); + private final Set rawConstructorCallRecords = new HashSet<>(); private final Set rawMethodReferenceRecords = new HashSet<>(); private final Set rawConstructorReferenceRecords = new HashSet<>(); @@ -242,11 +242,11 @@ void registerFieldAccess(RawAccessRecord.ForField record) { rawFieldAccessRecords.add(record); } - void registerMethodCall(RawAccessRecord record) { + void registerMethodCall(RawAccessRecord.ForMethodCall record) { rawMethodCallRecords.add(record); } - void registerConstructorCall(RawAccessRecord record) { + void registerConstructorCall(RawAccessRecord.ForMethodCall record) { rawConstructorCallRecords.add(record); } @@ -262,11 +262,11 @@ Set getRawFieldAccessRecords() { return ImmutableSet.copyOf(rawFieldAccessRecords); } - Set getRawMethodCallRecords() { + Set getRawMethodCallRecords() { return ImmutableSet.copyOf(rawMethodCallRecords); } - Set getRawConstructorCallRecords() { + Set getRawConstructorCallRecords() { return ImmutableSet.copyOf(rawConstructorCallRecords); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileProcessor.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileProcessor.java index dc70fa9711..7d7d2c7bae 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileProcessor.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileProcessor.java @@ -25,7 +25,6 @@ import com.tngtech.archunit.base.Optional; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaClasses; -import com.tngtech.archunit.core.domain.JavaFieldAccess.AccessType; import com.tngtech.archunit.core.importer.DomainBuilders.JavaAnnotationBuilder; import com.tngtech.archunit.core.importer.DomainBuilders.JavaClassTypeParametersBuilder; import com.tngtech.archunit.core.importer.DomainBuilders.JavaConstructorBuilder; @@ -33,16 +32,13 @@ import com.tngtech.archunit.core.importer.DomainBuilders.JavaMethodBuilder; import com.tngtech.archunit.core.importer.DomainBuilders.JavaParameterizedTypeBuilder; import com.tngtech.archunit.core.importer.DomainBuilders.JavaStaticInitializerBuilder; -import com.tngtech.archunit.core.importer.JavaClassProcessor.AccessHandler; import com.tngtech.archunit.core.importer.RawAccessRecord.CodeUnit; -import com.tngtech.archunit.core.importer.RawAccessRecord.TargetInfo; import com.tngtech.archunit.core.importer.resolvers.ClassResolver; import com.tngtech.archunit.core.importer.resolvers.ClassResolver.ClassUriImporter; import org.objectweb.asm.ClassReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static com.tngtech.archunit.core.domain.JavaConstructor.CONSTRUCTOR_NAME; import static org.objectweb.asm.Opcodes.ASM9; class ClassFileProcessor { @@ -56,7 +52,8 @@ class ClassFileProcessor { JavaClasses process(ClassFileSource source) { ClassFileImportRecord importRecord = new ClassFileImportRecord(); DependencyResolutionProcess dependencyResolutionProcess = new DependencyResolutionProcess(); - RecordAccessHandler accessHandler = new RecordAccessHandler(importRecord, dependencyResolutionProcess); + TryCatchTracker tryCatchTracker = new TryCatchTracker(); + RecordAccessHandler accessHandler = new RecordAccessHandler(importRecord, dependencyResolutionProcess, tryCatchTracker); ClassDetailsRecorder classDetailsRecorder = new ClassDetailsRecorder(importRecord, dependencyResolutionProcess); for (ClassFileLocation location : source) { try (InputStream s = location.openStream()) { @@ -196,72 +193,6 @@ public void onDeclaredGenericSignatureType(String typeName) { } } - private static class RecordAccessHandler implements AccessHandler { - private static final Logger LOG = LoggerFactory.getLogger(RecordAccessHandler.class); - - private final ClassFileImportRecord importRecord; - private final DependencyResolutionProcess dependencyResolutionProcess; - private CodeUnit codeUnit; - private int lineNumber; - - private RecordAccessHandler(ClassFileImportRecord importRecord, DependencyResolutionProcess dependencyResolutionProcess) { - this.importRecord = importRecord; - this.dependencyResolutionProcess = dependencyResolutionProcess; - } - - @Override - public void setContext(CodeUnit codeUnit) { - this.codeUnit = codeUnit; - } - - @Override - public void setLineNumber(int lineNumber) { - this.lineNumber = lineNumber; - } - - @Override - public void handleFieldInstruction(int opcode, String owner, String name, String desc) { - AccessType accessType = AccessType.forOpCode(opcode); - LOG.trace("Found {} access to field {}.{}:{} in line {}", accessType, owner, name, desc, lineNumber); - TargetInfo target = new TargetInfo(owner, name, desc); - importRecord.registerFieldAccess(filled(new RawAccessRecord.ForField.Builder(), target) - .withAccessType(accessType) - .build()); - dependencyResolutionProcess.registerAccessToType(target.owner.getFullyQualifiedClassName()); - } - - @Override - public void handleMethodInstruction(String owner, String name, String desc) { - LOG.trace("Found call of method {}.{}:{} in line {}", owner, name, desc, lineNumber); - TargetInfo target = new TargetInfo(owner, name, desc); - if (CONSTRUCTOR_NAME.equals(name)) { - importRecord.registerConstructorCall(filled(new RawAccessRecord.Builder(), target).build()); - } else { - importRecord.registerMethodCall(filled(new RawAccessRecord.Builder(), target).build()); - } - dependencyResolutionProcess.registerAccessToType(target.owner.getFullyQualifiedClassName()); - } - - @Override - public void handleMethodReferenceInstruction(String owner, String name, String desc) { - LOG.trace("Found method reference {}.{}:{} in line {}", owner, name, desc, lineNumber); - TargetInfo target = new TargetInfo(owner, name, desc); - if (CONSTRUCTOR_NAME.equals(name)) { - importRecord.registerConstructorReference(filled(new RawAccessRecord.Builder(), target).build()); - } else { - importRecord.registerMethodReference(filled(new RawAccessRecord.Builder(), target).build()); - } - dependencyResolutionProcess.registerAccessToType(target.owner.getFullyQualifiedClassName()); - } - - private > BUILDER filled(BUILDER builder, TargetInfo target) { - return builder - .withCaller(codeUnit) - .withTarget(target) - .withLineNumber(lineNumber); - } - } - private ClassResolver getClassResolver(ClassDetailsRecorder classDetailsRecorder) { ClassResolver classResolver = classResolverFactory.create(); classResolver.setClassUriImporter(new UriImporterOfProcessor(classDetailsRecorder, md5InClassSourcesEnabled)); diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassGraphCreator.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassGraphCreator.java index 3866429a5f..e291e9c6b1 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassGraphCreator.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassGraphCreator.java @@ -15,64 +15,28 @@ */ package com.tngtech.archunit.core.importer; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import com.google.common.collect.HashMultimap; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Multimap; -import com.google.common.collect.SetMultimap; -import com.google.common.collect.Sets; +import com.google.common.collect.*; import com.tngtech.archunit.base.Function; import com.tngtech.archunit.base.HasDescription; import com.tngtech.archunit.base.Optional; -import com.tngtech.archunit.core.domain.AccessTarget; -import com.tngtech.archunit.core.domain.AccessTarget.ConstructorCallTarget; +import com.tngtech.archunit.core.domain.*; import com.tngtech.archunit.core.domain.AccessTarget.ConstructorReferenceTarget; -import com.tngtech.archunit.core.domain.AccessTarget.MethodCallTarget; import com.tngtech.archunit.core.domain.AccessTarget.MethodReferenceTarget; -import com.tngtech.archunit.core.domain.ImportContext; -import com.tngtech.archunit.core.domain.JavaAnnotation; -import com.tngtech.archunit.core.domain.JavaClass; -import com.tngtech.archunit.core.domain.JavaClasses; -import com.tngtech.archunit.core.domain.JavaCodeUnit; -import com.tngtech.archunit.core.domain.JavaConstructor; -import com.tngtech.archunit.core.domain.JavaConstructorCall; -import com.tngtech.archunit.core.domain.JavaConstructorReference; -import com.tngtech.archunit.core.domain.JavaField; -import com.tngtech.archunit.core.domain.JavaFieldAccess; -import com.tngtech.archunit.core.domain.JavaMember; -import com.tngtech.archunit.core.domain.JavaMethod; -import com.tngtech.archunit.core.domain.JavaMethodCall; -import com.tngtech.archunit.core.domain.JavaMethodReference; -import com.tngtech.archunit.core.domain.JavaStaticInitializer; -import com.tngtech.archunit.core.domain.JavaType; -import com.tngtech.archunit.core.domain.JavaTypeVariable; +import com.tngtech.archunit.core.importer.AccessRecord.ConstructorCallRecord; import com.tngtech.archunit.core.importer.AccessRecord.FieldAccessRecord; +import com.tngtech.archunit.core.importer.AccessRecord.MethodCallRecord; import com.tngtech.archunit.core.importer.DomainBuilders.JavaAnnotationBuilder.ValueBuilder; -import com.tngtech.archunit.core.importer.DomainBuilders.JavaClassTypeParametersBuilder; -import com.tngtech.archunit.core.importer.DomainBuilders.JavaConstructorCallBuilder; -import com.tngtech.archunit.core.importer.DomainBuilders.JavaConstructorReferenceBuilder; -import com.tngtech.archunit.core.importer.DomainBuilders.JavaFieldAccessBuilder; -import com.tngtech.archunit.core.importer.DomainBuilders.JavaMethodCallBuilder; -import com.tngtech.archunit.core.importer.DomainBuilders.JavaMethodReferenceBuilder; -import com.tngtech.archunit.core.importer.DomainBuilders.JavaParameterizedTypeBuilder; import com.tngtech.archunit.core.importer.ImportedClasses.MethodReturnTypeGetter; import com.tngtech.archunit.core.importer.RawAccessRecord.CodeUnit; import com.tngtech.archunit.core.importer.resolvers.ClassResolver; -import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.completeAnnotations; -import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.completeClassHierarchy; -import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.completeEnclosingDeclaration; -import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.completeGenericInterfaces; -import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.completeGenericSuperclass; -import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.completeMembers; -import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.completeTypeParameters; -import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createJavaClasses; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.*; import static com.tngtech.archunit.core.importer.DomainBuilders.BuilderWithBuildParameter.BuildFinisher.build; -import static com.tngtech.archunit.core.importer.DomainBuilders.buildAnnotations; +import static com.tngtech.archunit.core.importer.DomainBuilders.*; class ClassGraphCreator implements ImportContext { private final ImportedClasses classes; @@ -81,8 +45,8 @@ class ClassGraphCreator implements ImportContext { private final DependencyResolutionProcess dependencyResolutionProcess; private final SetMultimap processedFieldAccessRecords = HashMultimap.create(); - private final SetMultimap> processedMethodCallRecords = HashMultimap.create(); - private final SetMultimap> processedConstructorCallRecords = HashMultimap.create(); + private final SetMultimap processedMethodCallRecords = HashMultimap.create(); + private final SetMultimap processedConstructorCallRecords = HashMultimap.create(); private final SetMultimap> processedMethodReferenceRecords = HashMultimap.create(); private final SetMultimap> processedConstructorReferenceRecords = HashMultimap.create(); @@ -120,10 +84,10 @@ private void completeAccesses() { for (RawAccessRecord.ForField fieldAccessRecord : importRecord.getRawFieldAccessRecords()) { tryProcess(fieldAccessRecord, AccessRecord.Factory.forFieldAccessRecord(), processedFieldAccessRecords); } - for (RawAccessRecord methodCallRecord : importRecord.getRawMethodCallRecords()) { + for (RawAccessRecord.ForMethodCall methodCallRecord : importRecord.getRawMethodCallRecords()) { tryProcess(methodCallRecord, AccessRecord.Factory.forMethodCallRecord(), processedMethodCallRecords); } - for (RawAccessRecord constructorCallRecord : importRecord.getRawConstructorCallRecords()) { + for (RawAccessRecord.ForMethodCall constructorCallRecord : importRecord.getRawConstructorCallRecords()) { tryProcess(constructorCallRecord, AccessRecord.Factory.forConstructorCallRecord(), processedConstructorCallRecords); } @@ -160,8 +124,10 @@ public Set createFieldAccessesFor(JavaCodeUnit codeUnit) { @Override public Set createMethodCallsFor(JavaCodeUnit codeUnit) { ImmutableSet.Builder result = ImmutableSet.builder(); - for (AccessRecord record : processedMethodCallRecords.get(codeUnit)) { - result.add(accessBuilderFrom(new JavaMethodCallBuilder(), record).build()); + for (MethodCallRecord record : processedMethodCallRecords.get(codeUnit)) { + result.add(accessBuilderFrom(new JavaMethodCallBuilder(), record) + .withCaughtThrowables(record.getCaughtThrowables()) + .build()); } return result.build(); } @@ -169,8 +135,10 @@ public Set createMethodCallsFor(JavaCodeUnit codeUnit) { @Override public Set createConstructorCallsFor(JavaCodeUnit codeUnit) { ImmutableSet.Builder result = ImmutableSet.builder(); - for (AccessRecord record : processedConstructorCallRecords.get(codeUnit)) { - result.add(accessBuilderFrom(new JavaConstructorCallBuilder(), record).build()); + for (ConstructorCallRecord record : processedConstructorCallRecords.get(codeUnit)) { + result.add(accessBuilderFrom(new JavaConstructorCallBuilder(), record) + .withCaughtThrowables(record.getCaughtThrowables()) + .build()); } return result.build(); } 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 fabd004a71..4c559787cb 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 @@ -34,41 +34,9 @@ import com.tngtech.archunit.base.Function; import com.tngtech.archunit.base.HasDescription; import com.tngtech.archunit.base.Optional; -import com.tngtech.archunit.core.domain.AccessTarget; -import com.tngtech.archunit.core.domain.AccessTarget.CodeUnitAccessTarget; -import com.tngtech.archunit.core.domain.AccessTarget.ConstructorCallTarget; -import com.tngtech.archunit.core.domain.AccessTarget.ConstructorReferenceTarget; -import com.tngtech.archunit.core.domain.AccessTarget.FieldAccessTarget; -import com.tngtech.archunit.core.domain.AccessTarget.MethodCallTarget; -import com.tngtech.archunit.core.domain.AccessTarget.MethodReferenceTarget; -import com.tngtech.archunit.core.domain.DomainObjectCreationContext; -import com.tngtech.archunit.core.domain.Formatters; -import com.tngtech.archunit.core.domain.InstanceofCheck; -import com.tngtech.archunit.core.domain.JavaAnnotation; -import com.tngtech.archunit.core.domain.JavaClass; -import com.tngtech.archunit.core.domain.JavaClassDescriptor; -import com.tngtech.archunit.core.domain.JavaCodeUnit; -import com.tngtech.archunit.core.domain.JavaConstructor; -import com.tngtech.archunit.core.domain.JavaConstructorCall; -import com.tngtech.archunit.core.domain.JavaConstructorReference; -import com.tngtech.archunit.core.domain.JavaEnumConstant; -import com.tngtech.archunit.core.domain.JavaField; -import com.tngtech.archunit.core.domain.JavaFieldAccess; +import com.tngtech.archunit.core.domain.*; +import com.tngtech.archunit.core.domain.AccessTarget.*; import com.tngtech.archunit.core.domain.JavaFieldAccess.AccessType; -import com.tngtech.archunit.core.domain.JavaMember; -import com.tngtech.archunit.core.domain.JavaMethod; -import com.tngtech.archunit.core.domain.JavaMethodCall; -import com.tngtech.archunit.core.domain.JavaMethodReference; -import com.tngtech.archunit.core.domain.JavaModifier; -import com.tngtech.archunit.core.domain.JavaParameter; -import com.tngtech.archunit.core.domain.JavaParameterizedType; -import com.tngtech.archunit.core.domain.JavaStaticInitializer; -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; import com.tngtech.archunit.core.domain.properties.HasTypeParameters; import static com.google.common.base.Preconditions.checkArgument; @@ -1002,7 +970,8 @@ JavaFieldAccess build() { } @Internal - public static final class JavaMethodCallBuilder extends JavaAccessBuilder { + public static final class JavaMethodCallBuilder extends JavaCallBuilder { + JavaMethodCallBuilder() { } @@ -1022,7 +991,35 @@ JavaMethodReference build() { } @Internal - public static class JavaConstructorCallBuilder extends JavaAccessBuilder { + public abstract static class JavaCallBuilder< + TARGET extends CodeUnitCallTarget, + RESULT extends JavaCall, + SELF extends JavaCallBuilder> + extends JavaAccessBuilder { + private Set caughtThrowables; + + SELF withCaughtThrowables(final Set caughtThrowables) { + this.caughtThrowables = caughtThrowables; + return self(); + } + + JavaCallBuilder() {} + + public Set getCaughtThrowables() { + return caughtThrowables; + } + + abstract RESULT build(); + + @SuppressWarnings("unchecked") + private SELF self() { + return (SELF) this; + } + } + + @Internal + public static class JavaConstructorCallBuilder extends JavaCallBuilder { + JavaConstructorCallBuilder() { } 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 65a3b79c62..6563454c09 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 @@ -372,6 +372,18 @@ public void visitLdcInsn(Object value) { } } + @Override + public void visitLabel(final Label label) { + accessHandler.handleLabel(label); + } + + @Override + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + if (type != null) { // not try .. finally + accessHandler.handleTryCatchBlock(start, end, JavaClassDescriptorImporter.createFromAsmObjectTypeName(type)); + } + } + @Override public void visitFieldInsn(int opcode, String owner, String name, String desc) { accessHandler.handleFieldInstruction(opcode, owner, name, desc); @@ -514,6 +526,10 @@ interface AccessHandler { void handleMethodReferenceInstruction(String owner, String name, String desc); + void handleTryCatchBlock(Label start, Label end, JavaClassDescriptor throwableType); + + void handleLabel(Label label); + @Internal class NoOp implements AccessHandler { @Override @@ -535,6 +551,17 @@ public void handleMethodInstruction(String owner, String name, String desc) { @Override public void handleMethodReferenceInstruction(String owner, String name, String desc) { } + + @Override + public void handleTryCatchBlock(Label start, Label end, + JavaClassDescriptor throwableType) { + + } + + @Override + public void handleLabel(Label label) { + + } } } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/RawAccessRecord.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/RawAccessRecord.java index c4c3ab7e92..517b8f988a 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/RawAccessRecord.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/RawAccessRecord.java @@ -15,15 +15,16 @@ */ package com.tngtech.archunit.core.importer; -import java.util.List; -import java.util.Objects; - import com.google.common.collect.ImmutableList; import com.tngtech.archunit.core.domain.JavaClassDescriptor; import com.tngtech.archunit.core.domain.JavaCodeUnit; import com.tngtech.archunit.core.domain.JavaFieldAccess.AccessType; import com.tngtech.archunit.core.domain.properties.HasName; +import java.util.List; +import java.util.Objects; +import java.util.Set; + import static com.google.common.base.Preconditions.checkNotNull; class RawAccessRecord { @@ -256,4 +257,47 @@ ForField build() { } } } + + static class ForMethodCall extends RawAccessRecord { + final Set caughtThrowables; + + private ForMethodCall(CodeUnit caller, TargetInfo target, int lineNumber, Set caughtThrowables) { + super(caller, target, lineNumber); + this.caughtThrowables = caughtThrowables; + } + + @Override + public int hashCode() { + return 31 * super.hashCode() + Objects.hash(caughtThrowables); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + if (!super.equals(obj)) { + return false; + } + final ForMethodCall other = (ForMethodCall) obj; + return Objects.equals(this.caughtThrowables, other.caughtThrowables); + } + + static class Builder extends BaseBuilder { + private Set caughtThrowables; + + ForMethodCall.Builder withCaughtThrowables(Set caughtThrowables) { + this.caughtThrowables = caughtThrowables; + return this; + } + + @Override + ForMethodCall build() { + return new ForMethodCall(super.caller, super.target, super.lineNumber, caughtThrowables); + } + } + } } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/RecordAccessHandler.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/RecordAccessHandler.java new file mode 100644 index 0000000000..1bc6e89072 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/RecordAccessHandler.java @@ -0,0 +1,106 @@ +/* + * Copyright 2014-2022 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 com.tngtech.archunit.core.domain.JavaFieldAccess; +import org.objectweb.asm.Label; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.tngtech.archunit.core.domain.JavaConstructor.CONSTRUCTOR_NAME; + +class RecordAccessHandler implements JavaClassProcessor.AccessHandler { + private static final Logger LOG = LoggerFactory.getLogger(RecordAccessHandler.class); + + private final ClassFileImportRecord importRecord; + private final DependencyResolutionProcess dependencyResolutionProcess; + private final TryCatchTracker tryCatchTracker; + private RawAccessRecord.CodeUnit codeUnit; + private int lineNumber; + + RecordAccessHandler(ClassFileImportRecord importRecord, DependencyResolutionProcess dependencyResolutionProcess, TryCatchTracker tryCatchTracker) { + this.importRecord = importRecord; + this.dependencyResolutionProcess = dependencyResolutionProcess; + this.tryCatchTracker = tryCatchTracker; + } + + @Override + public void setContext(RawAccessRecord.CodeUnit codeUnit) { + this.codeUnit = codeUnit; + } + + @Override + public void setLineNumber(int lineNumber) { + this.lineNumber = lineNumber; + } + + @Override + public void handleFieldInstruction(int opcode, String owner, String name, String desc) { + JavaFieldAccess.AccessType accessType = JavaFieldAccess.AccessType.forOpCode(opcode); + LOG.trace("Found {} access to field {}.{}:{} in line {}", accessType, owner, name, desc, lineNumber); + RawAccessRecord.TargetInfo target = new RawAccessRecord.TargetInfo(owner, name, desc); + importRecord.registerFieldAccess(filled(new RawAccessRecord.ForField.Builder(), target) + .withAccessType(accessType) + .build()); + dependencyResolutionProcess.registerAccessToType(target.owner.getFullyQualifiedClassName()); + } + + @Override + public void handleMethodInstruction(String owner, String name, String desc) { + LOG.trace("Found call of method {}.{}:{} in line {}", owner, name, desc, lineNumber); + RawAccessRecord.TargetInfo target = new RawAccessRecord.TargetInfo(owner, name, desc); + RawAccessRecord.ForMethodCall call = filled(new RawAccessRecord.ForMethodCall.Builder(), target) + .withCaughtThrowables(tryCatchTracker.getCaughtThrowablesInCurrentScope()) + .build(); + if (CONSTRUCTOR_NAME.equals(name)) { + importRecord.registerConstructorCall(call); + } else { + importRecord.registerMethodCall(call); + } + dependencyResolutionProcess.registerAccessToType(target.owner.getFullyQualifiedClassName()); + } + + @Override + public void handleMethodReferenceInstruction(String owner, String name, String desc) { + LOG.trace("Found method reference {}.{}:{} in line {}", owner, name, desc, lineNumber); + RawAccessRecord.TargetInfo target = new RawAccessRecord.TargetInfo(owner, name, desc); + if (CONSTRUCTOR_NAME.equals(name)) { + importRecord.registerConstructorReference(filled(new RawAccessRecord.Builder(), target).build()); + } else { + importRecord.registerMethodReference(filled(new RawAccessRecord.Builder(), target).build()); + } + dependencyResolutionProcess.registerAccessToType(target.owner.getFullyQualifiedClassName()); + } + + @Override + public void handleTryCatchBlock(Label start, Label end, JavaClassDescriptor throwableType) { + LOG.trace("Found try/catch block for throwable {}", throwableType); + tryCatchTracker.registerTryCatchBlock(start, end, throwableType); + } + + @Override + public void handleLabel(Label label) { + tryCatchTracker.onLabel(label); + } + + private > BUILDER filled(BUILDER builder, RawAccessRecord.TargetInfo target) { + return builder + .withCaller(codeUnit) + .withTarget(target) + .withLineNumber(lineNumber); + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/TryCatchTracker.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/TryCatchTracker.java new file mode 100644 index 0000000000..cfa788640b --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/TryCatchTracker.java @@ -0,0 +1,146 @@ +/* + * Copyright 2014-2022 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.google.common.base.Function; +import com.google.common.base.Predicate; +import com.tngtech.archunit.core.domain.JavaClassDescriptor; +import org.objectweb.asm.Label; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import static com.google.common.collect.Iterables.filter; +import static com.google.common.collect.Iterables.transform; +import static com.google.common.collect.Lists.newArrayList; +import static com.google.common.collect.Sets.newHashSet; + +class TryCatchTracker { + private static final Function EXTRACTING_CAUGHT_THROWABLES = new Function() { + + @Override + public JavaClassDescriptor apply(TryCatchBlock input) { + return input.getCaughtThrowable(); + } + }; + + private static Predicate startingAt(final Label label) { + return new Predicate() { + + @Override + public boolean apply(TryCatchBlock input) { + return input.startsAt(label); + } + }; + } + + private static Predicate endingAt(final Label label) { + return new Predicate() { + + @Override + public boolean apply(TryCatchBlock input) { + return input.endsAt(label); + } + }; + } + + private final Set blocks = new HashSet<>(); + private final Set activeBlocks = new HashSet<>(); + + void onLabel(Label label) { + activeBlocks.addAll(newArrayList(filter(blocks, startingAt(label)))); + activeBlocks.removeAll(newArrayList(filter(blocks, endingAt(label)))); + } + + Set getCaughtThrowablesInCurrentScope() { + return newHashSet(transform(activeBlocks, EXTRACTING_CAUGHT_THROWABLES)); + } + + public void registerTryCatchBlock(Label start, Label end, JavaClassDescriptor throwableType) { + blocks.add(new TryCatchBlock(LabelSpan.of(start, end), throwableType)); + } + + private static class TryCatchBlock { + private final LabelSpan span; + private final JavaClassDescriptor caughtThrowable; + + public TryCatchBlock(LabelSpan span, JavaClassDescriptor caughtThrowable) { + this.span = span; + this.caughtThrowable = caughtThrowable; + } + + boolean startsAt(Label label) { + return span.startsAt(label); + } + + boolean endsAt(Label label) { + return span.endsAt(label); + } + + JavaClassDescriptor getCaughtThrowable() { + return caughtThrowable; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TryCatchBlock that = (TryCatchBlock) o; + return span.equals(that.span) && caughtThrowable.equals(that.caughtThrowable); + } + + @Override + public int hashCode() { + return Objects.hash(span, caughtThrowable); + } + } + + private static class LabelSpan { + private final Label start; + private final Label end; + + private LabelSpan(Label start, Label end) { + this.start = start; + this.end = end; + } + + public static LabelSpan of(Label start, Label end) { + return new LabelSpan(start, end); + } + + boolean startsAt(Label label) { + return start.equals(label); + } + + boolean endsAt(Label label) { + return end.equals(label); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LabelSpan labelSpan = (LabelSpan) o; + return start.equals(labelSpan.start) && end.equals(labelSpan.end); + } + + @Override + public int hashCode() { + return Objects.hash(start, end); + } + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterAccessesTest.java b/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterAccessesTest.java index 25e99d22cb..d38db53ffe 100644 --- a/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterAccessesTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterAccessesTest.java @@ -1,13 +1,12 @@ package com.tngtech.archunit.core.importer; +import com.tngtech.archunit.core.importer.testexamples.trycatch.ClassHoldingMethods; + +import java.io.FileNotFoundException; +import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import com.google.common.base.Predicate; import com.google.common.base.Suppliers; @@ -77,6 +76,7 @@ import com.tngtech.archunit.core.importer.testexamples.integration.ClassXDependingOnClassesABCD; import com.tngtech.archunit.core.importer.testexamples.integration.InterfaceOfClassX; import com.tngtech.archunit.core.importer.testexamples.specialtargets.ClassCallingSpecialTarget; +import com.tngtech.archunit.core.importer.testexamples.trycatch.ClassWithTryCatchBlocks; import com.tngtech.java.junit.dataprovider.DataProviderRunner; import org.junit.Test; import org.junit.runner.RunWith; @@ -98,6 +98,7 @@ import static com.tngtech.archunit.testutil.Assertions.assertThatCall; import static com.tngtech.archunit.testutil.Assertions.assertThatType; import static com.tngtech.archunit.testutil.Assertions.assertThatTypes; +import static com.tngtech.archunit.testutil.JavaCallQuery.methodCallTo; import static com.tngtech.archunit.testutil.ReflectionTestUtils.constructor; import static com.tngtech.archunit.testutil.ReflectionTestUtils.field; import static com.tngtech.archunit.testutil.ReflectionTestUtils.method; @@ -752,6 +753,66 @@ public void classes_know_method_calls_to_themselves() { assertThat(calls).as("Method calls to class").isEqualTo(expected); } + @Test + public void method_calls_know_which_exceptions_are_handled() { + JavaClasses classes = new ClassFileImporter().importUrl(getClass().getResource("testexamples/trycatch")); + JavaClass classHoldingMethods = classes.get(ClassHoldingMethods.class); + JavaClass classWithTryCatchBlocks = classes.get(ClassWithTryCatchBlocks.class); + JavaMethod setSomeInt = classHoldingMethods.getMethod("setSomeInt", int.class); + JavaMethod setSomeString = classHoldingMethods.getMethod("setSomeString", String.class); + JavaMethod doSomething = classHoldingMethods.getMethod("doSomething"); + JavaMethod openStream = classHoldingMethods.getMethod("openStream"); + JavaMethod first = classWithTryCatchBlocks.getMethod("first"); + JavaMethod second = classWithTryCatchBlocks.getMethod("second"); + + Set calls = classHoldingMethods.getMethodCallsToSelf(); + + assertThatCall(methodCallTo(classHoldingMethods) + .from(first) + .to(setSomeInt) + .inLineNumber(14) + ).isNotWrappedInTryCatch(); + + assertThatCall(methodCallTo(classHoldingMethods) + .from(first) + .to(setSomeInt) + .inLineNumber(16)) + .isWrappedWithTryCatchFor(Exception.class); + + assertThatCall(methodCallTo(classHoldingMethods) + .from(first) + .to(doSomething) + .inLineNumber(19)) + .isWrappedWithTryCatchFor(Exception.class) + .isWrappedWithTryCatchFor(IllegalArgumentException.class) + .isWrappedWithTryCatchFor(UnsupportedOperationException.class); + + assertThatCall(methodCallTo(classHoldingMethods) + .from(first) + .to(setSomeString) + .inLineNumber(28)) + .isWrappedWithTryCatchFor(Throwable.class); + + assertThatCall(methodCallTo(classHoldingMethods) + .from(second) + .to(setSomeString) + .inLineNumber(36)) + .isWrappedWithTryCatchFor(RuntimeException.class); + + assertThatCall(methodCallTo(classHoldingMethods) + .from(second) + .to(doSomething) + .inLineNumber(43)) + .isNotWrappedInTryCatch(); + + assertThatCall(methodCallTo(classHoldingMethods) + .from(second) + .to(openStream) + .inLineNumber(49)) + .isWrappedWithTryCatchFor(FileNotFoundException.class) + .isWrappedWithTryCatchFor(IOException.class); + } + @Test public void classes_know_overridden_method_calls_to_themselves() { @SuppressWarnings("unused") @@ -1023,6 +1084,15 @@ public boolean apply(T input) { }); } + private Set getByNameAndLineNumber(final Set calls, final String name, final int lineNumber) { + return getBy(calls, new Predicate() { + @Override + public boolean apply(JavaMethodCall input) { + return name.equals(input.getName()) && lineNumber == input.getLineNumber(); + } + }); + } + private > Set getBy(Set calls, Predicate predicate) { return FluentIterable.from(calls).filter(predicate).toSet(); } 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 768fa27875..0f69250b83 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 @@ -18,16 +18,10 @@ import com.google.common.collect.Sets; import com.tngtech.archunit.ArchConfiguration; import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.*; import com.tngtech.archunit.core.domain.AccessTarget.CodeUnitAccessTarget; -import com.tngtech.archunit.core.domain.JavaClass; -import com.tngtech.archunit.core.domain.JavaClasses; -import com.tngtech.archunit.core.domain.JavaEnumConstant; -import com.tngtech.archunit.core.domain.JavaField; -import com.tngtech.archunit.core.domain.JavaMethod; -import com.tngtech.archunit.core.domain.JavaMethodCall; -import com.tngtech.archunit.core.domain.JavaModifier; -import com.tngtech.archunit.core.domain.JavaPackage; -import com.tngtech.archunit.core.domain.Source; +import com.tngtech.archunit.core.domain.AccessTarget.CodeUnitCallTarget; +import com.tngtech.archunit.core.domain.AccessTarget.MethodCallTarget; import com.tngtech.archunit.core.importer.testexamples.OtherClass; import com.tngtech.archunit.core.importer.testexamples.SomeClass; import com.tngtech.archunit.core.importer.testexamples.SomeEnum; @@ -924,10 +918,10 @@ public boolean includes(Location location) { }; } - private Condition targetWithFullName(final String name) { - return new Condition(String.format("target with name '%s'", name)) { + private Condition targetWithFullName(final String name) { + return new Condition(String.format("target with name '%s'", name)) { @Override - public boolean matches(CodeUnitAccessTarget value) { + public boolean matches(MethodCallTarget value) { return value.getFullName().equals(name); } }; diff --git a/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/trycatch/ClassHoldingMethods.java b/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/trycatch/ClassHoldingMethods.java new file mode 100644 index 0000000000..423b080af9 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/trycatch/ClassHoldingMethods.java @@ -0,0 +1,37 @@ +package com.tngtech.archunit.core.importer.testexamples.trycatch; + +import java.io.FileNotFoundException; + +public class ClassHoldingMethods { + int someInt; + String someString; + + public ClassHoldingMethods() { + } + + public ClassHoldingMethods(int someInt) { + this(); + this.someInt = someInt; + } + + public void setSomeInt(int someInt) { + this.someInt = someInt; + } + + public void setSomeString(String someString) { + this.someString = someString; + } + + void doSomething() { + setSomeInt(5); + } + + AutoCloseable openStream() throws FileNotFoundException { + return new AutoCloseable() { + @Override + public void close() throws Exception { + + } + }; + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/trycatch/ClassWithTryCatchBlocks.java b/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/trycatch/ClassWithTryCatchBlocks.java new file mode 100644 index 0000000000..624da0c7a8 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/trycatch/ClassWithTryCatchBlocks.java @@ -0,0 +1,57 @@ +package com.tngtech.archunit.core.importer.testexamples.trycatch; + +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.Serializable; + +public class ClassWithTryCatchBlocks { + void first() { + ClassHoldingMethods instanceOne = new ClassHoldingMethods(); + Object someOtherObject = new Object(); + Serializable someSerializable = new Serializable() { + }; + instanceOne.setSomeInt(2); + try { + instanceOne.setSomeInt(1); + someOtherObject.toString(); + try { + instanceOne.doSomething(); + } catch (IllegalArgumentException | UnsupportedOperationException e) { + } + } catch (Exception e) { + } finally { + } + instanceOne.someInt = 5; + + try { + instanceOne.setSomeString("hello"); + } catch (Throwable e) { + } + } + + void second() throws Exception { + ClassHoldingMethods instanceTwo = new ClassHoldingMethods(1); + try { + instanceTwo.setSomeString("string"); + } catch (RuntimeException e) { + instanceTwo.setSomeString("fallbackString"); + } + instanceTwo.someString = "other"; + + try { + instanceTwo.doSomething(); + } finally { + instanceTwo.setSomeInt(3); + } + + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(new byte[0]); + AutoCloseable stream = instanceTwo.openStream()) { + instanceTwo.doSomething(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} 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 88551f0263..4343c50f33 100644 --- a/archunit/src/test/java/com/tngtech/archunit/testutil/Assertions.java +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/Assertions.java @@ -1,79 +1,18 @@ package com.tngtech.archunit.testutil; import java.util.Collection; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; import java.util.Set; -import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.base.Optional; -import com.tngtech.archunit.core.domain.AccessTarget; -import com.tngtech.archunit.core.domain.AccessTarget.CodeUnitAccessTarget; -import com.tngtech.archunit.core.domain.AccessTarget.ConstructorCallTarget; +import com.tngtech.archunit.core.domain.*; import com.tngtech.archunit.core.domain.AccessTarget.FieldAccessTarget; -import com.tngtech.archunit.core.domain.Dependency; -import com.tngtech.archunit.core.domain.JavaAccess; -import com.tngtech.archunit.core.domain.JavaAnnotation; -import com.tngtech.archunit.core.domain.JavaClass; -import com.tngtech.archunit.core.domain.JavaClassDescriptor; -import com.tngtech.archunit.core.domain.JavaCodeUnit; -import com.tngtech.archunit.core.domain.JavaCodeUnitAccess; -import com.tngtech.archunit.core.domain.JavaConstructor; -import com.tngtech.archunit.core.domain.JavaConstructorCall; -import com.tngtech.archunit.core.domain.JavaEnumConstant; -import com.tngtech.archunit.core.domain.JavaField; -import com.tngtech.archunit.core.domain.JavaFieldAccess; -import com.tngtech.archunit.core.domain.JavaMember; -import com.tngtech.archunit.core.domain.JavaMethod; -import com.tngtech.archunit.core.domain.JavaMethodCall; -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.core.domain.properties.HasName; +import com.tngtech.archunit.core.domain.AccessTarget.MethodCallTarget; import com.tngtech.archunit.lang.ArchCondition; import com.tngtech.archunit.lang.ArchRule; import com.tngtech.archunit.lang.ConditionEvents; -import com.tngtech.archunit.testutil.assertion.ArchConditionAssertion; -import com.tngtech.archunit.testutil.assertion.ArchRuleAssertion; -import com.tngtech.archunit.testutil.assertion.ConditionEventsAssertion; -import com.tngtech.archunit.testutil.assertion.DependenciesAssertion; -import com.tngtech.archunit.testutil.assertion.DependencyAssertion; -import com.tngtech.archunit.testutil.assertion.DescribedPredicateAssertion; -import com.tngtech.archunit.testutil.assertion.JavaAnnotationAssertion; -import com.tngtech.archunit.testutil.assertion.JavaAnnotationsAssertion; -import com.tngtech.archunit.testutil.assertion.JavaClassAssertion; -import com.tngtech.archunit.testutil.assertion.JavaClassDescriptorAssertion; -import com.tngtech.archunit.testutil.assertion.JavaCodeUnitAssertion; -import com.tngtech.archunit.testutil.assertion.JavaConstructorAssertion; -import com.tngtech.archunit.testutil.assertion.JavaFieldAssertion; -import com.tngtech.archunit.testutil.assertion.JavaFieldsAssertion; -import com.tngtech.archunit.testutil.assertion.JavaMemberAssertion; -import com.tngtech.archunit.testutil.assertion.JavaMembersAssertion; -import com.tngtech.archunit.testutil.assertion.JavaMethodAssertion; -import com.tngtech.archunit.testutil.assertion.JavaMethodsAssertion; -import com.tngtech.archunit.testutil.assertion.JavaPackagesAssertion; -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.AbstractObjectAssert; -import org.assertj.core.api.Condition; -import org.assertj.core.api.ObjectAssert; -import org.assertj.core.api.ObjectAssertFactory; - -import static com.google.common.base.Strings.emptyToNull; -import static com.tngtech.archunit.core.domain.Formatters.formatMethodSimple; -import static com.tngtech.archunit.core.domain.Formatters.formatNamesOf; -import static com.tngtech.archunit.core.domain.JavaConstructor.CONSTRUCTOR_NAME; -import static com.tngtech.archunit.core.domain.TestUtils.targetFrom; +import com.tngtech.archunit.testutil.assertion.*; public class Assertions extends org.assertj.core.api.Assertions { public static ArchConditionAssertion assertThat(ArchCondition archCondition) { @@ -217,61 +156,6 @@ public static ExpectedAccessCreation expectedAccess() { return new ExpectedAccessCreation(); } - public static class ExpectedAccessCreation { - private ExpectedAccessCreation() { - } - - public Step2 from(Class originClass, String codeUnitName) { - return new Step2(originClass, codeUnitName); - } - - public static class Step2 { - private final Class originClass; - private final String originCodeUnitName; - - private Step2(Class originClass, String originCodeUnitName) { - this.originClass = originClass; - this.originCodeUnitName = originCodeUnitName; - } - - public Condition> to(final Class targetClass, final String targetName) { - return new Condition>( - String.format("%s from %s.%s to %s.%s", - JavaAccess.class.getSimpleName(), - originClass.getName(), originCodeUnitName, - targetClass.getSimpleName(), targetName)) { - @Override - public boolean matches(JavaAccess access) { - return access.getOriginOwner().isEquivalentTo(originClass) && - access.getOrigin().getName().equals(originCodeUnitName) && - access.getTargetOwner().isEquivalentTo(targetClass) && - access.getTarget().getName().equals(targetName); - } - }; - } - - public Condition> toConstructor(final Class targetClass, final Class... paramTypes) { - final List paramTypeNames = formatNamesOf(paramTypes); - return new Condition>( - String.format("%s from %s.%s to %s", - JavaAccess.class.getSimpleName(), - originClass.getName(), originCodeUnitName, - formatMethodSimple(targetClass.getSimpleName(), CONSTRUCTOR_NAME, paramTypeNames))) { - @Override - public boolean matches(JavaAccess access) { - return to(targetClass, CONSTRUCTOR_NAME).matches(access) && - rawParameterTypeNamesOf(access).equals(paramTypeNames); - } - - private List rawParameterTypeNamesOf(JavaAccess access) { - ConstructorCallTarget target = (ConstructorCallTarget) access.getTarget(); - return HasName.Utils.namesOf(target.getRawParameterTypes()); - } - }; - } - } - } - public static AccessToFieldAssertion assertThatAccess(JavaFieldAccess access) { return new AccessToFieldAssertion(access); } @@ -280,217 +164,12 @@ public static CodeUnitAccessAssertion assertThatAccess(JavaCodeUnitAccess ref return new CodeUnitAccessAssertion(reference); } - public static CodeUnitAccessAssertion assertThatCall(JavaMethodCall call) { - return assertThatAccess(call); - } - - public static CodeUnitAccessAssertion assertThatCall(JavaConstructorCall call) { - return assertThatAccess(call); - } - - public static class JavaEnumConstantAssertion extends AbstractObjectAssert { - private JavaEnumConstantAssertion(JavaEnumConstant enumConstant) { - super(enumConstant, JavaEnumConstantAssertion.class); - } - - public void isEquivalentTo(Enum enumConstant) { - assertThat(actual).as(describePartialAssertion()).isNotNull(); - assertThat(actual.getDeclaringClass().getName()).as(describePartialAssertion("type")).isEqualTo(enumConstant.getDeclaringClass().getName()); - assertThat(actual.name()).as(describePartialAssertion("name")).isEqualTo(enumConstant.name()); - } - - private String describePartialAssertion() { - return describePartialAssertion(""); - } - - private String describePartialAssertion(String partialAssertionDescription) { - return Joiner.on(": ").skipNulls().join(emptyToNull(descriptionText()), emptyToNull(partialAssertionDescription)); - } - } - - public static class JavaEnumConstantsAssertion extends AbstractObjectAssert { - private JavaEnumConstantsAssertion(JavaEnumConstant[] enumConstants) { - super(enumConstants, JavaEnumConstantsAssertion.class); - } - - public void matches(Enum... enumConstants) { - assertThat((Object[]) actual).as("Enum constants").hasSize(enumConstants.length); - for (int i = 0; i < actual.length; i++) { - assertThat(actual[i]).as("Element %d", i).isEquivalentTo(enumConstants[i]); - } - } - } - - public static class ThrowsDeclarationAssertion extends AbstractObjectAssert> { - private ThrowsDeclarationAssertion(ThrowsDeclaration throwsDeclaration) { - super(throwsDeclaration, ThrowsDeclarationAssertion.class); - } - - public void matches(Class clazz) { - assertThatType(actual.getRawType()).as("Type of " + actual) - .matches(clazz); - } - } - - public static class ThrowsClauseAssertion extends - AbstractIterableAssert, ThrowsDeclaration, ObjectAssert>> { - private ThrowsClauseAssertion(ThrowsClause throwsClause) { - super(throwsClause, ThrowsClauseAssertion.class); - } - - public void matches(Class... classes) { - assertThatThrowsClause(actual).as("ThrowsClause").hasSize(classes.length); - for (int i = 0; i < actual.size(); i++) { - assertThat(Iterables.get(actual, i)).as("Element %d", i).matches(classes[i]); - } - } - - @Override - protected ObjectAssert> toAssert(ThrowsDeclaration value, String description) { - return new ObjectAssertFactory>().createAssert(value).as(description); - } - } - - public static class AccessesAssertion { - private final Set> actualRemaining; - - AccessesAssertion(Collection> accesses) { - this.actualRemaining = new HashSet<>(accesses); - } - - public AccessesAssertion contain(Condition> condition) { - for (Iterator> iterator = actualRemaining.iterator(); iterator.hasNext(); ) { - if (condition.matches(iterator.next())) { - iterator.remove(); - return this; - } - } - throw new AssertionError("No access matches " + condition); - } - - @SafeVarargs - public final AccessesAssertion containOnly(Condition>... conditions) { - for (Condition> condition : conditions) { - contain(condition); - } - assertThat(actualRemaining).as("Unexpected " + JavaAccess.class.getSimpleName()).isEmpty(); - return this; - } - } - - protected abstract static class BaseAccessAssertion< - SELF extends BaseAccessAssertion, - ACCESS extends JavaAccess, - TARGET extends AccessTarget> { - - ACCESS access; - - BaseAccessAssertion(ACCESS access) { - this.access = access; - } - - public SELF isFrom(String name, Class... parameterTypes) { - return isFrom(access.getOrigin().getOwner().getCodeUnitWithParameterTypes(name, parameterTypes)); - } - - public SELF isFrom(Class originClass, String name, Class... parameterTypes) { - assertThatType(access.getOriginOwner()).matches(originClass); - return isFrom(name, parameterTypes); - } - - public SELF isFrom(JavaCodeUnit codeUnit) { - assertThat(access.getOrigin()).as("Origin of access").isEqualTo(codeUnit); - return newAssertion(access); - } - - public SELF isTo(TARGET target) { - assertThat(access.getTarget()).as("Target of " + access.getName()).isEqualTo(target); - return newAssertion(access); - } - - public SELF isTo(Condition target) { - assertThat(access.getTarget()).as("Target of " + access.getName()).is(target); - return newAssertion(access); - } - - public void inLineNumber(int number) { - assertThat(access.getLineNumber()) - .as("Line number of access to " + access.getName()) - .isEqualTo(number); - } - - protected abstract SELF newAssertion(ACCESS access); + public static CallAssertion assertThatCall(JavaMethodCall call) { + return new CallAssertion<>(call); } - public static class AccessToFieldAssertion extends BaseAccessAssertion { - private AccessToFieldAssertion(JavaFieldAccess access) { - super(access); - } - - @Override - protected AccessToFieldAssertion newAssertion(JavaFieldAccess access) { - return new AccessToFieldAssertion(access); - } - - public AccessToFieldAssertion isTo(final String name) { - return isTo(new Condition("field with name '" + name + "'") { - @Override - public boolean matches(FieldAccessTarget fieldAccessTarget) { - return fieldAccessTarget.getName().equals(name); - } - }); - } - - public AccessToFieldAssertion isTo(JavaField field) { - return isTo(targetFrom(field)); - } - - public AccessToFieldAssertion isOfType(JavaFieldAccess.AccessType type) { - assertThat(access.getAccessType()).isEqualTo(type); - return newAssertion(access); - } + public static CallAssertion assertThatCall(JavaConstructorCall call) { + return new CallAssertion<>(call); } - public static class CodeUnitAccessAssertion - extends BaseAccessAssertion, CodeUnitAccessTarget> { - @SuppressWarnings({"rawtypes", "unchecked"}) - private CodeUnitAccessAssertion(JavaCodeUnitAccess call) { - super((JavaCodeUnitAccess) call); - } - - public CodeUnitAccessAssertion isTo(final JavaCodeUnit target) { - return isTo(new Condition("method " + target.getFullName()) { - @Override - public boolean matches(CodeUnitAccessTarget codeUnitAccessTarget) { - return codeUnitAccessTarget.getOwner().equals(target.getOwner()) - && codeUnitAccessTarget.getName().equals(target.getName()) - && codeUnitAccessTarget.getRawParameterTypes().equals(target.getRawParameterTypes()); - } - }); - } - - public CodeUnitAccessAssertion isTo(final Class codeUnitOwner) { - return isTo(new Condition() { - @Override - public boolean matches(CodeUnitAccessTarget target) { - return target.getOwner().isEquivalentTo(codeUnitOwner); - } - }); - } - - public CodeUnitAccessAssertion isTo(final String codeUnitName, final Class... parameterTypes) { - return isTo(new Condition("code unit " + codeUnitName + "(" + formatNamesOf(parameterTypes) + ")") { - @Override - public boolean matches(CodeUnitAccessTarget target) { - return target.getName().equals(codeUnitName) - && HasName.Utils.namesOf(target.getRawParameterTypes()).equals(formatNamesOf(parameterTypes)); - } - }); - } - - @Override - protected CodeUnitAccessAssertion newAssertion(JavaCodeUnitAccess reference) { - return new CodeUnitAccessAssertion(reference); - } - } } diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/ExpectedAccessCreation.java b/archunit/src/test/java/com/tngtech/archunit/testutil/ExpectedAccessCreation.java new file mode 100644 index 0000000000..78fcbdd16c --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/ExpectedAccessCreation.java @@ -0,0 +1,67 @@ +package com.tngtech.archunit.testutil; + +import com.tngtech.archunit.core.domain.AccessTarget; +import com.tngtech.archunit.core.domain.JavaAccess; +import com.tngtech.archunit.core.domain.properties.HasName; +import org.assertj.core.api.Condition; + +import java.util.List; + +import static com.tngtech.archunit.core.domain.Formatters.formatMethodSimple; +import static com.tngtech.archunit.core.domain.Formatters.formatNamesOf; +import static com.tngtech.archunit.core.domain.JavaConstructor.CONSTRUCTOR_NAME; + +public class ExpectedAccessCreation { + ExpectedAccessCreation() { + } + + public Step2 from(Class originClass, String codeUnitName) { + return new Step2(originClass, codeUnitName); + } + + public static class Step2 { + private final Class originClass; + private final String originCodeUnitName; + + private Step2(Class originClass, String originCodeUnitName) { + this.originClass = originClass; + this.originCodeUnitName = originCodeUnitName; + } + + public Condition> to(final Class targetClass, final String targetName) { + return new Condition>( + String.format("%s from %s.%s to %s.%s", + JavaAccess.class.getSimpleName(), + originClass.getName(), originCodeUnitName, + targetClass.getSimpleName(), targetName)) { + @Override + public boolean matches(JavaAccess access) { + return access.getOriginOwner().isEquivalentTo(originClass) && + access.getOrigin().getName().equals(originCodeUnitName) && + access.getTargetOwner().isEquivalentTo(targetClass) && + access.getTarget().getName().equals(targetName); + } + }; + } + + public Condition> toConstructor(final Class targetClass, final Class... paramTypes) { + final List paramTypeNames = formatNamesOf(paramTypes); + return new Condition>( + String.format("%s from %s.%s to %s", + JavaAccess.class.getSimpleName(), + originClass.getName(), originCodeUnitName, + formatMethodSimple(targetClass.getSimpleName(), CONSTRUCTOR_NAME, paramTypeNames))) { + @Override + public boolean matches(JavaAccess access) { + return to(targetClass, CONSTRUCTOR_NAME).matches(access) && + rawParameterTypeNamesOf(access).equals(paramTypeNames); + } + + private List rawParameterTypeNamesOf(JavaAccess access) { + AccessTarget.ConstructorCallTarget target = (AccessTarget.ConstructorCallTarget) access.getTarget(); + return HasName.Utils.namesOf(target.getRawParameterTypes()); + } + }; + } + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/JavaCallQuery.java b/archunit/src/test/java/com/tngtech/archunit/testutil/JavaCallQuery.java new file mode 100644 index 0000000000..afb72fbe3b --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/JavaCallQuery.java @@ -0,0 +1,88 @@ +package com.tngtech.archunit.testutil; + +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import com.tngtech.archunit.core.domain.*; +import com.tngtech.archunit.core.domain.AccessTarget.MethodCallTarget; + +import java.util.Set; + +public class JavaCallQuery { + + private Iterable calls; + + private JavaCallQuery(Set calls) { + this.calls = calls; + } + + public static JavaCallQuery methodCallsTo(final JavaClass javaClass) { + return new JavaCallQuery(javaClass.getMethodCallsToSelf()); + } + + public static JavaCallQuery methodCallTo(final JavaClass javaClass) { + return new JavaCallQuery(javaClass.getMethodCallsToSelf()); + } + + public JavaCallQuery from(JavaCodeUnit source) { + calls = Iterables.filter(calls, hasOrigin(source)); + return this; + } + + public JavaCallQuery to(final String name) { + calls = Iterables.filter(calls, hasName(name)); + return this; + } + + public JavaCallQuery to(final JavaMethod method) { + calls = Iterables.filter(calls, hasTarget(method)); + return this; + } + + private Predicate hasTarget(final JavaMethod method) { + return new Predicate() { + @Override + public boolean apply(JavaMethodCall input) { + MethodCallTarget target = input.getTarget(); + return method.getOwner().equals(target.getOwner()) + && method.getName().equals(target.getName()) + && method.getRawParameterTypes().equals(target.getRawParameterTypes()); + } + }; + } + + private Predicate hasOrigin(final JavaCodeUnit origin) { + return new Predicate() { + @Override + public boolean apply(JavaMethodCall input) { + return origin.equals(input.getOrigin()); + } + }; + } + + public JavaMethodCall inLineNumber(final int lineNumber) { + calls = Iterables.filter(calls, hasLine(lineNumber)); + return first(); + } + + public JavaMethodCall first() { + return calls.iterator().next(); + } + + private Predicate hasLine(final int lineNumber) { + return new Predicate() { + @Override + public boolean apply(JavaMethodCall input) { + return input.getLineNumber() == lineNumber; + } + }; + } + + private static Predicate hasName(final String name) { + return new Predicate() { + @Override + public boolean apply(JavaMethodCall input) { + return name.equals(input.getName()); + } + }; + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/AccessToFieldAssertion.java b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/AccessToFieldAssertion.java new file mode 100644 index 0000000000..e844040276 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/AccessToFieldAssertion.java @@ -0,0 +1,37 @@ +package com.tngtech.archunit.testutil.assertion; + +import com.tngtech.archunit.core.domain.AccessTarget; +import com.tngtech.archunit.core.domain.JavaField; +import com.tngtech.archunit.core.domain.JavaFieldAccess; +import org.assertj.core.api.Condition; + +import static com.tngtech.archunit.core.domain.TestUtils.targetFrom; + +public class AccessToFieldAssertion extends BaseAccessAssertion { + public AccessToFieldAssertion(JavaFieldAccess access) { + super(access); + } + + @Override + protected AccessToFieldAssertion newAssertion(JavaFieldAccess access) { + return new AccessToFieldAssertion(access); + } + + public AccessToFieldAssertion isTo(final String name) { + return isTo(new Condition("field with name '" + name + "'") { + @Override + public boolean matches(AccessTarget.FieldAccessTarget fieldAccessTarget) { + return fieldAccessTarget.getName().equals(name); + } + }); + } + + public AccessToFieldAssertion isTo(JavaField field) { + return isTo(targetFrom(field)); + } + + public AccessToFieldAssertion isOfType(JavaFieldAccess.AccessType type) { + org.assertj.core.api.Assertions.assertThat(access.getAccessType()).isEqualTo(type); + return newAssertion(access); + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/AccessesAssertion.java b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/AccessesAssertion.java new file mode 100644 index 0000000000..93a6d8baa9 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/AccessesAssertion.java @@ -0,0 +1,37 @@ +package com.tngtech.archunit.testutil.assertion; + +import com.tngtech.archunit.core.domain.JavaAccess; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.Condition; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +public class AccessesAssertion { + private final Set> actualRemaining; + + public AccessesAssertion(Collection> accesses) { + this.actualRemaining = new HashSet<>(accesses); + } + + public AccessesAssertion contain(Condition> condition) { + for (Iterator> iterator = actualRemaining.iterator(); iterator.hasNext(); ) { + if (condition.matches(iterator.next())) { + iterator.remove(); + return this; + } + } + throw new AssertionError("No access matches " + condition); + } + + @SafeVarargs + public final AccessesAssertion containOnly(Condition>... conditions) { + for (Condition> condition : conditions) { + contain(condition); + } + Assertions.assertThat(actualRemaining).as("Unexpected " + JavaAccess.class.getSimpleName()).isEmpty(); + return this; + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/BaseAccessAssertion.java b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/BaseAccessAssertion.java new file mode 100644 index 0000000000..b9c94c9cb8 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/BaseAccessAssertion.java @@ -0,0 +1,51 @@ +package com.tngtech.archunit.testutil.assertion; + +import com.tngtech.archunit.core.domain.AccessTarget; +import com.tngtech.archunit.core.domain.JavaAccess; +import com.tngtech.archunit.core.domain.JavaCodeUnit; +import com.tngtech.archunit.testutil.Assertions; +import org.assertj.core.api.Condition; + +public abstract class BaseAccessAssertion< + SELF extends BaseAccessAssertion, + ACCESS extends JavaAccess, + TARGET extends AccessTarget> { + + ACCESS access; + + public BaseAccessAssertion(ACCESS access) { + this.access = access; + } + + public SELF isFrom(String name, Class... parameterTypes) { + return isFrom(access.getOrigin().getOwner().getCodeUnitWithParameterTypes(name, parameterTypes)); + } + + public SELF isFrom(Class originClass, String name, Class... parameterTypes) { + Assertions.assertThatType(access.getOriginOwner()).matches(originClass); + return isFrom(name, parameterTypes); + } + + public SELF isFrom(JavaCodeUnit codeUnit) { + Assertions.assertThat(access.getOrigin()).as("Origin of access").isEqualTo(codeUnit); + return newAssertion(access); + } + + public SELF isTo(TARGET target) { + org.assertj.core.api.Assertions.assertThat(access.getTarget()).as("Target of " + access.getName()).isEqualTo(target); + return newAssertion(access); + } + + public SELF isTo(Condition target) { + org.assertj.core.api.Assertions.assertThat(access.getTarget()).as("Target of " + access.getName()).is(target); + return newAssertion(access); + } + + public void inLineNumber(int number) { + org.assertj.core.api.Assertions.assertThat(access.getLineNumber()) + .as("Line number of access to " + access.getName()) + .isEqualTo(number); + } + + protected abstract SELF newAssertion(ACCESS access); +} diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/BaseCodeUnitAccessAssertion.java b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/BaseCodeUnitAccessAssertion.java new file mode 100644 index 0000000000..7b70553f64 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/BaseCodeUnitAccessAssertion.java @@ -0,0 +1,53 @@ +package com.tngtech.archunit.testutil.assertion; + +import com.tngtech.archunit.core.domain.AccessTarget.CodeUnitAccessTarget; +import com.tngtech.archunit.core.domain.JavaCodeUnit; +import com.tngtech.archunit.core.domain.JavaCodeUnitAccess; +import com.tngtech.archunit.core.domain.properties.HasName; +import org.assertj.core.api.Condition; + +import static com.tngtech.archunit.core.domain.Formatters.formatNamesOf; + +public abstract class BaseCodeUnitAccessAssertion< + SELF extends BaseCodeUnitAccessAssertion, + ACCESS extends JavaCodeUnitAccess, + TARGET extends CodeUnitAccessTarget> + extends BaseAccessAssertion { + @SuppressWarnings({"rawtypes", "unchecked"}) + public BaseCodeUnitAccessAssertion(JavaCodeUnitAccess call) { + super((ACCESS) call); + } + + public SELF isTo(final JavaCodeUnit target) { + return isTo(new Condition("method " + target.getFullName()) { + @Override + public boolean matches(TARGET codeUnitAccessTarget) { + return codeUnitAccessTarget.getOwner().equals(target.getOwner()) + && codeUnitAccessTarget.getName().equals(target.getName()) + && codeUnitAccessTarget.getRawParameterTypes().equals(target.getRawParameterTypes()); + } + }); + } + + public SELF isTo(final Class codeUnitOwner) { + return isTo(new Condition() { + @Override + public boolean matches(CodeUnitAccessTarget target) { + return target.getOwner().isEquivalentTo(codeUnitOwner); + } + }); + } + + public SELF isTo(final String codeUnitName, final Class... parameterTypes) { + return isTo(new Condition("code unit " + codeUnitName + "(" + formatNamesOf(parameterTypes) + ")") { + @Override + public boolean matches(TARGET target) { + return target.getName().equals(codeUnitName) + && HasName.Utils.namesOf(target.getRawParameterTypes()).equals(formatNamesOf(parameterTypes)); + } + }); + } + + @Override + protected abstract SELF newAssertion(ACCESS reference); +} diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/CallAssertion.java b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/CallAssertion.java new file mode 100644 index 0000000000..091150e0cf --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/CallAssertion.java @@ -0,0 +1,46 @@ +package com.tngtech.archunit.testutil.assertion; + +import com.tngtech.archunit.core.domain.AccessTarget; +import com.tngtech.archunit.core.domain.AccessTarget.CodeUnitCallTarget; +import com.tngtech.archunit.core.domain.JavaCall; +import com.tngtech.archunit.core.domain.JavaCodeUnitAccess; +import com.tngtech.archunit.core.importer.CaughtThrowable; +import org.assertj.core.api.Condition; +import org.assertj.core.api.IterableAssert; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CallAssertion extends BaseCodeUnitAccessAssertion, JavaCall, T> { + + public CallAssertion(JavaCodeUnitAccess call) { + super(call); + } + + @Override + protected CallAssertion newAssertion(JavaCall reference) { + return new CallAssertion<>(reference); + } + + public CallAssertion isWrappedWithTryCatchFor(Class throwableType) { + assertThat(access.getCaughtThrowables()) + .as("wrapped with try/catch for %s", throwableType.getSimpleName()) + .areExactly(1, caughtThrowableOfType(throwableType)); + return newAssertion(access); + } + + public CallAssertion isNotWrappedInTryCatch() { + assertThat(access.getCaughtThrowables()) + .as("not wrapped with try/catch") + .isEmpty(); + return newAssertion(access); + } + + private Condition caughtThrowableOfType(final Class exceptionClass) { + return new Condition("caught throwable of type " + exceptionClass.getSimpleName()) { + @Override + public boolean matches(CaughtThrowable value) { + return value.getThrowableType().resolveClass().equals(exceptionClass); + } + }; + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/CodeUnitAccessAssertion.java b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/CodeUnitAccessAssertion.java new file mode 100644 index 0000000000..4b983e2511 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/CodeUnitAccessAssertion.java @@ -0,0 +1,19 @@ +package com.tngtech.archunit.testutil.assertion; + +import com.tngtech.archunit.core.domain.AccessTarget.CodeUnitAccessTarget; +import com.tngtech.archunit.core.domain.JavaCodeUnitAccess; + +public class CodeUnitAccessAssertion extends BaseCodeUnitAccessAssertion< + CodeUnitAccessAssertion, + JavaCodeUnitAccess, + CodeUnitAccessTarget> { + + public CodeUnitAccessAssertion(JavaCodeUnitAccess call) { + super(call); + } + + @Override + protected CodeUnitAccessAssertion newAssertion(JavaCodeUnitAccess reference) { + return new CodeUnitAccessAssertion(reference); + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/JavaEnumConstantAssertion.java b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/JavaEnumConstantAssertion.java new file mode 100644 index 0000000000..4631fda5cb --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/JavaEnumConstantAssertion.java @@ -0,0 +1,28 @@ +package com.tngtech.archunit.testutil.assertion; + +import com.google.common.base.Joiner; +import com.tngtech.archunit.core.domain.JavaEnumConstant; +import com.tngtech.archunit.testutil.Assertions; +import org.assertj.core.api.AbstractObjectAssert; + +import static com.google.common.base.Strings.emptyToNull; + +public class JavaEnumConstantAssertion extends AbstractObjectAssert { + public JavaEnumConstantAssertion(JavaEnumConstant enumConstant) { + super(enumConstant, JavaEnumConstantAssertion.class); + } + + public void isEquivalentTo(Enum enumConstant) { + Assertions.assertThat(actual).as(describePartialAssertion()).isNotNull(); + org.assertj.core.api.Assertions.assertThat(actual.getDeclaringClass().getName()).as(describePartialAssertion("type")).isEqualTo(enumConstant.getDeclaringClass().getName()); + org.assertj.core.api.Assertions.assertThat(actual.name()).as(describePartialAssertion("name")).isEqualTo(enumConstant.name()); + } + + private String describePartialAssertion() { + return describePartialAssertion(""); + } + + private String describePartialAssertion(String partialAssertionDescription) { + return Joiner.on(": ").skipNulls().join(emptyToNull(descriptionText()), emptyToNull(partialAssertionDescription)); + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/JavaEnumConstantsAssertion.java b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/JavaEnumConstantsAssertion.java new file mode 100644 index 0000000000..df38578cab --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/JavaEnumConstantsAssertion.java @@ -0,0 +1,18 @@ +package com.tngtech.archunit.testutil.assertion; + +import com.tngtech.archunit.core.domain.JavaEnumConstant; +import org.assertj.core.api.AbstractObjectAssert; +import org.assertj.core.api.Assertions; + +public class JavaEnumConstantsAssertion extends AbstractObjectAssert { + public JavaEnumConstantsAssertion(JavaEnumConstant[] enumConstants) { + super(enumConstants, JavaEnumConstantsAssertion.class); + } + + public void matches(Enum... enumConstants) { + Assertions.assertThat((Object[]) actual).as("Enum constants").hasSize(enumConstants.length); + for (int i = 0; i < actual.length; i++) { + com.tngtech.archunit.testutil.Assertions.assertThat(actual[i]).as("Element %d", i).isEquivalentTo(enumConstants[i]); + } + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/ThrowsClauseAssertion.java b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/ThrowsClauseAssertion.java new file mode 100644 index 0000000000..4d6b70d4aa --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/ThrowsClauseAssertion.java @@ -0,0 +1,28 @@ +package com.tngtech.archunit.testutil.assertion; + +import com.google.common.collect.Iterables; +import com.tngtech.archunit.core.domain.ThrowsClause; +import com.tngtech.archunit.core.domain.ThrowsDeclaration; +import com.tngtech.archunit.testutil.Assertions; +import org.assertj.core.api.AbstractIterableAssert; +import org.assertj.core.api.ObjectAssert; +import org.assertj.core.api.ObjectAssertFactory; + +public class ThrowsClauseAssertion extends + AbstractIterableAssert, ThrowsDeclaration, ObjectAssert>> { + public ThrowsClauseAssertion(ThrowsClause throwsClause) { + super(throwsClause, ThrowsClauseAssertion.class); + } + + public void matches(Class... classes) { + Assertions.assertThatThrowsClause(actual).as("ThrowsClause").hasSize(classes.length); + for (int i = 0; i < actual.size(); i++) { + Assertions.assertThat(Iterables.get(actual, i)).as("Element %d", i).matches(classes[i]); + } + } + + @Override + protected ObjectAssert> toAssert(ThrowsDeclaration value, String description) { + return new ObjectAssertFactory>().createAssert(value).as(description); + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/ThrowsDeclarationAssertion.java b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/ThrowsDeclarationAssertion.java new file mode 100644 index 0000000000..0d138ab1d9 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/ThrowsDeclarationAssertion.java @@ -0,0 +1,16 @@ +package com.tngtech.archunit.testutil.assertion; + +import com.tngtech.archunit.core.domain.ThrowsDeclaration; +import com.tngtech.archunit.testutil.Assertions; +import org.assertj.core.api.AbstractObjectAssert; + +public class ThrowsDeclarationAssertion extends AbstractObjectAssert> { + public ThrowsDeclarationAssertion(ThrowsDeclaration throwsDeclaration) { + super(throwsDeclaration, ThrowsDeclarationAssertion.class); + } + + public void matches(Class clazz) { + Assertions.assertThatType(actual.getRawType()).as("Type of " + actual) + .matches(clazz); + } +}