Как проверки типов макросов Scala преобразуют идентификаторы в типы?

Я пытаюсь создать макрос аннотации, который можно применить только к определенному типу. Когда я запускаю свои тесты, я вижу ошибку типа не найден, когда аннотация применяется только к объектам верхнего уровня.

Мой код макроса:

trait Labelled[T] {
  def label: T
}

@compileTimeOnly("DoSomethingToLabelled requires the macro paradise plugin")
class DoSomethingToLabelled extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro DoSomethingToLabelled.impl
}

object DoSomethingToLabelled {
  def impl(c: whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
    import c.universe._

    annottees.map(_.tree).head match {
      case expr @ ModuleDef(mods: Modifiers, name: TermName, impl: Template) =>
        println(showRaw(impl.parents))
        val parentTypes = impl.parents.map(c.typecheck(_, c.TYPEmode))

        if (parentTypes.exists(_.tpe <:< typeOf[Labelled[_]])) {
          c.Expr[Any](expr)
        } else {
          c.abort(c.enclosingPosition, s"DoSomethingToLabelled can only be applied to a Labelled. Received types: $parentTypes")
        }
    }
  }
}

Мой тестовый код:

class DoSomethingToLabelledSpec extends Specification {

  private def classPathUrls(cl: ClassLoader): List[String] = cl match {
    case null => Nil
    case u: java.net.URLClassLoader => u.getURLs.toList.map(systemPath) ++ classPathUrls(cl.getParent)
    case _ => classPathUrls(cl.getParent)
  }

  private def systemPath(url: URL): String = {
    Paths.get(url.toURI).toString
  }

  private def paradiseJarLocation: String = {
    val classPath = classPathUrls(getClass.getClassLoader)
    classPath.find(_.contains("paradise")).getOrElse {
      throw new RuntimeException(s"Could not find macro paradise on the classpath: ${classPath.mkString(";")}")
    }
  }

  lazy val toolbox = runtimeMirror(getClass.getClassLoader)
    .mkToolBox(options = s"-Xplugin:$paradiseJarLocation -Xplugin-require:macroparadise")

  "The DoSomethingToLabelled annotation macro" should {

    "be applicable for nested object definitions extending Labelled" in {
      toolbox.compile {
        toolbox.parse {
          """
            |import macrotests.Labelled
            |import macrotests.DoSomethingToLabelled
            |
            |object Stuff {
            |  @DoSomethingToLabelled
            |  object LabelledWithHmm extends Labelled[String] {
            |    override val label = "hmm"
            |  }
            |}
            |""".stripMargin
        }
      } should not (throwAn[Exception])
    }

    "be applicable for top level object definitions extending Labelled" in {
      toolbox.compile {
        toolbox.parse {
          """
            |import macrotests.Labelled
            |import macrotests.DoSomethingToLabelled
            |
            |@DoSomethingToLabelled
            |object LabelledWithHmm extends Labelled[String] {
            |  override val label = "hmm"
            |}
            |""".stripMargin
        }
      } should not (throwAn[Exception])
    }
  }
}

И мой тестовый журнал:

sbt:macro-type-extraction> test
[info] Compiling 1 Scala source to C:\Users\WilliamCarter\workspace\macro-type-extraction\target\scala-2.11\classes ...
[info] Done compiling.
List(AppliedTypeTree(Ident(TypeName("Labelled")), List(Ident(TypeName("String")))))
List(AppliedTypeTree(Ident(TypeName("Labelled")), List(Ident(TypeName("String")))))
[info] DoSomethingToLabelledSpec
[info] The DoSomethingToLabelled annotation macro should
[info]   + be applicable for nested object definitions extending Labelled
[error] scala.tools.reflect.ToolBoxError: reflective compilation has failed:
[error]
[error] exception during macro expansion:
[error] scala.reflect.macros.TypecheckException: not found: type Labelled
[error]         at scala.reflect.macros.contexts.Typers$$anonfun$typecheck$2$$anonfun$apply$1.apply(Typers.scala:34)
[error]         at scala.reflect.macros.contexts.Typers$$anonfun$typecheck$2$$anonfun$apply$1.apply(Typers.scala:28)
[error]         at scala.reflect.macros.contexts.Typers$$anonfun$3.apply(Typers.scala:24)
[error]         at scala.reflect.macros.contexts.Typers$$anonfun$3.apply(Typers.scala:24)
[error]         at scala.reflect.macros.contexts.Typers$$anonfun$withContext$1$1.apply(Typers.scala:25)
[error]         at scala.reflect.macros.contexts.Typers$$anonfun$withContext$1$1.apply(Typers.scala:25)
[error]         at scala.reflect.macros.contexts.Typers$$anonfun$1.apply(Typers.scala:23)
[error]         at scala.reflect.macros.contexts.Typers$$anonfun$1.apply(Typers.scala:23)
[error]         at scala.reflect.macros.contexts.Typers$class.withContext$1(Typers.scala:25)
[error]         at scala.reflect.macros.contexts.Typers$$anonfun$typecheck$2.apply(Typers.scala:28)
[error]         at scala.reflect.macros.contexts.Typers$$anonfun$typecheck$2.apply(Typers.scala:28)
[error]         at scala.reflect.macros.contexts.Typers$class.withWrapping$1(Typers.scala:26)
[error]         at scala.reflect.macros.contexts.Typers$class.typecheck(Typers.scala:28)
[error]         at scala.reflect.macros.contexts.Context.typecheck(Context.scala:6)
[error]         at scala.reflect.macros.contexts.Context.typecheck(Context.scala:6)
[error]         at macrotests.DoSomethingToLabelled$$anonfun$2.apply(DoSomethingToLabelled.scala:19)
[error]         at macrotests.DoSomethingToLabelled$$anonfun$2.apply(DoSomethingToLabelled.scala:19)
[error]         at scala.collection.immutable.List.map(List.scala:284)
[error]         at macrotests.DoSomethingToLabelled$.impl(DoSomethingToLabelled.scala:19)

