Сбой генерации Scaladoc при обращении к методам, сгенерированным макросами аннотаций
У меня есть два класса, позвоните им Foo
а также Fizz
, Foo
использует макрос аннотации под названием expand
создавать псевдонимы некоторых из его методов (фактическая реализация делает немного больше, чем создание псевдонимов, но простая версия все еще имеет проблему, которая следует). Для простоты, скажем, expand
макрос просто берет все методы в аннотированном классе и создает их копию, добавляя "Копировать" в конец имени метода, а затем перенаправляя вызов исходному методу.
Моя проблема в том, что если я использую expand
макрос на Foo
, который создает копию метода Foo#bar
называется barCopy
, когда barCopy
вызывается в другом классе, Fizz
, все компилируется, но генерация скаладока завершается неудачей, как таковая:
[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 запускал раннюю фазу компилятора без использования включенного подключаемого модуля macro paradise, но все же каким-то образом он работает, если документы были удалены из оскорбительного метода.
Это 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")
}
Это похоже на ошибку или, возможно, на отсутствующую функцию, но есть ли способ генерировать скалярные документы для вышеуказанных классов, не удаляя документы из скопированных методов? Я пробовал это с Scala 2.11.8 и 2.12.1. Это простой проект SBT, который демонстрирует мою проблему.
0 ответов
Это ошибка Scala, все еще присутствующая в 2.13. Суть проблемы заключается в том, что при компиляции для Scaladoc (как сsbt doc
) компилятор вводит дополнительные DocDef
Узлы AST для хранения комментариев. Те, которые не соответствуют по quasiquote шаблону. Хуже того, их даже не видно изscala-reflect
API.
Вот отрывок из комментария @driuzz, объясняющего ситуацию по аналогичной проблеме вsimulacrum
:
[...] Во время обычной компиляции методы доступны как
DefDef
введите, даже если у них есть комментарий scaladoc, который просто игнорируется. Но во времяsbt doc
компилятор генерирует немного другой AST. Каждый метод, имеющий скалярный комментарий, описывается как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")
}
Конечно, поскольку это импортирует что-то из компилятора 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: _*)