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
85: sbt Plugins and Custom Tasks
I've been looking through our current project, and I noticed we have dozens of TODO comments scattered across the codebase. It's starting to feel like a graveyard of "I'll fix this later" promises. I want a way to scan the source tree and print a summary of these TODOs directly from the sbt shell, so I don't have to rely on my IDE's search tool every time.
Trying to cheat with a simple method
My first instinct is to just throw a helper method right into build.sbt. It seems easy enough. I'll just write a Scala function that reads the files and prints the lines.
def listTodos() = {
val files = new java.io.File("src").walk().toList
files.filter(_.getName.endsWith(".scala")).forEach { f =>
scala.io.Source.fromFile(f).getLines().filter(_.contains("TODO")).foreach(println)
}
}
I load this into sbt and try to run listTodos. It works... technically. But it's just a method call in the shell. It doesn't feel like a part of the build. I can't make it depend on other tasks, and it doesn't have access to sbt's internal logging system. If I run this in a CI environment, it might just vanish into stdout without any proper formatting. It's a script, not a task.
Turning a method into a real Task
To make this a first-class citizen in sbt, I need a taskKey. This is how sbt tracks what a task is and what it returns. I'll define a key and then assign a task to it. I'll make it return a Unit since we're just printing information.
val listTodos = taskKey[Unit]("Scans source files for TODO comments")
listTodos := {
val files = new java.io.File("src").walk().toList
files.filter(_.getName.endsWith(".scala")).forEach { f =>
scala.io.Source.fromFile(f).getLines().filter(_.contains("TODO")).foreach(println)
}
}
Now when I run sbt listTodos, it feels right. It's an official part of the project. But I've hit a snag: println is a bad habit in sbt. Because sbt runs tasks in parallel and handles logging specifically, using println can lead to interleaved output or messages that get swallowed. I need to use the sbt logger.
Getting the console to actually talk back
To get the logger, I need the streams task. This is a built-in sbt task that provides access to the standard output and error streams. I have to tell sbt that my listTodos task depends on streams.
listTodos := {
val log = streams.value.log
val root = baseDirectory.value // Using the sbt-aware base directory instead of "src"
val files = new java.io.File(root, "src").walk().toList
files.filter(_.getName.endsWith(".scala")).forEach { f =>
scala.io.Source.fromFile(f).getLines().filter(_.contains("TODO")).foreach { line =>
log.info(s"Found TODO in ${f.getName}: $line")
}
}
}
Note the .value calls. In sbt, tasks are essentially "recipes." When you call .value, you're telling sbt: "Go execute that task now and give me the result." This is where the magic happens—if streams or baseDirectory were complex tasks themselves, sbt would handle their execution order automatically.
Packaging this as an AutoPlugin
This works great for one project. But what if I have five different Scala services? I don't want to copy-paste this logic into every build.sbt. That's where AutoPlugin comes in. I can wrap this logic into a plugin that automatically adds the task to any project that enables it.
I'll create a file in project/TodoPlugin.scala (since anything in the project/ folder is compiled and available to the build):
import sbt._
import Keys._
object TodoPlugin extends AutoPlugin {
override def trigger = allRequirements // This makes it activate automatically
object autoInterface {
val listTodos = taskKey[Unit]("Scans source files for TODO comments")
}
import autoInterface._
lazy val projectSettings = Seq(
listTodos := {
val log = streams.value.log
val root = baseDirectory.value
// ... same logic as before ...
log.info("TODO scan complete.")
}
)
}
By extending AutoPlugin and setting trigger = allRequirements, I've essentially told sbt: "Whenever you see this plugin in the classpath, just inject these settings into every project." Now my build.sbt is clean again, and I have a reusable tool across my entire organization.
📋 Practical Task
Build a "Dependency Version Checker" Task
Your goal is to create a custom sbt task that checks if a specific "forbidden" library version is being used in the project. This is common in large teams to prevent developers from accidentally using a version of a library known to have a critical bug.
- Create a
taskKeynamedcheckForbiddenDeps. - The task should access the
updatetask (which contains the resolved dependencies of the project). - Iterate through the resolved dependencies and check if any of them match a specific organization and version (e.g.,
"org.apache.logging.log4j"version"2.14.1"). - If a forbidden version is found, use
streams.value.log.errorto print a warning; otherwise, print a success message. - Implement this as an
AutoPluginwithin theproject/directory so it is automatically applied.
There are no comments for now.