I have noticed that for String type isNullOrEmpty() is not treated the same as (this == null || this.isEmpty()). It seems that the two are semantically identical, at least from the point of view of the function name.
Below, I added infered type to with(), explicitly:
object Test {
val isNullable: String? = null
val isNotNullable = ""
val test1 = with<String?, String?>(isNullable) { if (this.isNullOrEmpty()) isNotNullable else this }
val test2 = with<String?, String>(isNullable) { if (this == null || this.isEmpty()) isNotNullable else this }
}
The behaviour of isNullOrEmpty() seems counter intuitive. It applies across the board not just lambdas or with() expressions. The same holds for if()...
What am I missing in this code that causes the compiler or at least the Kotlin plugin to treat the two results as having different nullability?
isNullOrEmpty is a plain function and the compiler doesn’t know anything about its contract: when it returns false it means that the receiver string was definitely not null.
It is possible to specify contract of a function with a @Contact annotation: https://www.jetbrains.com/idea/help/contract-annotations.html, but unfortunately the data flow analysis (a process which allows to smart cast values after their type was statically proven) doesn’t take this annotation into account.
I think that annotation may be supported later, I’ve found it is mentioned in the issue https://youtrack.jetbrains.com/issue/KT-8889. Feel free to upvote.
I know this topic is pretty old but is there any chance that Kotlin’s standard functions will be annotated by @Contract so that the IDE/compiler for instance knows that certain values can never be null (for example in if (!string.isNullOrEmpty() && string.toLowerCase() == "hello"))? This would greatly help in avoiding the usage of the double bang !! operator.
Perhaps I should be clearer, I meant that for non-inline functions living in some library the compilation at the usage side will not be able to infer the semantics (even if it could, class substitution could break it after-the-fact). Inline functions (when used inline) can be semantically inlined into the call-site and then be used in the nullability analysis (it might require a second pass tough (where the first pass is needed for overload resolution of the inline function itself)).
Separate compilation is the only issue here. Otherwise, an inline function is compiled to the same bytecode as a non-inline one, and the abililty to analyze it is the same.
Of course, the main problem here is the cost of analyzing the function in terms of compilation performance (especially given that a function can call other functions, so the depth of analysis required may be significant).
Would the class file representation of an inline function not contain (a representation of) the source code / abstract syntax tree? Without that things get hard. Of course there may need be a limit on function complexity as well (esp. with recursive inline functions), but for this specific case that is obviously not needed. Java may be sufficiently abstract to get away with just storing the compilation result, but machine code (native target) certainly is not, LLVM IR may be (haven’t looked into that).