Skip to content

Commit

Permalink
Use AST transform instead of MOP
Browse files Browse the repository at this point in the history
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
ajoberstar committed Jul 29, 2017
1 parent 5f5a960 commit 92c1798
Show file tree
Hide file tree
Showing 40 changed files with 329 additions and 231 deletions.
26 changes: 2 additions & 24 deletions src/main/groovy/org/ajoberstar/grgit/Grgit.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,10 @@
*/
package org.ajoberstar.grgit

import org.ajoberstar.grgit.internal.WithOperations
import org.ajoberstar.grgit.operation.*
import org.ajoberstar.grgit.service.*
import org.ajoberstar.grgit.util.JGitUtil
import org.ajoberstar.grgit.util.OpSyntaxUtil

import org.eclipse.jgit.api.Git

/**
* Provides support for performing operations on and getting information about
Expand Down Expand Up @@ -111,18 +109,8 @@ import org.eclipse.jgit.api.Git
*
* @since 0.1.0
*/
@WithOperations(staticOperations=[InitOp, CloneOp, OpenOp], instanceOperations=[CleanOp, StatusOp, AddOp, RmOp, ResetOp, ApplyOp, PullOp, PushOp, FetchOp, CheckoutOp, LogOp, CommitOp, RevertOp, MergeOp, DescribeOp, ShowOp])
class Grgit implements AutoCloseable {
private static final Map STATIC_OPERATIONS = [
init: InitOp, clone: CloneOp, open: OpenOp
]

private static final Map OPERATIONS = [
clean: CleanOp, status: StatusOp, add: AddOp, remove: RmOp,
reset: ResetOp, apply: ApplyOp, pull: PullOp, push: PushOp,
fetch: FetchOp, checkout: CheckoutOp, log: LogOp, commit: CommitOp,
revert: RevertOp, merge: MergeOp, describe: DescribeOp, show: ShowOp
].asImmutable()

/**
* The repository opened by this object.
*/
Expand Down Expand Up @@ -184,14 +172,4 @@ class Grgit implements AutoCloseable {
void close() {
repository.jgit.close()
}

def methodMissing(String name, args) {
OpSyntaxUtil.tryOp(this.class, OPERATIONS, [repository] as Object[], name, args)
}

static {
Grgit.metaClass.static.methodMissing = { name, args ->
OpSyntaxUtil.tryOp(Grgit, STATIC_OPERATIONS, [] as Object[], name, args)
}
}
}
41 changes: 41 additions & 0 deletions src/main/groovy/org/ajoberstar/grgit/internal/OpSyntax.groovy
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 src/main/groovy/org/ajoberstar/grgit/internal/Operation.java
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 src/main/groovy/org/ajoberstar/grgit/internal/WithOperations.java
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 {};
}
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;
}
}
3 changes: 2 additions & 1 deletion src/main/groovy/org/ajoberstar/grgit/operation/AddOp.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import java.util.concurrent.Callable

import org.ajoberstar.grgit.Repository
import org.ajoberstar.grgit.exception.GrgitException

import org.ajoberstar.grgit.internal.Operation
import org.eclipse.jgit.api.AddCommand
import org.eclipse.jgit.api.errors.GitAPIException

Expand All @@ -46,6 +46,7 @@ import org.eclipse.jgit.api.errors.GitAPIException
* @since 0.1.0
* @see <a href="http://git-scm.com/docs/git-add">git-add Manual Page</a>
*/
@Operation('add')
class AddOp implements Callable<Void> {
private final Repository repo

Expand Down
2 changes: 2 additions & 0 deletions src/main/groovy/org/ajoberstar/grgit/operation/ApplyOp.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import java.util.concurrent.Callable

import org.ajoberstar.grgit.Repository
import org.ajoberstar.grgit.exception.GrgitException
import org.ajoberstar.grgit.internal.Operation
import org.ajoberstar.grgit.util.CoercionUtil

import org.eclipse.jgit.api.ApplyCommand
Expand All @@ -38,6 +39,7 @@ import org.eclipse.jgit.api.errors.GitAPIException
* @since 0.1.0
* @see <a href="http://git-scm.com/docs/git-apply">git-apply Manual Page</a>
*/
@Operation('apply')
class ApplyOp implements Callable<Void> {
private final Repository repo

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import java.util.concurrent.Callable
import org.ajoberstar.grgit.Branch
import org.ajoberstar.grgit.Repository
import org.ajoberstar.grgit.exception.GrgitException
import org.ajoberstar.grgit.internal.Operation
import org.ajoberstar.grgit.service.ResolveService
import org.ajoberstar.grgit.util.JGitUtil

Expand Down Expand Up @@ -68,6 +69,7 @@ import org.eclipse.jgit.lib.Ref
* @since 0.2.0
* @see <a href="http://git-scm.com/docs/git-branch">git-branch Manual Page</a>
*/
@Operation('add')
class BranchAddOp implements Callable<Branch> {
private final Repository repo

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import java.util.concurrent.Callable
import org.ajoberstar.grgit.Branch
import org.ajoberstar.grgit.Repository
import org.ajoberstar.grgit.exception.GrgitException
import org.ajoberstar.grgit.internal.Operation
import org.ajoberstar.grgit.service.ResolveService
import org.ajoberstar.grgit.util.JGitUtil

Expand Down Expand Up @@ -62,6 +63,7 @@ import org.eclipse.jgit.lib.Ref
* @since 0.2.0
* @see <a href="http://git-scm.com/docs/git-branch">git-branch Manual Page</a>
*/
@Operation('change')
class BranchChangeOp implements Callable<Branch> {
private final Repository repo

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import java.util.concurrent.Callable
import org.ajoberstar.grgit.Branch
import org.ajoberstar.grgit.Repository
import org.ajoberstar.grgit.exception.GrgitException
import org.ajoberstar.grgit.internal.Operation
import org.ajoberstar.grgit.util.JGitUtil

import org.eclipse.jgit.api.ListBranchCommand
Expand Down Expand Up @@ -60,6 +61,7 @@ import org.eclipse.jgit.api.errors.GitAPIException
* @since 0.2.0
* @see <a href="http://git-scm.com/docs/git-branch">git-branch Manual Page</a>
*/
@Operation('list')
class BranchListOp implements Callable<List<Branch>> {
private final Repository repo

Expand Down
Loading

0 comments on commit 92c1798

Please sign in to comment.