-
Notifications
You must be signed in to change notification settings - Fork 35
Method invocation refactor
Let's say we have an interface in our code base, FooBarInterface
:
public interface FooBarInterface {
@Deprecated
void doFoo();
void doBar();
}
It has two methods, doFoo
and doBar
. The doFoo
method has been deprecated and a new preferred method named doBar
has been added. We want to update all the existing callers of doFoo
so that they call doBar
instead.
Here is an example caller of our FooBarInterface
methods, along with how we want it to look after our refactor.
Before | After |
---|---|
public class FooBarCaller { private FooBarInterface fooBarInterface; FooBarCaller(FooBarInterface fooBarInterface) { this.fooBarInterface = fooBarInterface; } void doThing() { fooBarInterface.doFoo(); } } |
public class FooBarCaller { private FooBarInterface fooBarInterface; FooBarCaller(FooBarInterface fooBarInterface) { this.fooBarInterface = fooBarInterface; } void doThing() { fooBarInterface.doBar(); } } |
If you haven't already, please see Start with tests! to see how files like these can be used for unit testing.
A method invocation can be largely distinguished by three main properties:
- The method name
- The type which declares it
- The parameters
When we build a MethodInvocationRefactor
, we use a MethodMatcher
to specify these properties, telling Astra which method invocations we want to match on, and refactor. Here's an example for our case:
MethodMatcher.builder()
.withFullyQualifiedDeclaringType("org.alfasoftware.astra.example.target.FooBarInterface")
.withMethodName("doFoo")
.build()
Note that we didn't supply the parameters - which means we just haven't filtered out any candidates by that property. If we had two methods with the same name and declaring type, we could specify these to make sure we just refactor the method we want.
We supply this MethodMatcher
to a MethodInvocationRefactor
, along with a MethodInvocationRefactor.Changes
describing the changes we want to make:
MethodInvocationRefactor
.from(
MethodMatcher.builder()
.withFullyQualifiedDeclaringType("org.alfasoftware.astra.example.target.FooBarInterface")
.withMethodName("doFoo")
.build())
.to(
new MethodInvocationRefactor.Changes().toNewMethodName("doBar"))
Here, we want to update the name of the method invocation. There are other options we could specify using Changes
, including changing the type that we invoke on. That would be useful if we wanted to invoke a different static method, for example - and would handle things like updating the static import in our file.
The inputs to AstraCore
are bundled up in a UseCase
. This contains:
- A set of
ASTOperations
- visitors forASTNodes
which specify analysis or refactoring tasks, - Any additional classpaths needed for building a detailed AST.
You can write a new UseCase
using ASTOperations
, like our MethodInvocationRefactor
.
public class FooBarUseCase implements UseCase {
@Override
public Set<? extends ASTOperation> getOperations() {
return Sets.newHashSet(
MethodInvocationRefactor
.from(
MethodMatcher.builder()
.withFullyQualifiedDeclaringType("org.alfasoftware.astra.example.target.FooBarInterface")
.withMethodName("doFoo")
.build())
.to(
new MethodInvocationRefactor.Changes().toNewMethodName("doBar"))
);
}
@Override
public Set<String> getAdditionalClassPathEntries() {
return new HashSet<>(Arrays.asList(
"C:\Users\Me\.m2\repository\com\example\1.0-SNAPSHOT\foobar-api-1.0-SNAPSHOT.jar",
"C:\Users\Me\.m2\repository\com\example\1.0-SNAPSHOT\foobar-impl-1.0-SNAPSHOT.jar"
));
}
}
Here, we also supply the classpath to the jar files containing the FooBarInterface
and FooBarClass
, by overriding UseCase.getAdditionalClassPathEntries()
. These classpaths help Astra to interpret our source code. In this case they allow Astra to make inferences, like seeing that a FooBarClass
implements FooBarInterface
.
To illustrate this example, we could imagine that our interface is in foobar-api
, and the class in foobar-impl
, so we supply these as absolute paths to local jar files. The example shows paths to a local maven repository.
To apply the UseCase
, we need to use it as an argument to AstraCore.run()
. This method accepts 2 arguments:
- The
directory
to apply theUseCase
over. - The
UseCase
to apply.
public class AstraRunner {
private static final String directoryPath = "C:/Code/MyRepository";
private static final UseCase useCase = new FooBarUseCase();
public static void main(String[] args) {
AstraCore.run(
directoryPath,
useCase);
}
}
And that's it! Astra should now update all the invocations of method doFoo
to doBar
in all the source files in "C:/Code/MyRepository"
.