From bd1168ac5ba9ef7895b134aa40c6de2e71774820 Mon Sep 17 00:00:00 2001 From: Martin Monperrus Date: Tue, 27 Feb 2018 17:57:56 +0100 Subject: [PATCH] feature: add Refactoring#copyType and #copyMethod for advanced cloning (#1884) --- .../java/spoon/refactoring/Refactoring.java | 119 +++++++++++++++++- .../spoon/reflect/declaration/CtElement.java | 4 + .../spoon/reflect/declaration/CtMethod.java | 12 ++ .../spoon/reflect/declaration/CtType.java | 9 ++ .../reflect/declaration/CtMethodImpl.java | 7 ++ .../reflect/declaration/CtTypeImpl.java | 6 + .../java/spoon/reflect/ast/CloneTest.java | 75 ++++++++++- src/test/resources/noclasspath/A2.java | 5 + 8 files changed, 233 insertions(+), 4 deletions(-) diff --git a/src/main/java/spoon/refactoring/Refactoring.java b/src/main/java/spoon/refactoring/Refactoring.java index 62a6bc7d1f0..50b440da9d6 100644 --- a/src/main/java/spoon/refactoring/Refactoring.java +++ b/src/main/java/spoon/refactoring/Refactoring.java @@ -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; @@ -41,7 +48,6 @@ private Refactoring() { } public static void changeTypeName(final CtType type, String name) { final String typeQFN = type.getQualifiedName(); - final List> references = Query.getElements(type.getFactory(), new TypeFilter>(CtTypeReference.class) { @Override public boolean matches(CtTypeReference reference) { @@ -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> references = Query.getElements(method.getFactory(), new TypeFilter>(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 void visitCtExecutableReference(CtExecutableReference reference) { + CtExecutable 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 void visitCtTypeReference(CtTypeReference 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 void visitCtExecutableReference(CtExecutableReference reference) { + CtExecutable 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 void visitCtFieldReference(CtFieldReference reference) { + CtField 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}. * diff --git a/src/main/java/spoon/reflect/declaration/CtElement.java b/src/main/java/spoon/reflect/declaration/CtElement.java index 51f95629093..8d5b4921919 100644 --- a/src/main/java/spoon/reflect/declaration/CtElement.java +++ b/src/main/java/spoon/reflect/declaration/CtElement.java @@ -331,6 +331,10 @@ List 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(); diff --git a/src/main/java/spoon/reflect/declaration/CtMethod.java b/src/main/java/spoon/reflect/declaration/CtMethod.java index ee039ec2466..64edbe9f211 100644 --- a/src/main/java/spoon/reflect/declaration/CtMethod.java +++ b/src/main/java/spoon/reflect/declaration/CtMethod.java @@ -16,6 +16,7 @@ */ package spoon.reflect.declaration; +import spoon.refactoring.Refactoring; import spoon.reflect.annotations.PropertyGetter; import spoon.reflect.annotations.PropertySetter; @@ -59,4 +60,15 @@ public interface CtMethod extends CtExecutable, CtTypeMember, CtFormalType * Returns the empty collection if defined here for the first time. */ Collection> 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(); + } diff --git a/src/main/java/spoon/reflect/declaration/CtType.java b/src/main/java/spoon/reflect/declaration/CtType.java index c0baa80c873..c254d8c14cf 100644 --- a/src/main/java/spoon/reflect/declaration/CtType.java +++ b/src/main/java/spoon/reflect/declaration/CtType.java @@ -345,4 +345,13 @@ public interface CtType extends CtNamedElement, CtTypeInformation, CtTypeMemb @Override CtType 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(); } diff --git a/src/main/java/spoon/support/reflect/declaration/CtMethodImpl.java b/src/main/java/spoon/support/reflect/declaration/CtMethodImpl.java index f67a5a48488..226a8553ccd 100644 --- a/src/main/java/spoon/support/reflect/declaration/CtMethodImpl.java +++ b/src/main/java/spoon/support/reflect/declaration/CtMethodImpl.java @@ -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; @@ -276,4 +277,10 @@ public boolean isStatic() { public boolean isAbstract() { return this.modifierHandler.isAbstract(); } + + @Override + public CtMethod copyMethod() { + return Refactoring.copyMethod(this); + } + } diff --git a/src/main/java/spoon/support/reflect/declaration/CtTypeImpl.java b/src/main/java/spoon/support/reflect/declaration/CtTypeImpl.java index 03ed57e7b8d..391415f5f88 100644 --- a/src/main/java/spoon/support/reflect/declaration/CtTypeImpl.java +++ b/src/main/java/spoon/support/reflect/declaration/CtTypeImpl.java @@ -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; @@ -1053,4 +1054,9 @@ public boolean isStatic() { public boolean isAbstract() { return this.modifierHandler.isAbstract(); } + + @Override + public CtType copyType() { + return Refactoring.copyType(this); + } } diff --git a/src/test/java/spoon/reflect/ast/CloneTest.java b/src/test/java/spoon/reflect/ast/CloneTest.java index a9d25d17133..d1d4f6be50c 100644 --- a/src/test/java/spoon/reflect/ast/CloneTest.java +++ b/src/test/java/spoon/reflect/ast/CloneTest.java @@ -4,6 +4,7 @@ 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; @@ -11,18 +12,18 @@ 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; @@ -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 klass = l.getFactory().Class().get("A2"); + CtMethod method = klass.getMethodsByName("c").get(0); + List 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(); + } + } + } + } diff --git a/src/test/resources/noclasspath/A2.java b/src/test/resources/noclasspath/A2.java index 7d8ae7c1f29..f50a1373734 100644 --- a/src/test/resources/noclasspath/A2.java +++ b/src/test/resources/noclasspath/A2.java @@ -11,4 +11,9 @@ public void b(int param) { throw e; } } + + public void c(int param) { + c(param); + } +} } \ No newline at end of file