Report usage of non-null assertion operator in Kotlin

In Kotlin, nullability is encoded in the type system. It means that one has to use a different type if something may accept null (for instance String?) than if it cannot be null (for instance String).

This is very useful, because it allows the compiler to detect potential null-pointer exception at compile-time.

Kotlin also provide a set of convenient language construct in order to deal with nullability:

  • Safe call: nullable?.doSomething() will only call doSomething if nullable is not null. And thus never throw any NPE
  • Elvis operator: val x: String = input ?: "default" allow to choose what to do if something return null. It can also be used to return the function or throw an exception.
  • Smart-cast: if (x != null) x.length compiles, because the Kotlin compiler knows that it cannot be null when length is called.
  • Contracts: requireNotNull(x) ; x.lenght compiles, because requireNotNull defines a contract stating that if the function does not throw, it means the argument is not null. Therefore the Kotlin compiler is able to smart-cast.
  • lateinit: In case of late-initialization (like in a test), there is a specific language keyworkd lateinit which allow to declare a non-null member without initializing it in the constructor.

All of that makes dealing with nullability really great in Kotlin. However this is true until we use !!.

The !! operator in Kotlin allow the programmer to tell the compiler “Ok, I know what I do, and I promise, it won’t be null”. It is nothing more than a way to stop using all the benefits of Kotlin type system.

It is very error-prone as plain old NPE may appear at run-time again. It kills one major benefit of using Kotlin.

And if we think about it, it is kind of a Kotlin equivalent of squid:S2637:

Java Noncompliant snippet for squid:S2637

@Nonnull
public String indirectMix() {
  String mix = null;
  return mix;  // <-- squid:S2637 Noncompliant; return value is Nonnull, but null is returned.}}
}

The equivalent of the above snippet in Kotlin is:

fun indirectMix(): String { // <-- this declaration is equivalent as using `@NotNull` in Java.
  val mix: String? = null;
  return mix!!;  // <-- This should not be compliant in Kotlin
}

Snippet of non-compliant Code

fun example1(x: Interface?) {
  x!!.callMethod()
}

fun example2(x: Interface?) {
  callFunction(x!!)
}

fun example3(x: Interface?): List<String> {
  return x!!.names
}

Snippet of compilant Code (fixing the above noncompliant code)

Option 1: Use non-null type declaration

fun example1(x: Interface) { // <-- Doesn't accept null. If called from Kotlin, the compiler will enforce it at compile-time.
  x.callMethod()
}

fun example2(x: Interface) { // <-- Doesn't accept null. If called from Kotlin, the compiler will enforce it at compile-time.
  callFunction(x)
}

fun example3(x: Interface): List<String> { // <-- Doesn't accept null. If called from Kotlin, the compiler will enforce it at compile-time.
  return x.names.orEmpty()
}

Option 2: Use safe calls, elvis operator and extension funcions defined on nullable type

fun example1(x: Interface?) {
  x?.callMethod() // <-- call `callMethod` only if x is not null
}

fun example2(x: Interface?) {
  callFunction(x ?: return) // <-- terminate the function immediately if x is null (and thus don't execute `callFunction`)
}

fun example3(x: Interface?): List<String> {
  return x?.names.orEmpty() // <-- return an empty list if x is null
}

Option 3: Throw explicit exception

fun example1(x: Interface?) {
  requireNotNull(x) { "null is not a valid argument because..." }
  x.callMethod() // <-- Smart cast by Kotlin compiler
}

fun example2(x: Interface?) {
  requireNotNull(x) { "null is not a valid argument because..." }
  callFunction(x) // <-- Smart cast by Kotlin compiler
}

fun example3(x: Interface?): List<String> {
  return x?.names ?: throw IllegalArgumentException("null is not a valid argument because...")
}

External references and/or language specifications

Kotlin null-safety: https://kotlinlang.org/docs/reference/null-safety.html#null-safety
Combating non-null assertions: https://medium.com/@igorwojda/kotlin-combating-non-null-assertions-5282d7b97205

Type : Bug

May cause null pointer exception

I’d personally put the default severity to “minor”

Tags

suspicious, bad-practice

Thank you very much, @jcornaz, for the effort put into specifying this rule. We will come up with a follow-up message once we evaluate all the rules you have suggested.