Step by step Scala Compiler plug-in

In this article I'll try to show how to implement a basic Scala compiler plug-in. My starting point is this excellent article from Lex Spoon which provides the main information required to understand how compiler plug-ins work. It gives a simple example: a static code checker that raises compilation errors as soon as a division by zero occurs (division by the constant 0).

A compiler plug-in is some Scala code that can be injected as a specific phase of the compilation process of a given project and that allows you to access to the Abstract Syntax Tree (AST) corresponding to the Scala source code that is being compiled. Then it's a matter of matching the proper patterns, raise erros and warnings and, if needed, transform the AST (this, I did not try so far...).

Our goal in this article is to implement the so called "mutable-phobia" plug-in which raises errors as soon as a var definition gets detected !!!
Here is step by step how to do this:

Creating a compiler plug-in project under eclipse

Create a standard Scala project. Then take the scala.tools.nsc.jar from your eclipse plug-in repository (i.e. the <eclipse_root>/plugins/ folder) and add it as a library of your "java build path" (from the project properties):



Note that the jar name depends on the version of Scala that you're using within eclipse. So, in case of update you may need to change your project settings. Otherwise if you are referencing a specific Scala release you can add the <Scala release root folder>/lib/scala-compiler.jar instead. This additional jar provides you with the access to the scala.tools.nsc package that contains all the entities required to implement a compiler plug-in.
But before implementing our "var hunter" plug-in, we need to get a bit more familiar with the Scala's AST and more specifically the way to match specific patterns. That's why we'll try first to create a very simple plug-in that displays the AST.

Creating the “Display AST” plug-in

The plug-in skeleton requires two files. One configuration file called scalac-plugin.xml which declares the plug-in class:

  varhunter
  mycompilerplugin.VarHunter

And of course, the plug-in class itself:
package mycompilerplugin

import scala.tools.nsc
import nsc.Global
import nsc.Phase
import nsc.ast.TreeBrowsers
import nsc.plugins.Plugin
import nsc.plugins.PluginComponent

class VarHunter(val global: Global) extends Plugin {
  import global._

  val name = "varhunter"
  val description = "Detects ver definition"
  val components = List[PluginComponent](Component)
  
  private object Component extends PluginComponent {
    val global: VarHunter.this.global.type = VarHunter.this.global
    val runsAfter = "refchecks"
    // Using the Scala Compiler 2.8.x the runsAfter should be written as below
    // val runsAfter = List[String]("refchecks");
    val phaseName = VarHunter.this.name
    def newPhase(_prev: Phase) = new VarHunterPhase(_prev)    
    
    class VarHunterPhase(prev: Phase) extends StdPhase(prev) {
      override def name = VarHunter.this.name
      def apply(unit: CompilationUnit) {
          //TODO: add the plugin code here
      }
    }
  }
}
Your project should look like this:
         

When the plug-in is executed, the apply method of the VarHunterPhase class is invoked, giving you the access to a scala.tools.nsc.CompilationUnits.CompilationUnit instance (called unit) which reflects the source code being compiled. Then unit.body(an instance of scala.reflect.Tree) represents the AST itself.

For the moment, it's hard to figure out what kind of pattern has to be matched in order to detect var definitions. That's why we start with displaying the AST. Let's add the following code (within the apply method):
// allows to browse the whole AST
VarHunter.this.global.treeBrowsers.create().browse(unit.body)
Deploying the plug-in is straightforward: basically export your project as a jar file in a given folder.

Creating a test project

Create another standard Scala project (e.g. CompilerPluginTest) and, from the "Project properties" dialog, go to the "Scala Compiler properties" tab. Check the "Use Project Settings" checkbox:
           

And, in the "Xpluginsdir" field, enter the path to the folder containing the plug-in:
           

Then add a new Scala object to this new project. As soon as this newly created file is added, eclipse starts a first compilation automatically. The plug-in is then invoked and, if everything works fine, you should see the following dialog:



The displayed tree reflects the following source code:
package mycompilerplugintest

object pluginTest {

}
Close the dialog (it blocks the compilation).
Then change the pluginTest implementation to this one:
 object pluginTest extends Application {
  def aMethod(x:Int) = x*2
  var aVar = 5
  val aVal = 7
}
The AST browser allows you to see the trees corresponding to each construct.
The method definition is reflected by the DefDef case class:



Both var and val definitions are reflected by the ValDef case class, but with different attributes.
For the var and val definitions, the "Symbol Attributes" values are respectively private <mutable> <local> and private <local>.

Here is the snapshot corresponding to the var definition:



And the one corresponding to the val definition:



Implementing the var hunter

In order to detect var definition, we need to match a ValDef occurrence with a symbol that has the 'mutable' attribute.
If you take a look to the scala\tools\nsc\ast\Trees.scala source where you can access to the ValDef case class implementation:
case class ValDef(mods: Modifiers, name: Name, tpt: Tree, rhs: Tree)
       extends ValOrDefDef {
  ...
}
So let's go back to the plug-in implementation. Enter the following code:
def apply(unit: CompilationUnit) {
  for (tree @ ValDef(_, _, _, _) <- unit.body) {
    // TODO: check against mutability
  }
This loop allows to capture any ValDef occurrence (assigned to the tree value).
Then, we need to check the value of tree.symbol.flags against the scala.nsc.symtab.Flags.MUTUABLE flag.

Don't forget to add the following import:
import nsc.symtab.Flags
Then check the MUTABLE flag and raise an error if applicable:
 
def apply(unit: CompilationUnit) {
  for (tree @ ValDef(_, _, _, _) <- unit.body) {
     if ((tree.symbol.flags & Flags.MUTABLE) != 0) {
       unit.error(tree.pos, "variables are not allowed !!!")
     }
   }
}
Finally export the plug-in jar again and compile the plug-in test.
You should see the following error:

       

I hope you are convinced that this feature allows to do quite complex things with ease !!! That kind of example shows another powerful aspect of the language scalabilty offered by Scala.