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
201: Compiler Plugins in Scala
I've noticed a recurring pattern when developers first dive into the "dark arts" of the Scala compiler: they assume that a compiler plugin is just a macro that you've told to run on every file in the project. It's a logical leap, but it's fundamentally wrong. If you treat a plugin like a global macro, you're going to hit a wall the moment you need to perform a transformation that depends on information the macro system simply can't see.
Plugins aren't just "Global Macros"
Here is the concrete difference. A macro is triggered by a specific call site—an annotation or a method call. The compiler says, "Oh, I see a macro here, let me expand this specific piece of code." It's local and surgical. A compiler plugin, however, is a hook into the scalac pipeline itself. It doesn't wait to be called; it owns a Phase.
Imagine you want to enforce a rule that every var in your project must be prefixed with the word mutable (e.g., mutableUserAge). If you tried to do this with macros, you'd have to annotate every single class in your codebase with something like @EnforceNaming. That's tedious and defeats the purpose. A compiler plugin doesn't care about annotations. It can simply walk the entire Abstract Syntax Tree (AST) of every single file during the typer phase and throw a compile-time error if it finds a var that doesn't follow your rule. I've used this approach in large-scale migrations to force teams to adhere to new patterns without having to manually review a thousand pull requests.
Hooking into the Compiler Phase Pipeline
To write a plugin, you aren't writing a function; you're defining a CompilerPlugin trait. The heart of the plugin is the Phase. The Scala compiler is essentially a series of transformations: it takes source code, turns it into an AST, types it, optimizes it, and finally generates bytecode. Each of these steps is a phase.
When you define your own phase, you decide where it fits. Do you want to run before the typer phase (working with "raw" untyped trees) or after it? Most of the time, you'll want to run after typer because you'll want to know the actual types of the expressions you're modifying. If you try to check if a variable is a String before the typer phase has run, you're just guessing.
import scala.compiler.plugins.*
import scala.compiler.scala.nodes.*
class NamingEnforcerPlugin extends Plugin {
override def name: String = "MutableNamingEnforcer"
override def install(settings: Settings): PluginComponents = {
new PluginComponents(settings) {
override def phaseNames: Seq[String] = Seq("namingEnforcer")
override def execute(args: Seq[String]): Unit = {
// This is where the magic happens.
// We add a new phase to the compiler's pipeline.
addPhase(new Phase {
override def name: String = "namingEnforcer"
override def run(tree: Tree): Tree = {
tree.transform {
case v: ValDef if v.isVar && !v.name.startsWith("mutable") =>
reporter.report(v.pos, "All vars must start with 'mutable'!")
v
case other => other
}
}
})
}
}
}
}
I'll be honest with you: the Tree API is dense. You'll spend a lot of time digging through the Scala source code to figure out if you're looking at a ValDef, a Select, or an Apply. But once you get the hang of the transform method, you realize you have total control over the program's structure before it ever hits the JVM.
Wiring the Plugin into the Build
You can't just "import" a compiler plugin into your source code. Since the plugin modifies the compiler itself, it has to be loaded by the build tool (sbt) before the compilation of your main code begins. In your build.sbt, you'll typically add it as a compiler option.
It looks something like server.optimizations = true or using the addCompilerPlugin helper. If you're distributing the plugin, you'll package it as a separate JAR. The compiler then loads this JAR, instantiates your Plugin class, and merges your Phase into the pipeline. It's a bit of a dance, but it's the only way to achieve truly transparent, project-wide transformations.
📋 Practical Task
Exercise: The "Forbidden API" Guardian
Your task is to build a compiler plugin that prevents developers from using a specific "forbidden" method across the entire project. In this case, let's pretend java.lang.System.out.println is banned in favor of a corporate logging library.
Requirements:
- Create a Scala compiler plugin that hooks into the pipeline after the
typerphase. - The plugin must scan the AST for any method call to
printlnon theSystem.outobject. - When such a call is found, the plugin should use the
reporterto issue a compile-time error:"Direct use of System.out.println is forbidden. Please use the Logger class instead." - The plugin should not crash the compiler when encountering other method calls.
Hint: You will need to pattern match on Apply nodes and check if the function being applied is a Select that points to the println method of System.out.
There are no comments for now.