Моя отладочная печать сообщает мне, что извлеченные родительские типы одинаковы в каждом тесте, но по какой-то причине объект верхнего уровня не может определить, что TypeName("Labelled") на самом деле является macrotests.Labelled. Может ли кто-нибудь помочь пролить здесь свет? Макрос, похоже, работает вне контекста тестирования, но я действительно хотел бы понять, что происходит, чтобы я мог написать несколько подходящих тестов.


person William Carter    schedule 16.08.2019    source источник


Ответы (1)


Пытаться

toolbox.compile {
  toolbox.parse {
    """
      |import macrotests.DoSomethingToLabelled
      |
      |@DoSomethingToLabelled
      |object LabelledWithHmm extends macrotests.Labelled[String] {
      |  override val label = "hmm"
      |}
      |""".stripMargin
  }
}

или даже

toolbox.compile {
  toolbox.parse {
    """
      |import macrotests.DoSomethingToLabelled
      |
      |@DoSomethingToLabelled
      |object LabelledWithHmm extends _root_.macrotests.Labelled[String] {
      |  override val label = "hmm"
      |}
      |""".stripMargin
  }
}

Кстати, а зачем вам ящик для инструментов? Почему бы не написать просто

@DoSomethingToLabelled
object LabelledWithHmm extends Labelled[String] {
  override val label = "hmm"
}

в тестах? Тогда тот факт, что код компилируется, будет проверяться во время компиляции, а не во время выполнения с помощью набора инструментов.

https://github.com/scala/bug/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+%28toolbox+%26%26+%28import+%7C%7C+package%29%29

https://github.com/scala/bug/issues/6393

@ xeno-by сказал: Похоже, с этим мы обречены.

Проблема в том, что отражающий и отражающий компилятор Scala (который является базовым набором инструментов) использует другую модель загрузки файлов классов, чем vanilla scalac. Компилятор Vanilla имеет свой путь к классам в виде списка каталогов / jar-файлов в файловой системе, поэтому он может исчерпывающе перечислить пакеты в пути к классам. Отражающий компилятор работает с произвольными загрузчиками классов, а у загрузчиков классов нет концепции перечисления пакетов.

В результате, когда рефлексивный компилятор видит «математику», имеющую «import scala .; import java.lang.» импортирует в лексическом контексте, он не знает, означает ли эта «математика» root.math, scala.math или java.lang.math. Поэтому он должен предположить и временно создать пакет для root.math, что в конечном итоге оказалось неправильным выбором.

Вероятно, мы могли бы поддержать понятие «перегруженных» пакетов, чтобы компилятор не размышлял и мог хранить все возможные параметры, но это потребовало бы переделки отражения и, возможно, типизатора.

person Dmytro Mitin    schedule 16.08.2019
comment
Интересно, что, похоже, использовать Toolbox тогда не получится. Причина, по которой я делал это так, заключалась в том, что это единственный способ гарантировать, что макрос действительно выполняется при каждом тестовом запуске, поскольку gradle пропускал этап компиляции тестового модуля, несмотря на изменение реализации макроса. Даже средство сопоставления should compile ScalaTest не будет повторно запускать макрос, если считает, что он уже скомпилирован. Думаю, мне просто нужно будет правильно настроить gradle. Спасибо за вашу помощь. - person William Carter; 16.08.2019