An annotation and Kotlin compiler plugin for enforcing a when statement is exhaustive

Related tags

Kotlin exhaustive
Overview

Exhaustive

An annotation and Kotlin compiler plugin for enforcing a when statement is exhaustive.

Note: This plugin is reaching its end-of-life and is soft deprecated. Learn more.

println("black") } } ">
enum class RouletteColor { Red, Black, Green }

fun printColor(color: RouletteColor) {
  @Exhaustive
  when (color) {
    Red -> println("red")
    Black -> println("black")
  }
}
e: Example.kt:5: @Exhaustive when is not exhaustive!

Missing branches:
- RouletteColor.Green

No more assigning to dummy local properties or referencing pointless functions or properties to force the when to be an expression for exhaustiveness checking. The plugin reuses the same check that is used for a when expression.

In addition to being forced to be exhaustive, an annotated when statement is forbidden from using an else branch.

println("black") else -> println("green") } } ">
fun printColor(color: RouletteColor) {
  @Exhaustive
  when (color) {
    Red -> println("red")
    Black -> println("black")
    else -> println("green")
  }
}
e: Example.kt:5: @Exhaustive when must not contain an 'else' branch

The presence of an else block indicates support for a default action. The exhaustive check would otherwise always pass with this branch which is why it is disallowed.

Sealed classes are also supported.

println("black") } } ">
sealed class RouletteColor {
  object Red : RouletteColor()
  object Black : RouletteColor()
  object Green : RouletteColor()
}

fun printColor(color: RouletteColor) {
  @Exhaustive
  when (color) {
    RouletteColor.Red -> println("red")
    RouletteColor.Black -> println("black")
  }
}
e: Example.kt:9: @Exhaustive when is not exhaustive!

Missing branches:
- RouletteColor.Green

Soft Deprecated

We did it! Kotlin 1.7 will make all when statements exhaustive by default (where possible) and this plugin will no longer be required.

In order to change the language behavior, Kotlin 1.6 will first warn when a when statement is not exhaustive. Kotlin 1.5.30 ships with this warning as opt-in behavior by setting your languageVersion to 1.6.

kotlin {
  sourceSets.all {
    languageSettings {
      languageVersion = '1.6'
    }
  }
}

If you want to opt-in to errors instead of warnings, enable progressive mode.

kotlin {
  sourceSets.all {
    languageSettings {
      languageVersion = '1.6'
      progressiveMode = true
    }
  }
}

This plugin will continue to be maintained until Kotlin 1.7.0 is released for those who cannot enable these in their builds.

Usage

buildscript {
  dependencies {
    classpath 'app.cash.exhaustive:exhaustive-gradle:0.2.0'
  }
  repositories {
    mavenCentral()
  }
}

apply plugin: 'org.jetbrains.kotlin.jvm' // or .android or .multiplatform or .js
apply plugin: 'app.cash.exhaustive'

The @Exhaustive annotation will be made available in your main and test source sets but will not be shipped as a dependency of the module.

Since Kotlin compiler plugins are an unstable API, certain versions of Exhaustive only work with certain versions of Kotlin.

Kotlin Exhaustive
1.4.10 - 1.5.10 0.1.1
1.5.20 - 1.5.30 0.2.0

Versions of Kotlin older than 1.4.10 are not supported. Versions newer than those listed may be supported but are untested.

Snapshots of the development version are available in Sonatype's snapshots repository.

buildscript {
  dependencies {
    classpath 'app.cash.exhaustive:exhaustive-gradle:0.3.0-SNAPSHOT'
  }
  repositories {
    maven {
      url 'https://oss.sonatype.org/content/repositories/snapshots/'
    }
  }
}

// 'apply' same as above

Alternatives Considered

In the weeks prior to building this project a set of alternatives were explored and rejected for various reasons. They are listed below. If you evaluate their merits differently, you are welcome to use them instead. The solution provided by this plugin is not perfect either.

