Repository: scalameta/scalameta
Pull request #3493 (2024-01-14 15:31:27)

Tree: add `maybeParse` extension, for a parsed tree
If we convert a tree to a parsed tree, the latter will contain proper origin, with input, and will not require regenerating tokens, syntax etc. on every access.


Patch:
diff --git a/scalameta/scalameta/shared/src/main/scala/scala/meta/package.scala b/scalameta/scalameta/shared/src/main/scala/scala/meta/package.scala
index 5fab786d8..df9a1e196 100644
--- a/scalameta/scalameta/shared/src/main/scala/scala/meta/package.scala
+++ b/scalameta/scalameta/shared/src/main/scala/scala/meta/package.scala
@@ -78,4 +78,20 @@ package object meta
       tree.syntax
     }
   }
+
+  implicit class XtensionTree(private val tree: Tree) extends AnyVal {
+    def maybeParseAs[A <: Tree](implicit dialect: Dialect, parse: parsers.Parse[A]): Parsed[A] =
+      if (tree.isInstanceOf[A]) tree.asInstanceOf[A].maybeParse else reparseAs[A]
+
+    def reparseAs[A <: Tree](implicit dialect: Dialect, parse: parsers.Parse[A]): Parsed[A] =
+      parse(tree.textAsInput, dialect)
+  }
+
+  implicit class XtensionTreeT[A <: Tree](private val tree: A) extends AnyVal {
+    def maybeParse(implicit dialect: Dialect, parse: parsers.Parse[A]): Parsed[A] =
+      tree.origin match {
+        case o: trees.Origin.Parsed if o.dialect.isEquivalentTo(dialect) => Parsed.Success(tree)
+        case _ => tree.reparseAs[A]
+      }
+  }
 }


