Scala
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Scala
-
Section 4: Functional Scala
-
Section 5: Collections in Depth
-
Section 6: Type System
-
Section 7: Concurrency and Ecosystem
-
Section 8: Practical Projects
-
Section 9: Interview Practice
-
Section 10: Data Structures and Algorithms in Scala
-
Section 11: More Practice Exercises
-
Section 12: Advanced Functional Patterns
-
Section 13: More Ecosystem
-
Section 14: Scala Collections Library Deep Dive
-
Section 15: Scala Standard Library Deep Dive
-
Section 16: Akka Ecosystem Deep Dive
-
Section 17: Cats and Cats Effect Deep Dive
-
Section 18: Play Framework Deep Dive
-
Section 19: Apache Spark with Scala Deep Dive
-
Section 20: Scala Build Tools Deep Dive
-
Section 21: Scala 3 Specific Features
-
90: Union and Intersection Types
-
Section 22: Scala Testing Deep Dive
-
Section 23: Functional Domain Modeling
-
Section 24: More Data Structures and Algorithms in Scala
-
Section 25: Scala for Data Engineering
-
Section 26: More Practical Projects
-
Section 27: More Interview and Review
-
Section 28: ZIO Ecosystem Deep Dive
-
Section 29: Scala for Machine Learning
-
Section 30: Scala Microservices Architecture
-
Section 31: Scala Type System Deep Dive
-
Section 32: More Practice and Drills
-
Section 33: Scala Performance Deep Dive
-
Section 34: Scala Ecosystem Tooling
-
Section 35: Scala for Reactive Systems
-
Section 36: More Real-World Case Studies
-
Section 37: Scala for Financial Systems
-
Section 38: Scala GraphQL and gRPC
-
Section 39: More Final Projects
-
Section 40: More Interview and Final Review
-
Section 41: Scala for Streaming Data
-
Section 42: Scala Security Practices
-
Section 43: More Language Deep Dive
-
Section 44: Scala Command-Line Tools
-
Section 45: Scala Documentation and Style
-
Section 46: Scala Dependency Management
-
Section 47: More Practical Backend Patterns
-
Section 48: Scala for Event-Driven Architecture
-
Section 49: More Practice Drills Round 2
-
Section 50: Scala Compiler Deep Dive
-
Section 51: Scala for Web Frontends
-
Section 52: More Data Engineering Practice
-
Section 53: Scala Observability
-
Section 54: More Advanced Practice Projects
-
Section 55: Scala for Legacy Java Integration
-
Section 56: More Testing Practice
-
Section 57: Final Mastery Review
-
Section 58: Scala History and Ecosystem Context
-
Section 59: More Concurrency Patterns
-
Section 60: Scala for Configuration Management
-
Section 61: More Domain Modeling Practice
144: Scalafix for Automated Refactoring
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:
- Create a Scalafix rule that searches for all occurrences of the method call
SessionManager.getCurrentUser(). - Rewrite these calls to
UserContext.fetchActiveUser(authToken). - Ensure the rule only targets calls to
SessionManagerand does not accidentally replace other methods that might be namedgetCurrentUserin other classes. - 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.
There are no comments for now.