Scaladoc generation fails when referencing methods generated by annotation macros - scala

I have two classes, call them Foo and Fizz. Foo uses an annotation macro called expand to make aliases of some of its methods (the actual implementation does a little more than creating aliases, but the simple version still exhibits the problem that follows). For simplicity, let's say the expand macro simply takes all of the methods within the annotated class, and makes a copy of them, appending "Copy" to the end of the method name, then forwarding the call to the original method.
My problem is that if I use the expand macro on Foo, which creates a copy of a method Foo#bar called barCopy, when barCopy is called within another class, Fizz, everything compiles but scaladoc generation fails, as such:
[error] ../src/main/scala/Foo.scala:11: value barCopy is not a member of Foo
[error] def str = foo.barCopy("hey")
[error] ^
[info] No documentation generated with unsuccessful compiler run
If I remove the scaladoc that tags the method being copied (Foo#bar), the sbt doc command works again. It's as if the scaladoc generator invokes an early phase of the compiler without using the macro paradise plugin that's enabled, yet somehow it works if the docs are removed from the offending method.
This is the expand macro:
import scala.annotation.{ StaticAnnotation, compileTimeOnly }
import scala.language.experimental.macros
import scala.reflect.macros.whitebox.Context
#compileTimeOnly("You must enable the macro paradise plugin.")
class expand extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro Impl.impl
}
object Impl {
def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
val result = annottees map (_.tree) match {
case (classDef #
q"""
$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents {
$self => ..$stats
}
""") :: _ =>
val copies = for {
q"def $tname[..$tparams](...$paramss): $tpt = $expr" <- stats
ident = TermName(tname.toString + "Copy")
} yield {
val paramSymbols = paramss.map(_.map(_.name))
q"def $ident[..$tparams](...$paramss): $tpt = $tname(...$paramSymbols)"
}
q"""
$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self =>
..$stats
..$copies
}
"""
case _ => c.abort(c.enclosingPosition, "Invalid annotation target: not a class")
}
c.Expr[Any](result)
}
}
And the classes, which exist in a separate project:
/** This is a class that will have some methods copied. */
#expand class Foo {
/** Remove this scaladoc comment, and `sbt doc` will run just fine! */
def bar(value: String) = value
}
/** Another class. */
class Fizz(foo: Foo) {
/** More scaladoc, nothing wrong here. */
def str = foo.barCopy("hey")
}
This seems like a bug, or perhaps a missing feature, but is there a way to generate scaladoc for the above classes without removing docs from copied methods? I've tried this with both Scala 2.11.8 and 2.12.1. This is a simple sbt project that demonstrates the issue I'm having.

This is a bug in Scala, still present in 2.13. This gist of the issue is that when compiling for Scaladoc (as with sbt doc), the compiler introduces extra DocDef AST nodes for holding comments. Those are not matched by the quasiquote pattern. Even worse, they aren't even visible from the scala-reflect API.
Here is an excerpt from a comment by #driuzz explaining the situation on a similar issue in simulacrum:
[...] During normal compilation methods are available as DefDef type even when they have scaladoc comment which is just ignored.
But during sbt doc compiler generates a little different AST. Every method that has scaladoc comment is described as DocDef(comment, DefDef(...)) which causes that macro isn't recognizing them at all [...]
The fix that #driuzz implemented is here. The idea is to try casting the scala-reflect trees into their Scala compiler representations. For the code from the question, this means defining some unwrapDocDef to help remove the docstrings from the methods.
val result = annottees map (_.tree) match {
case (classDef #
q"""
$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents {
$self => ..$stats
}
""") :: _ =>
// If the outer layer of the Tree is a `DocDef`, peel it back
val unwrapDocDef = (t: Tree) => {
import scala.tools.nsc.ast.Trees
if (t.isInstanceOf[Trees#DocDef]) {
t.asInstanceOf[Trees#DocDef].definition.asInstanceOf[Tree]
} else {
t
}
}
val copies = for {
q"def $tname[..$tparams](...$paramss): $tpt = $expr" <- stats.map(unwrapDocDef)
ident = TermName(tname.toString + "Copy")
} yield {
val paramSymbols = paramss.map(_.map(_.name))
q"def $ident[..$tparams](...$paramss): $tpt = $tname(...$paramSymbols)"
}
q"""
$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self =>
..$stats
..$copies
}
"""
case _ => c.abort(c.enclosingPosition, "Invalid annotation target: not a class")
}
Of course, since this imports something from the Scala compiler, the SBT definition of the macro project has to change:
lazy val macros = (project in file("macros")).settings(
name := "macros",
libraryDependencies ++= Seq(
"org.scala-lang" % "scala-reflect" % scalaV,
"org.scala-lang" % "scala-compiler" % scalaV // new
)
).settings(commonSettings: _*)

Related

Using Scala Macro Annotation in Intellij

I am trying to use scala macro annotation to simply print return value from a method. I am using Intellij 2017. Here is the code:
class PrintResult extends StaticAnnotation {
def macroTransform(annottees: Any*) = macro PrintResult.impl
}
object PrintResult {
def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
val result = {
annottees.map(_.tree).toList match {
case q"$mods def $methodName(...$args): $returnType = { ..$body }" :: Nil => {
q"""$mods def $methodName(...$args): $returnType = {
val res = {..$body}
println($res)
res
}"""
}
case _ => c.abort(c.enclosingPosition, "Annotation #PrintResult can be used only with methods")
}
}
c.Expr[Any](result)
}
}
#PrintResult
object Test extends App {
def add(a: Int, b: Int): Int = {
a+b
}
}
I added this config to sbt:
addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)
I also added SBT Compile to Intellij Run Configuration.
I am getting this error:
macro annotation could not be expanded (the most common reason for that is that you need to enable the macro paradise plugin; another possibility is that you try to use macro annotation in the same compilation run that defines it)
To quote the error message:
another possibility is that you try to use macro annotation in the same compilation run that defines it
object Test must be in a different subproject or in src/test/scala (when PrintResult is in src/main/scala).

