Skip to content

Method invocation refactor

Joseph Hoare edited this page Mar 15, 2021 · 2 revisions

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.

Building a method invocation refactor

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.

Writing the UseCase

The inputs to AstraCore are bundled up in a UseCase. This contains:

  • A set of ASTOperations - visitors for ASTNodes 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.

Applying the UseCase

To apply the UseCase, we need to use it as an argument to AstraCore.run(). This method accepts 2 arguments:

  • The directory to apply the UseCase 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".