S-JIS[2011-02-24] 変更履歴

Scalaコンパイラプラグイン サンプル

Scala2.9.1のコンパイラプラグインのサンプル。

    override def newPhase(prev: Phase) = new StdPhase(prev) {
      override def name = selfPlugin.name

      // プラグイン実行時に呼ばれるメソッド
      override def apply(unit: CompilationUnit): Unit = {
        printTree(unit.body)                //全要素の巡回
        newTraverser().traverse(unit.body) //全要素の巡回
        printMethod(unit.body) //メソッド呼び出しを表示する例
        warningPrintln(unit)   //printlnがあったら警告にする例
      }
    }

全要素の巡回(探索)

tree.childrenList[Tree])を再帰的に探索する例。

      def printTree(tree: Tree, tab: Int=0) {
        println("--" * tab)
        printf("%s\t[%s][%s]%n", tree.getClass, tree.tpe, tree.symbol)
        println(tree)

//      for(t <- tree.children){ printTree(t, tab+1) }
        tree.children.foreach{ printTree(_, tab+1) }
      }

Treeを探索する為のTraverserを使う方法。(色々なTraverserがscala.tools.nsc.ast.Treesに用意されている)

      def newTraverser(): Traverser = new ForeachTreeTraverser(printTraverse)

      def printTraverse(tree: Tree) {
        printf("%s\t[%s][%s]%n", tree.getClass, tree.tpe, tree.symbol)
      }

特定要素の探索

Treeの具体的な構成要素はTreeを継承したクラスによって保持されている。
例えばpackage宣言はPackageDef、クラス定義はClassDef、変数定義はValDef(valもvarも)、メソッド定義はDefDef、メソッド呼び出しはApplyといった具合。
これらはケースクラスなので、unapplyメソッドが使える(パターンマッチが出来る)。


メソッド呼び出しを表示する例

メソッド呼び出しはApplyというケースクラスで表現されている。
パターンマッチの第1引数はSelect(というTree)で、Selectはクラス名とメソッド名を保持している。
パターンマッチの第2引数はListで、メソッド呼び出しの引数。

マッチしたい条件が1種類しか無いならfor式、複数あるならTraverserを使うのが美しいかな?

      def printMethod(tree: Tree) {
        def print(tree: Tree): Unit = tree match {
          case Apply(Select(obj, name), List(args @ _*)) =>
            printf("%s#%s(%s)%n", obj, name, args.mkString(", "))
          case _ => ()
        }
        new ForeachTreeTraverser(print).traverse(tree)
      }
      def printMethod(tree: Tree) {
        for (Apply(Select(obj, name), List(args @ _*)) <- tree) {
          printf("%s#%s(%s)%n", obj, name, args.mkString(", "))
        }
      }
>scalac -Xplugin:cplugin.jar hello.scala
Hello.super#<init>()
scala.this.Predef#println("hello")

※Hello.superの<init>は、object Helloのコンストラクター呼び出しと思われる。


Predef.println()の呼び出しがあったら警告を出力する例。

Selectで受け取れるオブジェクト名・メソッド名(下記のobjとname)はNameというクラスであり、Stringではない。
したがって、比較するためにはStringに変換する必要がある。

      def warningPrintln(unit: CompilationUnit) {
        for (tree @ Apply(Select(obj, name), _) <- unit.body) {
          val oname = obj.toString
          val mname = name.toString
          if (oname == "scala.this.Predef" && mname == "println") {
            unit.warning(tree.pos, "Predef.println() found")
          }
        }
      }
>scalac -Xplugin:cplugin.jar hello.scala
hello.scala:3: warning: Predef.println() found
    println("hello")
           ^
one warning found

ちなみに、Scala的にはfor式の中にフィルターで条件を書く方がいいのかな?

      def warningPrintln(unit: CompilationUnit) {
        for {tree @ Apply(Select(obj, name), _) <- unit.body
          val oname = obj.toString
          val mname = name.toString
          if oname == "scala.this.Predef" && mname == "println"
        } {
          unit.warning(tree.pos, "Predef.println() found")
        }
      }

コンパイラプラグインへ戻る / Scala目次へ戻る / 技術メモへ戻る
メールの送信先:ひしだま