Not sure if this is the right place to ask but I w...
# getting-started
t
Not sure if this is the right place to ask but I was wondering whether it was possible when writing Kotlin in Intellij/Android Studio to mark a function parameter as "nullable only" and receive warnings when passing a non-null value to it. My use case is that we're currently doing a major refactor and I'd like to be able to see at a glance whether values we pass to our
assertNotNull()
function are no longer nullable. Would I have to write a custom linter rule to achieve this?
s
😬
Copy code
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
fun <T: Any> assertNotNull(value: @kotlin.internal.Exact T?) {}
Obviously the big
@Suppress
annotation should be a sign that this isn’t a particularly sensible thing to do 😄 but I think it works
r
Worth mentioning #detekt already has a rule which reports this case: https://detekt.dev/docs/rules/potential-bugs/#unnecessarynotnullcheck
a
I get a warning in IntelliJ when I try and pass a non-nullable value into
requireNotNull()
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/require-not-null.html - do you want the same thing for your
assertNotNull()
?
t
Yes, that does seem to be what I want! Any idea if that's possible for my own functions?
And thank you for mentioning detekt by the way, the project looks interesting and I'll definitely be checking it out!
a
take a look at the source code for
requireNotNull()
, it’s the
contract {}
block that does the magic. It can be difficult to set up the Kotlin contracts, and they’re experimental, which I suspect might be too much work for a nice helper during refactoring? You might be better off just using
requireNotNull()
, and then you can remove it later?
r
Yeah, I was going to ask why you need a custom assertNotNull rather than the stdlib versions because this sounds like it might be an XY problem
t
I did actually try adding a contract before but that did not seem to have the effect I hoped for. The problem might be that our assertNotNull call does actually throw an exception in production/release builds. This is what we currently have:
Copy code
@OptIn(ExperimentalContracts::class)
fun <T> assertNotNull(value: T?): T? {
    contract {
        returnsNotNull() implies (value != null)
    }
    assertInDebug(value != null)
    return value
}
Our current usage typically boils down to the following:
Copy code
val foo = assertNotNull(nullableFoo) ?: return
In production scenarios this will log the failed assertion to our backend but keep the application going (even though this might cause unexpected behavior down the line) and only in debug builds will this actually throw an exception. Ideally I'd also like the
nullableFoo
to be automatically smart cast to a non-null value after the
?: return
but even with the contract that does not seem to happen. For now though I'd be happy if I could make sure that we can catch the cases where
nullableFoo
is not actually nullable anymore.
a
the contract has no effect because it has
returnsNotNull()
, but the function does return null
t
I see. My assumption was that the contract would provide the compiler some information for when the returned value was not null (which could then be used after we handle the null value in the caller with
?: return
) but I guess that's not the case. Is there any way around this (by e.g. changing the contract) or should I just look into using something like detekt?
a
I’m trying to figure out the logic… so is the summary of it that in production you want to ignore NullPointerExceptions, but not in debug builds?
I just realised I got confused. The warning from
requireNotNull()
comes from an IntelliJ inspection, it’s not related to the Kotlin contracts stuff. So you could try adapting the inspection to also check your
assertNotNull()
function to show a warning if the the argument isn’t nullable
t
To answer your previous question, we'd like to avoid crashing our application in production if we can avoid it, so say we have a function that shows a dialog:
Copy code
fun showDialog(dialogViewModel: DialogViewModel) {
    val dialogText = assertNotNull(dialogViewModel.textContent) ?: return

    // Code to show dialog goes here
}
In this case the DialogViewModel's might be nullable for legacy reasons. During development we'd like to know if there are still scenarios where the textContent could be null and fail fast, but in production we'd rather have the assertion be logged to our backend (so we still know the assertion triggered) without crashing the application. In this case the
showDialog()
function would return before showing a dialog due to one of the preconditions failing which avoids a NullPointerException but also causes unexpected behavior for the end user (i.e. the dialog does not open). In the past we'd have a lot of functions like the the snippet below which would silently hide errors in our application:
Copy code
fun foo(foo: String?, bar: Int?, baz: Float?) {
    foo ?: return
    bar ?: return
    baz ?: return

    // ...
}
That's why we opted for our
assertNotNull
implementation while we're cleaning up and trying to avoid having nullables where they don't make sense altogether. As for your second answer, good to know that the warning is from an inspection! I'll see if I can make this work for our scenario. Thanks for the help everyone!