Created
June 12, 2013 19:00
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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