Mic*_*jac 5 scala scaladoc scala-macros scala-macro-paradise
我有两个类,将它们称为Foo和Fizz。Foo使用一个被调用的注释宏expand来为其某些方法创建别名(实际实现所做的不仅仅是创建别名,但简单版本仍然会出现以下问题)。为简单起见,假设expand宏简单地获取带注释的类中的所有方法,并制作它们的副本,将“Copy”附加到方法名称的末尾,然后将调用转发到原始方法。
我的问题是,如果我使用expand宏 on Foo,它会创建一个Foo#bar名为的方法的副本barCopy,当barCopy在另一个类中调用时Fizz,一切都会编译,但 scaladoc 生成失败,如下所示:
[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
Run Code Online (Sandbox Code Playgroud)
如果我删除标记正在复制的方法 ( Foo#bar)的 scaladoc ,该sbt doc命令将再次运行。就好像 scaladoc 生成器在不使用启用的宏天堂插件的情况下调用编译器的早期阶段一样,但如果从有问题的方法中删除文档,它会以某种方式工作。
这是expand宏:
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)
}
}
Run Code Online (Sandbox Code Playgroud)
以及存在于单独项目中的类:
/** 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")
}
Run Code Online (Sandbox Code Playgroud)
这似乎是一个错误,或者可能是一个缺失的功能,但是有没有办法为上述类生成 scaladoc 而不从复制的方法中删除文档?我在 Scala 2.11.8 和 2.12.1 上都试过这个。这是一个简单的 sbt 项目,演示了我遇到的问题。
这是Scala 中的一个错误,在 2.13 中仍然存在。问题的要点是,在编译 Scaladoc 时(与 一样sbt doc),编译器会引入额外的DocDefAST 节点来保存注释。这些与准引号模式不匹配。更糟糕的是,它们甚至从 API 中看不到scala-reflect。
以下是@driuzz 的评论摘录,解释了 中类似问题的情况simulacrum:
[...]在正常编译期间可用的方法为
DefDef类型使用,即使它们具有被忽略的 scaladoc 注释。但在sbt doc编译过程中会生成一些不同的 AST。每个具有 scaladoc 注释的方法都被描述为DocDef(comment, DefDef(...))导致宏根本无法识别它们 [...]
@driuzz 实施的修复在这里。这个想法是尝试将scala-reflect树转换为 Scala 编译器表示。对于问题中的代码,这意味着定义一些unwrapDocDef以帮助从方法中删除文档字符串。
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")
}
Run Code Online (Sandbox Code Playgroud)
当然,由于这会从 Scala 编译器导入一些内容,因此项目的 SBT 定义macro必须更改:
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: _*)
Run Code Online (Sandbox Code Playgroud)