Сбой генерации 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: _*)
Другие вопросы по тегам