Unused local and warning suppression

println("red") RouletteColor.Black -> println("black") } } ">
fun printColor(color: RouletteColor) {
  @Suppress("UNUSED_VARIABLE")
  val exhaustive = when (color) {
    RouletteColor.Red -> println("red")
    RouletteColor.Black -> println("black")
  }
}

Pros:

  • Works everywhere without library or plugin
  • No overhead or impact on compiled code

Neutral:

  • Somewhat self-describing as to the intent, assuming good local property names
  • Good locality as the exhaustiveness forcing is very close to the when keyword, although somewhat overshadowed by the warning suppression

Cons:

  • Requires suppression of warning which need to be put into a shared template or requires alt+enter,enter-ing to create the final form
  • Requires the use of unique local property names (_ is not allowed here)

Built-in trailing property or function call

println("black") }.javaClass // or .hashCode() or anything else... } ">
fun printColor(color: RouletteColor) {
  when (color) {
    RouletteColor.Red -> println("red")
    RouletteColor.Black -> println("black")
  }.javaClass // or .hashCode() or anything else...
}

Pros:

  • Works everywhere without library or plugin

Cons:

  • Not self-describing as to the effect on the when and the developer intent behind adding it
  • Poor locality as the property is far away from the when keyword it modifies
  • Impact on compiled code in the form of a property call, function call, and/or additional instructions at the call-site

Library trailing property

println("red") RouletteColor.Black -> println("black") }.exhaustive } ">
@Suppress("unused") // Receiver reference forces when into expression form.
inline val Any?.exhaustive get() = Unit

fun printColor(color: RouletteColor) {
  when (color) {
    RouletteColor.Red -> println("red")
    RouletteColor.Black -> println("black")
  }.exhaustive
}

Pros:

  • Self-describing effect on the when
  • No impact on compiled code

Cons:

  • Requires a library
  • Poor locality as the property is far away from the when keyword it modifies
  • Pollutes the extension namespace by showing up for everything, not just when

Library leading expression

println("red") RouletteColor.Black -> println("black") } } ">
@Suppress("NOTHING_TO_INLINE", "ClassName", "UNUSED_PARAMETER") // Faking a soft keyword.
object exhaustive {
  inline operator fun minus(other: Any?) = Unit
}

fun printColor(color: RouletteColor) {
  exhaustive-when (color) {
    RouletteColor.Red -> println("red")
    RouletteColor.Black -> println("black")
  }
}

Pro:

  • Great locality as the syntactical trick appears almost like a soft keyword modifying the when

Neutral:

  • Slight impact on compiled code (which could be mitigated on Android with an embedded R8 rule)

Cons:

  • Requires a library
  • Feels too clever
  • Code formatting will insert a space before and after the minus sign breaking the effect

Use soft keyword in compiler

println("black") } } ">
fun printColor(color: RouletteColor) {
  sealed when (color) {
    RouletteColor.Red -> println("red")
    RouletteColor.Black -> println("black")
  }
}

Pros:

  • Great locality as a soft keyword directly modifying the when
  • No impact on compiled code
  • Part of the actual language

Cons:

  • Requires forking the compiler and IDE plugin which is an overwhelming long-term commitment!!!

License

