Skip to content

Commit d25c629

Browse files
committed
add support for dependencies from caught exceptions
Referencing an exception class in the `catch` clause of a try-catch block can certainly be considered a dependency on that class, similar to a `throws` clause or an `instanceof` check: if the referenced class is renamed or deleted, the dependent code will break. To prevent this from happening, one could write architecture rules that e.g. limit the use of a certain third-party library to a specific layer, or allow-list only certain "stable" or "vetted" classes. Unfortunately, such rules will not notice when exception classes are referenced in `catch` clauses of try-catch blocks. The reason is that so far, ArchUnit does not generate a `Dependency` for them (it only does that once a member of the exception class is used, e.g. calling a method). This behavior defeats the purpose of implementing such a rule in the first place. Thus, we change ArchUnit so that it considers each exception type referenced in `catch` clauses of try-catch blocks to be a dependency on that type. As the information is already available in the model via `TryCatchBlock.caughtThrowables`, all it takes is wiring it up to creation and handling of `Dependency` instances. Resolves: #1232 Signed-off-by: Jens Bannmann <jens.b@web.de>
1 parent 72d10e9 commit d25c629

24 files changed

+452
-64
lines changed

archunit/src/main/java/com/tngtech/archunit/core/domain/Dependency.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.Objects;
2020
import java.util.Optional;
2121
import java.util.Set;
22+
import java.util.stream.Collectors;
2223

2324
import com.google.common.base.MoreObjects;
2425
import com.google.common.collect.ComparisonChain;
@@ -51,6 +52,7 @@
5152
* <li>a class (or method/constructor of this class) declares a type parameter referencing another class</li>
5253
* <li>a class (or method/constructor of this class) is annotated with an annotation of a certain type or referencing another class as annotation parameter</li>
5354
* <li>a method/constructor of a class references another class in a throws declaration</li>
55+
* <li>a class references another class in a {@code catch} clause</li>
5456
* <li>a class references another class object (e.g. {@code Example.class})</li>
5557
* <li>a class references another class in an {@code instanceof} check</li>
5658
* </ul>
@@ -125,6 +127,12 @@ static Set<Dependency> tryCreateFromThrowsDeclaration(ThrowsDeclaration<? extend
125127
return tryCreateDependency(declaration.getLocation(), "throws type", declaration.getRawType());
126128
}
127129

