Skip to content

Commit

Permalink
feature: add methods Refactoring#copy* for advanced cloning
Browse files Browse the repository at this point in the history
  • Loading branch information
monperrus committed Feb 25, 2018
1 parent e28e158 commit 2428627
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 4 deletions.
119 changes: 118 additions & 1 deletion src/main/java/spoon/refactoring/Refactoring.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,16 @@
*/
package spoon.refactoring;

import spoon.SpoonException;
import spoon.reflect.code.CtLocalVariable;
import spoon.reflect.declaration.CtExecutable;
import spoon.reflect.declaration.CtField;
import spoon.reflect.declaration.CtMethod;
import spoon.reflect.declaration.CtType;
import spoon.reflect.reference.CtExecutableReference;
import spoon.reflect.reference.CtFieldReference;
import spoon.reflect.reference.CtTypeReference;
import spoon.reflect.visitor.CtScanner;
import spoon.reflect.visitor.Query;
import spoon.reflect.visitor.filter.TypeFilter;

Expand All @@ -41,7 +48,6 @@ private Refactoring() { }
public static void changeTypeName(final CtType<?> type, String name) {

final String typeQFN = type.getQualifiedName();

final List<CtTypeReference<?>> references = Query.getElements(type.getFactory(), new TypeFilter<CtTypeReference<?>>(CtTypeReference.class) {
@Override
public boolean matches(CtTypeReference<?> reference) {
Expand All @@ -56,6 +62,117 @@ public boolean matches(CtTypeReference<?> reference) {
}
}

/**
* Changes name of a method, propagates the change in the executable references of the model.
*/
public static void changeMethodName(final CtMethod<?> method, String newName) {

final List<CtExecutableReference<?>> references = Query.getElements(method.getFactory(), new TypeFilter<CtExecutableReference<?>>(CtExecutableReference.class) {
@Override
public boolean matches(CtExecutableReference<?> reference) {
return reference.getDeclaration() == method;
}
});

method.setSimpleName(newName);

for (CtExecutableReference<?> reference : references) {
reference.setSimpleName(newName);
}
}

/** See doc in {@link CtMethod#copyMethod()} */
public static CtMethod<?> copyMethod(final CtMethod<?> method) {
CtMethod<?> clone = method.clone();
String tentativeTypeName = method.getSimpleName() + "Copy";
CtType parent = method.getParent(CtType.class);
while (parent.getMethodsByName(tentativeTypeName).size() > 0) {
tentativeTypeName += "X";
}
final String cloneMethodName = tentativeTypeName;
clone.setSimpleName(cloneMethodName);
parent.addMethod(clone);
new CtScanner() {
@Override
public <T> void visitCtExecutableReference(CtExecutableReference<T> reference) {
CtExecutable<T> declaration = reference.getDeclaration();
if (declaration == null) {
return;
}
if (declaration == method) {
reference.setSimpleName(cloneMethodName);
}
if (reference.getDeclaration() != clone) {
throw new SpoonException("post condition broken " + reference);
}
super.visitCtExecutableReference(reference);

}
}.scan(clone);
return clone;
}


/** See doc in {@link CtType#copyType()} */
public static CtType<?> copyType(final CtType<?> type) {
CtType<?> clone = type.clone();
String tentativeTypeName = type.getSimpleName() + "Copy";
while (type.getFactory().Type().get(type.getPackage().getQualifiedName() + "." + tentativeTypeName) != null) {
tentativeTypeName += "X";
}
final String cloneTypeName = tentativeTypeName;
clone.setSimpleName(cloneTypeName);
type.getPackage().addType(clone);
new CtScanner() {
@Override
public <T> void visitCtTypeReference(CtTypeReference<T> reference) {
if (reference.getDeclaration() == null) {
return;
}
if (reference.getDeclaration() == type) {
reference.setSimpleName(cloneTypeName);
}
if (reference.getDeclaration() != clone) {
throw new SpoonException("post condition broken " + reference);
}
super.visitCtTypeReference(reference);
}

@Override
public <T> void visitCtExecutableReference(CtExecutableReference<T> reference) {
CtExecutable<T> declaration = reference.getDeclaration();
if (declaration == null) {
return;
}
if (declaration.hasParent(type)) {
reference.getDeclaringType().setSimpleName(cloneTypeName);
}
if (!reference.getDeclaration().hasParent(clone)) {
throw new SpoonException("post condition broken " + reference);
}
super.visitCtExecutableReference(reference);

}

@Override
public <T> void visitCtFieldReference(CtFieldReference<T> reference) {
CtField<T> declaration = reference.getDeclaration();
if (declaration == null) {
return;
}
if (declaration.hasParent(type)) {
reference.getDeclaringType().setSimpleName(cloneTypeName);
}
if (reference.getDeclaration() == null || !reference.getDeclaration().hasParent(clone)) {
throw new SpoonException("post condition broken " + reference);
}
super.visitCtFieldReference(reference);
}

}.scan(clone);
return clone;
}

/**
* Changes name of a {@link CtLocalVariable}.
*
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/spoon/reflect/declaration/CtElement.java
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,10 @@ <E extends CtElement> List<E> getAnnotatedChildren(

/**
* Clone the element which calls this method in a new object.
*
* Note that that references are kept as is, and thus, so if you clone whole classes
* or methods, some parts of the cloned element (eg executable references) may still point to the initial element.
* In this case, consider using methods {@link spoon.refactoring.Refactoring#copyType(CtType)} and {@link spoon.refactoring.Refactoring#copyMethod(CtMethod)} instead which does additional work beyond cloning.
*/
CtElement clone();

Expand Down
12 changes: 12 additions & 0 deletions src/main/java/spoon/reflect/declaration/CtMethod.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package spoon.reflect.declaration;

import spoon.refactoring.Refactoring;
import spoon.reflect.annotations.PropertyGetter;
import spoon.reflect.annotations.PropertySetter;

Expand Down Expand Up @@ -59,4 +60,15 @@ public interface CtMethod<T> extends CtExecutable<T>, CtTypeMember, CtFormalType
* Returns the empty collection if defined here for the first time.
*/
Collection<CtMethod<?>> getTopDefinitions();

/**
* Copy the method, where copy means cloning + porting all the references of the old method to the new method (important for recursive methods).
* The copied method is added to the type, with a suffix "Copy".
*
* A new unique method name is given for each copy, and this method can be called several times.
*
* If you want to rename the new method, use {@link Refactoring#changeMethodName(CtMethod, String)} (and not {@link #setSimpleName(String)}, which does not update the references)
*/
CtMethod<?> copyMethod();

}
9 changes: 9 additions & 0 deletions src/main/java/spoon/reflect/declaration/CtType.java
Original file line number Diff line number Diff line change
Expand Up @@ -345,4 +345,13 @@ public interface CtType<T> extends CtNamedElement, CtTypeInformation, CtTypeMemb

@Override
CtType<T> clone();

/**
* Copy the type, where copy means cloning + porting all the references in the clone from the old type to the new type.
*
* The copied type is added to the same package (and this to the factory as well).
*
* A new unique method name is given for each copy, and this method can be called several times.
*/
CtType<?> copyType();
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package spoon.support.reflect.declaration;

import spoon.refactoring.Refactoring;
import spoon.reflect.annotations.MetamodelPropertyField;
import spoon.reflect.declaration.CtFormalTypeDeclarer;
import spoon.reflect.declaration.CtMethod;
Expand Down Expand Up @@ -276,4 +277,10 @@ public boolean isStatic() {
public boolean isAbstract() {
return this.modifierHandler.isAbstract();
}

@Override
public CtMethod<?> copyMethod() {
return Refactoring.copyMethod(this);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package spoon.support.reflect.declaration;

import spoon.SpoonException;
import spoon.refactoring.Refactoring;
import spoon.reflect.annotations.MetamodelPropertyField;
import spoon.reflect.code.CtBlock;
import spoon.reflect.declaration.CtAnnotation;
Expand Down Expand Up @@ -1053,4 +1054,9 @@ public boolean isStatic() {
public boolean isAbstract() {
return this.modifierHandler.isAbstract();
}

@Override
public CtType<?> copyType() {
return Refactoring.copyType(this);
}
}
75 changes: 72 additions & 3 deletions src/test/java/spoon/reflect/ast/CloneTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,26 @@
import org.junit.Test;
import spoon.Launcher;
import spoon.processing.AbstractProcessor;
import spoon.refactoring.Refactoring;
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.reference.CtExecutableReference;
import spoon.reflect.reference.CtReference;
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.List;
import java.util.Map;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -148,4 +149,72 @@ private void onCloned(CtElement source, CtElement target) {
//contract: each visitable elements was cloned exactly once. No more no less.
assertTrue(cl.sourceToTarget.isEmpty());
}

@Test
public void testCopyMethod() throws Exception {
// contract: the copied method is well-formed, lookup of executable references is preserved after copying, esp for recursive methods
Launcher l = new Launcher();
l.getEnvironment().setNoClasspath(true);
l.addInputResource("./src/test/resources/noclasspath/A2.java");
l.buildModel();
CtClass<Object> klass = l.getFactory().Class().get("A2");
CtMethod<?> method = klass.getMethodsByName("c").get(0);
List<CtExecutableReference> elements = method.getElements(new TypeFilter<>(CtExecutableReference.class));
CtExecutableReference methodRef = elements.get(0);

// the lookup is OK in the original node
assertSame(method, methodRef.getDeclaration());

assertEquals("A2", methodRef.getDeclaringType().toString());

// we copy the method
CtMethod<?> methodClone = method.copyMethod();
assertEquals("cCopy", methodClone.getSimpleName());

// useful for debug
methodClone.getBody().insertBegin(l.getFactory().createCodeSnippetStatement("// debug info"));

CtExecutableReference reference = methodClone.getElements(new TypeFilter<>(CtExecutableReference.class)).get(0);
// all references have been updated
assertEquals("cCopy", reference.getSimpleName());
assertSame(methodClone, reference.getDeclaration());
assertEquals("A2", methodClone.getDeclaringType().getQualifiedName());

// now we may want to rename the copied method
Refactoring.changeMethodName(methodClone, "foo");
assertEquals("foo", methodClone.getSimpleName()); // the method has been changed
assertEquals("foo", reference.getSimpleName()); // the reference has been changed
assertSame(methodClone, reference.getDeclaration()); // the lookup still works
assertEquals("A2", methodClone.getDeclaringType().getQualifiedName());

// one can even copy the method several times
methodClone = Refactoring.copyMethod(method);
assertEquals("cCopy", methodClone.getSimpleName());
methodClone = Refactoring.copyMethod(method);
assertEquals("cCopyX", methodClone.getSimpleName());
}



@Test
public void testCopyType() throws Exception {
// contract: the copied type is well formed, it never points to the initial type
Factory factory = ModelUtils.build(new File("./src/main/java/spoon/reflect/visitor/DefaultJavaPrettyPrinter.java"));
CtType<?> intialElement = factory.Type().get(DefaultJavaPrettyPrinter.class);
CtType<?> cloneTarget = intialElement.copyType();
assertEquals("spoon.reflect.visitor.DefaultJavaPrettyPrinterCopy", cloneTarget.getQualifiedName());
// we go over all references
for (CtReference reference: cloneTarget.getElements(new TypeFilter<>(CtReference.class))) {
CtElement declaration = reference.getDeclaration();
if (declaration == null) {
continue;
}

// the core assertion: not a single reference points to the initial element
if (declaration.hasParent(intialElement)) {
fail();
}
}
}

}
5 changes: 5 additions & 0 deletions src/test/resources/noclasspath/A2.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,9 @@ public void b(int param) {
throw e;
}
}

public void c(int param) {
c(param);
}
}
}

0 comments on commit 2428627

Please sign in to comment.