Copyright 2020 Square, Inc.

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.
Comments
  • IDE can't find import in KMP module's commonMain source set

    IDE can't find import in KMP module's commonMain source set

    I've added the plugin to both my android app module and my KMP module but the import for the Exhaustive annotation can't be found in the KMP module's commonMain source set. It works fine in the androidMain source set.

    https://github.com/edenman/kmpPlayground/compare/edenman/exhaustive-when

    kotlin-bug 
    opened by edenman 6
  • NoClassDefFoundError: org/jetbrains/kotlin/cfg/WhenMissingCase

    NoClassDefFoundError: org/jetbrains/kotlin/cfg/WhenMissingCase

    Kotlin 1.5.20-M1 Exhaustive 0.1.1

    Looks like it might have just moved to org.jetbrains.kotlin.diagnostics.WhenMissingCase? At least that's the import here: https://github.com/JetBrains/kotlin/blob/master/compiler/frontend/src/org/jetbrains/kotlin/cfg/WhenChecker.kt

    e: java.lang.NoClassDefFoundError: org/jetbrains/kotlin/cfg/WhenMissingCase
    	at app.cash.exhaustive.compiler.DefaultErrorMessagesExhaustive$map$1$1$render$1.invoke(ExhaustiveErrors.kt:40)
    	at kotlin.text.StringsKt__AppendableKt.appendElement(Appendable.kt:85)
    	at kotlin.collections.CollectionsKt___CollectionsKt.joinTo(_Collections.kt:3344)
    	at kotlin.collections.CollectionsKt___CollectionsKt.joinToString(_Collections.kt:3361)
    	at kotlin.collections.CollectionsKt___CollectionsKt.joinToString$default(_Collections.kt:3360)
    	at app.cash.exhaustive.compiler.DefaultErrorMessagesExhaustive$map$1$1.render(ExhaustiveErrors.kt:43)
    	at app.cash.exhaustive.compiler.DefaultErrorMessagesExhaustive$map$1$1.render(ExhaustiveErrors.kt:40)
    	at org.jetbrains.kotlin.diagnostics.rendering.DiagnosticWithParametersMultiRenderer.renderParameters(diagnosticsWithParameterRenderers.kt:84)
    	at org.jetbrains.kotlin.diagnostics.rendering.DiagnosticWithParametersMultiRenderer.renderParameters(diagnosticsWithParameterRenderers.kt:78)
    	at org.jetbrains.kotlin.diagnostics.rendering.AbstractDiagnosticWithParametersRenderer.render(diagnosticsWithParameterRenderers.kt:27)
    	at org.jetbrains.kotlin.diagnostics.rendering.DefaultErrorMessages.render(DefaultErrorMessages.java:56)
    	at org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport$Companion.reportDiagnostic(AnalyzerWithCompilerReport.kt:149)
    	at org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport$Companion.reportDiagnostics(AnalyzerWithCompilerReport.kt:159)
    	at org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport$Companion.reportDiagnostics(AnalyzerWithCompilerReport.kt:165)
    	at org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport.analyzeAndReport(AnalyzerWithCompilerReport.kt:120)
    	at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze(KotlinToJVMBytecodeCompiler.kt:531)
    	at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.compileModules$cli(KotlinToJVMBytecodeCompiler.kt:188)
    	at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.compileModules$cli$default(KotlinToJVMBytecodeCompiler.kt:154)
    	at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:169)
    	at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:52)
    	at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:90)
    	at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:44)
    	at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:98)
    	at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:386)
    	at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:110)
    	at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileIncrementally(IncrementalCompilerRunner.kt:303)
    	at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl$rebuild(IncrementalCompilerRunner.kt:99)
    	at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:124)
    	at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:74)
    	at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:607)
    	at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:96)
    	at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1659)
    	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    	at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:359)
    	at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200)
    	at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197)
    	at java.base/java.security.AccessController.doPrivileged(Native Method)
    	at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196)
    	at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:562)
    	at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:796)
    	at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:677)
    	at java.base/java.security.AccessController.doPrivileged(Native Method)
    	at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:676)
    	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    	at java.base/java.lang.Thread.run(Thread.java:834)
    
    opened by edenman 3
  • Opt-out mechanism or class-based annotation?

    Opt-out mechanism or class-based annotation?

    Right now it's opt-in. Would it be feasible to support an opt-out approach instead? Or an annotation on the enum/sealed class itself like @MustBeExhaustive so all users of it use it rather than need to remember to opt in.

    opened by ZacSweers 3
  • `@Exhaustive` doesn't work with `return when`

    `@Exhaustive` doesn't work with `return when`

    For example: this does not compile with @Exhaustive. I use return when quite a bit and it cuts down on redundant returns. Android Studio also suggests me to do it, so it will be confusing when users try it and it won't compile.

    This annotation is not applicable to target 'expression'

            @Exhaustive
            return when (viewAction) {
                is AppViewAction.SettingsClicked -> reduceOnSettingsClicked(dispatcher, previousState)
                is AppViewAction.CountUpdated -> reduceOnCountUpdated(previousState, viewAction.count)
                else -> previousState.copy()
            }
    
    opened by seanfreiburg 2
  • One more alternative for the readme

    One more alternative for the readme

    I have two more alternative, you might consider adding to the readme:

    when (...) {
        ...
    }.let { /* it be exhaustive */ }
    

    The only con I see from your other points is the locality. This variant does not need a library or plugin, does not pollute the Any namespace and does not impact runtime performance as it is an inlined no-op.

    and

    Do exhaustive when (...) {
        ...
    }
    

    with an object and an infix function. There locality is better but it cannot be inlined to a no-op like the let solution.

    opened by Vampire 1
  • Use Gradle 7.3.2. Log4shell mitigation.

    Use Gradle 7.3.2. Log4shell mitigation.

    Gradle 7.3.2 adds dependency constraints to the build classpath to reject known-bad versions of log4j.

    See also https://blog.gradle.org/log4j-vulnerability.

    opened by autonomousapps 0
  • JS IR consumers of a module using exhaustive will fail to resolve the dependency

    JS IR consumers of a module using exhaustive will fail to resolve the dependency

    This is because it's defined as compileOnly, but the bug makes compileOnly basically impossible to use in JS IR.

    Tracking upstream at https://youtrack.jetbrains.com/issue/KT-43500

    kotlin-bug 
    opened by JakeWharton 0
  • Build failure due to variant mismatch on Android Java 11 project?

    Build failure due to variant mismatch on Android Java 11 project?

    A problem occurred configuring root project 'cash'.
    > Could not resolve all artifacts for configuration ':classpath'.
       > Could not resolve app.cash.exhaustive:exhaustive-gradle:0.1.0.
         Required by:
             project :
          > No matching variant of app.cash.exhaustive:exhaustive-gradle:0.1.0 was found. The consumer was configured to find a runtime of a library compatible with Java 11, packaged as a jar, and its dependencies declared externally but:
              - Variant 'apiElements' capability app.cash.exhaustive:exhaustive-gradle:0.1.0 declares a library, packaged as a jar, and its dependencies declared externally:
                  - Incompatible because this component declares an API of a component compatible with Java 14 and the consumer needed a runtime of a component compatible with Java 11
              - Variant 'runtimeElements' capability app.cash.exhaustive:exhaustive-gradle:0.1.0 declares a runtime of a library, packaged as a jar, and its dependencies declared externally:
                  - Incompatible because this component declares a component compatible with Java 14 and the consumer needed a component compatible with Java 11
    
    opened by jrodbx 0
