'ArchUnit: How to check if method throwing an exception also declares it
I'm trying to write an ArchUnit test which tests if a certain unchecked exception when thrown is also declared by that method, and recursively, if any method calling that method also declares it.
The intention is to ensure these exceptions are documented everywhere, and, since I can enforce anything that is declared to be thrown has to be documented (other tools can enforce this), this seemed a good way to go about it.
The scope of these checks can be limited to a certain package; ie. treat runtime exception X when used within package Y as a "checked" exception, and enforce it with a rule.
Passing code:
void someMethod() throws MyRunTimeException {
throws new MyRunTimeException();
}
void anotherMethod() throws MyRunTimeException {
someMethod();
}
Failing code:
void someMethod() { // doesn't declare exception
throws new MyRunTimeException();
}
void anotherMethod() { // should declare exception when someMethod declares it
someMethod();
}
Now I think I can detect methods that do not declare the exception as follows:
noMethods().should().declareThrowableOfType(MyRunTimeException.class)
And I think I can detect calls to create this exception (even better would be when it is actually thrown, but I couldn't find that):
noClasses().should().callConstructorWhere(
target(is(declaredIn(MyRunTimeException.class)))
.and(not(originOwner(is(assignableTo(MyRunTimeException.class)))))
);
... but I see no way how I could combine these two rules.
For some reason however ArchUnit only allows checking calls from classes, but not from methods (which seems to make more sense). In other words, I couldn't find a way to check calls given a method:
noMethods().should().callConstructorWhere( ... )
Or:
noMethods().should().throw(MyRuntimeException.class)
.and(not(declareThrowableOfType(MyRunTimeException.class)))
Any idea how I could go about enforcing such a rule?
Solution 1:[1]
You're right that the fluent MethodsShould API (as of ArchUnit 0.23.1) does not seem to support method calls, but as the information is present in the domain objects (see JavaCodeUnit, e.g. getMethodCallsFromSelf, getConstructorCallsFromSelf, or more generally getAccessesFromSelf), you can always implement a custom ArchCondition.
With
import static com.tngtech.archunit.base.DescribedPredicate.doNot;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.assignableTo;
import static com.tngtech.archunit.core.domain.properties.HasOwner.Predicates.With.owner;
import static com.tngtech.archunit.lang.conditions.ArchPredicates.have;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.codeUnits;
import static java.util.stream.Collectors.toSet;
I'd use the following tests:
@ArchTest
ArchRule codeUnits_should_declare_all_RuntimeExceptions_they_throw = codeUnits()
.that(doNot(have(owner(assignableTo(RuntimeException.class)))))
.should(new ArchCondition<JavaCodeUnit>("declare all RuntimeExceptions they throw") {
@Override
public void check(JavaCodeUnit codeUnit, ConditionEvents events) {
// TODO: ArchUnit 0.23.1 might not have an API to get exceptions actually thrown.
// As a first approximation, the following code collects all RuntimeExceptions that are instantiated
// – which has false positives (exceptions that are instantiated, but not thrown),
// as well as false negatives (exceptions that are created via factory methods and thrown).
// Accounting for the false negatives in the same way as here is left as an exercise for the interested reader.
Set<JavaClass> instantiatedRuntimeExceptions = codeUnit.getConstructorCallsFromSelf().stream()
.map(JavaAccess::getTargetOwner)
.filter(targetClass -> targetClass.isAssignableTo(RuntimeException.class))
.collect(toSet());
boolean satisfied = codeUnit.getExceptionTypes().containsAll(instantiatedRuntimeExceptions);
String message = String.format("%s does%s declare all RuntimeExceptions it instantiates in %s",
codeUnit.getDescription(), satisfied ? "" : " not", codeUnit.getSourceCodeLocation());
events.add(new SimpleConditionEvent(codeUnit, satisfied, message));
}
});
@ArchTest
ArchRule codeUnits_should_declare_all_RuntimeExceptions_of_methods_they_call = codeUnits()
.should(new ArchCondition<JavaCodeUnit>("declare all RuntimeExceptions of methods they call") {
@Override
public void check(JavaCodeUnit codeUnit, ConditionEvents events) {
Set<JavaClass> runtimeExceptionsDeclaredByCalledMethods = codeUnit.getMethodCallsFromSelf().stream()
.map(JavaAccess::getTarget)
.map(MethodCallTarget::resolveMember)
.filter(Optional::isPresent)
.map(Optional::get)
.flatMap(method -> method.getExceptionTypes().stream())
.filter(exceptionType -> exceptionType.isAssignableTo(RuntimeException.class))
.collect(toSet());
boolean satisfied = codeUnit.getExceptionTypes().containsAll(runtimeExceptionsDeclaredByCalledMethods);
String message = String.format("%s does%s declare all RuntimeExceptions of methods they call declare in %s",
codeUnit.getDescription(), satisfied ? "" : " not", codeUnit.getSourceCodeLocation());
events.add(new SimpleConditionEvent(codeUnit, satisfied, message));
}
});
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Manfred |
