Skip to content

Instantly share code, notes, and snippets.

@kimble
Created June 12, 2013 19:00
Show Gist options
  • Save kimble/5768117 to your computer and use it in GitHub Desktop.
Save kimble/5768117 to your computer and use it in GitHub Desktop.
Groovy AST transformation adding toString implementation based on simple pattern supplied in annotation.
package developerb.groovy.compiler;
import org.codehaus.groovy.transform.GroovyASTTransformationClass;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Target(TYPE)
@Retention(RUNTIME)
@GroovyASTTransformationClass("developerb.groovy.compile.HumanReadableToStringTransformation")
public @interface HumanReadableToString {
String value();
}
package developerb.groovy.compiler
import grails.plugin.anticrud.groovy.HumanReadableToString
import spock.lang.Specification
import spock.lang.Unroll
class HumanReadableToStringSpec extends Specification {
@Unroll
void "#name with #catKillCount cats killed => #expectedToString"() {
when:
def doggy = new Dog(name: name, catKillCount: catKillCount)
then:
doggy.toString() == expectedToString
where:
name | catKillCount | expectedToString
"Pluto" | 0 | "Pluto has killed 0 cats"
"Lassie" | 10 | "Lassie has killed 10 cats"
}
}
class Animal {
String name
}
@HumanReadableToString("#name has killed #catKillCount cats")
class Dog extends Animal {
int catKillCount
}
package developerb.groovy.compiler;
import org.codehaus.groovy.ast.*;
import org.codehaus.groovy.ast.expr.*;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.ast.stmt.ReturnStatement;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.transform.AbstractASTTransformation;
import org.codehaus.groovy.transform.GroovyASTTransformation;
import java.util.StringTokenizer;
import static org.codehaus.groovy.ast.ClassHelper.STRING_TYPE;
import static org.codehaus.groovy.ast.expr.MethodCallExpression.NO_ARGUMENTS;
import static org.codehaus.groovy.control.CompilePhase.CANONICALIZATION;
import static org.codehaus.groovy.transform.AbstractASTTransformUtil.declStatement;
/**
* Plenty of code and concepts shamelessly stolen from Groovy's ToStringASTTransformation
*/
@GroovyASTTransformation(phase = CANONICALIZATION)
public class HumanReadableToStringTransformation extends AbstractASTTransformation {
private static final ClassNode STRING_BUILDER_TYPE = ClassHelper.make(StringBuilder.class);
@Override
public void visit(ASTNode[] nodes, SourceUnit source) {
init(nodes, source);
AnnotatedNode parent = (AnnotatedNode) nodes[1];
AnnotationNode annotation = (AnnotationNode) nodes[0];
if (parent instanceof ClassNode) {
ClassNode classNode = (ClassNode) parent;
if (classNode.hasDeclaredMethod("toString", new Parameter[0])) {
addError("toString method already declared", classNode);
}
else {
appendToStringMethod(classNode, annotation.getMember("value").getText());
}
}
}
private void appendToStringMethod(ClassNode classNode, String pattern) {
final BlockStatement body = new BlockStatement();
final Expression buffer = createBuffer(body);
generateToStringCode(pattern, body, buffer);
createMethod(classNode, body, buffer);
}
private Expression createBuffer(BlockStatement body) {
final Expression buffer = new VariableExpression("_result");
final Expression init = new ConstructorCallExpression(STRING_BUILDER_TYPE, NO_ARGUMENTS);
body.addStatement(declStatement(buffer, init));
return buffer;
}
private void generateToStringCode(String pattern, BlockStatement body, Expression buffer) {
StringTokenizer tokenizer = new StringTokenizer(pattern, " ");
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
appendVariableOrConstant(body, buffer, token);
appendTrailingWhitespaceIfNecessary(body, buffer, tokenizer);
}
}
private void appendVariableOrConstant(BlockStatement body, Expression buffer, String token) {
if (token.startsWith("#")) {
String name = token.substring(1);
body.addStatement(append(buffer, new VariableExpression(name)));
}
else {
body.addStatement(append(buffer, new ConstantExpression(token)));
}
}
private ExpressionStatement append(Expression result, Expression expr) {
MethodCallExpression append = new MethodCallExpression(result, "append", expr);
append.setImplicitThis(false);
return new ExpressionStatement(append);
}
private void appendTrailingWhitespaceIfNecessary(BlockStatement body, Expression buffer, StringTokenizer tokenizer) {
if (tokenizer.hasMoreTokens()) {
body.addStatement(append(buffer, new ConstantExpression(" ")));
}
}
private void createMethod(ClassNode classNode, BlockStatement body, Expression buffer) {
MethodCallExpression toString = new MethodCallExpression(buffer, "toString", NO_ARGUMENTS);
toString.setImplicitThis(false); // Wtf...
body.addStatement(new ReturnStatement(toString));
classNode.addMethod(new MethodNode("toString", ACC_PUBLIC, STRING_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, body));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment