Skip to content

Commit

Permalink
feat: the cloning behavior can be subclassed (#1580)
Browse files Browse the repository at this point in the history
  • Loading branch information
pvojtechovsky authored and monperrus committed Oct 13, 2017
1 parent 79c3992 commit e204d07
Show file tree
Hide file tree
Showing 8 changed files with 413 additions and 344 deletions.
4 changes: 3 additions & 1 deletion src/main/java/spoon/generating/CloneVisitorGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public void process() {
final CtTypeReference<Object> cloneBuilder = factory.Type().createReference("spoon.support.visitor.clone.CloneBuilder");
final CtTypeAccess<Object> cloneBuilderType = factory.Code().createTypeAccess(cloneBuilder);
final CtVariableAccess<Object> builderFieldAccess = factory.Code().createVariableRead(factory.Field().createReference(target.getReference(), cloneBuilder, "builder"), false);
final CtVariableAccess<Object> cloneHelperFieldAccess = factory.Code().createVariableRead(factory.Field().createReference(target.getReference(), cloneBuilder, "cloneHelper"), false);
final CtFieldReference<Object> other = factory.Field().createReference((CtField) target.getField("other"));
final CtVariableAccess<Object> otherRead = factory.Code().createVariableRead(other, true);

Expand Down Expand Up @@ -138,7 +139,7 @@ private CtInvocation<?> createSetter(CtInvocation scanInvocation, CtVariableAcce
final CtExecutableReference<Object> setterRef = factory.Executable().createReference("void CtElement#set" + getterName.substring(3, getterName.length()) + "()");
final CtExecutableReference<Object> cloneRef = factory.Executable().createReference("CtElement spoon.support.visitor.equals.CloneHelper#clone()");
final CtInvocation<Object> cloneInv = factory.Code().createInvocation(null, cloneRef, getter);
cloneInv.setTarget(factory.Code().createTypeAccess(factory.Type().createReference("spoon.support.visitor.equals.CloneHelper")));
cloneInv.setTarget(cloneHelperFieldAccess);
return factory.Code().createInvocation(elementVarRead, setterRef, cloneInv);
}

Expand Down Expand Up @@ -515,6 +516,7 @@ public boolean matches(CtTypeReference reference) {
reference.setSimpleName(TARGET_CLONE_TYPE);
reference.setPackage(aPackage.getReference());
}
target.getConstructors().forEach(c -> c.addModifier(ModifierKind.PUBLIC));
return target;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,22 @@
import spoon.reflect.declaration.CtElement;
import spoon.reflect.visitor.CtScanner;
import spoon.support.visitor.clone.CloneBuilder;
import spoon.support.visitor.equals.CloneHelper;

/**
* Used to clone a given element.
*
* This class is generated automatically by the processor {@link spoon.generating.CloneVisitorGenerator}.
*/
class CloneVisitorTemplate extends CtScanner {
private final CloneHelper cloneHelper;
private final CloneBuilder builder = new CloneBuilder();
private CtElement other;

CloneVisitorTemplate(CloneHelper cloneHelper) {
this.cloneHelper = cloneHelper;
}

public <T extends CtElement> T getClone() {
return (T) other;
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/spoon/support/DefaultCoreFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ public DefaultCoreFactory() {
}

public <T extends CtElement> T clone(T object) {
return CloneHelper.clone(object);
return CloneHelper.INSTANCE.clone(object);
}

public <A extends Annotation> CtAnnotation<A> createAnnotation() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1026,7 +1026,6 @@ public boolean visit(ExplicitConstructorCall explicitConstructor, BlockScope sco
inv.setExecutable(references.getExecutableReference(explicitConstructor.binding));
CtTypeReference<?> declaringType = inv.getExecutable().getDeclaringType();
inv.getExecutable().setType(declaringType == null ? null : (CtTypeReference<Object>) declaringType.clone());

context.enter(inv, explicitConstructor);
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,6 @@ public <E extends CtElement> E setComments(List<CtComment> comments) {

@Override
public CtElement clone() {
return CloneHelper.clone(this);
return CloneHelper.INSTANCE.clone(this);
}
}
649 changes: 327 additions & 322 deletions src/main/java/spoon/support/visitor/clone/CloneVisitor.java

Large diffs are not rendered by default.

40 changes: 24 additions & 16 deletions src/main/java/spoon/support/visitor/equals/CloneHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,37 @@
import spoon.support.util.EmptyClearableSet;
import spoon.support.visitor.clone.CloneVisitor;

public final class CloneHelper {
public static <T extends CtElement> T clone(T element) {
final CloneVisitor cloneVisitor = new CloneVisitor();
/**
* {@link CloneHelper} is responsible for creating clones of {@link CtElement} AST nodes including the whole subtree.
*
* By default, the same instance of {@link CloneHelper} is used for whole clonning process.
*
* However, by subclassing this class and overriding method {@link #clone(CtElement)},
* one can extend and/or modify the cloning behavior.
*
* For instance, one can listen to each call to clone and get each pair of `clone source` and `clone target`.
*/
public class CloneHelper {
public static final CloneHelper INSTANCE = new CloneHelper();

public <T extends CtElement> T clone(T element) {
final CloneVisitor cloneVisitor = new CloneVisitor(this);
cloneVisitor.scan(element);
return cloneVisitor.getClone();
}

public static <T extends CtElement> Collection<T> clone(Collection<T> elements) {
public <T extends CtElement> Collection<T> clone(Collection<T> elements) {
if (elements == null || elements.isEmpty()) {
return new ArrayList<>();
}
Collection<T> others = new ArrayList<>();
for (T element : elements) {
others.add(CloneHelper.clone(element));
others.add(clone(element));
}
return others;
}

public static <T extends CtElement> List<T> clone(List<T> elements) {
public <T extends CtElement> List<T> clone(List<T> elements) {
if (elements instanceof EmptyClearableList) {
return elements;
}
Expand All @@ -57,12 +69,12 @@ public static <T extends CtElement> List<T> clone(List<T> elements) {
}
List<T> others = new ArrayList<>();
for (T element : elements) {
others.add(CloneHelper.clone(element));
others.add(clone(element));
}
return others;
}

private static <T extends CtElement> Set<T> createRightSet(Set<T> elements) {
private <T extends CtElement> Set<T> createRightSet(Set<T> elements) {
try {
if (elements instanceof TreeSet) {
// we copy the set, incl its comparator
Expand All @@ -78,7 +90,7 @@ private static <T extends CtElement> Set<T> createRightSet(Set<T> elements) {
}
}

public static <T extends CtElement> Set<T> clone(Set<T> elements) {
public <T extends CtElement> Set<T> clone(Set<T> elements) {
if (elements instanceof EmptyClearableSet) {
return elements;
}
Expand All @@ -88,23 +100,19 @@ public static <T extends CtElement> Set<T> clone(Set<T> elements) {

Set<T> others = createRightSet(elements);
for (T element : elements) {
others.add(CloneHelper.clone(element));
others.add(clone(element));
}
return others;
}

public static <T extends CtElement> Map<String, T> clone(Map<String, T> elements) {
public <T extends CtElement> Map<String, T> clone(Map<String, T> elements) {
if (elements == null || elements.isEmpty()) {
return new HashMap<>();
}
Map<String, T> others = new HashMap<>();
for (Map.Entry<String, T> tEntry : elements.entrySet()) {
others.put(tEntry.getKey(), CloneHelper.clone(tEntry.getValue()));
others.put(tEntry.getKey(), clone(tEntry.getValue()));
}
return others;
}

private CloneHelper() {
throw new AssertionError("No instance.");
}
}
53 changes: 51 additions & 2 deletions src/test/java/spoon/reflect/ast/CloneTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,27 @@
import spoon.processing.AbstractProcessor;
import spoon.reflect.code.CtConditional;
import spoon.reflect.declaration.CtClass;
import spoon.reflect.declaration.CtElement;
import spoon.reflect.declaration.CtInterface;
import spoon.reflect.declaration.CtMethod;
import spoon.reflect.declaration.CtType;
import spoon.reflect.factory.Factory;
import spoon.reflect.visitor.CtScanner;
import spoon.reflect.visitor.DefaultJavaPrettyPrinter;
import spoon.reflect.visitor.PrinterHelper;
import spoon.reflect.visitor.Query;
import spoon.reflect.visitor.filter.TypeFilter;
import spoon.support.visitor.equals.CloneHelper;
import spoon.testing.utils.ModelUtils;

import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.stream.Collectors;

import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.*;

public class CloneTest {
@Test
Expand Down Expand Up @@ -99,4 +110,42 @@ public void process(CtConditional<?> conditional) {
});
launcher.run();
}

@Test
public void testCloneListener() throws Exception {
// contract: it is possible to extend the cloning behavior

// in this example extension, a listener of cloning process gets access to origin node and cloned node
// we check the contract with some complicated class as target of cloning
Factory factory = ModelUtils.build(new File("./src/main/java/spoon/reflect/visitor/DefaultJavaPrettyPrinter.java"));
CtType<?> cloneSource = factory.Type().get(DefaultJavaPrettyPrinter.class);
class CloneListener extends CloneHelper {
Map<CtElement, CtElement> sourceToTarget = new IdentityHashMap<>();
@Override
public <T extends CtElement> T clone(T source) {
if (source == null) {
return null;
}
T target = super.clone(source);
onCloned(source, target);
return target;
}
private void onCloned(CtElement source, CtElement target) {
CtElement previousTarget = sourceToTarget.put(source, target);
assertNull(previousTarget);
}
}

CloneListener cl = new CloneListener();
CtType<?> cloneTarget = cl.clone(cloneSource);

cloneSource.filterChildren(null).forEach(sourceElement -> {
//contract: there exists cloned target for each visitable element
CtElement targetElement = cl.sourceToTarget.remove(sourceElement);
assertNotNull("Missing target for sourceElement\n" + sourceElement, targetElement);
assertEquals("Source and Target are not equal", sourceElement, targetElement);
});
//contract: each visitable elements was cloned exactly once. No more no less.
assertTrue(cl.sourceToTarget.isEmpty());
}
}

0 comments on commit e204d07

Please sign in to comment.