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

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

我有两个类,调用他们FooFizz . Foo使用名为 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

如果我删除标记正在复制的方法的 scaladoc ( 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 项目,它演示了我遇到的问题。

最佳答案

这是a bug in Scala ,仍然存在于 2.13 中。这个问题的要点是,在为 Scaladoc 编译时(与 sbt doc 一样),编译器引入了额外的 DocDef用于保存评论的 AST 节点。这些与 quasiquote 模式不匹配。更糟糕的是,它们甚至从 scala-reflect 中都看不到。 API。

以下是 a comment by @driuzz 的摘录在 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 [...]



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

关于引用注释宏生成的方法时,Scaladoc 生成失败,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42684101/

相关文章:

sbt - 使用doc任务生成scaladoc后如何运行bash脚本?

java - 如何将 JDK 中的类链接到 scaladoc 生成的文档中?

Scala - 解析模板类型

scala - 如何将 spark DataFrame 转换为 RDD mllib LabeledPoints?

scala - Gradle Scala 插件在 sbt 中添加 CompilerPlugin 的必然结果

scala - 从函数的返回值中提取符号

scala - 这个 def 宏有什么问题?

scala - 为什么 Scaladoc 方法签名是错误的?

java - Scalatra 独立部署时找不到 View

scala - 如何处理事件源应用程序中的域更改?