Skip to content

Commit

Permalink
Merge pull request #416 from sjrd/fix-inter-files-corruption
Browse files Browse the repository at this point in the history
Fix #412: Protect the state of packages and loaders from exceptions.
  • Loading branch information
sjrd authored Dec 6, 2023
2 parents 06ffbf0 + 692aa59 commit c1f1ba7
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 27 deletions.
63 changes: 51 additions & 12 deletions tasty-query/shared/src/main/scala/tastyquery/Symbols.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1801,6 +1801,8 @@ object Symbols {
// DeclaringSymbol-related fields
private var rootsInitialized: Boolean = false
private val myDeclarations = mutable.HashMap[UnsignedName, Symbol]()
private val pendingDeclarations = mutable.HashMap[UnsignedName, Symbol]()
private var isLoadingNewRoots: Boolean = false

// Cache fields
val packageRef: PackageRef = new PackageRef(this)
Expand Down Expand Up @@ -1852,19 +1854,58 @@ object Symbols {
end getPackageDecl

private[Symbols] final def addDecl(decl: Symbol): Unit =
assert(!myDeclarations.contains(decl.name), s"trying to add a second entry $decl for name ${decl.name} in $this")
myDeclarations(decl.name) = decl
assert(
!myDeclarations.contains(decl.name) && !pendingDeclarations.contains(decl.name),
s"trying to add a second entry $decl for name ${decl.name} in $this"
)

/* If we are loading new roots and the decl is not a package,
* add the declaration to the pending set only. They will be committed
* later by `loadingNewRoots` if loading is successful.
*
* Packages are always eagerly committed.
*/
decl match
case decl: TermOrTypeSymbol if isLoadingNewRoots =>
pendingDeclarations(decl.name) = decl
case _ =>
myDeclarations(decl.name) = decl
end addDecl

/** Performs an operation that can load new roots from the class loader.
*
* While loading new roots, any new non-package member sent to `addDecl`
* is added to `pendingDeclarations` instead of `myDeclarations`. They
* are committed to `myDeclarations` only after the `op` successfully
* completes.
*
* This way, any exception occurring during loading does not pollute the
* publicly visible state in `myDeclarations`.
*/
private def loadingNewRoots[A](op: Loader => A)(using Context): A =
if isLoadingNewRoots then throw IllegalStateException(s"Cyclic loading of new roots in package $this")

isLoadingNewRoots = true
try
if !rootsInitialized then
ctx.classloader.scanPackage(this)
rootsInitialized = true

val result = op(ctx.classloader)

// Upon success, commit pending declations
myDeclarations ++= pendingDeclarations

private final def ensureRootsInitialized()(using Context): Unit =
if !rootsInitialized then
ctx.classloader.scanPackage(this)
rootsInitialized = true
result
finally
pendingDeclarations.clear() // whether or not they were committed
isLoadingNewRoots = false
end loadingNewRoots

final def getDecl(name: Name)(using Context): Option[Symbol] = name match
case name: UnsignedName =>
myDeclarations.get(name).orElse {
ensureRootsInitialized()
if ctx.classloader.loadRoot(this, name) then myDeclarations.get(name)
if loadingNewRoots(_.loadRoot(this, name)) then myDeclarations.get(name)
else None
}
case _: SignedName =>
Expand Down Expand Up @@ -1893,8 +1934,7 @@ object Symbols {
}

final def declarations(using Context): List[Symbol] =
ensureRootsInitialized()
ctx.classloader.loadAllRoots(this)
loadingNewRoots(_.loadAllRoots(this))
myDeclarations.values.toList

// See PackageRef.findMember
Expand All @@ -1908,8 +1948,7 @@ object Symbols {
end allPackageObjectDecls

private def computeAllPackageObjectDecls()(using Context): List[ClassSymbol] =
ensureRootsInitialized()
ctx.classloader.loadAllPackageObjectRoots(this)
loadingNewRoots(_.loadAllPackageObjectRoots(this))
myDeclarations.valuesIterator.collect {
case cls: ClassSymbol if cls.name.isPackageObjectClassName => cls
}.toList
Expand Down
44 changes: 29 additions & 15 deletions tasty-query/shared/src/main/scala/tastyquery/reader/Loaders.scala
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,15 @@ private[tastyquery] object Loaders {

/** Loads all the roots of the given `pkg`. */
private[tastyquery] def loadAllRoots(pkg: PackageSymbol)(using Context): Unit =
for
entries <- roots.remove(pkg)
(rootName, entry) <- IArray.from(entries).sortBy(_(0).name) // sort for determinism.
do completeRoot(Loader.Root(pkg, rootName), entry)
roots.get(pkg) match
case Some(entries) =>
val allNames =
entries.keysIterator.toList.sortBy(_.name) // sort for determinism
for rootName <- allNames do doLoadRoot(pkg, entries, rootName)
// Upon success, we won't need anything from that package anymore
roots -= pkg
case None =>
()
end loadAllRoots

/** Loads all the roots of the given `pkg` that could be package objects. */
Expand All @@ -171,10 +176,9 @@ private[tastyquery] object Loaders {
case Some(entries) =>
val candidateNames =
entries.keysIterator.filter(_.isPackageObjectName).toList.sortBy(_.name) // sort for determinism
for
rootName <- candidateNames
entry <- entries.remove(rootName)
do completeRoot(Loader.Root(pkg, rootName), entry)
for rootName <- candidateNames do doLoadRoot(pkg, entries, rootName)
// Upon success, we won't need any of the loaded entries anymore
entries --= candidateNames
case None =>
()
end loadAllPackageObjectRoots
Expand All @@ -196,16 +200,26 @@ private[tastyquery] object Loaders {
roots.get(pkg) match
case Some(entries) =>
val rootName = topLevelSymbolNameToRootName(name)
entries.remove(rootName) match
case Some(entry) =>
completeRoot(Loader.Root(pkg, rootName), entry)
true
case None =>
false
val result = doLoadRoot(pkg, entries, rootName)
if result then
// Upon success, we won't need that particular entry anymore
entries -= rootName
result
case None =>
false
end loadRoot

private def doLoadRoot(pkg: PackageSymbol, pkgEntries: mutable.Map[SimpleName, Entry], rootName: SimpleName)(
using Context
): Boolean =
pkgEntries.get(rootName) match
case Some(entry) =>
completeRoot(Loader.Root(pkg, rootName), entry)
true
case None =>
false
end doLoadRoot

private def foreachEntry(data: PackageData)(f: (SimpleName, Entry) => Unit): Unit =
def binaryNameToRootName(binaryName: String): SimpleName =
termName(NameTransformer.decode(binaryName))
Expand Down Expand Up @@ -239,11 +253,11 @@ private[tastyquery] object Loaders {
require(searched)
packages.get(pkg) match {
case Some(datas) =>
packages -= pkg
val localRoots = mutable.HashMap.empty[SimpleName, Entry]
for data <- datas do
foreachEntry(data)(localRoots.getOrElseUpdate) // duplicate roots from later classpath entries are ignored
roots(pkg) = localRoots
packages -= pkg
case _ => // probably a synthetic package that only has other packages as members. (i.e. `package java`)
}
}
Expand Down

0 comments on commit c1f1ba7

Please sign in to comment.