diff --git a/src/main/java/spoon/refactoring/CtDeprecatedRefactoring.java b/src/main/java/spoon/refactoring/CtDeprecatedRefactoring.java new file mode 100644 index 00000000000..013d20c77bf --- /dev/null +++ b/src/main/java/spoon/refactoring/CtDeprecatedRefactoring.java @@ -0,0 +1,80 @@ +/** + * Copyright (C) 2006-2019 INRIA and contributors + * + * Spoon is available either under the terms of the MIT License (see LICENSE-MIT.txt) of the Cecill-C License (see LICENSE-CECILL-C.txt). You as the user are entitled to choose the terms under which to adopt Spoon. + */ +package spoon.refactoring; + +import java.util.Collection; +import java.util.Iterator; +import spoon.Launcher; +import spoon.reflect.CtModel; +import spoon.reflect.declaration.CtExecutable; +import spoon.reflect.declaration.CtField; +import spoon.reflect.visitor.filter.TypeFilter; +import spoon.support.sniper.SniperJavaPrettyPrinter; + +public class CtDeprecatedRefactoring { + + protected void removeDeprecatedMethods(String inputPath, String resultPath) { + Launcher spoon = new Launcher(); + spoon.addInputResource(inputPath); + spoon.setSourceOutputDirectory(resultPath); + doRefactor(spoon); + } + + protected void removeDeprecatedMethods(String path) { + removeDeprecatedMethods(path, path); + } + + private void doRefactor(Launcher spoon) { + MethodInvocationSearch processor = new MethodInvocationSearch(); + spoon.getEnvironment().setPrettyPrinterCreator(() -> { + return new SniperJavaPrettyPrinter(spoon.getEnvironment()); + }); + CtModel model = spoon.buildModel(); + model.getElements(new TypeFilter<>(CtField.class)).forEach(processor::scan); + model.getElements(new TypeFilter<>(CtExecutable.class)).forEach(processor::scan); + + Collection invocationsOfMethod = processor.getInvocationsOfMethod(); + removeUncalledMethods(invocationsOfMethod); + + for (CtExecutable method : model.getElements(new TypeFilter<>(CtExecutable.class))) { + if (method.hasAnnotation(Deprecated.class) + && invocationsOfMethod.stream().noneMatch(v -> v.getMethod().equals(method))) { + method.delete(); + } + } + spoon.prettyprint(); + } + + private void removeUncalledMethods(Collection invocationsOfMethod) { + boolean changed = false; + do { + changed = false; + Iterator iterator = invocationsOfMethod.iterator(); + while (iterator.hasNext()) { + MethodCallState entry = iterator.next(); + if (!entry.getMethod().hasAnnotation(Deprecated.class)) { + // only look at deprecated Methods + continue; + } + if (entry.checkCallState()) { + // removes never called and deprecated Methods + changed = true; + // remove the method for further lookups in value collections + invocationsOfMethod.forEach(v -> v.remove(entry.getMethod())); + // remove the method as key + iterator.remove(); + } + if (entry.getCallerMethods().size() == 1 && entry.getCallerMethods().contains(entry.getMethod()) + && entry.getCallerFields().isEmpty()) { + // removes deprecated methods, that are only called by itself + changed = true; + invocationsOfMethod.forEach(invocation -> invocation.remove(entry.getMethod())); + iterator.remove(); + } + } + } while (changed); + } +} diff --git a/src/main/java/spoon/refactoring/MethodCallState.java b/src/main/java/spoon/refactoring/MethodCallState.java new file mode 100644 index 00000000000..951e0300b2d --- /dev/null +++ b/src/main/java/spoon/refactoring/MethodCallState.java @@ -0,0 +1,117 @@ +/** + * Copyright (C) 2006-2019 INRIA and contributors + * + * Spoon is available either under the terms of the MIT License (see LICENSE-MIT.txt) of the Cecill-C License (see LICENSE-CECILL-C.txt). You as the user are entitled to choose the terms under which to adopt Spoon. + */ +package spoon.refactoring; + +import java.util.ArrayList; +import java.util.Collection; + +import spoon.reflect.declaration.CtExecutable; +import spoon.reflect.declaration.CtType; + +/** + * This class is for the call state of a method. A method can be called by + * fields in a type e.g. class or by methods. Both cases are handled in this + * class. For checking calls by fields use methods using CtType, for fields use + * the methods using CtExecutable. A method is never called if both collections + * are empty. + */ +public class MethodCallState { + private CtExecutable method; + private Collection> callerMethods; + private Collection> callerFields; + + /** + * + * @param method for saving it's call state. + */ + public MethodCallState(CtExecutable method) { + this.method = method; + this.callerFields = new ArrayList<>(); + this.callerMethods = new ArrayList<>(); + } + + /** + * Adds a CtExecutable to the methods invoking this method. Adding the same + * method again doesn't change the state. + * + * @param method invoking the method. + * @see java.util.Collection#add(java.lang.Object) + */ + public void add(CtExecutable method) { + callerMethods.add(method); + } + + /** + * Adds a CtType to the fields invoking this method. Adding the same CtType + * again doesn't change the state. + * + * @param type invoking the method with an initializer. + * @see java.util.Collection#add(java.lang.Object) + */ + public void add(CtType type) { + callerFields.add(type); + } + + /** + * Getter for the method, without saved call state. Returns the CtExecutable and + * not a copy. + * + * @return method without saved call state. + */ + public CtExecutable getMethod() { + return method; + } + + /** + * Returns a collection containing all types invoking the method with a field. + * Even if a CtType invokes multiple times with different fields the method, the + * type is only present once. Returns the collection and not a copy. Changes to + * collection are directly backed in the state. + * + * @return Collection containing all types invoking the method with a field. + */ + public Collection> getCallerFields() { + return callerFields; + } + + /** + * Returns a collection containing all CtExecutable invoking the method. Even if + * a CtExecutable invokes multiple times the method, the CtExecutable is only + * present once. Returns the collection and not a copy. Changes to collection + * are directly backed in the state. + * + * @return Collection containing all CtExecutable invoking the method. + */ + public Collection> getCallerMethods() { + return callerMethods; + } + + /** + * Checks the call state for the method. + * + * @return True if the method has no known call, false otherwise. + * @see java.util.Collection#isEmpty() + */ + public boolean checkCallState() { + return callerMethods.isEmpty() && callerFields.isEmpty(); + } + + public boolean contains(CtType o) { + return callerFields.contains(o); + } + + public boolean contains(CtExecutable o) { + return callerMethods.contains(o); + } + + public void remove(CtType o) { + callerFields.remove(o); + } + + public void remove(CtExecutable o) { + callerMethods.remove(o); + } +} diff --git a/src/main/java/spoon/refactoring/MethodInvocationSearch.java b/src/main/java/spoon/refactoring/MethodInvocationSearch.java new file mode 100644 index 00000000000..014b7d42b3b --- /dev/null +++ b/src/main/java/spoon/refactoring/MethodInvocationSearch.java @@ -0,0 +1,96 @@ +/** + * Copyright (C) 2006-2019 INRIA and contributors + * + * Spoon is available either under the terms of the MIT License (see LICENSE-MIT.txt) of the Cecill-C License (see LICENSE-CECILL-C.txt). You as the user are entitled to choose the terms under which to adopt Spoon. + */ +package spoon.refactoring; + +import java.util.HashSet; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import spoon.reflect.code.CtConstructorCall; +import spoon.reflect.code.CtInvocation; +import spoon.reflect.code.CtLambda; +import spoon.reflect.declaration.CtExecutable; +import spoon.reflect.declaration.CtField; +import spoon.reflect.declaration.CtMethod; +import spoon.reflect.declaration.CtType; +import spoon.reflect.visitor.CtScanner; +import spoon.reflect.visitor.filter.TypeFilter; +import spoon.support.reflect.code.CtLambdaImpl; + +/** + * Class for creating a mapping from CtExecutable to all known calls from fields + * and methods. + */ +public class MethodInvocationSearch extends CtScanner { + private Map, Collection>> invocationsOfMethod = new HashMap<>(); + private Map, Collection>> invocationsOfField = new HashMap<>(); + + @Override + public void visitCtMethod(CtMethod method) { + if (!method.getPosition().isValidPosition()) { + return; + } + final CtExecutable transformedMethod; + if (method instanceof CtLambda) { + // because lambdas are difficult we transform them + // in a method public void foo(){ List.of("a").stream().forEach($method)} + // we need the ref to foo() and not the ref to the lambda expression + transformedMethod = method.getParent(CtExecutable.class); + } else { + transformedMethod = method; + } + List> invocations = method.getElements(new TypeFilter<>(CtInvocation.class)); + + List> constructors = method.getElements(new TypeFilter<>(CtConstructorCall.class)); + if (!invocationsOfMethod.containsKey(method) && !method.isImplicit() && !(method instanceof CtLambdaImpl)) { + // now every method should be key + invocationsOfMethod.put(method, Collections.emptyList()); + } + invocations.stream().filter(v -> !v.isImplicit()).map(v -> v.getExecutable().getExecutableDeclaration()) + .filter(Objects::nonNull).filter(v -> v.getPosition().isValidPosition()) + .forEach(v -> invocationsOfMethod.merge(v, new HashSet<>(Arrays.asList(transformedMethod)), + (o1, o2) -> Stream.concat(o1.stream(), o2.stream()).collect(Collectors.toCollection(HashSet::new)))); + constructors.stream().filter(v -> !v.isImplicit()).map(v -> v.getExecutable().getExecutableDeclaration()) + .filter(Objects::nonNull) + .forEach(v -> invocationsOfMethod.merge(v, new HashSet<>(Arrays.asList(transformedMethod)), + (o1, o2) -> Stream.concat(o1.stream(), o2.stream()).collect(Collectors.toCollection(HashSet::new)))); + super.visitCtMethod(method); + } + + public Collection getInvocationsOfMethod() { + Collection transformedResult = new HashSet<>(); + Stream.concat(invocationsOfMethod.keySet().stream(), invocationsOfField.keySet().stream()).map(MethodCallState::new) + .forEach(transformedResult::add); + for (MethodCallState methodCallState : transformedResult) { + invocationsOfField.getOrDefault(methodCallState.getMethod(), Collections.emptyList()) + .forEach(methodCallState::add); + invocationsOfMethod.getOrDefault(methodCallState.getMethod(), Collections.emptyList()) + .forEach(methodCallState::add); + } + return transformedResult; + } + + @Override + public void visitCtField(CtField field) { + field.getElements(new TypeFilter<>(CtInvocation.class)).stream() + .map(call -> call.getExecutable().getExecutableDeclaration()) + .forEach(method -> invocationsOfField.merge(method, new HashSet<>(Arrays.asList(field.getDeclaringType())), + (o1, o2) -> Stream.concat(o1.stream(), o2.stream()).collect(Collectors.toCollection(HashSet::new)))); + field.getElements(new TypeFilter<>(CtConstructorCall.class)).stream() + .map(call -> call.getExecutable().getExecutableDeclaration()) + .forEach(method -> invocationsOfField.merge(method, new HashSet<>(Arrays.asList(field.getDeclaringType())), + (o1, o2) -> Stream.concat(o1.stream(), o2.stream()).collect(Collectors.toCollection(HashSet::new)))); + super.visitCtField(field); + } + +} diff --git a/src/main/java/spoon/refactoring/Refactoring.java b/src/main/java/spoon/refactoring/Refactoring.java index 4f6396b2fa6..948d466eeaa 100644 --- a/src/main/java/spoon/refactoring/Refactoring.java +++ b/src/main/java/spoon/refactoring/Refactoring.java @@ -5,9 +5,7 @@ */ package spoon.refactoring; -import spoon.Launcher; import spoon.SpoonException; -import spoon.processing.AbstractProcessor; import spoon.reflect.code.CtLocalVariable; import spoon.reflect.declaration.CtExecutable; import spoon.reflect.declaration.CtField; @@ -19,7 +17,6 @@ import spoon.reflect.visitor.CtScanner; import spoon.reflect.visitor.Query; import spoon.reflect.visitor.filter.TypeFilter; -import spoon.support.sniper.SniperJavaPrettyPrinter; import java.util.List; @@ -209,34 +206,35 @@ public static void changeLocalVariableName(CtLocalVariable localVariable, Str new CtRenameLocalVariableRefactoring().setTarget(localVariable).setNewName(newName).refactor(); } - /** Deletes all deprecated methods in the given path */ + /** + * Removes all deprecated methods for all java files in the given path. + * + * Only use it if you have > jdk8. Older jdk versions may result in wrong results. + * Only removes deprecated methods, that are no longer invoked by any method or + * field. If a deprecated method is invoked by a deprecated method only, which + * can be removed, the deprecated method is removed too. + * + * Result is written to /$Path/$Package. For different output folder see + * {@link Refactoring#removeDeprecatedMethods(String, String)}. + * + * @param input Path to java files in folder. + */ public static void removeDeprecatedMethods(String path) { - Launcher spoon = new Launcher(); - spoon.addInputResource(path); - spoon.setSourceOutputDirectory(path); - spoon.addProcessor(new AbstractProcessor() { - @Override - public void process(CtMethod method) { - if (method.hasAnnotation(Deprecated.class)) { - method.delete(); - } - } - }); + new CtDeprecatedRefactoring().removeDeprecatedMethods(path); + } - // does not work, see https://github.com/INRIA/spoon/issues/3183 -// spoon.addProcessor(new AbstractProcessor() { -// @Override -// public void process(CtType type) { -// if (type.hasAnnotation(Deprecated.class)) { -// type.delete(); -// } -// } -// }); - - spoon.getEnvironment().setPrettyPrinterCreator(() -> { - return new SniperJavaPrettyPrinter(spoon.getEnvironment()); - } - ); - spoon.run(); + /** + * Removes all deprecated methods for all java files in the given path. + * + * Only use it if you have > jdk8. Older jdk versions may result in wrong results. + * Only removes deprecated methods, that are no longer invoked by any method or + * field. If a deprecated method is invoked by a deprecated method only, which + * can be removed, the deprecated method is removed too. + * + * @param input Path to java files in folder. + * @param resultPath Path for the refactored java files. + */ + public static void removeDeprecatedMethods(String input, String resultPath) { + new CtDeprecatedRefactoring().removeDeprecatedMethods(input, resultPath); } } diff --git a/src/test/java/spoon/test/refactoring/RefactoringTest.java b/src/test/java/spoon/test/refactoring/RefactoringTest.java index 00de4dda872..1c18e35be0a 100644 --- a/src/test/java/spoon/test/refactoring/RefactoringTest.java +++ b/src/test/java/spoon/test/refactoring/RefactoringTest.java @@ -16,13 +16,28 @@ */ package spoon.test.refactoring; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Comparator; +import java.util.List; + import org.junit.Test; + import spoon.Launcher; import spoon.refactoring.Refactoring; import spoon.reflect.code.BinaryOperatorKind; import spoon.reflect.code.CtBinaryOperator; import spoon.reflect.code.CtInvocation; import spoon.reflect.declaration.CtClass; +import spoon.reflect.declaration.CtMethod; import spoon.reflect.reference.CtTypeReference; import spoon.reflect.visitor.Query; import spoon.reflect.visitor.filter.AbstractFilter; @@ -31,13 +46,6 @@ import spoon.test.refactoring.processors.ThisTransformationProcessor; import spoon.test.refactoring.testclasses.AClass; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; - public class RefactoringTest { @Test public void testRefactoringClassChangeAllCtTypeReferenceAssociatedWithClassConcerned() { @@ -132,12 +140,53 @@ public void testTransformedInstanceofAfterATransformation() { assertEquals("o", instanceofInvocation.getLeftHandOperand().toString()); assertEquals("spoon.test.refactoring.testclasses.AClassX", instanceofInvocation.getRightHandOperand().toString()); } - @Test - public void testremoveDeprecatedMethods() { - // this shows that we can remove deprecated methods - // with Spoon - // ... and that the sniper mode works well-enough to be applied on Spoon itself - // Refactoring.removeDeprecatedMethods("src/main/java"); + public void testRemoveDeprecatedMethods() { + if (checkJavaVersion()) { + return; + } + // clean dir if exists + try { + Files.walk(Paths.get("target/deprecated-refactoring")).sorted(Comparator.reverseOrder()) + .map(Path::toFile).forEach(File::delete); + } catch (Exception e) { + // error is kinda okay + } + // create Spoon + String input = "src/test/resources/deprecated/input"; + String resultPath = "target/deprecated-refactoring"; + String correctResultPath = "src/test/resources/deprecated/correctResult"; + Launcher spoon = new Launcher(); + + spoon.addInputResource(correctResultPath); + List> correctResult = spoon.buildModel().getElements(new TypeFilter<>(CtMethod.class)); + // save Methods before cleaning + // now refactor code + Refactoring.removeDeprecatedMethods(input, resultPath); + // verify result + spoon = new Launcher(); + spoon.addInputResource(resultPath); + List> calculation = spoon.buildModel().getElements(new TypeFilter<>(CtMethod.class)); + assertTrue(calculation.stream().allMatch(correctResult::contains)); + assertTrue(correctResult.stream().allMatch(calculation::contains)); + //clean again + try { + Files.walk(Paths.get("target/deprecated-refactoring")).sorted(Comparator.reverseOrder()) + .map(Path::toFile).forEach(File::delete); + } catch (Exception e) { + // error is kinda okay + } + } + /** + * checks if current java version is >=9. + * True if <=8 else false; + */ + private boolean checkJavaVersion() { + String property = System.getProperty("java.version"); + if (property != null && !property.isEmpty()) { + // java 8 and less are versionning "1.X" where 9 and more are directly versioned "X" + return property.startsWith("1."); + } + return false; } -} \ No newline at end of file +} diff --git a/src/test/resources/deprecated/correctResult/Bar.java b/src/test/resources/deprecated/correctResult/Bar.java new file mode 100644 index 00000000000..a44166dc3fc --- /dev/null +++ b/src/test/resources/deprecated/correctResult/Bar.java @@ -0,0 +1,12 @@ +package deprecated; + +import java.util.List; + +public class Bar { + private boolean test = new Foo("a").test5(); + private boolean unused; + + public static foo() { + List.of("a").stream().forEach((v) -> new Foo(v).test4()); + } +} diff --git a/src/test/resources/deprecated/correctResult/Foo.java b/src/test/resources/deprecated/correctResult/Foo.java new file mode 100644 index 00000000000..834ac6e98d7 --- /dev/null +++ b/src/test/resources/deprecated/correctResult/Foo.java @@ -0,0 +1,18 @@ +package deprecated; + +public class Foo { + // ref in Bar.test and Bar.foo() + @Deprecated + public Foo(String s) { + } + + // ref in Bar.foo() + @Deprecated + public void test4() { + } + + // ref in Bar.test + @Deprecated + public void test5() { + } +} diff --git a/src/test/resources/deprecated/input/Bar.java b/src/test/resources/deprecated/input/Bar.java new file mode 100644 index 00000000000..a44166dc3fc --- /dev/null +++ b/src/test/resources/deprecated/input/Bar.java @@ -0,0 +1,12 @@ +package deprecated; + +import java.util.List; + +public class Bar { + private boolean test = new Foo("a").test5(); + private boolean unused; + + public static foo() { + List.of("a").stream().forEach((v) -> new Foo(v).test4()); + } +} diff --git a/src/test/resources/deprecated/input/Foo.java b/src/test/resources/deprecated/input/Foo.java new file mode 100644 index 00000000000..162e3afe90c --- /dev/null +++ b/src/test/resources/deprecated/input/Foo.java @@ -0,0 +1,36 @@ +package deprecated; + +public class Foo { + // ref in Bar.test and Bar.foo() + @Deprecated + public Foo(String s) { + } + + // no ref should be deleted + @Deprecated + public void test1() { + test2(); + } + + // ref in Foo.test1() + @Deprecated + public boolean test2() { + return true; + } + + // should be deleted because only ref in Foo.test3() + @Deprecated + public void test3() { + test3(); + } + + // ref in Bar.foo() + @Deprecated + public void test4() { + } + + // ref in Bar.test + @Deprecated + public void test5() { + } +}