0

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.

2
  • delegatesTo is using metadata from current context github.com/groovy/groovy-core/blob/… Commented May 5 at 20:48
  • @daggett as far as I can tell, the typeCheckingVisitor.typeCheckingContext.delegationMetadata is null at that point of time. Commented May 6 at 7:09

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.