Accessing class definition by name from scala macro

I'm trying to create a Scala macro which defines a single a Class argument, and which modifies the class to which it is attached based on the implementation of the Class which is provided as an argument.
//Simple class with a few arguments
class A(a: String, b: String)
//Definition of this class should be modified based on class definition of A
#parameterized(classOf[A])
class B
I've managed to create a simple macro that is able to extract the argument from the annotation, resulting in a TypeName object that contains a string representation of the full class name.
The problem is now that I need to access the definition of A from the macro implementation (specifically, I want to see what the constructor arguments are).
Is there a way to access/create a TypeTag[A] in some way? Is there a way I can access the AST of class A?
To illustrate what I'm trying to achieve, this is what I currently have as a macro definition:
object parameterizedMacro {
def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
import Flag._
//Extract the parameter type which was provided as an argument (rather hacky way of getting this info)
val parameterType = c.macroApplication match {
case Apply(Select(Apply(_, List(
TypeApply(Ident(TermName("classOf")), List(Ident(TypeName(parameterType))))
)) , _), _) => parameterType
case _ =>
sys.error("Could not match #parameterized arguments. Was a class provided?")
}
//Should generate method list based on the code of parameterType
val methods = ???
val result = {
annottees.map(_.tree).toList match {
case q"object $name extends ..$parents { ..$body }" :: Nil =>
q"""
object $name extends ..$parents {
..${methods}
..$body
}
"""
case q"class $name (..$args) extends ..$parents { ..$body }" :: Nil =>
q"""
class $name (..$args) extends ..$parents {
..${methods}
..$body
}
"""
}
}
c.Expr[Any](result)
}
}
It took a lot of trial and error, as I didn't manage to find a lot of documentation on the various classes, but I did end up managing to achieve what I wanted to do.
To acquire the parameter, I ended up having to put the fully qualified name in the annotation instead of the original TypeOf[Name].
#parameterized(the.fully.qualified.Name)
Which I could then get using
val parameterType = c.macroApplication match {
case Apply(Select(Apply(_, List(
parameterType
)) , _), _) => parameterType
case _ =>
error("Could not match #parameterized arguments. Was a class provided?")
}
val fullClassName = parameterType.toString()
Using TypeOf as mentioned in the original post didn't work for me as I didnt manage to get from the TypeApply to the correct fully qualified name. This doesn't really make the whole thing insecure as everything is checked at compile time, but it does look a bit less like standard Scala.
Once I had the fully qualified name, I could use the universe's mirror (The documentation on mirrors helped here) to get a ClassSymbol, from which it is possible to get the class constructor arguments:
val constructorArguments = {
val clazz = c.mirror.staticClass(fullClassName) //Get ClassSymbol
val clazzInfo = clazz.info //Turn ClassSymbol into Type
val constructor = clazzInfo.member(termNames.CONSTRUCTOR) //Get constructor member Symbol
val constructorMethod = constructor.asMethod //Turn into MethodSymbol
val parametersList = constructorMethod.paramLists //Finally extract list of parameters
if (parametersList.size != 1)
error("Expected only a single constructor in " + fullClassName)
val parameters = parametersList.head
for (parameter <- parameters) yield {
val term = parameter.asTerm //parameter is a term
(term.name.toString, term.typeSignature)
}
}

