Disallow non-nullable argument in function with nullable argument

Hello

I faintly remember coming across a way to trigger a compiler error/warning when calling a function expecting an argument of a nullable type (e.g., Any?) with an argument of a non-nullable type (e.g., Any), but I’m unable to google it now…

For example:

fun rejectIfNull(value: Any?) { // how can I statically disallow Any here?
  if (value == null) {
    // do something
  }
}

rejectIfNull("I'm not null") // should generate a compiler error/warning

Is that possible? Thank you for your help.

This is generally not doable, because technically speaking, a non-nullable type is a subtype of nullable type and we assume the upcasting is always safe. In other words, Any is always at the same time an Any?.

Of course, we can create some code rules on top of the language logic, but we should be aware this is a non-standard behavior and could be confusing to others. One way is using some kind of a linter or compiler plugin which enforces such additional rules.

Also, one potential trick I see is by utilizing the fact that if there are overloaded functions, the compiler chooses the one with the most specific arguments - in that case Any. This way we can easily target cases where we provided a compile-time subtype. We can return Nothing, we can use @Deprecated or @RequiresOptIn:

fun main() {
    rejectIfNull("hello") // compile error
    rejectIfNull("hello" as String?) // ok
}

fun rejectIfNull(value: Any?) {}

@InvalidArgumentTypes
fun rejectIfNull(value: Any) {}

@RequiresOptIn
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.FUNCTION)
annotation class InvalidArgumentTypes

Still, this may be a little confusing and it won’t work in the case like:

fun main() {
    foo("hello") // ok
}

fun foo(s: String?) = rejectIfNull(s) // ok
2 Likes

I don’t see any benefit of this approach TBH. If you don’t need a value then a function parameter itself becomes redundant.

Thank you very much for your valuable insights.

I think the way I had in mind involved an overloaded version of the function with @Deprecated, so the trick you describe is spot on :+1:

@madmax1028 My use case was avoiding copy/paste errors in the context of implementing validators for various classes.

For instance:

fun rejectIfNull(value: Any?) {
  if (value == null) { TODO("reject") }
}

data class Foo(val x: String?)

class FooValidator {
  fun validate(target: Foo) {
    rejectIfNull(target.x) // ok

    if (target.x == null) { TODO("reject") } // logic inlined, ok
  }
}

data class Bar(val x: String)

class BarValidator {
  fun validate(target: Bar) {
    // copy/paste error potentially going unnoticed (meant to be rejectIfBlank(target.x) or similar)
    rejectIfNull(target.x)

    // copy/paste error, but a warning is displayed by static analysis tools
    if (target.x == null) { TODO("reject") }
  }
}

You can check out contracts, I think you should be able to solve validation problem with them:

Example:

@kotlin.internal.InlineOnly
public inline fun <T : Any> checkNotNull(value: T?, lazyMessage: () -> Any): T {
    contract {
        returns() implies (value != null)
    }

    if (value == null) {
        val message = lazyMessage()
        throw IllegalStateException(message.toString())
    } else {
        return value
    }
}
1 Like