Skip to Content
Course content

144: Scalafix for Automated Refactoring

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was tasked with upgrading a massive monolith from an older version of a core library to a new one. The breaking change was simple in theory: a method called getUser(id: String) had been replaced by getUser(request: UserRequest). The problem? That method was called in over 400 places across twelve different modules. I spent an entire Tuesday doing a "find and replace" and manually fixing the resulting compiler errors. It was soul-crushing work, and I inevitably missed two calls that caused a production regression. I remember staring at the screen thinking, "There has to be a way to tell the compiler to just fix this for me." That's exactly why Scalafix exists.

Unlike a regular expression or a simple search-and-replace, Scalafix doesn't just look at your code as text; it looks at it as an Abstract Syntax Tree (AST). It understands the scope, the types, and the actual structure of your Scala program. This means you can write a rule that says "find every call to this specific method in this specific class and rewrite the arguments," and it will do it accurately without accidentally touching a variable that happens to have the same name in a different class.

Harnessing the AST for Large-Scale Changes

When you use Scalafix, you're essentially writing a script that traverses your code. The tool provides a set of built-in rules for common tasks—like removing unused imports or fixing common Scala 2 to 3 migrations—but the real power comes when you define your own rules. You define a Rule that matches specific patterns in the AST and returns a Patch. A patch is basically a set of instructions telling Scalafix how to modify the source code.

I usually start by using the built-in rules to clean up the technical debt before moving to custom migrations. For example, if you've recently changed a data model and need to update how every instance of a case class is instantiated, Scalafix can handle the boilerplate of adding a new parameter to every constructor call across your entire codebase in seconds. It's a massive relief to move from "manual editing" to "automated refactoring."

Implementing a Custom Migration Rule

To write a custom rule, you'll typically create a separate Scala project that depends on the Scalafix library. You define a class that extends Rule and override the fix method. Inside this method, you can use pattern matching on the syntax tree to find the exact code you want to change.

import scalafix.rewrite.File
import scalafix.rewrite.Patch
import scalafix.scala.ast.Tree

class MyMigrationRule extends Rule {
  override def fix(file: File): Patch = {
    // We look for method calls named "oldMethod"
    file.findCalls("oldMethod").map { call =>
      // We replace the call with "newMethod" and adjust arguments
      Patch.replaceTree(call, "newMethod(newArg)")
    }.merge
  }
}

Notice how we aren't manipulating strings here. We are manipulating Tree objects. If you have a method named oldMethod in a different package that you don't want to change, you can check the symbol of the tree to ensure it originates from the correct class. This precision is what makes it safe for production environments.

Integrating Scalafix into the sbt Workflow

You don't want to run these tools in a vacuum. The most common way to use Scalafix is via the sbt plugin. Once you've added the plugin to your plugins.sbt, you can configure the rules you want to run in your build.sbt. I highly recommend setting up a specific configuration for "heavy" refactorings so you don't accidentally run a massive code rewrite every time you compile.

You can run Scalafix in two modes: dry-run and apply. I always start with a dry run. It will show you a diff of every single change it intends to make. Once the diff looks sane and you've verified a few samples, you run the apply command to actually overwrite the files. This workflow—Analyze, Verify, Apply—is the only way to maintain sanity when refactoring thousands of lines of code.




📋 Practical Task

Exercise: Migrating Legacy UserSession Calls to UserContext

You have a legacy codebase where a service method SessionManager.getCurrentUser() (which returns a User object) is being deprecated. It is being replaced by UserContext.fetchActiveUser(token: String). To make this transition, the token is now available as a global variable in the current scope called authToken.

Your Task:

  1. Create a Scalafix rule that searches for all occurrences of the method call SessionManager.getCurrentUser().
  2. Rewrite these calls to UserContext.fetchActiveUser(authToken).
  3. Ensure the rule only targets calls to SessionManager and does not accidentally replace other methods that might be named getCurrentUser in other classes.
  4. Test your rule against a sample file containing both the target call and a "decoy" call (e.g., AccountManager.getCurrentUser()) to verify that only the correct one is refactored.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.