I have following classes
final class GroovyValidator {
@Override
public List<String> validate(String script) {
final CompilerConfiguration config = new CompilerConfiguration();
config.setScriptBaseClass("groovy.util.DelegatingScript");
config.addCompilationCustomizers(new ASTTransformationCustomizer(
Map.of("extensions", List.of(ConnectorTypeCheckExtension.class.getName())),
CompileStatic.class));
try {
final GroovyShell shell = new GroovyShell(this.getClass().getClassLoader(), config);
// compile the script
final DelegatingScript parsedScript = (DelegatingScript) shell.parse(script);
parsedScript.setDelegate(new ObjectClass());
parsedScript.run();
} catch (MultipleCompilationErrorsException e) {
return e.getErrorCollector().getErrors().stream()
.map(m -> {
final StringWriter stringWriter = new StringWriter();
m.write(new PrintWriter(stringWriter));
return stringWriter.toString();
})
.toList();
}
return Collections.emptyList();
}
}
public class ObjectClass {
public ObjectClass objectClass(String name,
@DelegatesTo(value = String.class, strategy = Closure.DELEGATE_FIRST) Closure<?> closure) {
closure.setDelegate(name);
closure.setResolveStrategy(Closure.DELEGATE_FIRST);
closure.call();
return this;
}
}
public class ConnectorTypeCheckExtension extends AbstractTypeCheckingExtension {
public ConnectorTypeCheckExtension(StaticTypeCheckingVisitor typeCheckingVisitor) {
super(typeCheckingVisitor);
}
@Override
public List<MethodNode> handleMissingMethod(ClassNode receiver, String name, ArgumentListExpression argumentList,
ClassNode[] argumentTypes, MethodCall call) {
System.out.printf("Handling missing %s\n", name);
if ("objectClass".equals(name)) {
final ClassNode objectClassNode = classNodeFor(ObjectClass.class);
/*
Without bellow call to `delegatesTo` I am getting
[Script1.groovy: 2: [Static type checking] - Cannot find matching method Script1#concat(java.lang.String). Please check if the declared type is correct and if the method exists.
*/
delegatesTo(classNodeFor(String.class), Closure.DELEGATE_FIRST);
return List.of(makeDynamic(call, objectClassNode));
}
return Collections.emptyList();
}
}
and following test
@Test
void scriptContainsKnownMethodClosure_validateIsCalled_validationShouldPass() {
final String script = """
objectClass("User") {
println concat("X")
}
""";
final GroovyValidator validator = new GroovyValidator();
final List<String> errors = validator.validate(script);
System.out.println(errors);
assertTrue(errors.isEmpty());
}
I would expect, that the test will pass. In particular I would expect, that the objectClass method from the ObjectClass class would be called, and then the concatenation of the "User" with letter "X" would be printed. But instead it fails at runtime, with this error message:
java.lang.ClassCastException: class Script1 cannot be cast to class groovy.lang.Closure (Script1 is in unnamed module of loader groovy.lang.GroovyClassLoader$InnerLoader @6cc0bcf6; groovy.lang.Closure is in unnamed module of loader 'app')
I really do not understand this error. Why it happens? Is the problem somewhere in the type check extension? Or is it not possible to have script like above compiled statically, given that the method called in script is a method on delegate?
I am also confused, how does the delegatesTo method work. How does it know what argument is it about? I mean the objectClass method has two arguments name which is String and then the closure. I guess in this case it could pick the correct one, but what if there would be two or more closure parameters?
Note I know, that if I use the ObjectClass as a base script (after necessary tweaks) it works. In fact I will most likely use that approach. However, I would really like to understand what is going on in the described case.
typeCheckingVisitor.typeCheckingContext.delegationMetadatais null at that point of time.