Written on
@NullSafe Reloaded
In almost all projects of mine I got used to a utility class called Default. Default would provide static methods, whereas each method comes with a single parameter. The purpose of Default was nothing more than to provide default values if the argument is null at runtime.
public class Default {
// ...
public static Float floatObject(Float n) {
return n != null ? n : 0.0f;
}
public static Integer integerObject(Integer i) {
return i != null ? i : 0;
}
// ...
}
Doin it the Groovy way
With the upcoming popularity of Groovy AST transformations, I thought it would be time to write a small NullSafeASTTransformation with the attached @NullSafe annotation. As Groovy is dynamically typed, @NullSafe is restricted to work on statically declared local variables.
@NullSafe String name = data.name
@NullSafe Integer count = data.count
@NullSafe BigDecimal amount = data.amount
assert name == ''
assert count == 0
assert amount == 0.0
def doSomeStuff() {
@NullSafe BigDecimal bd = 12.0
bd = compute()
assert bd == 0.0
}
def compute() { return null }
@GroovyASTTransformation(phase=CompilePhase.SEMANTIC_ANALYSIS)
class NullSafeASTTransformation implements ASTTransformation {
SourceUnit sourceUnit
ClassNode NULL_SAFE_UTIL_CLASS = ClassHelper.make(NullSafeUtil.class)
void visit(org.codehaus.groovy.ast.ASTNode[] nodes, org.codehaus.groovy.control.SourceUnit source) {
this.sourceUnit = source
if (sourceUnit.getAST().classes.size() != 1) {
addError('@NullSafe annotation can only be applied on local variables!', nodes[0])
return
}
if (nodes.length != 2 || !(nodes[0] instanceof AnnotationNode) || !(nodes[1] instanceof DeclarationExpression)) {
addError('@NullSafe annotation can only be applied on local variables!', nodes[0])
return
}
DeclarationExpression declarationExpression = nodes[1]
if (!(declarationExpression.leftExpression instanceof VariableExpression)) {
addError('@NullSafe can only be applied on the left side of a variable declaration!', declarationExpression)
return
}
injectDeclarationNullSafeCheck(declarationExpression)
injectAssignmentNullSafeCheck(declarationExpression)
}
void injectDeclarationNullSafeCheck(DeclarationExpression declarationExpression) {
VariableExpression variableExpression = declarationExpression.leftExpression
if (variableExpression.dynamicTyped || variableExpression.accessedVariable.type == ClassHelper.DYNAMIC_TYPE) {
addError('@NullSafe is not supported on dynamically typed variables!', variableExpression)
return
}
def rightExpression = declarationExpression.getRightExpression()
if (rightExpression == null) {
addError("@NullSafeNumber right expression is empty", declarationExpression)
return
}
ClassNode variableType = variableExpression.accessedVariable.type
declarationExpression.rightExpression = new StaticMethodCallExpression(NULL_SAFE_UTIL_CLASS, "nullSafe", new ArgumentListExpression([new CastExpression(variableType, rightExpression)]))
}
void injectAssignmentNullSafeCheck(DeclarationExpression declarationExpression) {
VariableExpression variableExpression = declarationExpression.leftExpression
if (variableExpression.dynamicTyped || variableExpression.accessedVariable.type == ClassHelper.DYNAMIC_TYPE) {
addError('@NullSafe is not supported on dynamically typed variables!', variableExpression)
return
}
def rightExpression = declarationExpression.getRightExpression()
if (rightExpression == null) {
addError("@NullSafeNumber right expression is empty", declarationExpression)
return
}
NullSafeClassCodeVisitor visitor = new NullSafeClassCodeVisitor(sourceUnit, variableExpression.accessedVariable)
visitor.visitClass(sourceUnit.getAST().classes[0])
}
protected void addError(String msg, ASTNode expr) {
int line = expr.getLineNumber();
int col = expr.getColumnNumber();
sourceUnit.getErrorCollector().addErrorAndContinue(
new SyntaxErrorMessage(new SyntaxException(msg + '\n', line, col), sourceUnit)
);
}
}