Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

review: feat: CloneMaker can be used to listen on cloning process #1580

Merged
merged 11 commits into from
Oct 13, 2017
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());
}
}