130+
static Set<Dependency> tryCreateFromTryCatchBlock(TryCatchBlock tryCatchBlock) {
131+
return tryCatchBlock.getCaughtThrowables().stream()
132+
.flatMap(caughtThrowable -> tryCreateDependency(tryCatchBlock.getOwner(), "catches type", caughtThrowable, tryCatchBlock.getSourceCodeLocation()).stream())
133+
.collect(Collectors.toSet());
134+
}
135+
128136
static Set<Dependency> tryCreateFromInstanceofCheck(InstanceofCheck instanceofCheck) {
129137
return tryCreateDependency(
130138
instanceofCheck.getOwner(), "checks instanceof",
@@ -269,6 +277,10 @@ public String getDescription() {
269277
return description;
270278
}
271279

280+
/**
281+
* @implNote For dependencies created by {@code catch} clauses, the line number reported <i>currently</i> refers to
282+
* the first access in the {@code try} block, not to the {@code catch} clause.
283+
*/
272284
@Override
273285
@PublicAPI(usage = ACCESS)
274286
public SourceCodeLocation getSourceCodeLocation() {

archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,11 @@ public Set<InstanceofCheck> getInstanceofChecks() {
646646
return members.getInstanceofChecks();
647647
}
648648

649+
@PublicAPI(usage = ACCESS)
650+
public Set<TryCatchBlock> getTryCatchBlocks() {
651+
return members.getTryCatchBlocks();
652+
}
653+
649654
@PublicAPI(usage = ACCESS)
650655
public Set<ReferencedClassObject> getReferencedClassObjects() {
651656
return members.getReferencedClassObjects();
@@ -1310,6 +1315,14 @@ public Set<ThrowsDeclaration<JavaMethod>> getMethodThrowsDeclarationsWithTypeOfS
13101315
return reverseDependencies.getMethodThrowsDeclarationsWithTypeOf(this);
13111316
}
13121317

1318+
/**
1319+
* @return {@link TryCatchBlock TryCatchBlocks} of all imported classes that declare to catch this class.
1320+
*/
1321+
@PublicAPI(usage = ACCESS)
1322+
public Set<TryCatchBlock> getTryCatchBlocksThatCatchSelf() {
1323+
return reverseDependencies.getTryCatchBlocksThatCatch(this);
1324+
}
1325+
13131326
/**
13141327
* @return Constructors of all imported classes that have a parameter type of this class.
13151328
*/

archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDependencies.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ private Supplier<Set<Dependency>> createDirectDependenciesFromClassSupplier() {
4747
returnTypeDependenciesFromSelf(),
4848
codeUnitParameterDependenciesFromSelf(),
4949
throwsDeclarationDependenciesFromSelf(),
50+
tryCatchBlockDependenciesFromSelf(),
5051
annotationDependenciesFromSelf(),
5152
instanceofCheckDependenciesFromSelf(),
5253
referencedClassObjectDependenciesFromSelf(),
@@ -166,6 +167,11 @@ private Stream<Dependency> throwsDeclarationDependenciesFromSelf() {
166167
.flatMap(throwsDeclaration -> Dependency.tryCreateFromThrowsDeclaration(throwsDeclaration).stream());
167168
}
168169

170+
private Stream<Dependency> tryCatchBlockDependenciesFromSelf() {
171+
return javaClass.getTryCatchBlocks().stream()
172+
.flatMap(tryCatchBlock -> Dependency.tryCreateFromTryCatchBlock(tryCatchBlock).stream());
173+
}
174+
169175
private Stream<Dependency> annotationDependenciesFromSelf() {
170176
return Streams.concat(
171177
annotationDependencies(javaClass),

archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassMembers.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,14 @@ Set<InstanceofCheck> getInstanceofChecks() {
191191
return result.build();
192192
}
193193

194+
Set<TryCatchBlock> getTryCatchBlocks() {
195+
ImmutableSet.Builder<TryCatchBlock> result = ImmutableSet.builder();
196+
for (JavaCodeUnit codeUnit : codeUnits) {
197+
result.addAll(codeUnit.getTryCatchBlocks());
198+
}
199+
return result.build();
200+
}
201+
194202
Set<ReferencedClassObject> getReferencedClassObjects() {
195203
ImmutableSet.Builder<ReferencedClassObject> result = ImmutableSet.builder();
196204
for (JavaCodeUnit codeUnit : codeUnits) {

archunit/src/main/java/com/tngtech/archunit/core/domain/ReverseDependencies.java

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ final class ReverseDependencies {
4141
private final SetMultimap<JavaClass, JavaMethod> methodParameterTypeDependencies;
4242
private final SetMultimap<JavaClass, JavaMethod> methodReturnTypeDependencies;
4343
private final SetMultimap<JavaClass, ThrowsDeclaration<JavaMethod>> methodsThrowsDeclarationDependencies;
44+
private final SetMultimap<JavaClass, TryCatchBlock> tryCatchBlockDependencies;
4445
private final SetMultimap<JavaClass, JavaConstructor> constructorParameterTypeDependencies;
4546
private final SetMultimap<JavaClass, ThrowsDeclaration<JavaConstructor>> constructorThrowsDeclarationDependencies;
4647
private final SetMultimap<JavaClass, JavaAnnotation<?>> annotationTypeDependencies;
@@ -58,6 +59,7 @@ private ReverseDependencies(ReverseDependencies.Creation creation) {
5859
this.methodParameterTypeDependencies = creation.methodParameterTypeDependencies.build();
5960
this.methodReturnTypeDependencies = creation.methodReturnTypeDependencies.build();
6061
this.methodsThrowsDeclarationDependencies = creation.methodsThrowsDeclarationDependencies.build();
62+
this.tryCatchBlockDependencies = creation.tryCatchBlockDependencies.build();
6163
this.constructorParameterTypeDependencies = creation.constructorParameterTypeDependencies.build();
6264
this.constructorThrowsDeclarationDependencies = creation.constructorThrowsDeclarationDependencies.build();
6365
this.annotationTypeDependencies = creation.annotationTypeDependencies.build();
@@ -114,6 +116,10 @@ Set<ThrowsDeclaration<JavaMethod>> getMethodThrowsDeclarationsWithTypeOf(JavaCla
114116
return methodsThrowsDeclarationDependencies.get(clazz);
115117
}
116118

119+
Set<TryCatchBlock> getTryCatchBlocksThatCatch(JavaClass clazz) {
120+
return tryCatchBlockDependencies.get(clazz);
121+
}
122+
117123
Set<JavaConstructor> getConstructorsWithParameterTypeOf(JavaClass clazz) {
118124
return constructorParameterTypeDependencies.get(clazz);
119125
}
@@ -150,6 +156,7 @@ static class Creation {
150156
private final ImmutableSetMultimap.Builder<JavaClass, JavaMethod> methodParameterTypeDependencies = ImmutableSetMultimap.builder();
151157
private final ImmutableSetMultimap.Builder<JavaClass, JavaMethod> methodReturnTypeDependencies = ImmutableSetMultimap.builder();
152158
private final ImmutableSetMultimap.Builder<JavaClass, ThrowsDeclaration<JavaMethod>> methodsThrowsDeclarationDependencies = ImmutableSetMultimap.builder();
159+
private final ImmutableSetMultimap.Builder<JavaClass, TryCatchBlock> tryCatchBlockDependencies = ImmutableSetMultimap.builder();
153160
private final ImmutableSetMultimap.Builder<JavaClass, JavaConstructor> constructorParameterTypeDependencies = ImmutableSetMultimap.builder();
154161
private final ImmutableSetMultimap.Builder<JavaClass, ThrowsDeclaration<JavaConstructor>> constructorThrowsDeclarationDependencies = ImmutableSetMultimap.builder();
155162
private final ImmutableSetMultimap.Builder<JavaClass, JavaAnnotation<?>> annotationTypeDependencies = ImmutableSetMultimap.builder();
@@ -200,6 +207,11 @@ private void registerMethods(JavaClass clazz) {
200207
for (ThrowsDeclaration<JavaMethod> throwsDeclaration : method.getThrowsClause()) {
201208
methodsThrowsDeclarationDependencies.put(throwsDeclaration.getRawType(), throwsDeclaration);
202209
}
210+
for (TryCatchBlock tryCatchBlock : method.getTryCatchBlocks()) {
211+
for (JavaClass caughtThrowable : tryCatchBlock.getCaughtThrowables()) {
212+
tryCatchBlockDependencies.put(caughtThrowable.toErasure(), tryCatchBlock);
213+
}
214+
}
203215
for (InstanceofCheck instanceofCheck : method.getInstanceofChecks()) {
204216
instanceofCheckDependencies.put(instanceofCheck.getRawType(), instanceofCheck);
205217
}
@@ -214,6 +226,11 @@ private void registerConstructors(JavaClass clazz) {
214226
for (ThrowsDeclaration<JavaConstructor> throwsDeclaration : constructor.getThrowsClause()) {
215227
constructorThrowsDeclarationDependencies.put(throwsDeclaration.getRawType(), throwsDeclaration);
216228
}
229+
for (TryCatchBlock tryCatchBlock : constructor.getTryCatchBlocks()) {
230+
for (JavaClass caughtThrowable : tryCatchBlock.getCaughtThrowables()) {
231+
tryCatchBlockDependencies.put(caughtThrowable.toErasure(), tryCatchBlock);
232+
}
233+
}
217234
for (InstanceofCheck instanceofCheck : constructor.getInstanceofChecks()) {
218235
instanceofCheckDependencies.put(instanceofCheck.getRawType(), instanceofCheck);
219236
}
@@ -252,11 +269,16 @@ private Set<JavaAnnotation<?>> findAnnotations(JavaClass clazz) {
252269
}
253270

254271
private void registerStaticInitializer(JavaClass clazz) {
255-
if (clazz.getStaticInitializer().isPresent()) {
256-
for (InstanceofCheck instanceofCheck : clazz.getStaticInitializer().get().getInstanceofChecks()) {
272+
clazz.getStaticInitializer().ifPresent(staticInitializer -> {
273+
for (TryCatchBlock tryCatchBlock : staticInitializer.getTryCatchBlocks()) {
274+
for (JavaClass caughtThrowable : tryCatchBlock.getCaughtThrowables()) {
275+
tryCatchBlockDependencies.put(caughtThrowable.toErasure(), tryCatchBlock);
276+
}
277+
}
278+
for (InstanceofCheck instanceofCheck : staticInitializer.getInstanceofChecks()) {
257279
instanceofCheckDependencies.put(instanceofCheck.getRawType(), instanceofCheck);
258280
}
259-
}
281+
});
260282
}
261283

262284
void finish(Iterable<JavaClass> classes) {

archunit/src/test/java/com/tngtech/archunit/core/domain/DependencyTest.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import com.google.common.base.MoreObjects;
1414
import com.tngtech.archunit.base.DescribedPredicate;
1515
import com.tngtech.archunit.core.domain.testobjects.ClassWithArrayDependencies;
16+
import com.tngtech.archunit.core.domain.testobjects.ClassWithDependencyOnCaughtException;
1617
import com.tngtech.archunit.core.domain.testobjects.ClassWithDependencyOnInstanceofCheck;
1718
import com.tngtech.archunit.core.domain.testobjects.ClassWithDependencyOnInstanceofCheck.InstanceOfCheckTarget;
1819
import com.tngtech.archunit.core.domain.testobjects.DependenciesOnClassObjects;
@@ -200,6 +201,67 @@ public void Dependency_from_throws_declaration() {
200201
.contains("Method <" + origin.getFullName() + "> throws type <" + IOException.class.getName() + ">");
201202
}
202203

204+
@DataProvider
205+
public static Object[][] with_try_catch_block_members() {
206+
JavaClass javaClass = importClassesWithContext(ClassWithDependencyOnCaughtException.class, IOException.class)
207+
.get(ClassWithDependencyOnCaughtException.class);
208+
209+
return $$(
210+
$(javaClass.getStaticInitializer().get(), 9),
211+
$(javaClass.getConstructor(), 16),
212+
$(javaClass.getMethod("simpleCatchClauseMethod"), 23)
213+
);
214+
}
215+
216+
@Test
217+
@UseDataProvider("with_try_catch_block_members")
218+
public void Dependency_from_simple_catch_clause(JavaCodeUnit memberWithTryCatchBlock, int expectedLineNumber) {
219+
TryCatchBlock tryCatchBlock = getOnlyElement(memberWithTryCatchBlock.getTryCatchBlocks());
220+
221+
Dependency dependency = getOnlyElement(Dependency.tryCreateFromTryCatchBlock(tryCatchBlock));
222+
223+
Assertions.assertThatDependency(dependency)
224+
.satisfies(catchesType(IOException.class, memberWithTryCatchBlock, expectedLineNumber, ClassWithDependencyOnCaughtException.class));
225+
}
226+
227+
private static Consumer<Dependency> catchesType(Class<? extends Throwable> targetClass, JavaCodeUnit javaCodeUnit, int expectedLineNumber, Class<?> originClass) {
228+
return dependency -> Assertions.assertThatDependency(dependency)
229+
.matches(originClass, targetClass)
230+
.hasDescription(javaCodeUnit.getFullName(), "catches type", targetClass.getName())
231+
.inLocation(originClass, expectedLineNumber);
232+
}
233+
234+
@Test
235+
public void Dependency_from_union_catch_clause() {
236+
JavaMethod method = importClassesWithContext(ClassWithDependencyOnCaughtException.class, IllegalStateException.class, IOException.class)
237+
.get(ClassWithDependencyOnCaughtException.class)
238+
.getMethod("unionCatchClauseMethod");
239+
TryCatchBlock tryCatchBlock = getOnlyElement(method.getTryCatchBlocks());
240+
241+
Set<Dependency> dependencies = Dependency.tryCreateFromTryCatchBlock(tryCatchBlock);
242+
243+
Assertions.assertThatDependencies(dependencies).satisfiesExactlyInAnyOrder(
244+
catchesType(IllegalStateException.class, method, 30, ClassWithDependencyOnCaughtException.class),
245+
catchesType(IOException.class, method, 30, ClassWithDependencyOnCaughtException.class)
246+
);
247+
}
248+
249+
@Test
250+
public void Dependency_from_multiple_catch_clauses() {
251+
JavaMethod method = importClassesWithContext(ClassWithDependencyOnCaughtException.class, IllegalStateException.class, IOException.class)
252+
.get(ClassWithDependencyOnCaughtException.class)
253+
.getMethod("multipleCatchClausesMethod");
254+
TryCatchBlock tryCatchBlock = getOnlyElement(method.getTryCatchBlocks());
255+
256+
Set<Dependency> dependencies = Dependency.tryCreateFromTryCatchBlock(tryCatchBlock);
257+
258+
Assertions.assertThatDependencies(dependencies).satisfiesExactlyInAnyOrder(
259+
catchesType(IllegalStateException.class, method, 37, ClassWithDependencyOnCaughtException.class),
260+
catchesType(RuntimeException.class, method, 37, ClassWithDependencyOnCaughtException.class),
261+
catchesType(IOException.class, method, 37, ClassWithDependencyOnCaughtException.class)
262+
);
263+
}
264+
203265
@DataProvider
204266
public static Object[][] with_instanceof_check_members() {
205267
JavaClass javaClass = importClassesWithContext(ClassWithDependencyOnInstanceofCheck.class, InstanceOfCheckTarget.class)

0 commit comments

Comments
 (0)