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

feature: add methods Refactoring#copyType and Refactoring#copyMethod #1884

Merged
merged 1 commit into from
Feb 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
}