Use Scala macros to generate methods

I want to generate aliases of methods using annotation macros in Scala 2.11+. I am not even sure that is even possible. If yes, how?
Example - Given this below, I want the annotation macros to expand into
class Socket {
#alias(aliases = Seq("!", "ask", "read"))
def load(n: Int): Seq[Byte] = {/* impl */}
}
I want the above to generate the synonym method stubs as follows:
class Socket {
def load(n: Int): Seq[Byte] = // ....
def !(n: Int) = load(n)
def ask(n: Int) = load(n)
def read(n: Int) = load(n)
}
The above is of course a facetious example but I can see this technique being useful to auto generate sync/async versions of APIs or in DSLs with lots of synonyms. Is it possible to also expose these generated methods in the Scaladoc too? Is this something possible using Scala meta?
Note: What I am asking is quite different from: https://github.com/ktoso/scala-macro-method-alias
Also please don't mark this as a duplicate of this as the question is a bit different and a lot has changed in Scala macro land in past 3 years.
This doesn't seem possible exactly as stated. Using a macro annotation on a class member does not allow you to manipulate the tree of the class itself. That is, when you annotate a method within a class with a macro annotation, macroTransform(annottees: Any*) will be called, but the only annottee will be the method itself.
I was able to get a proof-of-concept working with two annotations. It's obviously not as nice as simply annotating the class, but I can't think of another way around it.
You'll need:
import scala.annotation.{ StaticAnnotation, compileTimeOnly }
import scala.language.experimental.macros
import scala.reflect.macros.whitebox.Context
The idea is, you can annotate each method with this annotation, so that a macro annotation on the parent class is able to find which methods you want to expand.
class alias(aliases: String *) extends StaticAnnotation
Then the macro:
// Annotate the containing class to expand aliased methods within
#compileTimeOnly("You must enable the macro paradise plugin.")
class aliased extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro AliasMacroImpl.impl
}
object AliasMacroImpl {
def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
val result = annottees map (_.tree) match {
// Match a class, and expand.
case (classDef # q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }") :: _ =>
val aliasedDefs = for {
q"#alias(..$aliases) def $tname[..$tparams](...$paramss): $tpt = $expr" <- stats
Literal(Constant(alias)) <- aliases
ident = TermName(alias.toString)
} yield {
val args = paramss map { paramList =>
paramList.map { case q"$_ val $param: $_ = $_" => q"$param" }
}
q"def $ident[..$tparams](...$paramss): $tpt = $tname(...$args)"
}
if(aliasedDefs.nonEmpty) {
q"""
$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self =>
..$stats
..$aliasedDefs
}
"""
} else classDef
// Not a class.
case _ => c.abort(c.enclosingPosition, "Invalid annotation target: not a class")
}
c.Expr[Any](result)
}
}
Keep in mind this implementation will be brittle. It only inspects the annottees to check that the first is a ClassDef. Then, it looks for members of the class that are methods annotated with #alias, and creates multiple aliased trees to splice back into the class. If there are no annotated methods, it simply returns the original class tree. As is, this will not detect duplicate method names, and strips away modifiers (the compiler would not let me match annotations and modifiers at the same time).
This can easily be expanded to handle companion objects as well, but I left them out to keep the code smaller. See the quasiquotes syntax summary for the matchers I used. Handling companion objects would require modifying the result match to handle case classDef :: objDef :: Nil, and case objDef :: Nil.
In use:
#aliased
class Socket {
#alias("ask", "read")
def load(n: Int): Seq[Byte] = Seq(1, 2, 3).map(_.toByte)
}
scala> val socket = new Socket
socket: Socket = Socket#7407d2b8
scala> socket.load(5)
res0: Seq[Byte] = List(1, 2, 3)
scala> socket.ask(5)
res1: Seq[Byte] = List(1, 2, 3)
scala> socket.read(5)
res2: Seq[Byte] = List(1, 2, 3)
It can also handle multiple parameter lists:
#aliased
class Foo {
#alias("bar", "baz")
def test(a: Int, b: Int)(c: String) = a + b + c
}
scala> val foo = new Foo
foo: Foo = Foo#3857a375
scala> foo.baz(1, 2)("4")
res0: String = 34

Is it possible to replace the default apply method generated for case classes in a Scala macro?

