From 9b3a512325363ce88592b5d9e4ec47ea38ea18bc Mon Sep 17 00:00:00 2001 From: Allan Jones Date: Sun, 12 Mar 2023 19:04:29 +0100 Subject: [PATCH] support type cast detection #710 Detect checkcast asm instructions as TypeCast (similar to InstanceofCheck) Implicit checkcast instructions generated by compiler due to method invocations are ignored Resolves #710 Signed-off-by: Allan Jones --- .../e77ec262-4d5c-4a7b-b41f-362a71e5a1d8 | 3 +- .../e77ec262-4d5c-4a7b-b41f-362a71e5a1d8 | 3 +- .../e77ec262-4d5c-4a7b-b41f-362a71e5a1d8 | 3 +- .../integration/ExamplesIntegrationTest.java | 12 +++ .../testutils/ExpectedDependency.java | 4 + .../archunit/core/domain/Dependency.java | 6 ++ .../domain/DomainObjectCreationContext.java | 4 + .../archunit/core/domain/ImportContext.java | 2 + .../archunit/core/domain/JavaClass.java | 5 + .../core/domain/JavaClassDependencies.java | 6 ++ .../core/domain/JavaClassMembers.java | 8 ++ .../archunit/core/domain/JavaCodeUnit.java | 7 ++ .../archunit/core/domain/TypeCast.java | 90 ++++++++++++++++ .../core/importer/ClassFileImportRecord.java | 13 +++ .../core/importer/ClassFileProcessor.java | 25 +++++ .../core/importer/ClassGraphCreator.java | 20 ++++ .../core/importer/DeclarationHandler.java | 2 + .../core/importer/JavaClassProcessor.java | 21 ++++ .../archunit/core/importer/RawTypeCast.java | 101 ++++++++++++++++++ .../core/importer/TypeCastRecorder.java | 36 +++++++ .../archunit/core/domain/DependencyTest.java | 26 +++++ .../ClassWithDependencyOnTypeCast.java | 21 ++++ ...ssFileImporterAutomaticResolutionTest.java | 18 ++++ ...assFileImporterLambdaDependenciesTest.java | 15 +++ .../ClassFileImporterMembersTest.java | 25 +++++ .../core/importer/ImportTestUtils.java | 6 ++ .../typecast/ClassWithMultipleTypeCasts.java | 73 +++++++++++++ .../typecast/TypeCastFromLambda.java | 20 ++++ .../typecast/TypeCastInConstructor.java | 10 ++ .../typecast/TypeCastInMethod.java | 8 ++ .../typecast/TypeCastInStaticInitializer.java | 9 ++ .../typecast/TypeCastTestedType.java | 4 + .../tngtech/archunit/testutil/Assertions.java | 6 ++ .../assertion/TypeCastsAssertion.java | 89 +++++++++++++++ 34 files changed, 698 insertions(+), 3 deletions(-) create mode 100644 archunit/src/main/java/com/tngtech/archunit/core/domain/TypeCast.java create mode 100644 archunit/src/main/java/com/tngtech/archunit/core/importer/RawTypeCast.java create mode 100644 archunit/src/main/java/com/tngtech/archunit/core/importer/TypeCastRecorder.java create mode 100644 archunit/src/test/java/com/tngtech/archunit/core/domain/testobjects/ClassWithDependencyOnTypeCast.java create mode 100644 archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/typecast/ClassWithMultipleTypeCasts.java create mode 100644 archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/typecast/TypeCastFromLambda.java create mode 100644 archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/typecast/TypeCastInConstructor.java create mode 100644 archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/typecast/TypeCastInMethod.java create mode 100644 archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/typecast/TypeCastInStaticInitializer.java create mode 100644 archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/typecast/TypeCastTestedType.java create mode 100644 archunit/src/test/java/com/tngtech/archunit/testutil/assertion/TypeCastsAssertion.java diff --git a/archunit-example/example-junit4/src/test/resources/frozen/e77ec262-4d5c-4a7b-b41f-362a71e5a1d8 b/archunit-example/example-junit4/src/test/resources/frozen/e77ec262-4d5c-4a7b-b41f-362a71e5a1d8 index 8b8178ae7f..25a211619e 100644 --- a/archunit-example/example-junit4/src/test/resources/frozen/e77ec262-4d5c-4a7b-b41f-362a71e5a1d8 +++ b/archunit-example/example-junit4/src/test/resources/frozen/e77ec262-4d5c-4a7b-b41f-362a71e5a1d8 @@ -6,4 +6,5 @@ Method has return type in (OtherDao.java:0) Method calls method in (OtherJpa.java:19) Method has return type in (OtherJpa.java:0) -Method calls method in (OtherJpa.java:24) \ No newline at end of file +Method calls method in (OtherJpa.java:24) +Method casts in (OtherJpa.java:27) \ No newline at end of file diff --git a/archunit-example/example-junit5/src/test/resources/frozen/e77ec262-4d5c-4a7b-b41f-362a71e5a1d8 b/archunit-example/example-junit5/src/test/resources/frozen/e77ec262-4d5c-4a7b-b41f-362a71e5a1d8 index 8b8178ae7f..25a211619e 100644 --- a/archunit-example/example-junit5/src/test/resources/frozen/e77ec262-4d5c-4a7b-b41f-362a71e5a1d8 +++ b/archunit-example/example-junit5/src/test/resources/frozen/e77ec262-4d5c-4a7b-b41f-362a71e5a1d8 @@ -6,4 +6,5 @@ Method has return type in (OtherDao.java:0) Method calls method in (OtherJpa.java:19) Method has return type in (OtherJpa.java:0) -Method calls method in (OtherJpa.java:24) \ No newline at end of file +Method calls method in (OtherJpa.java:24) +Method casts in (OtherJpa.java:27) \ No newline at end of file diff --git a/archunit-example/example-plain/src/test/resources/frozen/e77ec262-4d5c-4a7b-b41f-362a71e5a1d8 b/archunit-example/example-plain/src/test/resources/frozen/e77ec262-4d5c-4a7b-b41f-362a71e5a1d8 index 8b8178ae7f..25a211619e 100644 --- a/archunit-example/example-plain/src/test/resources/frozen/e77ec262-4d5c-4a7b-b41f-362a71e5a1d8 +++ b/archunit-example/example-plain/src/test/resources/frozen/e77ec262-4d5c-4a7b-b41f-362a71e5a1d8 @@ -6,4 +6,5 @@ Method has return type in (OtherDao.java:0) Method calls method in (OtherJpa.java:19) Method has return type in (OtherJpa.java:0) -Method calls method in (OtherJpa.java:24) \ No newline at end of file +Method calls method in (OtherJpa.java:24) +Method casts in (OtherJpa.java:27) \ No newline at end of file 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 69f5a3602a..ce7402a97d 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 @@ -672,6 +672,9 @@ Stream FrozenRulesTest() { .by(method(OtherJpa.class, "testConnection") .checkingInstanceOf(ProxiedConnection.class) .inLine(26)) + .by(method(OtherJpa.class, "testConnection") + .casting(ProxiedConnection.class) + .inLine(27)) .ofRule("no classes should depend on classes that are assignable to javax.persistence.EntityManager") .by(callFromMethod(ServiceViolatingDaoRules.class, "illegallyUseEntityManager"). @@ -847,6 +850,9 @@ Stream LayerDependencyRulesTest() { .by(method(OtherJpa.class, "testConnection") .checkingInstanceOf(ProxiedConnection.class) .inLine(26)) + .by(method(OtherJpa.class, "testConnection") + .casting(ProxiedConnection.class) + .inLine(27)) .ofRule("classes that reside in a package '..service..' should " + "only have dependent classes that reside in any package ['..controller..', '..service..']") @@ -866,6 +872,9 @@ Stream LayerDependencyRulesTest() { .by(method(OtherJpa.class, "testConnection") .checkingInstanceOf(ProxiedConnection.class) .inLine(26)) + .by(method(OtherJpa.class, "testConnection") + .casting(ProxiedConnection.class) + .inLine(27)) .ofRule("classes that reside in a package '..service..' should " + "only depend on classes that reside in any package ['..service..', '..persistence..', 'java..', 'javax..']") @@ -972,6 +981,9 @@ Stream LayeredArchitectureTest() { .toMethod(ProxiedConnection.class, "refresh") .inLine(27) .asDependency()) + .by(method(OtherJpa.class, "testConnection") + .casting(ProxiedConnection.class) + .inLine(27)) .by(typeParameter(ServiceHelper.class, "TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeUtility.class)) .by(typeParameter(ServiceHelper.class, "ANOTHER_TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeEnum.class)) 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 13172db16c..e9c84a8f17 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 @@ -295,6 +295,10 @@ public AddsLineNumber checkingInstanceOf(Class target) { return new AddsLineNumber(owner, getOriginName(), "checks instanceof", target); } + public AddsLineNumber casting(Class target) { + return new AddsLineNumber(owner, getOriginName(), "casts", target); + } + public AddsLineNumber referencingClassObject(Class target) { return new AddsLineNumber(owner, getOriginName(), "references class object", target); } 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 a16102f5c0..3fdc0b1551 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 @@ -128,6 +128,12 @@ static Set tryCreateFromInstanceofCheck(InstanceofCheck instanceofCh instanceofCheck.getRawType(), instanceofCheck.getSourceCodeLocation()); } + static Set tryCreateFromTypeCast(TypeCast typeCast) { + return tryCreateDependency( + typeCast.getOwner(), "casts", + typeCast.getRawType(), typeCast.getSourceCodeLocation()); + } + static Set tryCreateFromReferencedClassObject(ReferencedClassObject referencedClassObject) { return tryCreateDependency( referencedClassObject.getOwner(), "references class object", 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 63cecec171..775f8afe80 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 @@ -184,6 +184,10 @@ public static InstanceofCheck createInstanceofCheck(JavaCodeUnit codeUnit, JavaC return InstanceofCheck.from(codeUnit, type, lineNumber, declaredInLambda); } + public static TypeCast createTypeCast(JavaCodeUnit codeUnit, JavaClass type, int lineNumber, boolean declaredInLambda) { + return TypeCast.from(codeUnit, type, lineNumber, declaredInLambda); + } + public static JavaTypeVariable createTypeVariable(String name, OWNER owner, JavaClass erasure) { return new JavaTypeVariable<>(name, owner, erasure); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/ImportContext.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/ImportContext.java index dbbdaab49f..d32c6a6b55 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/ImportContext.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/ImportContext.java @@ -67,5 +67,7 @@ public interface ImportContext { Set createInstanceofChecksFor(JavaCodeUnit codeUnit); + Set createTypeCastsFor(JavaCodeUnit codeUnit); + JavaClass resolveClass(String fullyQualifiedClassName); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java index 013e452635..b9e91ff468 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 @@ -645,6 +645,11 @@ public Set getInstanceofChecks() { return members.getInstanceofChecks(); } + @PublicAPI(usage = ACCESS) + public Set getTypeCasts() { + return members.getTypeCasts(); + } + @PublicAPI(usage = ACCESS) public Set getReferencedClassObjects() { return members.getReferencedClassObjects(); 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 b08aff1c4e..0ec17769aa 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 @@ -49,6 +49,7 @@ private Supplier> createDirectDependenciesFromClassSupplier() { throwsDeclarationDependenciesFromSelf(), annotationDependenciesFromSelf(), instanceofCheckDependenciesFromSelf(), + typeCastDependenciesFromSelf(), referencedClassObjectDependenciesFromSelf(), typeParameterDependenciesFromSelf() ).collect(toImmutableSet()) @@ -182,6 +183,11 @@ private Stream instanceofCheckDependenciesFromSelf() { .flatMap(instanceofCheck -> Dependency.tryCreateFromInstanceofCheck(instanceofCheck).stream()); } + private Stream typeCastDependenciesFromSelf() { + return javaClass.getTypeCasts().stream() + .flatMap(typeCast -> Dependency.tryCreateFromTypeCast(typeCast).stream()); + } + private Stream referencedClassObjectDependenciesFromSelf() { return javaClass.getReferencedClassObjects().stream() .flatMap(referencedClassObject -> Dependency.tryCreateFromReferencedClassObject(referencedClassObject).stream()); diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassMembers.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassMembers.java index 94fc34c785..162417acdc 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassMembers.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassMembers.java @@ -191,6 +191,14 @@ Set getInstanceofChecks() { return result.build(); } + Set getTypeCasts() { + ImmutableSet.Builder result = ImmutableSet.builder(); + for (JavaCodeUnit codeUnit : codeUnits) { + result.addAll(codeUnit.getTypeCasts()); + } + return result.build(); + } + Set getReferencedClassObjects() { ImmutableSet.Builder result = ImmutableSet.builder(); for (JavaCodeUnit codeUnit : codeUnits) { 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 efe43febbb..5b654ed86c 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 @@ -72,6 +72,7 @@ public abstract class JavaCodeUnit private Set tryCatchBlocks = Collections.emptySet(); private Set referencedClassObjects; private Set instanceofChecks; + private Set typeCasts; JavaCodeUnit(JavaCodeUnitBuilder builder) { super(builder); @@ -207,6 +208,11 @@ public Set getInstanceofChecks() { return instanceofChecks; } + @PublicAPI(usage = ACCESS) + public Set getTypeCasts() { + return typeCasts; + } + @PublicAPI(usage = ACCESS) public Set getTryCatchBlocks() { return tryCatchBlocks; @@ -281,6 +287,7 @@ void completeFrom(ImportContext context) { .collect(toImmutableSet()); referencedClassObjects = context.createReferencedClassObjectsFor(this); instanceofChecks = context.createInstanceofChecksFor(this); + typeCasts = context.createTypeCastsFor(this); } @ResolvesTypesViaReflection diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/TypeCast.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/TypeCast.java new file mode 100644 index 0000000000..264a460fae --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/TypeCast.java @@ -0,0 +1,90 @@ +/* + * Copyright 2014-2023 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.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 TypeCast implements HasType, HasOwner, HasSourceCodeLocation { + + private final JavaCodeUnit owner; + private final JavaClass type; + private final int lineNumber; + private final boolean declaredInLambda; + private final SourceCodeLocation sourceCodeLocation; + + private TypeCast(JavaCodeUnit owner, JavaClass type, int lineNumber, boolean declaredInLambda) { + this.owner = checkNotNull(owner); + this.type = checkNotNull(type); + this.lineNumber = lineNumber; + this.declaredInLambda = declaredInLambda; + sourceCodeLocation = SourceCodeLocation.of(owner.getOwner(), lineNumber); + } + + @Override + @PublicAPI(usage = ACCESS) + public JavaClass getRawType() { + return type; + } + + @Override + @PublicAPI(usage = ACCESS) + public JavaType getType() { + return type; + } + + @Override + @PublicAPI(usage = ACCESS) + public JavaCodeUnit getOwner() { + return owner; + } + + @PublicAPI(usage = ACCESS) + public int getLineNumber() { + return lineNumber; + } + + @PublicAPI(usage = ACCESS) + public boolean isDeclaredInLambda() { + return declaredInLambda; + } + + @Override + public SourceCodeLocation getSourceCodeLocation() { + return sourceCodeLocation; + } + + @Override + public String toString() { + return toStringHelper(this) + .add("owner", owner) + .add("type", type) + .add("lineNumber", lineNumber) + .add("declaredInLambda", declaredInLambda) + .toString(); + } + + static TypeCast from(JavaCodeUnit owner, JavaClass type, int lineNumber, boolean declaredInLambda) { + return new TypeCast(owner, type, lineNumber, declaredInLambda); + } +} 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 0df029f4fd..6c049bebbf 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 @@ -85,6 +85,7 @@ class ClassFileImportRecord { private final Set rawConstructorReferenceRecords = new HashSet<>(); private final Set rawReferencedClassObjects = new HashSet<>(); private final Set rawInstanceofChecks = new HashSet<>(); + private final Set rawTypeCasts = new HashSet<>(); private final Set rawTryCatchBlocks = new HashSet<>(); private final SyntheticAccessRecorder syntheticLambdaAccessRecorder = createSyntheticLambdaAccessRecorder(); private final SyntheticAccessRecorder syntheticPrivateAccessRecorder = createSyntheticPrivateAccessRecorder(); @@ -253,6 +254,10 @@ void registerInstanceofCheck(RawInstanceofCheck instanceofCheck) { rawInstanceofChecks.add(instanceofCheck); } + void registerTypeCast(RawTypeCast typeCast) { + rawTypeCasts.add(typeCast); + } + void forEachRawFieldAccessRecord(Consumer doWithRecord) { resolveSyntheticOrigins(rawFieldAccessRecords, COPY_RAW_FIELD_ACCESS_RECORD, syntheticPrivateAccessRecorder, syntheticLambdaAccessRecorder) .forEach(doWithRecord); @@ -288,6 +293,11 @@ void forEachRawInstanceofCheck(Consumer doWithInstanceofChec .forEach(doWithInstanceofCheck); } + void forEachRawTypeCast(Consumer doWithTypeCast) { + resolveSyntheticOrigins(rawTypeCasts, COPY_RAW_TYPE_CAST, syntheticLambdaAccessRecorder) + .forEach(doWithTypeCast); + } + public void forEachRawTryCatchBlock(Consumer doWithTryCatchBlock) { resolveSyntheticOrigins(rawTryCatchBlocks, COPY_RAW_TRY_CATCH_BLOCK, syntheticLambdaAccessRecorder) .map(rawTryCatchBlock -> { @@ -332,6 +342,9 @@ Map getClasses() { private static final Function COPY_RAW_INSTANCEOF_CHECK = instanceofCheck -> copyInto(new RawInstanceofCheck.Builder(), instanceofCheck); + private static final Function COPY_RAW_TYPE_CAST = + typeCast -> copyInto(new RawTypeCast.Builder(), typeCast); + private static final Function COPY_RAW_TRY_CATCH_BLOCK = RawTryCatchBlock.Builder::from; private static > BUILDER copyInto(BUILDER builder, RawCodeUnitDependency referencedClassObject) { 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 d1c96c928a..40fc910baa 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 @@ -189,6 +189,11 @@ public void onDeclaredInstanceofCheck(String typeName) { dependencyResolutionProcess.registerAccessToType(typeName); } + @Override + public void onDeclaredTypeCast(String typeName) { + dependencyResolutionProcess.registerAccessToType(typeName); + } + @Override public void onDeclaredThrowsClause(Collection exceptionTypeNames) { dependencyResolutionProcess.registerMemberTypes(exceptionTypeNames); @@ -208,6 +213,7 @@ private static class RecordAccessHandler implements AccessHandler, TryCatchBlock private CodeUnit codeUnit; private int lineNumber; private final TryCatchRecorder tryCatchRecorder = new TryCatchRecorder(this); + private final TypeCastRecorder typeCastRecorder = new TypeCastRecorder(); private RecordAccessHandler(ClassFileImportRecord importRecord, DependencyResolutionProcess dependencyResolutionProcess) { this.importRecord = importRecord; @@ -230,6 +236,11 @@ public void onLabel(Label label) { tryCatchRecorder.onEncounteredLabel(label); } + @Override + public void handleVariableInstruction(int opcode, int varIndex) { + typeCastRecorder.reset(); + } + @Override public void handleFieldInstruction(int opcode, String owner, String name, String desc) { AccessType accessType = AccessType.forOpCode(opcode); @@ -240,6 +251,7 @@ public void handleFieldInstruction(int opcode, String owner, String name, String .build(); importRecord.registerFieldAccess(accessRecord); tryCatchRecorder.registerAccess(accessRecord); + typeCastRecorder.reset(); dependencyResolutionProcess.registerAccessToType(target.owner.getFullyQualifiedClassName()); } @@ -254,6 +266,7 @@ public void handleMethodInstruction(String owner, String name, String desc) { importRecord.registerMethodCall(accessRecord); } tryCatchRecorder.registerAccess(accessRecord); + typeCastRecorder.registerMethodInstruction(owner, name, desc); dependencyResolutionProcess.registerAccessToType(target.owner.getFullyQualifiedClassName()); } @@ -297,6 +310,18 @@ public void handleInstanceofCheck(JavaClassDescriptor instanceOfCheckType, int l .build()); } + @Override + public void handleTypeCast(JavaClassDescriptor typeCastType, int lineNumber) { + if (!typeCastRecorder.isImplicit()) { + importRecord.registerTypeCast(new RawTypeCast.Builder() + .withOrigin(codeUnit) + .withTarget(typeCastType) + .withLineNumber(lineNumber) + .withDeclaredInLambda(false) + .build()); + } + } + @Override public void handleTryCatchBlock(Label start, Label end, Label handler, JavaClassDescriptor throwableType) { LOG.trace("Found try/catch block between {} and {} for throwable {}", start, end, throwableType); 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 055e5f87db..d24236476a 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 @@ -53,6 +53,7 @@ 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.TypeCast; import com.tngtech.archunit.core.importer.AccessRecord.FieldAccessRecord; import com.tngtech.archunit.core.importer.DomainBuilders.JavaClassTypeParametersBuilder; import com.tngtech.archunit.core.importer.DomainBuilders.JavaConstructorCallBuilder; @@ -77,6 +78,7 @@ import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createInstanceofCheck; import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createJavaClasses; import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createReferencedClassObject; +import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createTypeCast; 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.JavaClassDescriptorImporter.isLambdaMethodName; @@ -95,6 +97,7 @@ class ClassGraphCreator implements ImportContext { private final SetMultimap> processedConstructorReferenceRecords = HashMultimap.create(); private final SetMultimap processedReferencedClassObjects = HashMultimap.create(); private final SetMultimap processedInstanceofChecks = HashMultimap.create(); + private final SetMultimap processedTypeCasts = HashMultimap.create(); private final SetMultimap processedTryCatchBlocks = HashMultimap.create(); ClassGraphCreator(ClassFileImportRecord importRecord, DependencyResolutionProcess dependencyResolutionProcess, ClassResolver classResolver) { @@ -135,6 +138,7 @@ private void completeCodeUnitDependencies() { tryProcess(record, AccessRecord.Factory.forConstructorReferenceRecord(), processedConstructorReferenceRecords)); importRecord.forEachRawReferencedClassObject(this::processReferencedClassObject); importRecord.forEachRawInstanceofCheck(this::processInstanceofCheck); + importRecord.forEachRawTypeCast(this::processTypeCast); importRecord.forEachRawTryCatchBlock(this::processTryCatchBlock); } @@ -169,6 +173,17 @@ private void processInstanceofCheck(RawInstanceofCheck rawInstanceofCheck) { processedInstanceofChecks.put(origin, instanceofCheck); } + private void processTypeCast(RawTypeCast rawTypeCast) { + JavaCodeUnit origin = rawTypeCast.getOrigin().resolveFrom(classes); + TypeCast typeCast = createTypeCast( + origin, + classes.getOrResolve(rawTypeCast.getTarget().getFullyQualifiedClassName()), + rawTypeCast.getLineNumber(), + rawTypeCast.isDeclaredInLambda() + ); + processedTypeCasts.put(origin, typeCast); + } + private void processTryCatchBlock(RawTryCatchBlock rawTryCatchBlock) { JavaCodeUnit declaringCodeUnit = rawTryCatchBlock.getDeclaringCodeUnit().resolveFrom(classes); TryCatchBlockBuilder tryCatchBlockBuilder = new TryCatchBlockBuilder() @@ -391,6 +406,11 @@ public Set createInstanceofChecksFor(JavaCodeUnit codeUnit) { return ImmutableSet.copyOf(processedInstanceofChecks.get(codeUnit)); } + @Override + public Set createTypeCastsFor(JavaCodeUnit codeUnit) { + return ImmutableSet.copyOf(processedTypeCasts.get(codeUnit)); + } + @Override public JavaClass resolveClass(String fullyQualifiedClassName) { return classes.getOrResolve(fullyQualifiedClassName); diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/DeclarationHandler.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/DeclarationHandler.java index 3a058e6f3c..bee50fb673 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/DeclarationHandler.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/DeclarationHandler.java @@ -57,6 +57,8 @@ interface DeclarationHandler { void onDeclaredInstanceofCheck(String typeName); + void onDeclaredTypeCast(String typeName); + void onDeclaredThrowsClause(Collection exceptionTypeNames); void onDeclaredGenericSignatureType(String typeName); 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 2092446fb4..45fb91bdae 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 @@ -333,6 +333,11 @@ public void visitCode() { actualLineNumber = 0; } + @Override + public void visitVarInsn(int opcode, int varIndex) { + accessHandler.handleVariableInstruction(opcode, varIndex); + } + @Override public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) { return new AnnotationProcessor(addAnnotationAtIndex(parameterAnnotationsByIndex, parameter), declarationHandler, handleAnnotationAnnotationProperty(desc, declarationHandler)); @@ -387,6 +392,10 @@ public void visitTypeInsn(int opcode, String type) { JavaClassDescriptor instanceOfCheckType = JavaClassDescriptorImporter.createFromAsmObjectTypeName(type); accessHandler.handleInstanceofCheck(instanceOfCheckType, actualLineNumber); declarationHandler.onDeclaredInstanceofCheck(instanceOfCheckType.getFullyQualifiedClassName()); + } else if (opcode == Opcodes.CHECKCAST) { + JavaClassDescriptor typeCastType = JavaClassDescriptorImporter.createFromAsmObjectTypeName(type); + accessHandler.handleTypeCast(typeCastType, actualLineNumber); + declarationHandler.onDeclaredTypeCast(typeCastType.getFullyQualifiedClassName()); } } @@ -505,6 +514,8 @@ public void setArrayResult(ValueBuilder valueBuilder) { } interface AccessHandler { + void handleVariableInstruction(int opcode, int varIndex); + void handleFieldInstruction(int opcode, String owner, String name, String desc); void setContext(CodeUnit codeUnit); @@ -523,6 +534,8 @@ interface AccessHandler { void handleInstanceofCheck(JavaClassDescriptor instanceOfCheckType, int lineNumber); + void handleTypeCast(JavaClassDescriptor typeCastType, int lineNumber); + void handleTryCatchBlock(Label start, Label end, Label handler, JavaClassDescriptor throwableType); void handleTryFinallyBlock(Label start, Label end, Label handler); @@ -531,6 +544,10 @@ interface AccessHandler { @Internal class NoOp implements AccessHandler { + @Override + public void handleVariableInstruction(int opcode, int varIndex) { + } + @Override public void handleFieldInstruction(int opcode, String owner, String name, String desc) { } @@ -567,6 +584,10 @@ public void handleReferencedClassObject(JavaClassDescriptor type, int lineNumber public void handleInstanceofCheck(JavaClassDescriptor instanceOfCheckType, int lineNumber) { } + @Override + public void handleTypeCast(JavaClassDescriptor typeCastType, int lineNumber) { + } + @Override public void handleTryCatchBlock(Label start, Label end, Label handler, JavaClassDescriptor throwableType) { } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/RawTypeCast.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/RawTypeCast.java new file mode 100644 index 0000000000..16c5da0fed --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/RawTypeCast.java @@ -0,0 +1,101 @@ +/* + * Copyright 2014-2023 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 RawTypeCast implements RawCodeUnitDependency { + private final RawAccessRecord.CodeUnit origin; + private final JavaClassDescriptor target; + private final int lineNumber; + private final boolean declaredInLambda; + + private RawTypeCast(RawAccessRecord.CodeUnit origin, JavaClassDescriptor target, int lineNumber, boolean declaredInLambda) { + this.origin = checkNotNull(origin); + this.target = checkNotNull(target); + this.lineNumber = lineNumber; + this.declaredInLambda = declaredInLambda; + } + + @Override + public RawAccessRecord.CodeUnit getOrigin() { + return origin; + } + + @Override + public JavaClassDescriptor getTarget() { + return target; + } + + @Override + public int getLineNumber() { + return lineNumber; + } + + @Override + public boolean isDeclaredInLambda() { + return declaredInLambda; + } + + @Override + public String toString() { + return toStringHelper(this) + .add("origin", origin) + .add("target", target) + .add("lineNumber", lineNumber) + .add("declaredInLambda", declaredInLambda) + .toString(); + } + + static class Builder implements RawCodeUnitDependency.Builder { + private RawAccessRecord.CodeUnit origin; + private JavaClassDescriptor target; + private int lineNumber; + private boolean declaredInLambda; + + @Override + public RawTypeCast.Builder withOrigin(RawAccessRecord.CodeUnit origin) { + this.origin = origin; + return this; + } + + @Override + public RawTypeCast.Builder withTarget(JavaClassDescriptor target) { + this.target = target; + return this; + } + + @Override + public RawTypeCast.Builder withLineNumber(int lineNumber) { + this.lineNumber = lineNumber; + return this; + } + + @Override + public RawTypeCast.Builder withDeclaredInLambda(boolean declaredInLambda) { + this.declaredInLambda = declaredInLambda; + return this; + } + + @Override + public RawTypeCast build() { + return new RawTypeCast(origin, target, lineNumber, declaredInLambda); + } + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/TypeCastRecorder.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/TypeCastRecorder.java new file mode 100644 index 0000000000..881ecc9ab7 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/TypeCastRecorder.java @@ -0,0 +1,36 @@ +/* + * Copyright 2014-2023 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; + +class TypeCastRecorder { + private static final String CLASS_INTERNAL_NAME = "java/lang/Class"; + private static final String METHOD_DESCRIPTOR = "(Ljava/lang/Object;)Ljava/lang/Object;"; + private static final String CAST_METHOD_NAME = "cast"; + + private boolean implicit; + + void reset() { + implicit = false; + } + + void registerMethodInstruction(String owner, String name, String desc) { + implicit = !CLASS_INTERNAL_NAME.equals(owner) || !CAST_METHOD_NAME.equals(name) || !METHOD_DESCRIPTOR.equals(desc); + } + + boolean isImplicit() { + return implicit; + } +} 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 e12df15b2a..ab48996c3f 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 @@ -15,6 +15,8 @@ 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.ClassWithDependencyOnTypeCast; +import com.tngtech.archunit.core.domain.testobjects.ClassWithDependencyOnTypeCast.TypeCastTarget; import com.tngtech.archunit.core.domain.testobjects.DependenciesOnClassObjects; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.testutil.Assertions; @@ -221,6 +223,30 @@ public void Dependency_from_instanceof_check_in_code_unit(JavaCodeUnit memberWit .hasDescription(memberWithInstanceofCheck.getFullName(), "checks instanceof", InstanceOfCheckTarget.class.getName()) .inLocation(ClassWithDependencyOnInstanceofCheck.class, expectedLineNumber); } + + @DataProvider + public static Object[][] with_type_cast_members() { + JavaClass javaClass = importClassesWithContext(ClassWithDependencyOnTypeCast.class, TypeCastTarget.class) + .get(ClassWithDependencyOnTypeCast.class); + + return $$( + $(javaClass.getStaticInitializer().get(), 7), + $(javaClass.getConstructor(Object.class), 12), + $(javaClass.getMethod("method", Object.class), 16)); + } + + @Test + @UseDataProvider("with_type_cast_members") + public void Dependency_from_type_cast_in_code_unit(JavaCodeUnit memberWithTypeCast, int expectedLineNumber) { + TypeCast typeCast = getOnlyElement(memberWithTypeCast.getTypeCasts()); + + Dependency dependency = getOnlyElement(Dependency.tryCreateFromTypeCast(typeCast)); + + Assertions.assertThatDependency(dependency) + .matches(ClassWithDependencyOnTypeCast.class, TypeCastTarget.class) + .hasDescription(memberWithTypeCast.getFullName(), "casts", TypeCastTarget.class.getName()) + .inLocation(ClassWithDependencyOnTypeCast.class, expectedLineNumber); + } @DataProvider public static Object[][] annotated_classes() { diff --git a/archunit/src/test/java/com/tngtech/archunit/core/domain/testobjects/ClassWithDependencyOnTypeCast.java b/archunit/src/test/java/com/tngtech/archunit/core/domain/testobjects/ClassWithDependencyOnTypeCast.java new file mode 100644 index 0000000000..fd0fa91ec5 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/core/domain/testobjects/ClassWithDependencyOnTypeCast.java @@ -0,0 +1,21 @@ +package com.tngtech.archunit.core.domain.testobjects; + +@SuppressWarnings({"unused", "ConstantConditions"}) +public class ClassWithDependencyOnTypeCast { + + private static final Object OBJ = new TypeCastTarget(); + private static final TypeCastTarget TARGET = (TypeCastTarget) OBJ; + + private final TypeCastTarget obj; + + ClassWithDependencyOnTypeCast(Object o) { + this.obj = (TypeCastTarget) o; + } + + TypeCastTarget method(Object o) { + return (TypeCastTarget) o; + } + + public static class TypeCastTarget { + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterAutomaticResolutionTest.java b/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterAutomaticResolutionTest.java index c394871fb6..4036fec311 100644 --- a/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterAutomaticResolutionTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterAutomaticResolutionTest.java @@ -31,6 +31,7 @@ import com.tngtech.archunit.core.domain.JavaWildcardType; import com.tngtech.archunit.core.domain.ReferencedClassObject; import com.tngtech.archunit.core.domain.ThrowsDeclaration; +import com.tngtech.archunit.core.domain.TypeCast; import com.tngtech.archunit.core.domain.properties.HasAnnotations; import com.tngtech.archunit.core.importer.DependencyResolutionProcessTestUtils.ImporterWithAdjustedResolutionRuns; import com.tngtech.archunit.core.importer.testexamples.SomeAnnotation; @@ -546,6 +547,23 @@ boolean call(Object obj) { assertThatType(instanceofCheck.getRawType()).matches(String.class); } + @Test + public void automatically_resolves_type_cast_targets() { + @SuppressWarnings("unused") + class Origin { + String call(Object obj) { + return (String) obj; + } + } + + JavaClass javaClass = ImporterWithAdjustedResolutionRuns.disableAllIterationsExcept(MAX_ITERATIONS_FOR_ACCESSES_TO_TYPES_PROPERTY_NAME) + .importClass(Origin.class); + TypeCast typeCast = getOnlyElement(javaClass.getTypeCasts()); + + assertThat(typeCast.getRawType()).isFullyImported(true); + assertThatType(typeCast.getRawType()).matches(String.class); + } + @Test public void automatically_resolves_types_of_throws_declarations() { @SuppressWarnings({"unused", "RedundantThrows"}) diff --git a/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterLambdaDependenciesTest.java b/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterLambdaDependenciesTest.java index 7942fedade..cb01b476ab 100644 --- a/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterLambdaDependenciesTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterLambdaDependenciesTest.java @@ -21,8 +21,10 @@ import com.tngtech.archunit.core.domain.JavaMethodReference; import com.tngtech.archunit.core.domain.ReferencedClassObject; import com.tngtech.archunit.core.domain.TryCatchBlock; +import com.tngtech.archunit.core.domain.TypeCast; import com.tngtech.archunit.core.importer.testexamples.instanceofcheck.CheckingInstanceofFromLambda; import com.tngtech.archunit.core.importer.testexamples.referencedclassobjects.ReferencingClassObjectsFromLambda; +import com.tngtech.archunit.core.importer.testexamples.typecast.TypeCastFromLambda; import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; import com.tngtech.java.junit.dataprovider.UseDataProvider; @@ -41,8 +43,10 @@ import static com.tngtech.archunit.testutil.Assertions.assertThatCall; import static com.tngtech.archunit.testutil.Assertions.assertThatInstanceofChecks; import static com.tngtech.archunit.testutil.Assertions.assertThatReferencedClassObjects; +import static com.tngtech.archunit.testutil.Assertions.assertThatTypeCasts; import static com.tngtech.archunit.testutil.assertion.AccessesAssertion.access; import static com.tngtech.archunit.testutil.assertion.InstanceofChecksAssertion.instanceofCheck; +import static com.tngtech.archunit.testutil.assertion.TypeCastsAssertion.typeCast; import static com.tngtech.archunit.testutil.assertion.ReferencedClassObjectsAssertion.referencedClassObject; import static com.tngtech.archunit.testutil.assertion.TryCatchBlockAssertion.tryCatchBlock; import static com.tngtech.java.junit.dataprovider.DataProviders.$; @@ -576,6 +580,17 @@ public void imports_instanceof_checks_in_lambda() { ); } + @Test + public void imports_type_casts_in_lambda() { + JavaClasses classes = new ClassFileImporter().importClasses(TypeCastFromLambda.class); + Set typeCasts = classes.get(TypeCastFromLambda.class).getTypeCasts(); + + assertThatTypeCasts(typeCasts).containTypeCasts( + typeCast(FilterInputStream.class, 10).declaredInLambda(), + typeCast(File.class, 18).declaredInLambda() + ); + } + private Condition syntheticLambdaMethods() { return new Condition<>(method -> isLambdaMethodName(method.getName()), "synthetic lambda methods"); } diff --git a/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterMembersTest.java b/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterMembersTest.java index 9661deb159..9a1a1a69c2 100644 --- a/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterMembersTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterMembersTest.java @@ -43,6 +43,11 @@ import com.tngtech.archunit.core.importer.testexamples.methodimport.ClassWithStringStringMethod; import com.tngtech.archunit.core.importer.testexamples.methodimport.ClassWithThrowingMethod; import com.tngtech.archunit.core.importer.testexamples.referencedclassobjects.ReferencingClassObjects; +import com.tngtech.archunit.core.importer.testexamples.typecast.ClassWithMultipleTypeCasts; +import com.tngtech.archunit.core.importer.testexamples.typecast.TypeCastInConstructor; +import com.tngtech.archunit.core.importer.testexamples.typecast.TypeCastInMethod; +import com.tngtech.archunit.core.importer.testexamples.typecast.TypeCastInStaticInitializer; +import com.tngtech.archunit.core.importer.testexamples.typecast.TypeCastTestedType; import com.tngtech.archunit.testutil.assertion.ReferencedClassObjectsAssertion.ExpectedReferencedClassObject; import org.assertj.core.util.Objects; import org.junit.Test; @@ -65,11 +70,13 @@ import static com.tngtech.archunit.testutil.Assertions.assertThatInstanceofChecks; import static com.tngtech.archunit.testutil.Assertions.assertThatReferencedClassObjects; import static com.tngtech.archunit.testutil.Assertions.assertThatThrowsClause; +import static com.tngtech.archunit.testutil.Assertions.assertThatTypeCasts; import static com.tngtech.archunit.testutil.Assertions.assertThatType; import static com.tngtech.archunit.testutil.Assertions.assertThatTypes; import static com.tngtech.archunit.testutil.ReflectionTestUtils.field; import static com.tngtech.archunit.testutil.assertion.InstanceofChecksAssertion.instanceofCheck; import static com.tngtech.archunit.testutil.assertion.ReferencedClassObjectsAssertion.referencedClassObject; +import static com.tngtech.archunit.testutil.assertion.TypeCastsAssertion.typeCast; import static java.util.stream.Collectors.toSet; public class ClassFileImporterMembersTest { @@ -307,6 +314,24 @@ public void imports_instanceof_checks() { instanceofCheck(InstanceofChecked.class, 15)); } + @Test + public void imports_type_casts() { + assertThatTypeCasts(new ClassFileImporter().importClass(TypeCastInConstructor.class).getConstructor(Object.class).getTypeCasts()) + .containTypeCasts(typeCast(TypeCastTestedType.class, 8)); + assertThatTypeCasts(new ClassFileImporter().importClass(TypeCastInMethod.class).getMethod("method", Object.class).getTypeCasts()) + .containTypeCasts(typeCast(TypeCastTestedType.class, 6)); + assertThatTypeCasts(new ClassFileImporter().importClass(TypeCastInStaticInitializer.class).getStaticInitializer().get().getTypeCasts()) + .containTypeCasts(typeCast(TypeCastTestedType.class, 7)); + assertThatTypeCasts(new ClassFileImporter().importClass(ClassWithMultipleTypeCasts.class).getTypeCasts()) + .hasSize(5) + .containTypeCasts( + typeCast(TypeCastTestedType.class, 9), + typeCast(TypeCastTestedType.class, 14), + typeCast(TypeCastTestedType.class, 26), + typeCast(TypeCastTestedType.class, 30), + typeCast(TypeCastTestedType.class, 36).declaredInLambda()); + } + @Test public void classes_know_which_fields_have_their_type() { JavaClasses classes = new ClassFileImporter().importClasses(SomeClass.class, OtherClass.class, SomeEnum.class); diff --git a/archunit/src/test/java/com/tngtech/archunit/core/importer/ImportTestUtils.java b/archunit/src/test/java/com/tngtech/archunit/core/importer/ImportTestUtils.java index 5a31d7f33a..610ea5885a 100644 --- a/archunit/src/test/java/com/tngtech/archunit/core/importer/ImportTestUtils.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/importer/ImportTestUtils.java @@ -36,6 +36,7 @@ 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.TypeCast; import com.tngtech.archunit.core.importer.DomainBuilders.BuilderWithBuildParameter; import com.tngtech.archunit.core.importer.DomainBuilders.FieldAccessTargetBuilder; import com.tngtech.archunit.core.importer.DomainBuilders.JavaAnnotationBuilder.ValueBuilder; @@ -441,6 +442,11 @@ public Set createInstanceofChecksFor(JavaCodeUnit codeUnit) { return Collections.emptySet(); } + @Override + public Set createTypeCastsFor(JavaCodeUnit codeUnit) { + return Collections.emptySet(); + } + @Override public JavaClass resolveClass(String fullyQualifiedClassName) { throw new UnsupportedOperationException("Override me where necessary"); diff --git a/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/typecast/ClassWithMultipleTypeCasts.java b/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/typecast/ClassWithMultipleTypeCasts.java new file mode 100644 index 0000000000..8db41ae940 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/typecast/ClassWithMultipleTypeCasts.java @@ -0,0 +1,73 @@ +package com.tngtech.archunit.core.importer.testexamples.typecast; + +import java.util.List; + + +@SuppressWarnings({"StatementWithEmptyBody", "unused", "ConstantConditions"}) +public class ClassWithMultipleTypeCasts { + private static final Object OBJ = new TypeCastTestedType(); + private static final TypeCastTestedType TARGET = (TypeCastTestedType) OBJ; + + private final TypeCastTestedType obj; + + ClassWithMultipleTypeCasts(Object o) { + this.obj = (TypeCastTestedType) o; + } + + TypeCastTestedType methodWithoutCast(TypeCastTestedType o) { + return o; + } + + TypeCastTestedType methodWithoutCast(Object o) { + return TypeCaster.cast(o); + } + + TypeCastTestedType methodWithExplicitCast(Object o) { + return (TypeCastTestedType) o; + } + + TypeCastTestedType methodWithImplicitCastUsingClassCast(Object o) { + return TypeCastTestedType.class.cast(o); + } + + TypeCastTestedType methodWithExplicitCastInLambdas(Object o) { + List objects = List.of(new TypeCastTestedType()); + + return objects.stream().map(obj -> (TypeCastTestedType) obj) + .findFirst() + .get(); + } + + TypeCastTestedType methodWithImplicitCastInLambdas(Object o) { + List objects = List.of(new TypeCastTestedType()); + + return objects.stream().map(TypeCastTestedType.class::cast) + .findFirst() + .get(); + } + + TypeCastTestedType methodWithImplicitCastDueToDelegation(Object o) { + TypeCaster caster = new TypeCaster<>(o, TypeCastTestedType.class); + TypeCastTestedType result = caster.cast(); + System.out.println(result); + + return result; + } + + private static class TypeCaster { + private T target; + + @SuppressWarnings("unchecked") + TypeCaster(Object target, Class type) { + this.target = (T) target; + } + + public T cast() { + return target; + } + + static TypeCastTestedType cast(Object o) { + return (TypeCastTestedType) o; + } + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/typecast/TypeCastFromLambda.java b/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/typecast/TypeCastFromLambda.java new file mode 100644 index 0000000000..0a42420852 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/typecast/TypeCastFromLambda.java @@ -0,0 +1,20 @@ +package com.tngtech.archunit.core.importer.testexamples.typecast; + +import java.io.File; +import java.io.FilterInputStream; +import java.util.function.Function; +import java.util.function.Supplier; + +public class TypeCastFromLambda { + Function reference() { + return (object) -> (FilterInputStream) object; + } + + Function referenceUsingMethodReference() { + return FilterInputStream.class::cast; // Not a cast, but a method reference which does a type cast + } + + Supplier> nestedReference() { + return () -> (object) -> (File) object; + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/typecast/TypeCastInConstructor.java b/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/typecast/TypeCastInConstructor.java new file mode 100644 index 0000000000..e64c4bbfaf --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/typecast/TypeCastInConstructor.java @@ -0,0 +1,10 @@ +package com.tngtech.archunit.core.importer.testexamples.typecast; + +public class TypeCastInConstructor { + @SuppressWarnings("unused") + private TypeCastTestedType value; + + public TypeCastInConstructor(Object param) { + value = (TypeCastTestedType) param; + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/typecast/TypeCastInMethod.java b/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/typecast/TypeCastInMethod.java new file mode 100644 index 0000000000..fa686cdde8 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/typecast/TypeCastInMethod.java @@ -0,0 +1,8 @@ +package com.tngtech.archunit.core.importer.testexamples.typecast; + +@SuppressWarnings("unused") +public class TypeCastInMethod { + TypeCastTestedType method(Object param) { + return (TypeCastTestedType) param; + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/typecast/TypeCastInStaticInitializer.java b/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/typecast/TypeCastInStaticInitializer.java new file mode 100644 index 0000000000..5a223f75d6 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/typecast/TypeCastInStaticInitializer.java @@ -0,0 +1,9 @@ +package com.tngtech.archunit.core.importer.testexamples.typecast; + +@SuppressWarnings({"unused", "ConstantConditions"}) +public class TypeCastInStaticInitializer { + static { + Object foo = new TypeCastTestedType(); + TypeCastTestedType bar = (TypeCastTestedType) foo; + } +} diff --git a/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/typecast/TypeCastTestedType.java b/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/typecast/TypeCastTestedType.java new file mode 100644 index 0000000000..3472abacaa --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/core/importer/testexamples/typecast/TypeCastTestedType.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.core.importer.testexamples.typecast; + +public class TypeCastTestedType { +} 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 a323933c1f..a69dc18779 100644 --- a/archunit/src/test/java/com/tngtech/archunit/testutil/Assertions.java +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/Assertions.java @@ -29,6 +29,7 @@ import com.tngtech.archunit.core.domain.ThrowsClause; import com.tngtech.archunit.core.domain.ThrowsDeclaration; import com.tngtech.archunit.core.domain.TryCatchBlock; +import com.tngtech.archunit.core.domain.TypeCast; import com.tngtech.archunit.lang.ArchCondition; import com.tngtech.archunit.lang.ArchRule; import com.tngtech.archunit.lang.ConditionEvents; @@ -65,6 +66,7 @@ import com.tngtech.archunit.testutil.assertion.ThrowsClauseAssertion; import com.tngtech.archunit.testutil.assertion.ThrowsDeclarationAssertion; import com.tngtech.archunit.testutil.assertion.TryCatchBlockAssertion; +import com.tngtech.archunit.testutil.assertion.TypeCastsAssertion; public class Assertions extends org.assertj.core.api.Assertions { public static ArchConditionAssertion assertThat(ArchCondition archCondition) { @@ -163,6 +165,10 @@ public static InstanceofChecksAssertion assertThatInstanceofChecks(Set typeCasts) { + return new TypeCastsAssertion(typeCasts); + } + public static JavaEnumConstantAssertion assertThat(JavaEnumConstant enumConstant) { return new JavaEnumConstantAssertion(enumConstant); } diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/TypeCastsAssertion.java b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/TypeCastsAssertion.java new file mode 100644 index 0000000000..1187406301 --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/testutil/assertion/TypeCastsAssertion.java @@ -0,0 +1,89 @@ +package com.tngtech.archunit.testutil.assertion; + +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; + +import com.google.common.collect.ImmutableSet; +import com.tngtech.archunit.core.domain.TypeCast; + +import org.assertj.core.api.AbstractIterableAssert; +import org.assertj.core.api.AbstractObjectAssert; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static java.util.stream.Collectors.toSet; +import static java.util.stream.StreamSupport.stream; +import static org.assertj.core.api.Assertions.assertThat; + +public class TypeCastsAssertion extends AbstractIterableAssert, TypeCast, TypeCastsAssertion.TypeCastAssertion> { + public TypeCastsAssertion(Set typeCasts) { + super(typeCasts, TypeCastsAssertion.class); + } + + @Override + protected TypeCastAssertion toAssert(TypeCast value, String description) { + return new TypeCastAssertion(value).as(description); + } + + @Override + protected TypeCastsAssertion newAbstractIterableAssert(Iterable iterable) { + return new TypeCastsAssertion(ImmutableSet.copyOf(iterable)); + } + + public void containTypeCasts(ExpectedTypeCast... expectedTypeCasts) { + containTypeCasts(List.of(expectedTypeCasts)); + } + + public void containTypeCasts(Iterable expectedTypeCasts) { + Set unmatchedClassObjects = stream(expectedTypeCasts.spliterator(), false) + .filter(expected -> actual.stream().noneMatch(expected)) + .collect(toSet()); + assertThat(unmatchedClassObjects).as("Type cast not contained in %s", actual).isEmpty(); + } + + static class TypeCastAssertion extends AbstractObjectAssert { + TypeCastAssertion(TypeCast typeCast) { + super(typeCast, TypeCastAssertion.class); + } + } + + public static ExpectedTypeCast typeCast(Class type, int lineNumber) { + return new ExpectedTypeCast(type, lineNumber); + } + + public static class ExpectedTypeCast implements Predicate { + private final Class type; + private final int lineNumber; + private final boolean declaredInLambda; + + private ExpectedTypeCast(Class type, int lineNumber) { + this(type, lineNumber, false); + } + + private ExpectedTypeCast(Class type, int lineNumber, boolean declaredInLambda) { + this.type = type; + this.lineNumber = lineNumber; + this.declaredInLambda = declaredInLambda; + } + + public ExpectedTypeCast declaredInLambda() { + return new ExpectedTypeCast(type, lineNumber, true); + } + + @Override + public boolean test(TypeCast input) { + return input.getRawType().isEquivalentTo(type) + && input.getLineNumber() == lineNumber + && input.isDeclaredInLambda() == declaredInLambda; + } + + @Override + public String toString() { + return toStringHelper(this) + .add("type", type) + .add("lineNumber", lineNumber) + .add("declaredInLambda", declaredInLambda) + .toString(); + } + } +}