Written on
Towards Null-Safe Groovy

null
is evil, but: null is not the new goto.
There are use-cases where assigning null
to a variable might still be appropriate. Think of a double-linked list data-structure, where null
could be needed to mark the first previous and the last next reference. However, the use of null
more often leads to errors whenever a method call is executed on a variable which has been assigned to null
.
In order to provide compile-time checking of instance variables, local variables and parameters I added a new annotation to GContracts annotation package: @NullSafe
:
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.FIELD, ElementType.LOCAL_VARIABLE, ElementType.PARAMETER})
public @interface NullSafe {}
NullSafeASTTransformation
which is supposed to do the compile-time checking stuff. To do that, we need a set of rules which enforce null-safety for variables annotated with @NullSafe
:
Local Variables, Parameters
Whenever a local variable or a parameter is marked as null-safe, only assignments to- constants (except
null
) - other null-safe variables
@NullSafe
and takes care of valid assignments. Whenever an assignment does not conform to the rules stated above, a compile-time error is thrown. E.g. the following source code snippet fails during compilation:
package tests
import org.gcontracts.annotations.*
class A {
def some_operation(@NullSafe def param1) {
def bla = "test"
param1 = bla
}
}
BUG! exception in phase 'semantic analysis' in source unit 'script1281039992077748355994.groovy' param1 must be assigned to NullSafe variables!
at org.gcontracts.ast.visitor.NullSafeVisitor.visitBinaryExpression(NullSafeVisitor.java:62)
package tests
import org.gcontracts.annotations.*
class A {
def some_operation() {
@NullSafe def bla = "test"
def otherBla = "test2"
bla = otherBla
}
}
package tests
import org.gcontracts.annotations.*
class A {
def some_operation() {
@NullSafe def bla = "test"
def otherBla = "test2"
bla = otherBla
}
}
BUG! exception in phase 'semantic analysis' in source unit 'script12810425282531878012710.groovy' bla must be assigned to NullSafe variables!
at org.gcontracts.ast.visitor.NullSafeVisitor.visitBinaryExpression(NullSafeVisitor.java:31)
otherBla
as @NullSafe
.
Instance Variables, Fields
Instance variables need to be assigned to a non-null value whenever the construction of the according object is finished, thus the object complies to the explicit class invariant. Whenever a method is called an assignment tonull
would trigger a compile-time failure:
package tests
import org.gcontracts.annotations.*
class A {
@NullSafe String myProp
def A() {
myProp = "test"
}
def some_op() { myProp = null }
}
BUG! exception in phase 'semantic analysis' in source unit 'script12810407257201736681757.groovy' myProp must be assigned to NullSafe variables!
at org.gcontracts.ast.visitor.NullSafeVisitor.visitBinaryExpression(NullSafeVisitor.java:59)
Assignments from Non-Void Method Calls
In fact, this is only half the story. Whenever a variable is assigned to the return value of a method, we have no simple way to check at compile-time whether that method returnsnull
. A possible solution for solving this scenario in terms of keeping code null-safe is to specify variable default values via annotation closures (btw: here starts the part I actually did not implement by now, so don't get confused with the annotation class above):
package tests
import org.gcontracts.annotations.*
class A {
def some_op() {
@NullSafe({ "empty" }) def i = "aloha"
i = some_function()
// at this point, i will be "empty" not null!
}
def some_function() { return null }
}
null
reference.
Annotation closures would provide great flexibility to define arbitrary complex initialization code:
class Address {
@NullSafe({ new City(name: 'dummy') }) City city
// ...
}
@NullSafe
, which would avoid calling methods not being marked with @NullSafe
in null-safe assignments.
What's next?
That's it with my experimental@NullSafe
feature. I know that this is a naive first approach, but at least its a starting point and I would really appreciate your feedback on this topic either in this article's comment section or at the Groovy-Dev mailing list [1].
It would be very important to know whether this method works or if it is not applicable in real-world projects from your point of view. One issue is, that although GContracts uses AST transformations, it is not an integral part of the programming language, which could result in e.g. meta- or reflection-calls which make null-safe variables null etc. Just imagine Hibernate injecting null values into @NullSave
instance variables - levering null-safe variables immediately . In fact, this is the point I am not completely clear about - does it make sense to introduce such a feature at library/AST transformation level?