Bartosz Bąbol

Software engineering

Scala Macros Part III- Intellij Idea Support

I. Introduction.

This is continuation of post Scala macros part II- macro annotations where we’ve implemented @TalkingAnimalSpell macro annotation. As a reminder: This macro annotation add new method before compilation to annotated class, and this method was invisible for Intellij Idea because its coding assistance is based on static code analysis and it is not aware of AST changes. In this post we will try to create plugin to solve this problem. You will need Intellij Idea 15. Code as always is on my Github:

Intellij plugin repo

Part 2 repo

II. Setup

In October 2015, Jetbrains team published this post. According to this blog let’s clone example repo for building intellij idea macros plugins.

1
git clone git@github.com:JetBrains/sbt-idea-example.git

And open Injector.scala:

Injector.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package org.jetbrains.example.injector

import org.jetbrains.plugins.scala.lang.psi.api.statements.ScValue
import org.jetbrains.plugins.scala.lang.psi.api.toplevel.typedef.ScTypeDefinition
import org.jetbrains.plugins.scala.lang.psi.impl.toplevel.typedef.SyntheticMembersInjector
import org.jetbrains.plugins.scala.lang.psi.types.result.TypingContext

/**
  * @author Alefas
  * @since  14/10/15
  */
class Injector extends SyntheticMembersInjector {
  override def injectFunctions(source: ScTypeDefinition): Seq[String] = {
    source.members.flatMap {
      case v: ScValue if v.hasAnnotation("example.JavaGetter").isDefined =>
        v.declaredElements.map { td =>
          s"def get${td.name.capitalize} : ${td.getType(TypingContext.empty).getOrAny.canonicalText} = ???"
        }
      case _ => Seq.empty
    }
  }
}

Building plugin which will support your macro annotations is about creating class which extends SyntheticMembersInjector class and implementing one of SyntheticMembersInjector functions:

  1. If you want to add methods to any class, object or trait you will have to implement this method:

    1
    2
    
        def injectFunctions(source: ScTypeDefinition): Seq[String]
    
    

    We will implement this method later for our plugin, and example implementation you have also in this cloned from Intellij sbt-plugin-example project.

  2. If you would like to custom inner class or object to any class, object or trait you would have to implement this method:

    1
    2
    
        def injectInners(source: ScTypeDefinition): Seq[String]
    
    

    Returning types for those 2 methods is Seq[String], it is just collection of texts which will appear on code completion menu when you will operate on proper object.

  3. SyntheticMembersInjector has also third method:

    1
    2
    
        def needsCompanionObject(source: ScTypeDefinition): Boolean = false
    
    

    And according to sources: “Use this method to mark class or trait, that it requires companion object.”

Actually, using this api for building macro support reminds me writing macros. Look, argument of, for example, injectFunction is weird ScTypeDefinition, so it sounds a little bit like one of the c.Tree subclasses. Then you’ve got ScValue, we can check if it has some annotations, so we are operating, not on Tree types of course, but some types provided by IntelliJ but they are just some code tokens. So let’s look how to use this api for out purposes.

III. Plugin for @TalkingAnimalSpell

This is our implementation of Injector:

Injector.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.jetbrains.example.injector

import org.jetbrains.plugins.scala.lang.psi.api.toplevel.typedef.{ScClass, ScTypeDefinition}
import org.jetbrains.plugins.scala.lang.psi.impl.toplevel.typedef.SyntheticMembersInjector

class Injector extends SyntheticMembersInjector {
  override def injectFunctions(source: ScTypeDefinition): Seq[String] = {
    source match {
      case c: ScClass if c.hasAnnotation("TalkingAnimalSpell").isDefined =>
        Seq(s"def sayHello: Unit = ???")
      case _ => Seq.empty
    }
  }
}

I hope that this code is pretty straightforward. If source, is class and has annotation called “TalkingAnimalSpell” then return seq with text corresponding to sayHello method signature. And that’s all :)

Now inside your plugin directory run:

1
sbt package-plugin

This command will create .jar with your plugin. Go to Idea settings, Plugins section, then click on install plugin from disc, and choose this .jar, restart Idea, open project from part II, and you should have this effect:

images

IV. Fun with the IntelliJ API

I encourage you to play this api a little bit, check what methods are available and what they do. For example we can slightly change behavior of this plugin and provide code completion only when annotated class extends proper Animal trait. Possible solution for this would be:

Injector.scala
1
2
3
4
5
6
7
    //...
      case c: ScClass if c.hasAnnotation("TalkingAnimalSpell").isDefined && c.superTypes.map(_.canonicalText).contains("Animal") =>
        Seq(s"def sayHello: Unit = ???")
      case _ => Seq.empty
    }
  }
}

V. Summary

In this blog series I tried to make some gentle introduction to scala macros. In Javeo I used them to build library which reduced huge amount of boilerplate code. You will find code of Gridzzly(That’s the name of our beast) on Javeo’s github, I will update repo in few days. The last missing factor was Intellij idea support for macros, but now nothing can stop you from writing your own library using scala macros ;) By the way, Gridzzly is also Macro Annotation so you probably want plugin for it ;) It’s here.

Thank you for reading this, feel free to comment and… have a great day!

Comments