It seems like this doesn't work ( Using 2.11.1 and macro paradise 2.0.1).
I was hoping that the case class generated methods would either be suppressed, or be in the tree so I could get rid of it. Is this a hard limitation?
class evis extends StaticAnnotation {
def macroTransform(annottees: Any*) = macro EvisMacro.impl
}
object EvisMacro {
def impl(c: blackbox.Context)(annottees: c.Expr[Any]*) : c.Expr[Any] = {
import c.universe._
def makeApply(tpName: TypeName, parents: List[Tree], params: List[List[ValDef]] ) : List[Tree]= {
List(q"""def apply(...$params): $tpName = null""")
}
val result = annottees map (_.tree) match {
case (classDef # q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }")
:: Nil if mods.hasFlag(Flag.CASE) =>
c.info(c.enclosingPosition, s"Eviscerating $tpname !($mods, $parents, $paramss)", true)
parents match {
case q"${pname: TypeName}" :: rest =>
c.info(c.enclosingPosition, s"${pname.decodedName}", true )
val sc = c.universe.rootMirror.staticClass( pname.decodedName.toString )
c.info(c.enclosingPosition, s"${sc}", true )
}
val name = tpname.toTermName
q"""
$classDef
object $name {
..${makeApply(tpname, parents, paramss)}
}
"""
case (classDef # q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }")
:: q"object $objName {..$objDefs}"
:: Nil if mods.hasFlag(Flag.CASE) =>
q"""
$classDef
object $objName {
..${makeApply(tpname, parents, paramss)}
..$objDefs
}
"""
case _ => c.abort(c.enclosingPosition, "Invalid annotation target: must be a case class")
}
c.Expr[Any](result)
}
}
Using it:
trait Thing
#evis
case class Trade(id: Long, notional: Long, comment: String) extends Thing
#evis
case class Pop(name: String) extends Thing
object Pop{
}
object TestTrade extends App{
val t = Trade (1, 1, "")
val p : Pop = Pop("")
println(t)
}
Results in:
Error:(2, 2) method apply is defined twice
conflicting symbols both originated in file 'core/src/main/scala/Test.scala'
#evis
^
The problem is caused by the fact that, to the compiler, code generated by macro annotations isn't any different from code written by hand. If you manually write the code produced by the macro provided in the example, you'll get exactly the same double definition error, so it's not a bug - it's a limitation of case class synthesis. Unfortunately, case class synthesis is not extensible, so this needs to be worked around.
One workaround that I'd propose is erasing the CASE flag from the mods of the class, and then the macro can be completely free in choosing which members to generate. This, however, means that the macro will have to generate all the code that case classes typically generate, which is not going to be very pleasant. Another caveat here is that the compiler treats pattern matching over CASE classes specially by emitting somewhat more efficient code, so such an emulation would also lose some performance (I'd reckon that the loss will be miniscule, and probably even non-existent with the new name-based pattern matching mechanism from Scala 2.11 - but that would need to be tested).

Scala macro expansion of class with companion: type not found

I'm trying to define a macro annotation on a case class that expands to a class with a companion object, but I'm running in some problems.
I can create an object with the same name as the class, and define methods on it.
But when I try to use the typename of the class as a return or argument type of a method, I get an "not found: type "
class term extends StaticAnnotation {
def macroTransform(annottees: Any*) = macro termMacro.impl
}
object termMacro {
def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
val inputs = annottees.map(_.tree).toList
val (cls, comp) = annottees.map(_.tree) match {
case cd#q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$_ } with ..$_ { $self => ..$stats }" :: tail =>
val paramnames = paramss.head.map {
case q"$mods val $name: $tpt = $default" => name
}
val ctorparams = List(paramss.head ++ Seq(
q"val position: SourcePosition = (0,0)"
))
val ctorname = TermName(tpname.decodedName.toString)
//val clstype =
(
q"""
import ast.Term
case class $tpname(...$ctorparams) extends Term { $self =>
def children() = {
List(..$paramnames)
}
..$stats
}
""",
q"""
object $ctorname {
def unapply(t: $tpname): Option[(Int, Int)] = {
Some(Tuple2(3, 4))
}
}
"""
)
case head :: tail =>
c.abort(c.enclosingPosition, s"The #Term annotation is for case classes, found $head")
}
c.error(c.enclosingPosition, showRaw(cls) + "\n" + showRaw(comp))
c.Expr[Any](Block(List(cls, comp), Literal(Constant(()))))
}
}
e.g. usage would be: #term case class A(x: Term) extends Term, and would give me the compiler error "not found: type A' at location #term in that definition.
I've narrowed the location down to the unapply method in the object definition.
Any help is appreciated.
I'm very much new to scala macros, so any further advice is also appreciated.
Side question: any advice on debugging macros in intellij / gradle projects?
q"import foo; class Bar" creates a block, so the macro annotation will inadvertently replace class Bar with { import foo; class Bar }, which makes Bar a local class, which is invisible from the outside of the block.