Repository: scalacenter/tasty-query (scala)

Issue:
cannot find member StringBuilder/T in symbol[package scala.collection.mutable]
On the current main branch, this test fails

```scala
test("scala.collection") {
    given Context = Contexts.init(testClasspath)

    val scala = resolve(RootPkg / name"scala").asClass
    scala.declarations

    val StringBuilder = resolve(RootPkg / name"scala" / name"collection" / name"mutable" / tname"StringBuilder").asClass
  }
```

If I remove the `scala.declarations` line then the test passes.

Pull request:
the problem was that `forceRoots` had some recursive forcing (not cyclic), and caused the roots map in Loader to be overwritten, solution is to update the roots map before forcing symbols.

The second commit then removes the unnecessary recursive forcing, but the test already passes in the first commit

fixes #105

Patch:
diff --git a/shared/src/main/scala/tastyquery/ast/Symbols.scala b/shared/src/main/scala/tastyquery/ast/Symbols.scala
index 3dcfc4c4..3487eb21 100644
--- a/shared/src/main/scala/tastyquery/ast/Symbols.scala
+++ b/shared/src/main/scala/tastyquery/ast/Symbols.scala
@@ -458,7 +458,8 @@ object Symbols {
       ctx.classloader.forceRoots(this)
 
     final def getPackageDecl(name: SimpleName)(using Context): Option[PackageClassSymbol] =
-      ensureInitialised()
+      // wrapper that ensures the Context is available, just in case we need it
+      // to be side-effecting in the future.
       getPackageDeclInternal(name)
 
     private[tastyquery] def getPackageDeclInternal(packageName: SimpleName): Option[PackageClassSymbol] =
@@ -517,6 +518,7 @@ object Symbols {
     def createRoots: (PackageClassSymbol, PackageClassSymbol) =
       val root = PackageClassSymbol(nme.RootName, null)
       val empty = PackageClassSymbol(nme.EmptyPackageName, root)
+      root.initialised = true // should not be any class files for the root package
       (root, empty)
 
     override def castSymbol(symbol: Symbol): PackageClassSymbol = symbol.asInstanceOf[PackageClassSymbol]
diff --git a/shared/src/main/scala/tastyquery/reader/classfiles/Classpaths.scala b/shared/src/main/scala/tastyquery/reader/classfiles/Classpaths.scala
index 4c79e46e..6bf23ec2 100644
--- a/shared/src/main/scala/tastyquery/reader/classfiles/Classpaths.scala
+++ b/shared/src/main/scala/tastyquery/reader/classfiles/Classpaths.scala
@@ -87,7 +87,7 @@ object Classpaths {
 
     private var searched = false
     private var packages: Map[PackageClassSymbol, PackageData] = compiletime.uninitialized
-    private var roots: Map[PackageClassSymbol, Map[SimpleName, Entry]] = Map.empty
+    private val roots: mutable.Map[PackageClassSymbol, mutable.Map[SimpleName, Entry]] = mutable.HashMap.empty
     private var topLevelTastys: Map[Loader.Root, List[Tree]] = Map.empty
 
     def toPackageName(dotSeparated: String): FullyQualifiedName =
@@ -160,25 +160,21 @@ object Classpaths {
     end completeRoots
 
     private[tastyquery] def forceRoots(pkg: PackageClassSymbol)(using Context): Unit =
-      roots = roots.updatedWith(pkg) {
-        case Some(entries) =>
-          for (rootName, entry) <- IArray.from(entries).sortBy(_(0)) do completeRoots(Loader.Root(pkg, rootName), entry)
-          None
-        case _ => None
-      }
+      for
+        entries <- roots.remove(pkg)
+        (rootName, entry) <- IArray.from(entries).sortBy(_(0)) // sort for determinism.
+      do completeRoots(Loader.Root(pkg, rootName), entry)
 
     /** @return true if loaded the classes inner definitions */
     private[tastyquery] def enterRoots(pkg: PackageClassSymbol, name: Name)(using Context): Option[Symbol] =
       roots.get(pkg) match
         case Some(entries) =>
           val rootName = name.toTermName.stripObjectSuffix.asSimpleName
-          entries.get(rootName) match
-            case Some(entry) =>
-              roots = roots.updated(pkg, entries - rootName)
-              if completeRoots(Loader.Root(pkg, rootName), entry) then
-                pkg.getDeclInternal(name) // should have entered some roots, now lookup the requested root
-              else None
-            case _ => None
+          for
+            entry <- entries.remove(rootName)
+            if completeRoots(Loader.Root(pkg, rootName), entry)
+            resolved <- pkg.getDeclInternal(name) // should have entered some roots, now lookup the requested root
+          yield resolved
         case _ => None
     end enterRoots
 
@@ -215,7 +211,7 @@ object Classpaths {
 
           packages -= pkg
 
-          var localRoots = Map.newBuilder[SimpleName, Entry]
+          val localRoots = mutable.HashMap.empty[SimpleName, Entry]
 
           if data.classes.isEmpty then
             for tData <- data.tastys if !isNestedOrModuleClassName(tData.simpleName) do
@@ -227,7 +223,7 @@ object Classpaths {
                 tastyMap.get(cData.simpleName).map(Entry.ClassAndTasty(cData, _)).getOrElse(Entry.ClassOnly(cData))
               localRoots += (cData.simpleName -> entry)
 
-          roots += pkg -> localRoots.result()
+          roots(pkg) = localRoots
 
         case _ => // probably a synthetic package that only has other packages as members. (i.e. `package java`)
       } andThen { pkg.initialised = true }
diff --git a/shared/src/main/scala/tastyquery/reader/pickles/PickleReader.scala b/shared/src/main/scala/tastyquery/reader/pickles/PickleReader.scala
index e5dba8a0..e0db0f9c 100644
--- a/shared/src/main/scala/tastyquery/reader/pickles/PickleReader.scala
+++ b/shared/src/main/scala/tastyquery/reader/pickles/PickleReader.scala
@@ -126,7 +126,8 @@ class PickleReader {
             case owner: PackageClassSymbol =>
               name match
                 case packageName: SimpleName =>
-                  owner.getPackageDecl(packageName).getOrElse {
+                  // lookup package, do not force anything
+                  owner.getPackageDeclInternal(packageName).getOrElse {
                     //errorBadSignature(s"cannot find symbol $owner.$name")
                     defaultRef
                   }


Test patch:
diff --git a/jvm/src/test/scala/tastyquery/TypeSuite.scala b/jvm/src/test/scala/tastyquery/TypeSuite.scala
index ed0aab07..3f6b13f8 100644
--- a/jvm/src/test/scala/tastyquery/TypeSuite.scala
+++ b/jvm/src/test/scala/tastyquery/TypeSuite.scala
@@ -775,4 +775,11 @@ class TypeSuite extends UnrestrictedUnpicklingSuite {
     // type parameter C <: SeqOps[A, CC, C]
     val `:+` = resolve(name"scala" / name"collection" / tname"package" / obj / tname":+" / obj).asClass
   }
+
+  testWithContext("read-scala.collection.mutable.StringBuilder_after-force-scala-pkg") {
+    val scala = resolve(RootPkg / name"scala").asClass
+    scala.declarations
+
+    val StringBuilder = resolve(RootPkg / name"scala" / name"collection" / name"mutable" / tname"StringBuilder").asClass
+  }
 }