user2483213 user2483213 - 4 months ago 19
Groovy Question

Getter and Setter AST Transformation

I wrote my own AST Transformation which should generate getter and setter methods (here creating getter method). But they don't work and can't understand reason.

create annotation with property

@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.FIELD])
@GroovyASTTransformationClass(['ua.home.gag.ast.GetterAndSetterASTTransformation'])
public @interface GetterAndSetter {
}


my code of AST transformation which should create getter method for annotated field

@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
class GetterAndSetterASTTransformation implements ASTTransformation {

@Override
void visit(ASTNode[] astNodes, SourceUnit sourceUnit) {
if (!checkNodes(astNodes)) return
List fields = astNodes.findAll { it instanceof FieldNode }
fields.each {
MethodNode getter = getterMethod(ClassHelper.make(it));
it.declaringClass.addMethod(getter);
}
}

static def checkNodes(ASTNode[] nodes) {
nodes &&
nodes[0] &&
nodes[1] &&
nodes[0] instanceof AnnotationNode &&
nodes[0].classNode?.name == GetterAndSetter.class.name &&
nodes[1] instanceof ClassNode
}

private MethodNode getterMethod(FieldNode fieldNode) {
return new MethodNode(
"getMy" + fieldNode.name.capitalize(),
Modifier.PUBLIC,
new ClassNode(fieldNode.type),
new Parameter[0],
new ClassNode[0],
new BlockStatement(
[new ReturnStatement(
new VariableExpression(fieldNode.name)
)],
new VariableScope())
)
}
}


Annotation check

import ua.home.gag.ast.GetterAndSetter

class Example {

@GetterAndSetter
int counter = 5;

static void main(String[] args) {
println new Example().getMyCounter();
}
}


In which place I did mistake?

The result of running :


Exception in thread "main" groovy.lang.MissingMethodException: No signature of method: ua.home.gag.usage.Example.getMyCounter() is applicable for argument types: () values: []
Possible solutions: getCounter(), setCounter(int)
at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.unwrap(ScriptBytecodeAdapter.java:56)
at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.call(PogoMetaClassSite.java:51)
at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:45)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:108)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:112)
at ua.home.gag.usage.Example.main(Example.groovy:12)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)


PS repo https://bitbucket.org/maxrdev/ast-gands

Answer

I think that the problem is in your checkNodes method. This expression nodes[1] instanceof ClassNode evaluates to false because nodes[1] is instanceof FieldNode.

You also don't have to filter and iterate over the fields, because this transformation will be applied to all fields annotated with @GetterAndSetter. That's why you have to only focus on a single case for annotated field. Usually all you have to do is:

@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
class GetterAndSetterASTTransformation implements ASTTransformation {

    @Override
    void visit(ASTNode[] astNodes, SourceUnit sourceUnit) {
        AnnotationNode parent = (AnnotationNode) astNodes[0]
        FieldNode node = (FieldNode) astNodes[1]

        if (!(parent instanceof AnnotationNode) && !(node instanceof FieldNode)) {
            throw new RuntimeException("Internal error: wrong types: ${node.class} / ${parent.class}");
        }

        Statement statement = new BlockStatement([
                new ReturnStatement(new VariableExpression(node))
        ], new VariableScope())

        MethodNode methodNode = new MethodNode("getMy${node.name.capitalize()}",
                Modifier.PUBLIC,
                node.type,
                new Parameter[0],
                new ClassNode[0],
                statement
        )

        node.declaringClass.addMethod(methodNode)
    }
}

Then below code will work:

class Example {

    @GetterAndSetter
    int counter = 5;

    @GetterAndSetter
    String lorem = 'asdasd'

    @Deprecated
    @GetterAndSetter
    BigDecimal ipsum = BigDecimal.ONE

    static void main(String[] args) {
        Example example = new Example()

        println example.getMyCounter()

        println example.getMyLorem()

        println example.getMyIpsum()
    }
}

And the result is:

/usr/lib/jvm/java-1.8.0/bin/java -Didea.launcher.port=7541 -Didea.launcher.bin.path=/opt/idea-IU-129.1525/bin -Dfile.encoding=UTF-8 -classpath /usr/lib/jvm/java-1.8.0/jre/lib/resources.jar:/usr/lib/jvm/java-1.8.0/jre/lib/jsse.jar:/usr/lib/jvm/java-1.8.0/jre/lib/jce.jar:/usr/lib/jvm/java-1.8.0/jre/lib/charsets.jar:/usr/lib/jvm/java-1.8.0/jre/lib/management-agent.jar:/usr/lib/jvm/java-1.8.0/jre/lib/rt.jar:/usr/lib/jvm/java-1.8.0/jre/lib/ext/cldrdata.jar:/usr/lib/jvm/java-1.8.0/jre/lib/ext/dnsns.jar:/usr/lib/jvm/java-1.8.0/jre/lib/ext/sunpkcs11.jar:/usr/lib/jvm/java-1.8.0/jre/lib/ext/sunec.jar:/usr/lib/jvm/java-1.8.0/jre/lib/ext/zipfs.jar:/usr/lib/jvm/java-1.8.0/jre/lib/ext/localedata.jar:/usr/lib/jvm/java-1.8.0/jre/lib/ext/nashorn.jar:/usr/lib/jvm/java-1.8.0/jre/lib/ext/sunjce_provider.jar:/home/wololock/workspace/idea/ast-gands/target/classes:/home/wololock/.m2/repository/org/codehaus/groovy/groovy-all/2.3.7/groovy-all-2.3.7.jar:/opt/idea-IU-129.1525/lib/idea_rt.jar com.intellij.rt.execution.application.AppMain ua.home.gag.usage.Example
5
asdasd
1

Process finished with exit code 0

You can find more examples in e.g. grails-core repository - go to https://github.com/grails/grails-core/, type t and search for ASTTransformation