Проверяем Switch Exhaustiveness в Java 8

Привет, друзья. Решил написать пару слов про своей текущий пет проект, в котором я реализую возможность проверки, что все ветки switch выражения для Enum покрыты. Не рокет саенс, но может быть полезно.

Не буду тянуть. Если хотите поглядеть на проект, то он тут — https://github.com/Hixon10/switch-exhaustiveness-checker.

История возникновения проекта довольно простая. В текущее время все современные языки стали завозить что-то, что позволяет проверять покрытость бранчей свитча (или аналога). Например, в одной из последних версий это появилось в Kotlin для when. В последних версиях Java это тоже есть в виде https://openjdk.org/jeps/361, правда это доступно только для новой формы свитчей. Со старыми работать не будет.

Мне стало завидно, что все могут надеяться на компилятор, который поможет не забыть написать код для всех енум значений, а я, кто не может использовать Java 17 в данную секунду, должен мучаться, терпеть, и надеяться только на себя (и свои тесты). так и родился этот проектик. Тут буквально 50 строк кода, один простой процессор аннотаций.

Интересный факт. Сначала я хотел сделать проект в виде gradle плагина, и просто анализировать class-файлы с помощью https://asm.ow2.io/. Всё работало хорошо, пока я не придумал один интересный пример:

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;
    }
}

Эти два разных метода компилируются в один и тот же байткод. Хотя, казалось бы, во втором методе нет case HALF_EVEN. Оказалось (я этого не знал), в байткоде свитч может быть представлен двумя разными инструкциями LookUpSwitch и TableSwitch. Так вот, если используется TableSwitch, то компилятор в некоторых случаях генерирует фейковые пустые бранчи — https://github.com/openjdk/jdk/blob/master/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/SWITCH.java#L85

for (int j = 1; j < gap; j++) {
    m_vec[count] = prev + j;
    t_vec[count] = target;
    count++;
}

Соответственно, на уровня анализа байткода классфайлов нет возможности узнать, покрыл ли пользователь все ветки свитча, или это фейковый бранч. Поэтому, пришлось выкинуть первое решение, и заиспользовать процессор аннотаций. К счастью, он выполняется до вставки фейковых бранчей компилятором, поэтому можно понять, какие ветки свитча покрыты.

В общем, это был хоть и маленький, но очень фановый проект. Если кто начнет пользоваться им и найдет баги - не стеснятйтесь - файлите их, я поправлю. Фича реквесты тоже велком.

Категории: Программирование