Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

GroovyClassLoader call to parseClass is successful, even when code does not compile

I'm trying to dynamically load a Groovy script as a class but the class object is created even when the script's code does not compile.

For example, a simplified version of my Groovy code to load the Groovy script is as follows:

GroovyCodeSource src = new GroovyCodeSource(
    "blah blah blah",
    "Foo.groovy",
    GroovyShell.DEFAULT_CODE_BASE
)
new GroovyClassLoader().parseClass(src, true)

Clearly, the code blah blah blah isn't a legitimate Groovy script. And yet, a class object is successfully created for this dynamic code. According to GroovyClassLoader's Javadoc for the parseClass method a CompilationFailedException should be thrown for cases such as this.

How is it possible that the class is still created for broken code and how can I successfully create a class from dynamic Groovy source code conditionally on whether or not the code will compile? I've done a lot of research and experimentation but to no avail.

like image 642
entpnerd Avatar asked Sep 11 '19 22:09

entpnerd


1 Answers

That's because groovy provides dynamic access to methods and properties and in terms of Groovy, the code blah blah blah, is valid. Actually you are providing code for Script (there is no class declaration). After compilation, you will get a class that extends groovy.lang.Script.

So, let me continue your code and show you how it could be valid...

GroovyCodeSource src = new GroovyCodeSource(
    'blah blah blah',
    "Foo.groovy",
    GroovyShell.DEFAULT_CODE_BASE
)
def c = new GroovyClassLoader().parseClass(src, true)
println c                     //class Foo
println c.getSuperclass()     //class groovy.lang.Script

def i = c.newInstance()
//i.run()                     //MissingPropertyException: No such property: blah for class: Foo
i.setBinding([
    blah: { x-> return [blah: "x.class =${x.getClass()}"] }
] as Binding)
i.run()                       //SUCCESS

I would also advise you to run groovyconsole, enter blah blah blah, press Ctrl+T, and check what class was generated for your script. Note that you could switch between different compilation/parsing phases. enter image description here


A possible workaround is to use the CompileStatic annotation on the methods or the class:

//compilation of this code will fail with message
//[Static type checking] - The variable [blah] is undeclared.
@groovy.transform.CompileStatic
def f(){
    blah blah blah
}
f()

You could force GroovyClassLoader to make static validation for the whole script.

Let's imagine you want your scripts to access only some pre-defined variables/methods and you want to check this at compile step and not at runtime.

The following example shows how to do that and it will fail the blah blah blah code during compilation:

import org.codehaus.groovy.control.customizers.builder.CompilerCustomizationBuilder
import org.codehaus.groovy.control.CompilerConfiguration
import groovy.transform.CompileStatic

//your base Script class that declares only valid members
//for example `log`
abstract class MyScript extends groovy.lang.Script{
    PrintStream log
}

//create compiler config with base script class 
CompilerConfiguration cc = new CompilerConfiguration()
cc.setScriptBaseClass(MyScript.class.getName())
//make static compilation set for class loader
cc = CompilerCustomizationBuilder.withConfig(cc){ 
    ast(CompileStatic) 
}
//create classloader with compile config
GroovyClassLoader gcl = new GroovyClassLoader(this.getClass().getClassLoader(),cc)


GroovyCodeSource src = new GroovyCodeSource(
    "log.println 'hello world'",
    "Foo.groovy",
    GroovyShell.DEFAULT_CODE_BASE
)
def c = gcl.parseClass(src, true)  //this will fail for 'blah blah blah' source
def i = c.newInstance(log:System.out)
i.run()

P.S. There are other code transformers available in Groovy.

like image 94
daggett Avatar answered Oct 17 '22 13:10

daggett