Skip to content

Commit

Permalink
fix(CtDeprecatedRef): removes only unused deprecated methods (INRIA#3200
Browse files Browse the repository at this point in the history
)
  • Loading branch information
MartinWitt committed Feb 11, 2020
1 parent c1f947f commit cfbaf62
Show file tree
Hide file tree
Showing 9 changed files with 462 additions and 44 deletions.
80 changes: 80 additions & 0 deletions src/main/java/spoon/refactoring/CtDeprecatedRefactoring.java
Original file line number Diff line number Diff line change
@@ -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<MethodCallState> 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<MethodCallState> invocationsOfMethod) {
boolean changed = false;
do {
changed = false;
Iterator<MethodCallState> 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);
}
}
117 changes: 117 additions & 0 deletions src/main/java/spoon/refactoring/MethodCallState.java
Original file line number Diff line number Diff line change
@@ -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<CtExecutable<?>> callerMethods;
private Collection<CtType<?>> 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<CtType<?>> 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<CtExecutable<?>> 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);
}
}
96 changes: 96 additions & 0 deletions src/main/java/spoon/refactoring/MethodInvocationSearch.java
Original file line number Diff line number Diff line change
@@ -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<CtExecutable<?>, Collection<CtExecutable<?>>> invocationsOfMethod = new HashMap<>();
private Map<CtExecutable<?>, Collection<CtType<?>>> invocationsOfField = new HashMap<>();

@Override
public <T> void visitCtMethod(CtMethod<T> 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<CtInvocation<?>> invocations = method.getElements(new TypeFilter<>(CtInvocation.class));

List<CtConstructorCall<?>> 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<MethodCallState> getInvocationsOfMethod() {
Collection<MethodCallState> 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 <T> void visitCtField(CtField<T> 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);
}

}
58 changes: 28 additions & 30 deletions src/main/java/spoon/refactoring/Refactoring.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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.
*
* <b>Only use it if you have &gt; jdk8. Older jdk versions may result in wrong results.</b>
* 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<CtMethod>() {
@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<CtType>() {
// @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.
*
* <b>Only use it if you have &gt; jdk8. Older jdk versions may result in wrong results.</b>
* 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);
}
}
Loading

0 comments on commit cfbaf62

Please sign in to comment.