How to implement switch exhaustiveness checker in Java 8?

Denis
5 min readJul 10, 2022

--

A few weeks ago I noticed that Kotlin has already supported exhaustive check for when expression. Besides that, I read https://openjdk.org/jeps/361, which introduces switch exhaustiveness check for switch expression (not statement) in Java. Unfortunately, these days I cannot use the latest Java versions. That’s why I decided to create something, which will check that I cover all case-branches for Enums in my switch cases. Of course, this solution should support Java 8.

TL;DR If you want to just use my solution, you could find it here https://github.com/Hixon10/switch-exhaustiveness-checker.

Initially, I decided to start with something simple. Let’s take a library, which could analyze class files bytecode, write code to check exhaustiveness, and execute it from gradle task, or maven plugin. It should be simple, right?

I started with well-known https://asm.ow2.io/, which allows to write kinda Visitor, which will be executed during a class file traversal. So, you just write needed code, and the library execute it with parsed AST of the file. To be honest, this library is super cool, and it is easy to start and write some checked code, even without reading docs.

This code snippet shows, how it is easy to start processing some class file with your visitor (CustomTraceClassVisitor):

InputStream classFileSteam = ...; // your class file 
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
CustomTraceClassVisitor classVisitor = new CustomTraceClassVisitor(null, new Textifier(), new PrintWriter(out, true));
new ClassReader(classFileSteam).accept(classVisitor, 0);
return out.toString("UTF-8").split("\n");
} catch (Exception e) {
// ...
}

In the example I just create an array for String, which contains bytecode instructions (e.g., TABLESWITCH, or LOOKUPSWITCH), but it shouldn’t be a problem to implement another logic in your Visitor.

Unfortunately, I haven’t succeeded in this solution. I found a problem. It turns out that javac (Java compiler) has two previously unknown for me features:

  1. Firstly, the compiler may insert fake switch-cases under specific conditions. https://github.com/openjdk/jdk/blob/master/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/SWITCH.java#L85 It means, that when we have final class-file, we cannot say for sure, whether this switch-case was written by user, or by compiler. But we need to know it, otherwise we cannot rise an error and ask a user to cover all cases. For example, these switch-cases have the same bytecode, but different java code.
public void example1(RoundingMode roundingMode) {
switch (roundingMode) {
case UP:
break;
case DOWN:
break;
case CEILING:
break;
case FLOOR:
break;
case HALF_UP:
System.out.println("Q");
break;
case HALF_DOWN:
break;
case HALF_EVEN:
case UNNECESSARY:
default:
System.out.println("Default");
break;
}
}

public void example2(RoundingMode roundingMode) {
switch (roundingMode) {
case UP:
break;
case DOWN:
break;
case CEILING:
break;
case FLOOR:
break;
case HALF_UP:
System.out.println("Q");
break;
case HALF_DOWN:
break;
case UNNECESSARY:
default:
System.out.println("Default");
break;
}
}

You can see, that that HALF_EVEN is not covered in the second method, but still we have the same bytecode, because javac inserted fake-branch.

2. Javac uses two different bytecode instructions for java switch statement. It could be either TABLESWITCH, or LOOKUPSWITCH.

So, at that moment I decided to try something else, instead of asm.ow2.io lib. I started creating an annotation processor, because it was obvious choice as second try. I thought that it should be pretty easy. Everything what I need is kinda AST with proper API to analyze it. I have it out of the box in case of annotation processors.

Unfortunately, I didn’t know one thing regarding this approach. There is the following lines in documentation:

Annotation processing occurs at a specific point in the timeline of a compilation, after all source files and classes specified on the command line have been read, and analyzed for the types and members they contain, but before the contents of any method bodies have been analyzed.

From the first glance, it is not obvious, why it matters for me. However, I cannot get Enum types, because an annotation processor executes before the contents of any method bodies have been analyzed. Let’s consider the following example:

switch (roundingMode) {
///
}

In this case, I got roundingMode in my annotation processor, but TreePath.getPath(compilationUnit, switchTreeExpression) returns null. So, I had two different options. I had to either resolve type by myself, or find some another solution. I understand that resolve type for some string problem is orders of magnitude more difficult, than my task, that’s why I decided to move to another solution.

Unfortunately, I haven’t found any solution, and decided to ask help on https://stackoverflow.com/q/72765952/1756750. Thanks to Sergey, I found out that you could register TaskListener, which will be executed when there is type information about methods bodies:

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
/// ....
JavacTask.instance(processingEnv).addTaskListener(analyzeTaskListener);
}

This block of code is essential of my switch checker. When I receive event, that some method has been successfully compiled and it is ready for analysis, I just check needed things to verify, that every switch-case are covered:

private void processCurrentSwitchStatement(SwitchTree statementTree, Name currentMethodName) {
// ...
Set<String> enumValues = getEnumValuesForGivenEnum(enumElement);
Set<String> nonDefaultCoveredEnums = getNonDefaultCoveredSwitchCases(statementTree);

if (nonDefaultCoveredEnums.size() != enumValues.size()) {
// ....
messager.printMessage(Diagnostic.Kind.ERROR, "Switch branches: [" + nonCoveredEnumsAsStr + "] in class: [" + classForAnalysis + "], " + methodOrConstructorStr + ": [" + methodOrConstructorNameAsString + "] are not covered", trees.getElement(treePath));
}
}

During development of the annotation processor, I had a few more funny problems, which I had to tackle. Firstly, it is inner classes. Unfortunately, I receive events in my Javac TaskListener only for top-level classes. I tried to google it, and read a code of the compiler, but I haven’t found any specific reason, why it happens. Because of this, I have to implement some amount of workarounds to support inner classes. Right now, it works correctly, at least for all test cases, which I came up.

The second problem is connected with Lombok. It turns out, that if you use Lombok, which is also implemented as an annotation processor, you need explicitly include both Lombok and my annotation processor. Otherwise, maven will execute only one of them. Apparently, it is well known problem, and I found a solution in 5 minutes, but still.

The last thing is connected with integration tests (I wanted to be sure, that my annotation processor works fine with at least Spring framework, gradle, maven and Java 8, 11, 17). Initially, I decided to use testcontainers, where I can start compilation process for different test-projects and verify results. But at some point I decided that I have already had some “VM”, provided by github actions infrastructure. So I just decided to use them as runners, even for testing compilation errors. For example, this yaml tried to compile a test project, and verifies, that the build has failed:

- name: Run integration test - Java 11 + Spring + Gradle + Should Fail
run: cd ./integration-tests/java11-gradle-spring-fail && ./gradlew clean test -i
id: java11-gradle-spring-fail
continue-on-error: true
- name: Check on failures
if: steps.java11-gradle-spring-fail.outcome != 'failure'
run: exit 1

Anyway, I’ve finished with the first version of annotation processor. You can find examples, how to start using it at https://github.com/Hixon10/switch-exhaustiveness-checker.

If you found any bugs, or have feature requests, please let me know, and I will try to fix it.

--

--

Denis

Someone who prefers to write more of $ apt-get install libstdc++6 > /dev/null and not much of showResults(getUserList())