Test patch:
diff --git a/tests/jvm/src/test/scala-2.13/scala/meta/tests/api/SurfaceSuite.scala b/tests/jvm/src/test/scala-2.13/scala/meta/tests/api/SurfaceSuite.scala
index 333f6252b..61b47f992 100644
--- a/tests/jvm/src/test/scala-2.13/scala/meta/tests/api/SurfaceSuite.scala
+++ b/tests/jvm/src/test/scala-2.13/scala/meta/tests/api/SurfaceSuite.scala
@@ -78,6 +78,8 @@ class SurfaceSuite extends FunSuite {
       |scala.meta.XtensionDialectTokenSyntax
       |scala.meta.XtensionDialectTokensSyntax
       |scala.meta.XtensionDialectTreeSyntax
+      |scala.meta.XtensionTree
+      |scala.meta.XtensionTreeT
       |scala.meta.classifiers
       |scala.meta.classifiers.Classifiable *
       |scala.meta.classifiers.Classifier
@@ -224,6 +226,9 @@ class SurfaceSuite extends FunSuite {
       |* (scala.meta.Dialect, scala.meta.tokens.Tokens).tokenize(implicit scala.meta.tokenizers.Tokenize): scala.meta.tokenizers.Tokenized
       |* (scala.meta.inputs.Input, scala.meta.Dialect).parse(implicit scala.meta.parsers.Parse[U]): scala.meta.parsers.Parsed[U]
       |* (scala.meta.inputs.Input, scala.meta.Dialect).tokenize(implicit scala.meta.tokenizers.Tokenize): scala.meta.tokenizers.Tokenized
+      |* A.equals(Any): Boolean
+      |* A.hashCode(): Int
+      |* A.maybeParse(implicit scala.meta.Dialect, scala.meta.parsers.Parse[A]): scala.meta.package.Parsed[A]
       |* T(implicit scala.meta.classifiers.Classifiable[T]).is(implicit XtensionClassifiable.this.C[U]): Boolean
       |* T(implicit scala.meta.classifiers.Classifiable[T]).isAny(implicit XtensionClassifiable.this.C[U1], XtensionClassifiable.this.C[U2]): Boolean
       |* T(implicit scala.meta.classifiers.Classifiable[T]).isAny(implicit XtensionClassifiable.this.C[U1], XtensionClassifiable.this.C[U2], XtensionClassifiable.this.C[U3]): Boolean
@@ -242,6 +247,10 @@ class SurfaceSuite extends FunSuite {
       |* scala.meta.Dialect.equals(Any): Boolean
       |* scala.meta.Dialect.hashCode(): Int
       |* scala.meta.Tree.collect(PartialFunction[scala.meta.Tree,T]): List[T]
+      |* scala.meta.Tree.equals(Any): Boolean
+      |* scala.meta.Tree.hashCode(): Int
+      |* scala.meta.Tree.maybeParseAs(implicit scala.meta.Dialect, scala.meta.parsers.Parse[A]): scala.meta.package.Parsed[A]
+      |* scala.meta.Tree.reparseAs(implicit scala.meta.Dialect, scala.meta.parsers.Parse[A]): scala.meta.package.Parsed[A]
       |* scala.meta.Tree.transform(PartialFunction[scala.meta.Tree,scala.meta.Tree]): scala.meta.Tree
       |* scala.meta.Tree.traverse(PartialFunction[scala.meta.Tree,Unit]): Unit
     """.trim.stripMargin.split('\n').mkString(EOL)
diff --git a/tests/shared/src/test/scala/scala/meta/tests/prettyprinters/PublicSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/prettyprinters/PublicSuite.scala
index 3a7131d32..3136f574e 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/prettyprinters/PublicSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/prettyprinters/PublicSuite.scala
@@ -443,6 +443,8 @@ class PublicSuite extends TreeSuiteBase {
   test("scala.meta.XtensionDialectApply") {}
   test("scala.meta.XtensionDialectTreeSyntax") {}
   test("scala.meta.XtensionDialectTokensSyntax") {}
+  test("scala.meta.XtensionTree") {}
+  test("scala.meta.XtensionTreeT") {}
 
   test("scala.meta.trees.Origin") {}
   test("scala.meta.trees.Origin.DialectOnly") {}
diff --git a/tests/shared/src/test/scala/scala/meta/tests/trees/TokensSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/trees/TokensSuite.scala
index 49c65386c..2f3f28a65 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/trees/TokensSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/trees/TokensSuite.scala
@@ -19,7 +19,7 @@ class TokensSuite extends TreeSuiteBase {
   }
 
   test("Tree.tokens: manual") {
-    val tree: Term = Term.ApplyInfix(Term.Name("foo"), Term.Name("+"), Nil, List(Term.Name("bar")))
+    val tree = Term.ApplyInfix(Term.Name("foo"), Term.Name("+"), Nil, List(Term.Name("bar")))
     interceptMessage[trees.Error.MissingDialectException](
       "Tree missing a dialect; update root tree `.withDialectIfRootAndNotSet` first, or call `.dialectText`."
     )(tree.text)
@@ -38,6 +38,15 @@ class TokensSuite extends TreeSuiteBase {
     val dialectTokens = dialectTree.tokens
     assertEquals(dialectTokens.syntax, "foo + bar")
     assert(dialectTokens.forall(_.input.isInstanceOf[Input.VirtualFile]))
+
+    val parsedTree = tree.maybeParseAs[Stat].get
+    assert(parsedTree eq parsedTree.maybeParse.get)
+    assertEquals(parsedTree.printSyntaxFor(implicitly[Dialect]), "foo + bar")
+    assertEquals(parsedTree.syntax, "foo + bar")
+    assert(parsedTree.origin.isInstanceOf[trees.Origin.Parsed])
+    val parsedTokens = parsedTree.tokens
+    assertEquals(parsedTokens.syntax, "foo + bar")
+    assert(parsedTokens.forall(_.input.isInstanceOf[Input.VirtualFile]))
   }
 
   test("Tree.tokens: empty") {