3

I would like to make an AST transformation which wraps method body with a closure. My problem is that this closure must access to that method parameters, but it seems that there is no visibility.

My code is available on Github. Here is what I did:

Annotation as an entry point for AST transformation:

package wrap;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.codehaus.groovy.transform.GroovyASTTransformationClass;
import org.codehaus.groovy.transform.sc.StaticCompileTransformation;

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
@GroovyASTTransformationClass(classes = { WrapTransformation.class, StaticCompileTransformation.class })
public @interface Wrap {}

AST transformation (commented code with different things I tried):

package wrap;

import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.VariableScope;
import org.codehaus.groovy.ast.expr.ArgumentListExpression;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.transform.AbstractASTTransformation;
import org.codehaus.groovy.transform.GroovyASTTransformation;

@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
public class WrapTransformation extends AbstractASTTransformation {

    @Override
    public void visit(ASTNode[] astNodes, SourceUnit sourceUnit) {
        if (astNodes != null) {
            callToHandler((MethodNode) astNodes[1]);
        }
    }

    protected void callToHandler(MethodNode method) {

        method.getParameters()[0].setClosureSharedVariable(true);

        ClosureExpression body = new ClosureExpression( //

                // new Parameter[] { new Parameter(ClassHelper.OBJECT_TYPE, "stringParam") }, //
                Parameter.EMPTY_ARRAY,
                // new Parameter[0],
                // method.getParameters(),
                // null, //

                method.getCode());

        VariableScope scope = new VariableScope(method.getVariableScope());
        // method.getVariableScope().getDeclaredVariables().forEach((k, v) -> {
        // scope.putDeclaredVariable(v);
        // });
        body.setVariableScope(scope);

        MethodCallExpression callExp = new MethodCallExpression( //
                VariableExpression.THIS_EXPRESSION, //
                "handleWrapped", //
                new ArgumentListExpression(body));

        BlockStatement block = new BlockStatement();
        block.addStatement(new ExpressionStatement(callExp));
        method.setCode(block);
    }

}

Base class with method where to send closure to:

package wrap;

import groovy.lang.Closure;

public class BaseClass {

    public Object handleWrapped(Closure<?> closure) {
        return closure.call();
    }
}

JUnit test cases:

package wrap;

import java.io.File;
import java.util.Arrays;

import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.tools.ast.TransformTestHelper;
import org.junit.Test;

import wrap.WrapTransformation;

public class WrapTransformationTest {

    @Test
    public void wrapped() throws Exception {
        run("src/test/resources/test/MyClass.groovy", "wrapped", "my string");
    }

    @Test
    public void unwrapped() throws Exception {
        run("src/test/resources/test/MyClass.groovy", "unwrapped", "my string");
    }

    protected Object run(String src, String methodName, Object... args) throws Exception {
        TransformTestHelper invoker = new TransformTestHelper(new WrapTransformation(), CompilePhase.SEMANTIC_ANALYSIS);
        File filePath = new File(src);
        Class<?> clazz = invoker.parse(filePath);
        Object instance = clazz.newInstance();
        return clazz.getMethod(methodName, getTypes(args)).invoke(instance, (Object[]) args);
    }

    protected Class<?>[] getTypes(Object[] objs) {
        return Arrays.stream(objs).map(Object::getClass).toArray(Class[]::new);
    }
}

Groovy test class:

package test

class MyClass extends wrap.BaseClass {

    def unwrapped(String stringParam) {
        handleWrapped({ print stringParam })
    }

    @wrap.Wrap
    def wrapped(String stringParam) {
        print stringParam
    }
}

Second method should be equivalent to first one, but it isn't. First one prints correctly "my string". Second gives this exception:

org.codehaus.groovy.runtime.metaclass.MissingPropertyExceptionNoStack: No such property: stringParam for class: test.MyClass

After debugging with Eclipse I've found that closures which arrive at handleWrapped method are slightly different. First one contains an stringParam field, while one created with AST transformation does not.

enter image description here

sinuhepop
  • 20,010
  • 17
  • 72
  • 107

0 Answers0