引用注释宏生成的方法时,Scaladoc生成失败

时间:2017-03-08 23:45:11

标签: scala scala-macros scaladoc scala-macro-paradise

我有两个课程,称他们为FooFizzFoo使用名为expand的注释宏来创建其某些方法的别名(实际实现只比创建别名更多,但简单版本仍会出现以下问题)。为简单起见,我们假设expand宏只是采用带注释的类中的所有方法,并复制它们,附加"复制"到方法名称的末尾,然后将调用转发给原始方法。

我的问题是,如果我使用expand上的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

如果删除标记正在复制的方法(Foo#bar)的标量,则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)
  }

}

这些类存在于一个单独的项目中:

/** 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")
}

这似乎是一个错误,或者可能是一个缺失的功能,但有没有办法为上述类生成scaladoc而不从复制的方法中删除文档?我已经尝试过Scala 2.11.8和2.12.1。 This是一个简单的sbt项目,它展示了我遇到的问题。

1 个答案:

答案 0 :(得分:2)

这是a bug in Scala,仍存在于2.13中。问题的要点在于,在为Scaladoc进行编译时(与sbt doc一样),编译器引入了额外的DocDef AST节点来保存注释。那些与准引用模式匹配。更糟糕的是,它们甚至在scala-reflect API中也不可见。

以下是a comment by @driuzz的摘录,解释了simulacrum中类似问题的情况:

[...]在正常的编译过程中,即使方法具有scaladoc注释(只是被忽略),它们也可以作为DefDef类型使用。 但是在sbt doc期间,编译器会生成一些不同的AST。每个具有scaladoc注释的方法都描述为DocDef(comment, DefDef(...)),这导致宏根本无法识别它们。[...]

@driuzz实现的修复程序是here。这个想法是尝试将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")
    }

当然,由于这会从Scala编译器中导入某些内容,因此macro项目的SBT定义必须更改:

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: _*)