Repository: scalameta/scalameta
Pull request #3444 (2024-01-02 08:15:56)

Origin: add `position` method
Helps with #3425.


Patch:
diff --git a/scalameta/common/shared/src/main/scala/org/scalameta/adt/Reflection.scala b/scalameta/common/shared/src/main/scala/org/scalameta/adt/Reflection.scala
index 8d3a75502..c01397d9b 100644
--- a/scalameta/common/shared/src/main/scala/org/scalameta/adt/Reflection.scala
+++ b/scalameta/common/shared/src/main/scala/org/scalameta/adt/Reflection.scala
@@ -1,9 +1,10 @@
 package org.scalameta.adt
 
-import scala.reflect.api.Universe
 import org.scalameta.adt.{Metadata => AdtMetadata}
 import scala.meta.internal.trees.{Metadata => AstMetadata}
-import scala.reflect.{classTag, ClassTag}
+
+import scala.reflect.{ClassTag, classTag}
+import scala.reflect.api.Universe
 
 trait Reflection {
   val u: Universe
@@ -35,16 +36,14 @@ trait Reflection {
     def isRoot: Boolean = sym.hasAnnotation[AdtMetadata.root]
     def isBranch: Boolean = sym.hasAnnotation[AdtMetadata.branch]
     def isLeaf: Boolean = sym.hasAnnotation[AdtMetadata.leafClass]
-    def isField: Boolean = {
-      val isMethodInLeafClass = sym.isMethod && sym.owner.isLeaf
-      val isParamGetter =
-        sym.isTerm && sym.asTerm.isParamAccessor && sym.asTerm.isGetter && sym.isPublic
-      val isAstField = sym.hasAnnotation[AstMetadata.astField]
-      isMethodInLeafClass && (isParamGetter || isAstField)
+    def isField: Boolean = sym match {
+      case m: MethodSymbolApi if m.owner.isLeaf =>
+        sym.hasAnnotation[AstMetadata.astField] ||
+        m.isPublic && m.isParamAccessor && m.isGetter
+      case _ => false
     }
     def isPayload: Boolean = sym.isField && !sym.isAuxiliary
     def isAuxiliary: Boolean = sym.isField && sym.hasAnnotation[AstMetadata.auxiliary]
-    def isByNeed: Boolean = sym.isField && sym.hasAnnotation[AdtMetadata.byNeedField]
     def asAdt: Adt =
       if (isRoot) sym.asRoot
       else if (isBranch) sym.asBranch
@@ -77,7 +76,7 @@ trait Reflection {
 
     def root: Symbol = sym.asClass.baseClasses.reverse.find(_.isRoot).getOrElse(NoSymbol)
     def fields: List[Symbol] = allFields.filter(p => p.isPayload)
-    def allFields: List[Symbol] = sym.info.decls.filter(_.isField).toList
+    def allFields: List[Symbol] = sym.info.decls.sorted.filter(_.isField)
   }
 
   abstract class Adt(val sym: Symbol) {
@@ -126,11 +125,8 @@ trait Reflection {
     def owner: Leaf = sym.owner.asLeaf
     def name: TermName = TermName(sym.name.toString.stripPrefix("_"))
     def tpe: Type = sym.info.finalResultType
-    def isPayload: Boolean = sym.isPayload
-    def isAuxiliary: Boolean = sym.isAuxiliary
-    def isByNeed: Boolean = sym.isByNeed
     override def toString =
-      s"field ${owner.prefix}.$name: $tpe" + (if (isAuxiliary) " (auxiliary)" else "")
+      s"field ${owner.prefix}.$name: $tpe" + (if (sym.isAuxiliary) " (auxiliary)" else "")
   }
 
   private def isExemptParentSymbol(bsym: ClassSymbol): Boolean = {
diff --git a/scalameta/common/shared/src/main/scala/scala/meta/internal/trees/NamerMacros.scala b/scalameta/common/shared/src/main/scala/scala/meta/internal/trees/NamerMacros.scala
index a5aec1d4d..e41d37681 100644
--- a/scalameta/common/shared/src/main/scala/scala/meta/internal/trees/NamerMacros.scala
+++ b/scalameta/common/shared/src/main/scala/scala/meta/internal/trees/NamerMacros.scala
@@ -116,4 +116,26 @@ trait CommonNamerMacros extends MacroHelpers {
       }
     """
   }
+
+  protected case class PrivateField(field: ValOrDefDef)
+  protected case class PrivateFields(
+      prototype: PrivateField,
+      parent: PrivateField,
+      origin: PrivateField
+  ) {
+    def asList = prototype :: parent :: origin :: Nil
+  }
+
+  protected def getPrivateFields(iname: TypeName): PrivateFields = PrivateFields(
+    PrivateField(
+      q"@$TransientAnnotation private[meta] override val privatePrototype: $iname = null"
+    ),
+    PrivateField(
+      q"private[meta] override val privateParent: $TreeClass = null"
+    ),
+    PrivateField(
+      q"private[meta] override val privateOrigin: $OriginClass = $OriginModule.None"
+    )
+  )
+
 }
diff --git a/scalameta/common/shared/src/main/scala/scala/meta/internal/trees/ast.scala b/scalameta/common/shared/src/main/scala/scala/meta/internal/trees/ast.scala
index d84679b3c..c86d970ee 100644
--- a/scalameta/common/shared/src/main/scala/scala/meta/internal/trees/ast.scala
+++ b/scalameta/common/shared/src/main/scala/scala/meta/internal/trees/ast.scala
@@ -122,11 +122,9 @@ class AstNamerMacros(val c: Context) extends Reflection with CommonNamerMacros {
         stats1 ++= quasiExtraAbstractDefs
 
         // step 4: implement the unimplemented methods in InternalTree (part 1)
-        val bparams1 = List(
-          q"@$TransientAnnotation private[meta] val privatePrototype: $iname",
-          q"private[meta] val privateParent: $TreeClass",
-          q"private[meta] val privateOrigin: $OriginClass"
-        )
+        val privateFields = getPrivateFields(iname)
+        val privateParams = privateFields.asList
+        val bparams1 = privateParams.map(_.field)
 
         // step 5: turn all parameters into vars, create getters and setters
         params.foreach { p =>
@@ -171,9 +169,9 @@ class AstNamerMacros(val c: Context) extends Reflection with CommonNamerMacros {
         stats1 += q"""
         private[meta] def privateCopy(
             prototype: $TreeClass = this,
-            parent: $TreeClass = privateParent,
+            parent: $TreeClass = ${privateFields.parent.field.name},
             destination: $StringClass = null,
-            origin: $OriginClass = privateOrigin): Tree = {
+            origin: $OriginClass = ${privateFields.origin.field.name}): Tree = {
           $privateCopyParentChecks
           new $name(prototype.asInstanceOf[$iname], parent, origin)(..$privateCopyArgs)
         }
@@ -185,7 +183,7 @@ class AstNamerMacros(val c: Context) extends Reflection with CommonNamerMacros {
         // NOTE: Can't generate XXX.Quasi.copy, because XXX.Quasi already inherits XXX.copy,
         // and there can't be multiple overloaded methods with default parameters.
         // Not a big deal though, since XXX.Quasi is an internal class.
-        def getParamArg(p: ValDef): TermName = p.name
+        def getParamArg(p: ValOrDefDef) = q"${p.name}"
         def addCopy(params: List[ValDef], annots: Tree*) = {
           val mods = getDeferredModifiers(annots.toList)
           istats1 += q"""
@@ -324,12 +322,19 @@ class AstNamerMacros(val c: Context) extends Reflection with CommonNamerMacros {
           errorChecker.traverse(fieldCheck)
           if (!hasErrors) internalBody += fieldCheck
         }
-        val internalInitCount = 3 // privatePrototype, privateParent, privateOrigin
-        val internalInitss = 1.to(internalInitCount).map(_ => q"null")
         val paramInits = params.map { p =>
           q"$CommonTyperMacrosModule.initParam(${p.name})"
         }
-        internalBody += q"val node = new $name(..$internalInitss)(..$paramInits)"
+        privateParams.foreach { p =>
+          internalBody += asValDefn(p.field)
+        }
+        internalBody += q"""
+          val node = new $name(
+            ..${bparams1.map(getParamArg)}
+          )(
+            ..$paramInits
+          )
+        """
         params.foreach(p => internalBody += storeField(p))
         internalBody += q"node"
         val applyParams =
@@ -357,10 +362,20 @@ class AstNamerMacros(val c: Context) extends Reflection with CommonNamerMacros {
             defn.foreach(applyBodyBuilder += _)
           }
           val params = positionVersionedParams(applyParamsBuilder.result())
-          applyBodyBuilder += q"$mname.apply(..$internalArgs)"
           val applyBody = applyBodyBuilder.result()
-          verMstats += q"def apply(..$params): $iname = { ..$applyBody }"
-          mstats1 += q"@${getDeprecatedAnno(v)} def apply(..$params): $iname = { ..$applyBody }"
+          val applyCall = q"$mname.apply(..$internalArgs)"
+          verMstats += q"""
+            def apply(..$params): $iname = {
+              ..$applyBody
+              $applyCall
+            }
+          """
+          mstats1 += q"""
+            @${getDeprecatedAnno(v)} def apply(..$params): $iname = {
+              ..$applyBody
+              $applyCall
+            }
+          """
         }
 
         // step 14: generate Companion.unapply
diff --git a/scalameta/dialects/shared/src/main/scala/scala/meta/Dialect.scala b/scalameta/dialects/shared/src/main/scala/scala/meta/Dialect.scala
index 717a17212..bf1d9677e 100644
--- a/scalameta/dialects/shared/src/main/scala/scala/meta/Dialect.scala
+++ b/scalameta/dialects/shared/src/main/scala/scala/meta/Dialect.scala
@@ -7,7 +7,7 @@ import scala.collection.immutable.TreeMap
 /**
  * A dialect is used to configure what Scala syntax is allowed during tokenization and parsing.
  */
-final class Dialect private (
+final class Dialect private[meta] (
     // Are `&` intersection types supported by this dialect?
     @deprecated("allowAndTypes unneeded, infix types are supported", "4.5.1")
     val allowAndTypes: Boolean,
@@ -636,9 +636,13 @@ final class Dialect private (
     Dialect.inverseStandards.getOrElse(this, "Dialect()")
   }
 
-  def isEquivalentTo(that: Dialect): Boolean = (
-    // do not include deprecated values in this comparison
-    this.allowAtForExtractorVarargs == that.allowAtForExtractorVarargs
+  def isEquivalentTo(that: Dialect): Boolean =
+    isEquivalentTo(that, ignoreQuasiquotes = false)
+
+  def isEquivalentTo(that: Dialect, ignoreQuasiquotes: Boolean): Boolean = (
+    (this eq that) ||
+      // do not include deprecated values in this comparison
+      this.allowAtForExtractorVarargs == that.allowAtForExtractorVarargs
       && this.allowCaseClassWithoutParameterList == that.allowCaseClassWithoutParameterList
       && this.allowColonForExtractorVarargs == that.allowColonForExtractorVarargs
       && this.allowEnums == that.allowEnums
@@ -646,14 +650,14 @@ final class Dialect private (
       && this.allowInlineIdents == that.allowInlineIdents
       && this.allowInlineMods == that.allowInlineMods
       && this.allowLiteralTypes == that.allowLiteralTypes
-      && this.allowMultilinePrograms == that.allowMultilinePrograms
-      && this.allowPatUnquotes == that.allowPatUnquotes
+      && (ignoreQuasiquotes || this.allowMultilinePrograms == that.allowMultilinePrograms)
+      && (ignoreQuasiquotes || this.allowPatUnquotes == that.allowPatUnquotes)
       && this.allowSpliceUnderscores == that.allowSpliceUnderscores
-      && this.allowTermUnquotes == that.allowTermUnquotes
+      && (ignoreQuasiquotes || this.allowTermUnquotes == that.allowTermUnquotes)
       && this.allowToplevelTerms == that.allowToplevelTerms
       && this.allowTrailingCommas == that.allowTrailingCommas
       && this.allowTraitParameters == that.allowTraitParameters
-      && this.allowTypeLambdas == that.allowTypeLambdas
+      && (ignoreQuasiquotes || this.allowTypeLambdas == that.allowTypeLambdas)
       && this.allowViewBounds == that.allowViewBounds
       && this.allowXmlLiterals == that.allowXmlLiterals
       && this.allowNumericLiteralUnderscoreSeparators == that.allowNumericLiteralUnderscoreSeparators
diff --git a/scalameta/quasiquotes/shared/src/main/scala/scala/meta/internal/quasiquotes/ReificationMacros.scala b/scalameta/quasiquotes/shared/src/main/scala/scala/meta/internal/quasiquotes/ReificationMacros.scala
index 4f65b7c77..62dc27e13 100644
--- a/scalameta/quasiquotes/shared/src/main/scala/scala/meta/internal/quasiquotes/ReificationMacros.scala
+++ b/scalameta/quasiquotes/shared/src/main/scala/scala/meta/internal/quasiquotes/ReificationMacros.scala
@@ -33,7 +33,7 @@ class ReificationMacros(val c: Context) extends AstReflection with AdtLiftables
 
   // NOTE: only Mode.Pattern really needs holes, and that's only because of Scala's limitations
   // read a comment in liftUnquote for more information on that
-  case class Hole(name: TermName, arg: ReflectTree, var reifier: ReflectTree)
+  case class Hole(arg: ReflectTree, idx: Int, var reifier: ReflectTree = EmptyTree)
   sealed trait Mode {
     def isTerm: Boolean = this.isInstanceOf[Mode.Term]
     def multiline: Boolean
@@ -59,28 +59,33 @@ class ReificationMacros(val c: Context) extends AstReflection with AdtLiftables
     reifySkeleton(skeleton, mode)
   }
 
+  private def mkHoles(args: Seq[ReflectTree]) = {
+    val res = List.newBuilder[Hole]
+    var i = 0
+    args.foreach { arg =>
+      res += Hole(arg, i)
+      i += 1
+    }
+    res.result()
+  }
+
   private def extractQuasiquotee(): (Input, Mode) = {
     val reflectInput = c.macroApplication.pos.source
+    def isMultiline(firstPart: ReflectTree) = {
+      reflectInput.content(firstPart.pos.start - 2) == '"'
+    }
     val (parts, mode) = {
-      def isMultiline(firstPart: ReflectTree) = {
-        reflectInput.content(firstPart.pos.start - 2) == '"'
-      }
-      def mkHole(argi: (ReflectTree, Int)) = {
-        val (arg, i) = argi
-        val name = TermName(QuasiquotePrefix + "$hole$" + i)
-        Hole(name, arg, reifier = EmptyTree)
-      }
       try {
         c.macroApplication match {
           case q"$_($_.apply(..$parts)).$_.apply[..$_](..$args)($_)" =>
             require(parts.length == args.length + 1)
-            val holes = args.zipWithIndex.map(mkHole)
+            val holes = mkHoles(args)
             (parts, Mode.Term(isMultiline(parts.head), holes))
           case q"$_($_.apply(..$parts)).$_.unapply[..$_](${unapplySelector: Ident})($_)" =>
             require(unapplySelector.name == TermName("<unapply-selector>"))
             val args = c.internal.subpatterns(unapplySelector).get
             require(parts.length == args.length + 1)
-            val holes = args.zipWithIndex.map(mkHole)
+            val holes = mkHoles(args)
             (parts, Mode.Pattern(isMultiline(parts.head), holes, unapplySelector))
         }
       } catch {
@@ -96,16 +101,16 @@ class ReificationMacros(val c: Context) extends AstReflection with AdtLiftables
       val start = firstPart.pos.start // looks like we can trust this position to point to the character right after the opening quotes
       val end = { // we have to infer this for ourselves, because there's no guarantee we run under -Yrangepos
         var remaining = s_lastpart.length
-        var curr = lastPart.pos.start - 1
+        var curr = lastPart.pos.start
         while (remaining > 0) {
-          curr += 1
           if (reflectInput.content(curr) == '$') {
             curr += 1
             require(reflectInput.content(curr) == '$')
           }
+          curr += 1
           remaining -= 1
         }
-        curr + 1
+        curr
       }
       val metaInput = Input.VirtualFile(reflectInput.path, new String(reflectInput.content))
       Input.Slice(metaInput, start, end)
@@ -326,9 +331,9 @@ class ReificationMacros(val c: Context) extends AstReflection with AdtLiftables
                   case pq"$_: $explicitPt" => explicitPt
                   case _ => TypeTree(inferredPt)
                 }
-                hole.reifier =
-                  atPos(quasi.pos)(q"$InternalUnlift.unapply[$unliftedPt](${hole.name})")
-                pq"${hole.name}"
+                val name = TermName(QuasiquotePrefix + "$hole$" + hole.idx)
+                hole.reifier = atPos(quasi.pos)(q"$InternalUnlift.unapply[$unliftedPt]($name)")
+                pq"$name"
             }
             atPos(quasi.pos)(lifted)
           } else {
diff --git a/scalameta/trees/shared/src/main/scala/scala/meta/internal/trees/InternalTrees.scala b/scalameta/trees/shared/src/main/scala/scala/meta/internal/trees/InternalTrees.scala
index 20464c40c..65cdcb673 100644
--- a/scalameta/trees/shared/src/main/scala/scala/meta/internal/trees/InternalTrees.scala
+++ b/scalameta/trees/shared/src/main/scala/scala/meta/internal/trees/InternalTrees.scala
@@ -35,9 +35,7 @@ trait InternalTree extends Product {
   // Getters for pieces of internal state defined above.
   // ==============================================================
 
-  def parent: Option[Tree] = {
-    if (privateParent != null) scala.Some(privateParent) else None
-  }
+  def parent: Option[Tree] = Option(privateParent)
 
   // NOTE: InternalTree inherits traditional productXXX methods from Product
   // and also adds a new method called productFields.
@@ -48,15 +46,11 @@ trait InternalTree extends Product {
   def productFields: List[String]
 
   private[meta] def origin: Origin = {
-    if (privateOrigin != null) privateOrigin else Origin.None
+    val nullableOrigin = privateOrigin
+    if (nullableOrigin != null) nullableOrigin else Origin.None
   }
 
-  def pos: Position = {
-    origin match {
-      case x: Origin.Parsed => x.position
-      case _ => Position.None
-    }
-  }
+  def pos: Position = origin.position
 
   def tokens(implicit dialect: Dialect): Tokens = {
     origin match {
@@ -93,7 +87,6 @@ trait InternalTree extends Product {
 
 trait InternalTreeXtensions {
   private[meta] implicit class XtensionOriginTree[T <: Tree](tree: T) {
-    def origin: Origin = if (tree.privateOrigin != null) tree.privateOrigin else Origin.None
     def withOrigin(origin: Origin): T = tree.privateWithOrigin(origin).asInstanceOf[T]
   }
 }
diff --git a/scalameta/trees/shared/src/main/scala/scala/meta/internal/trees/Origin.scala b/scalameta/trees/shared/src/main/scala/scala/meta/internal/trees/Origin.scala
index 57f2588fc..996c2f27c 100644
--- a/scalameta/trees/shared/src/main/scala/scala/meta/internal/trees/Origin.scala
+++ b/scalameta/trees/shared/src/main/scala/scala/meta/internal/trees/Origin.scala
@@ -10,10 +10,15 @@ import scala.meta.tokens._
 import scala.meta.tokenizers._
 
 @adt.root
-trait Origin extends Optional
+trait Origin extends Optional {
+  def position: Position
+}
+
 object Origin {
   @adt.none
-  object None extends Origin
+  object None extends Origin {
+    val position: Position = Position.None
+  }
 
   @adt.leaf
   class Parsed(input: Input, dialect: Dialect, pos: TokenStreamPosition) extends Origin {


Test patch:
diff --git a/tests/shared/src/test/scala/scala/meta/tests/quasiquotes/SuccessSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/quasiquotes/SuccessSuite.scala
index 22c5c3da0..d391a28f0 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/quasiquotes/SuccessSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/quasiquotes/SuccessSuite.scala
@@ -3032,7 +3032,7 @@ class SuccessSuite extends TreeSuiteBase {
   test("#3409") {
     val code: Tree = source"object Generated {}"
     code.privateOrigin match {
-      case null =>
+      case Origin.None =>
       case x => fail(s"origin doesn't match: $x")
     }
   }