Releases(0.2.0)
Owner
Cash App
Cash App
Lightweight compiler plugin intended for Kotlin/JVM library development and symbol visibility control.

Restrikt A Kotlin/JVM compiler plugin to restrict symbols access, from external project sources. This plugin offers two ways to hide symbols: An autom

Lorris Creantor 18 Nov 24, 2022
A Kotlin compiler plugin that allows Java callers to pass in null for default parameters

kotlin-null-defaults (Compiler plugin) (Gradle Plugin) ( Currently pending approval) A Kotlin compiler plugin that allows Java callers to pass in null

Youssef Shoaib 7 Oct 14, 2022
A simple example of kotlim compiler plugin with FIR and IR.

A simple Kotlin compiler plugin example This Kotlin compiler plugin generates a top level class: public final class foo.bar.MyClass { fun foo(): S

Anastasiia Birillo 10 Dec 2, 2022
An experimental tool for building console UI in Kotlin using the Jetpack Compose compiler/runtime

An experimental tool for building console UI in Kotlin using the Jetpack Compose compiler/runtime

Jake Wharton 1.4k Dec 28, 2022
Build a compiler in Kotlin (based on the original tutorial by Jack Crenshaw)

Let's Build a Compiler Based on the original series "Let’s Build a Compiler!" by Jack Crenshaw. This is an adaptation of the original series to Kotlin

null 2 Oct 9, 2022
Annotation based Android lint check generation

Intervention Annotation based Android lint check generation Generate custom Android lint checks and have lint warn you about code you may be dealing w

Fanis Veizis 25 Aug 18, 2022
API-Annotate - Single annotation to mark API elements

API-Annotate A single annotation for annotating externally-consumed elements in

null 0 Feb 5, 2022
Playground project for Koin Koin Compiler - Sandbox

Koin Compiler - Sandbox The goal of Koin compiler & Annotations project is to help declare Koin definition in a very fast and intuitive way, and gener

insert-koin.io 17 Nov 22, 2021
This repository is part of a Uni-Project to write a complete Compiler for a subset of Java.

Compiler This repository is part of a Uni-Project to write a complete Compiler for a subset of Java. Features error recovery using context sensitive a

null 3 Jan 10, 2022
A Open GAL compiler based on OpenGAL 0.3.1

A Open GAL compiler based on OpenGAL 0.3.1

Li Plum 3 Dec 21, 2022
A CLI tool to convert multi-module Jetpack Compose compiler metrics into beautiful HTML reports

A CLI tool to convert multi-module Jetpack Compose compiler metrics into beautiful HTML reports 1. What are Jetpack Compose compiler metrics? The Comp

Jaya Surya Thotapalli 116 Jan 3, 2023
Plugin-shared-preferences - Pluto plugin to manage your Shared Preferences

Pluto Shared Preferences Plugin Pluto Shared Preferences is a Pluto plugin to in

Pluto 1 Feb 14, 2022
A somewhat copy past of Jetbrain's code from the kotlin plugin repo to make it humanly possible to test Intellij IDEA kotlin plugins that work on kotlin

A somewhat copy past of Jetbrain's code from the kotlin plugin repo to make it humanly possible to test Intellij IDEA kotlin plugins that work on kotlin

common sense OSS 0 Jan 20, 2022
Kotlin-client-dsl - A kotlin-based dsl project for a (Client) -> (Plugin) styled program

kotlin-client-dsl a kotlin-based dsl project for a (Client) -> (Plugin) styled p

jackson 3 Dec 10, 2022
The KPy gradle plugin allows you to write Kotlin/Native code and use it from python.

The KPy gradle plugin allows you to write Kotlin/Native code and use it from python.

Martmists 14 Dec 26, 2022
Danger-kotlin plugin to parse and report Detekt violations

Detekt Plugin for Danger Kotlin Plugin for danger/kotlin which helps to parse and report detekt violations from its XML report files. How it looks lik

Pavel Korolev 10 Sep 20, 2022
A Gradle plugin for Kotlin Multiplatform projects that generate a XCFramework for Apple targets or a FatFramework for iOS targets, and manages the publishing process in a CocoaPod Repository.

KMP Framework Bundler KMP Framework Bundler is a Gradle plugin for Kotlin Multiplatform projects that generate a XCFramework for Apple targets or a Fa

Marco Gomiero 17 Oct 29, 2022
Flutter plugin that leverages Storage Access Framework (SAF) API to get access and perform the operations on files and folders

Flutter plugin that leverages Storage Access Framework (SAF) API to get access and perform the operations on files and folders.

Vehement 8 Nov 26, 2022
A flutter plugin to scan stripe readers and connect to the them and get the payment methods.

stripe_terminal A flutter plugin to scan stripe readers and connect to the them and get the payment methods. Installation Android No Configuration nee

Aawaz Gyawali 8 Dec 29, 2022