Repository: scalacenter/tasty-query
Pull request #420 (2023-12-12 09:56:50)

Do not crash when reading a Scala 2 `macro` definition.
It contains an annotation with a subtree that is an `IDENTtree` without prefix nor symbol, with the name `macro`. That is then `Apply`ed and `TypeApply`ed in ways that make no semantic sense.

We define a special symbol in `Definitions`, which we attach to such fake identifiers.

sjrd: Only for things that we can find in the standard library. We don't have a setup for other weird stuff that we only find elsewhere, unfortunately.
adpi2: What about adding a Scala 2 test project like this one?
https://github.com/scalacenter/tasty-query/blob/917d4d6111d49820b070eb784ba2eb601ef6937d/build.sbt#L64C1-L72C4

Patch:
diff --git a/build.sbt b/build.sbt
index 6d1b804..1336b65 100644
--- a/build.sbt
+++ b/build.sbt
@@ -5,6 +5,7 @@ import org.scalajs.jsenv.nodejs.NodeJSEnv
 
 val usedScalaCompiler = "3.3.1"
 val usedTastyRelease = usedScalaCompiler
+val scala2Version = "2.13.12"
 
 val SourceDeps = config("sourcedeps").hide
 
@@ -61,6 +62,17 @@ lazy val root = project.in(file("."))
     publish / skip := true,
   )
 
+lazy val scala2TestSources = crossProject(JSPlatform, JVMPlatform)
+  .crossType(CrossType.Pure)
+  .in(file("scala2-test-sources"))
+  .settings(commonSettings)
+  .settings(
+    scalaVersion := scala2Version,
+    publish / skip := true,
+    scalacOptions += "-Xfatal-warnings",
+    libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value % "provided",
+  )
+
 lazy val testSources = crossProject(JSPlatform, JVMPlatform)
   .crossType(CrossType.Pure)
   .in(file("test-sources"))
@@ -70,6 +82,7 @@ lazy val testSources = crossProject(JSPlatform, JVMPlatform)
     scalacOptions += "-Xfatal-warnings",
     javacOptions += "-parameters",
   )
+  .dependsOn(scala2TestSources)
 
 lazy val tastyQuery =
   crossProject(JSPlatform, JVMPlatform).in(file("tasty-query"))
