-
Notifications
You must be signed in to change notification settings - Fork 95
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
In 1.x Grgit, operations were made available to the user via the MOP with a methodMissing implementation. This meant that you couldn't get IDE support to see that you were calling a valid method. There's now an ASTTransformation that generates four methods for each operation that take different arguments: - no args - map - consumer - closure These should provide more static help in the IDE and generally increase consistency. This resolves #85.
- Loading branch information
1 parent
5f5a960
commit 92c1798
Showing
40 changed files
with
329 additions
and
231 deletions.
There are no files selected for viewing
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
41 changes: 41 additions & 0 deletions
41
src/main/groovy/org/ajoberstar/grgit/internal/OpSyntax.groovy
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package org.ajoberstar.grgit.internal | ||
|
||
import java.util.concurrent.Callable | ||
import java.util.function.Consumer | ||
|
||
import groovy.transform.PackageScope | ||
|
||
class OpSyntax { | ||
static def noArgOperation(Class<Callable> opClass, Object[] classArgs) { | ||
def op = opClass.newInstance(classArgs) | ||
return op.call() | ||
} | ||
|
||
static def mapOperation(Class<Callable> opClass, Object[] classArgs, Map args) { | ||
def op = opClass.newInstance(classArgs) | ||
|
||
args.forEach { key, value -> | ||
op[key] = value | ||
} | ||
|
||
return op.call() | ||
} | ||
|
||
static def consumerOperation(Class<Callable> opClass, Object[] classArgs, Consumer arg) { | ||
def op = opClass.newInstance(classArgs) | ||
arg.accept(op) | ||
return op.call() | ||
} | ||
|
||
static def closureOperation(Class<Callable> opClass, Object[] classArgs, Closure closure) { | ||
def op = opClass.newInstance(classArgs) | ||
|
||
Object originalDelegate = closure.delegate | ||
closure.delegate = op | ||
closure.resolveStrategy = Closure.DELEGATE_FIRST | ||
closure.call() | ||
closure.delegate = originalDelegate | ||
|
||
return op.call() | ||
} | ||
} |
12 changes: 12 additions & 0 deletions
12
src/main/groovy/org/ajoberstar/grgit/internal/Operation.java
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package org.ajoberstar.grgit.internal; | ||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
@Retention(RetentionPolicy.SOURCE) | ||
@Target(ElementType.TYPE) | ||
public @interface Operation { | ||
String value(); | ||
} |
17 changes: 17 additions & 0 deletions
17
src/main/groovy/org/ajoberstar/grgit/internal/WithOperations.java
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package org.ajoberstar.grgit.internal; | ||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
import java.util.concurrent.Callable; | ||
|
||
import org.codehaus.groovy.transform.GroovyASTTransformationClass; | ||
|
||
@Retention(RetentionPolicy.SOURCE) | ||
@Target(ElementType.TYPE) | ||
@GroovyASTTransformationClass("org.ajoberstar.grgit.internal.WithOperationsASTTransformation") | ||
public @interface WithOperations { | ||
Class<? extends Callable<?>>[] staticOperations() default {}; | ||
Class<? extends Callable<?>>[] instanceOperations() default {}; | ||
} |
192 changes: 192 additions & 0 deletions
192
src/main/groovy/org/ajoberstar/grgit/internal/WithOperationsASTTransformation.java
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
package org.ajoberstar.grgit.internal; | ||
|
||
import java.lang.reflect.Modifier; | ||
import java.lang.reflect.ParameterizedType; | ||
import java.lang.reflect.Type; | ||
import java.util.Arrays; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.function.Consumer; | ||
|
||
import org.codehaus.groovy.ast.ASTNode; | ||
import org.codehaus.groovy.ast.AnnotatedNode; | ||
import org.codehaus.groovy.ast.AnnotationNode; | ||
import org.codehaus.groovy.ast.ClassHelper; | ||
import org.codehaus.groovy.ast.ClassNode; | ||
import org.codehaus.groovy.ast.FieldNode; | ||
import org.codehaus.groovy.ast.GenericsType; | ||
import org.codehaus.groovy.ast.MethodNode; | ||
import org.codehaus.groovy.ast.Parameter; | ||
import org.codehaus.groovy.ast.expr.ArgumentListExpression; | ||
import org.codehaus.groovy.ast.expr.ArrayExpression; | ||
import org.codehaus.groovy.ast.expr.ClassExpression; | ||
import org.codehaus.groovy.ast.expr.Expression; | ||
import org.codehaus.groovy.ast.expr.FieldExpression; | ||
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression; | ||
import org.codehaus.groovy.ast.expr.VariableExpression; | ||
import org.codehaus.groovy.ast.stmt.ExpressionStatement; | ||
import org.codehaus.groovy.ast.stmt.Statement; | ||
import org.codehaus.groovy.control.CompilePhase; | ||
import org.codehaus.groovy.control.SourceUnit; | ||
import org.codehaus.groovy.transform.AbstractASTTransformation; | ||
import org.codehaus.groovy.transform.GroovyASTTransformation; | ||
|
||
import groovy.lang.Closure; | ||
|
||
@GroovyASTTransformation(phase=CompilePhase.CANONICALIZATION) | ||
public class WithOperationsASTTransformation extends AbstractASTTransformation { | ||
|
||
@Override | ||
public void visit(ASTNode[] nodes, SourceUnit source) { | ||
AnnotationNode annotation = (AnnotationNode) nodes[0]; | ||
AnnotatedNode parent = (AnnotatedNode) nodes[1]; | ||
|
||
if (parent instanceof ClassNode) { | ||
ClassNode clazz = (ClassNode) parent; | ||
List<ClassNode> staticOps = getClassList(annotation, "staticOperations"); | ||
List<ClassNode> instanceOps = getClassList(annotation, "instanceOperations"); | ||
|
||
staticOps.forEach(op -> { | ||
makeMethods(clazz, op, true); | ||
}); | ||
instanceOps.forEach(op -> { | ||
makeMethods(clazz, op, false); | ||
}); | ||
|
||
} | ||
} | ||
|
||
private void makeMethods(ClassNode targetClass, ClassNode opClass, boolean isStatic) { | ||
AnnotationNode annotation = opClass.getAnnotations(classFromType(Operation.class)).stream().findFirst() | ||
.orElseThrow(() -> new IllegalArgumentException("Class is not annotated with @Operation: " + opClass)); | ||
String opName = getMemberStringValue(annotation, "value"); | ||
ClassNode opReturn = opClass.getDeclaredMethod("call", new Parameter[] {}).getReturnType(); | ||
|
||
targetClass.addMethod(makeNoArgMethod(targetClass, opName, opClass, opReturn, isStatic)); | ||
targetClass.addMethod(makeMapMethod(targetClass, opName, opClass, opReturn, isStatic)); | ||
targetClass.addMethod(makeConsumerMethod(targetClass, opName, opClass, opReturn, isStatic)); | ||
targetClass.addMethod(makeClosureMethod(targetClass, opName, opClass, opReturn, isStatic)); | ||
} | ||
|
||
private MethodNode makeNoArgMethod(ClassNode targetClass, String opName, ClassNode opClass, ClassNode opReturn, boolean isStatic) { | ||
Parameter[] parms = new Parameter[] {}; | ||
|
||
Statement code = new ExpressionStatement( | ||
new StaticMethodCallExpression( | ||
classFromType(OpSyntax.class), | ||
"noArgOperation", | ||
new ArgumentListExpression( | ||
new ClassExpression(opClass), | ||
new ArrayExpression(classFromType(Object.class), opConstructorParms(targetClass, isStatic)) | ||
) | ||
) | ||
); | ||
|
||
return new MethodNode(opName, modifiers(isStatic), opReturn, parms, new ClassNode[] {}, code); | ||
} | ||
|
||
private MethodNode makeMapMethod(ClassNode targetClass, String opName, ClassNode opClass, ClassNode opReturn, boolean isStatic) { | ||
ClassNode parmType = classFromType(Map.class); | ||
GenericsType[] generics = genericsFromTypes(String.class, Object.class); | ||
parmType.setGenericsTypes(generics); | ||
Parameter[] parms = new Parameter[] { new Parameter(parmType, "args") }; | ||
|
||
Statement code = new ExpressionStatement( | ||
new StaticMethodCallExpression( | ||
classFromType(OpSyntax.class), | ||
"mapOperation", | ||
new ArgumentListExpression( | ||
new ClassExpression(opClass), | ||
new ArrayExpression(classFromType(Object.class), opConstructorParms(targetClass, isStatic)), | ||
new VariableExpression("args") | ||
) | ||
) | ||
); | ||
|
||
return new MethodNode(opName, modifiers(isStatic), opReturn, parms, new ClassNode[] {}, code); | ||
} | ||
|
||
private MethodNode makeConsumerMethod(ClassNode targetClass, String opName, ClassNode opClass, ClassNode opReturn, boolean isStatic) { | ||
ClassNode parmType = classFromType(Consumer.class); | ||
GenericsType[] generics = new GenericsType[] { new GenericsType(opReturn) }; | ||
parmType.setGenericsTypes(generics); | ||
Parameter[] parms = new Parameter[] { new Parameter(parmType, "arg") }; | ||
|
||
Statement code = new ExpressionStatement( | ||
new StaticMethodCallExpression( | ||
classFromType(OpSyntax.class), | ||
"consumerOperation", | ||
new ArgumentListExpression( | ||
new ClassExpression(opClass), | ||
new ArrayExpression(classFromType(Object.class), opConstructorParms(targetClass, isStatic)), | ||
new VariableExpression("arg") | ||
) | ||
) | ||
); | ||
|
||
return new MethodNode(opName, modifiers(isStatic), opReturn, parms, new ClassNode[] {}, code); | ||
} | ||
|
||
private MethodNode makeClosureMethod(ClassNode targetClass, String opName, ClassNode opClass, ClassNode opReturn, boolean isStatic) { | ||
ClassNode parmType = classFromType(Closure.class); | ||
Parameter[] parms = new Parameter[] { new Parameter(parmType, "arg") }; | ||
|
||
Statement code = new ExpressionStatement( | ||
new StaticMethodCallExpression( | ||
classFromType(OpSyntax.class), | ||
"closureOperation", | ||
new ArgumentListExpression( | ||
new ClassExpression(opClass), | ||
new ArrayExpression(classFromType(Object.class), opConstructorParms(targetClass, isStatic)), | ||
new VariableExpression("arg") | ||
) | ||
) | ||
); | ||
|
||
return new MethodNode(opName, modifiers(isStatic), opReturn, parms, new ClassNode[] {}, code); | ||
} | ||
|
||
public ClassNode classFromType(Type type) { | ||
if (type instanceof Class) { | ||
Class<?> clazz = (Class<?>) type; | ||
if (clazz.isPrimitive()) { | ||
return ClassHelper.make(clazz); | ||
} else { | ||
return ClassHelper.makeWithoutCaching(clazz, false); | ||
} | ||
} else if (type instanceof ParameterizedType) { | ||
ParameterizedType ptype = (ParameterizedType) type; | ||
ClassNode base = classFromType(ptype.getRawType()); | ||
GenericsType[] generics = genericsFromTypes(ptype.getActualTypeArguments()); | ||
base.setGenericsTypes(generics); | ||
return base; | ||
} else { | ||
throw new IllegalArgumentException("Unsupported type: " + type.getClass()); | ||
} | ||
} | ||
|
||
public GenericsType[] genericsFromTypes(Type... types) { | ||
return Arrays.stream(types) | ||
.map(this::classFromType) | ||
.map(GenericsType::new) | ||
.toArray(size -> new GenericsType[size]); | ||
} | ||
|
||
public List<Expression> opConstructorParms(ClassNode targetClass, boolean isStatic) { | ||
if (isStatic) { | ||
return Collections.emptyList(); | ||
} else { | ||
FieldNode repo = targetClass.getField("repository"); | ||
return Arrays.asList(new FieldExpression(repo)); | ||
} | ||
} | ||
|
||
public int modifiers(boolean isStatic) { | ||
int modifiers = Modifier.PUBLIC | Modifier.FINAL; | ||
if (isStatic) { | ||
modifiers |= Modifier.STATIC; | ||
} | ||
return modifiers; | ||
} | ||
} |
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
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
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
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
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
Oops, something went wrong.