Loading...

January 29, 2016

Groovy Goodness: Customising The Groovy Compiler

With Groovy we can configure the compiler for our source files just like we can configure the Groovy compilation unit when we use GroovyShell to execute scripts. We can for example add annotations to source files, before they are compiled, without adding them to the actual source code ourselves. Suppose we want to apply the TypeChecked or CompileStatic AST transformation annotation to all source files in our project. We only have to write a configuration file and specify the name of the configuration file with the --configscript option of the Groovy compiler. If we use Gradle to build our Groovy project we can also customise the GroovyCompile task to set the configuration file.

The configuration file has an implicit object with the name configuration of type CompilerConfiguration. Also there is a builder syntax available via the CompilerCustomizationBuilder class. Let's look at both ways to define our custom configuration. We want to add the CompileStatic annotation to all classes, together with the ToString AST transformation annotation. Next we also want to add the package java.time as implicit import for our source files. This means we don't have to write an import statement in our code to include classes from this package. Finally we add a ExpressionChecker that will fail the compilation of our project if a variable name is only 1 character. We assume we use Gradle to build our project and we place the file groovycConfig.groovy in the directory src/groovyCompile. We must not name the file configuration.groovy, because there is already a variable with the name configuration in the script and this will confuse the compiler.

// File: src/groovyCompile/groovycConfig.groovy
import groovy.transform.CompileStatic
import groovy.transform.ToString
import org.codehaus.groovy.ast.expr.VariableExpression
import org.codehaus.groovy.control.customizers.SecureASTCustomizer.ExpressionChecker
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
import org.codehaus.groovy.control.customizers.ImportCustomizer
import org.codehaus.groovy.control.customizers.SecureASTCustomizer

// Add the AST annotation @CompileStatic
// and @ToString to all classes.
configuration.addCompilationCustomizers(
        new ASTTransformationCustomizer(CompileStatic))
configuration.addCompilationCustomizers(
        new ASTTransformationCustomizer(ToString))

// Add implicit import for all classes
// for the package java.time.
def imports = new ImportCustomizer()
imports.addStarImports('java.time')
configuration.addCompilationCustomizers(imports)

// Define expression checker to deny 
// usage of variable names with length of 1.
def smallVariableNames = { expr ->
    if (expr instanceof VariableExpression) {
        expr.variable.size() > 1
    } else {
        true
    }
} as ExpressionChecker

def secureAstCustomizer = new SecureASTCustomizer()
secureAstCustomizer.addExpressionCheckers(smallVariableNames)
configuration.addCompilationCustomizers(secureAstCustomizer)

With the builder syntax of CompilerCustomizatioBuilder we also have the flexibility to pass parameters to the AST transformations we want to apply. We configure the ToString annotation to include the names of the properties in the generated toString method implementation:

// File: src/groovyCompile/groovycConfig.groovy
import org.codehaus.groovy.ast.expr.VariableExpression
import org.codehaus.groovy.control.customizers.SecureASTCustomizer.ExpressionChecker

// Using CompilerCustomizationBuilder.withConfig 
// method, where the class
// CompilerCustomizationBuilder is implicitly 
// imported for this script.
withConfig(configuration)  {

    ast(groovy.transform.CompileStatic)

    // Define includeNames parameter for ToString
    // AST annotation.
    ast(includeNames: true, groovy.transform.ToString)
    
    imports {
        star('java.time')
    }

    // Define expression checker to deny 
    // usage of variable names with length of 1.
    def smallVariableNames = { expr ->
        if (expr instanceof VariableExpression) {
            expr.variable.size() > 1
        } else {
            true
        }
    } as ExpressionChecker
    secureAst {
        addExpressionCheckers smallVariableNames
    }
}

Next we configure the GroovyCompile task compileGroovy in our Gradle project to use our configuration file when the code is compiled. We do this via the property groovyOptions of the compile task:

// File: build.gradle
plugins {
    id "groovy"
}

repositories {
    jcenter()
}

dependencies {
    compile "org.codehaus.groovy:groovy-all:2.4.5"
    testCompile "org.spockframework:spock-core:1.0-groovy-2.4"
}

// Add the configuration script file
// to the compiler options.
compileGroovy.groovyOptions.configurationScript = file('src/groovyCompile/groovycConfig.groovy')

Andre Steingress wrote a good blog post about the Groovy compiler configuration script.

Written with Groovy 2.4.5.