diff --git a/tasty-query/shared/src/main/scala/tastyquery/Definitions.scala b/tasty-query/shared/src/main/scala/tastyquery/Definitions.scala
index a347e83..6f924ee 100644
--- a/tasty-query/shared/src/main/scala/tastyquery/Definitions.scala
+++ b/tasty-query/shared/src/main/scala/tastyquery/Definitions.scala
@@ -145,6 +145,15 @@ final class Definitions private[tastyquery] (ctx: Context, rootPackage: PackageS
       .checkCompleted()
   end scala2FakeOwner
 
+  private[tastyquery] val scala2MacroInfoFakeMethod: TermSymbol =
+    TermSymbol
+      .createNotDeclaration(nme.m_macro, scalaPackage)
+      .withFlags(Synthetic, None)
+      .withDeclaredType(NothingType)
+      .setAnnotations(Nil)
+      .checkCompleted()
+  end scala2MacroInfoFakeMethod
+
   private def createSpecialTypeAlias(
     name: TypeName,
     owner: DeclaringSymbol,
diff --git a/tasty-query/shared/src/main/scala/tastyquery/Names.scala b/tasty-query/shared/src/main/scala/tastyquery/Names.scala
index 2a89882..1c30fa3 100644
--- a/tasty-query/shared/src/main/scala/tastyquery/Names.scala
+++ b/tasty-query/shared/src/main/scala/tastyquery/Names.scala
@@ -63,6 +63,8 @@ object Names {
 
     val m_apply: SimpleName = termName("apply")
     val m_unapply: SimpleName = termName("unapply")
+
+    private[tastyquery] val m_macro: SimpleName = termName("macro")
   }
 
   object tpnme {
diff --git a/tasty-query/shared/src/main/scala/tastyquery/reader/ReaderContext.scala b/tasty-query/shared/src/main/scala/tastyquery/reader/ReaderContext.scala
index 2b2b387..d1a4f7d 100644
--- a/tasty-query/shared/src/main/scala/tastyquery/reader/ReaderContext.scala
+++ b/tasty-query/shared/src/main/scala/tastyquery/reader/ReaderContext.scala
@@ -45,6 +45,8 @@ private[reader] final class ReaderContext(underlying: Context):
 
   def scala2FakeOwner: TermSymbol = underlying.defn.scala2FakeOwner
 
+  def scala2MacroInfoFakeMethod: TermSymbol = underlying.defn.scala2MacroInfoFakeMethod
+
   def findPackageFromRootOrCreate(fullyQualifiedName: PackageFullName): PackageSymbol =
     underlying.findPackageFromRootOrCreate(fullyQualifiedName)
 
diff --git a/tasty-query/shared/src/main/scala/tastyquery/reader/pickles/PickleReader.scala b/tasty-query/shared/src/main/scala/tastyquery/reader/pickles/PickleReader.scala
index 5571048..b9c5971 100644
--- a/tasty-query/shared/src/main/scala/tastyquery/reader/pickles/PickleReader.scala
+++ b/tasty-query/shared/src/main/scala/tastyquery/reader/pickles/PickleReader.scala
@@ -976,10 +976,12 @@ private[pickles] class PickleReader {
         val designator = readMaybeExternalSymbolRef()
         val name = readTermNameRef()
         val tpe: TermReferenceType = designator match
-          case sym: TermSymbol             => sym.localRef
-          case sym: PackageSymbol          => sym.packageRef
-          case external: ExternalSymbolRef => external.toTermRef(NoPrefix)
-          case _                           => errorBadSignature(s"illegal $designator for IDENTtree (name '$name')")
+          case sym: TermSymbol                               => sym.localRef
+          case sym: PackageSymbol                            => sym.packageRef
+          case external: ExternalSymbolRef                   => external.toTermRef(NoPrefix)
+          case _: NoExternalSymbolRef if name == nme.m_macro => rctx.scala2MacroInfoFakeMethod.localRef
+          case _ =>
+            errorBadSignature(s"illegal $designator for IDENTtree (name '$name')")
         Ident(name)(tpe)(pos)
 
       case LITERALtree =>


Test patch:
diff --git a/scala2-test-sources/src/main/scala/scalatwo/Macros.scala b/scala2-test-sources/src/main/scala/scalatwo/Macros.scala
new file mode 100644
index 0000000..6994ea4
--- /dev/null
+++ b/scala2-test-sources/src/main/scala/scalatwo/Macros.scala
@@ -0,0 +1,11 @@
+package scalatwo
+
+import scala.language.experimental.macros
+
+import scala.reflect.macros.blackbox.Context
+
+object Macros {
+  def foo[X](x: X): Any = macro fooImpl[X]
+
+  def fooImpl[X](c: Context)(x: c.Expr[X]): c.Expr[Any] = ???
+}
diff --git a/tasty-query/shared/src/test/scala/tastyquery/TypeSuite.scala b/tasty-query/shared/src/test/scala/tastyquery/TypeSuite.scala
index 71e008f..2bbe49f 100644
--- a/tasty-query/shared/src/test/scala/tastyquery/TypeSuite.scala
+++ b/tasty-query/shared/src/test/scala/tastyquery/TypeSuite.scala
@@ -3661,4 +3661,20 @@ class TypeSuite extends UnrestrictedUnpicklingSuite {
     val GenericJavaClass = ctx.findTopLevelClass("javadefined.GenericJavaClass")
     assert(clue(GenericJavaClass.getDecl(typeName("MyInner"))).exists(_.isClass))
   }
+
+  testWithContext("scala-2-macro-definition") {
+    val MacrosClass = ctx.findTopLevelModuleClass("scalatwo.Macros")
+    val macroImplAnnotClass = ctx.findTopLevelClass("scala.reflect.macros.internal.macroImpl")
+
+    val foo = MacrosClass.findNonOverloadedDecl(termName("foo"))
+    assert(foo.isMacro)
+
+    val annotImplMacro = foo.getAnnotation(macroImplAnnotClass).get
+    annotImplMacro.arguments.head match
+      case TypeApply(Apply(macroIdent @ Ident(macroName), _), _) =>
+        assert(clue(macroName) == nme.m_macro)
+        assert(clue(macroIdent.symbol) == defn.scala2MacroInfoFakeMethod)
+      case arg =>
+        fail("unexpected argument to @macroImpl", clues(arg))
+  }
 }