Skip to content

Commit 459fe60

Browse files
authored
Prioritize TASTy files over classfiles on classpath aggregation (#19431)
In most cases the TASTy file is chosen over the classfile in a classpath because they are packaged together. However, for the `scala-library` (Scala 2 compiled library) and `scala2-library-tasty` (Scala 3 compiled Scala 2 library) we have the classfiles in one jar and the TASTy files in another jar. Given that the classpaths order in guaranteed to be deterministic we might end up with the classfile being loaded first and the TASTy file second. The aggregator must be capable of choosing the TASTy file over the classfile in this case as well. The aggregator will only choose the new TASTy over the old classfile if the TASTy file has no classfile in its classpath. In other words, we only use this new behaviour for TASTy only classpaths. This also implies that we can just add the `scala2-library-tasty` as a dependency in any project to use it. Note that this jar is not published yet. This change required a refactoring of `ClassRepresentation` implementations. Here is the change in structure and naming. ```diff - ClassRepresentation - +- ClassFileEntry - | +- ClassFileEntryImpl(AbstractFile) - +- SourceFileEntry - | +- SourceFileEntryImpl(AbstractFile) - +- ClassAndSourceFilesEntry(AbstractFile, AbstractFile) + ClassRepresentation + +- BinaryFileEntry + | +- ClassFileEntry(AbstractFile) + | +- TastyWithClassFileEntry(AbstractFile) + | +- StandaloneTastyFileEntry(AbstractFile) + +- SourceFileEntry(AbstractFile) + +- BinaryAndSourceFilesEntry(BinaryFileEntry, SourceFileEntry) ```
2 parents 5c1f86a + ebe09ac commit 459fe60

File tree

11 files changed

+112
-77
lines changed

11 files changed

+112
-77
lines changed

compiler/src/dotty/tools/dotc/classpath/AggregateClassPath.scala

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,14 @@ case class AggregateClassPath(aggregates: Seq[ClassPath]) extends ClassPath {
3939
def findEntry(isSource: Boolean): Option[ClassRepresentation] =
4040
aggregatesForPackage(PackageName(pkg)).iterator.map(_.findClass(className)).collectFirst {
4141
case Some(s: SourceFileEntry) if isSource => s
42-
case Some(s: ClassFileEntry) if !isSource => s
42+
case Some(s: BinaryFileEntry) if !isSource => s
4343
}
4444

4545
val classEntry = findEntry(isSource = false)
4646
val sourceEntry = findEntry(isSource = true)
4747

4848
(classEntry, sourceEntry) match {
49-
case (Some(c: ClassFileEntry), Some(s: SourceFileEntry)) => Some(ClassAndSourceFilesEntry(c.file, s.file))
49+
case (Some(c: BinaryFileEntry), Some(s: SourceFileEntry)) => Some(BinaryAndSourceFilesEntry(c, s))
5050
case (c @ Some(_), _) => c
5151
case (_, s) => s
5252
}
@@ -63,7 +63,7 @@ case class AggregateClassPath(aggregates: Seq[ClassPath]) extends ClassPath {
6363
aggregatedPackages
6464
}
6565

66-
override private[dotty] def classes(inPackage: PackageName): Seq[ClassFileEntry] =
66+
override private[dotty] def classes(inPackage: PackageName): Seq[BinaryFileEntry] =
6767
getDistinctEntries(_.classes(inPackage))
6868

6969
override private[dotty] def sources(inPackage: PackageName): Seq[SourceFileEntry] =
@@ -102,10 +102,15 @@ case class AggregateClassPath(aggregates: Seq[ClassPath]) extends ClassPath {
102102
ClassPathEntries(distinctPackages, distinctClassesAndSources)
103103
}
104104

105-
/**
106-
* Returns only one entry for each name. If there's both a source and a class entry, it
107-
* creates an entry containing both of them. If there would be more than one class or source
108-
* entries for the same class it always would use the first entry of each type found on a classpath.
105+
/** Returns only one entry for each name.
106+
*
107+
* If there's both a source and a class entry, it
108+
* creates an entry containing both of them. If there would be more than one class or source
109+
* entries for the same class it always would use the first entry of each type found on a classpath.
110+
*
111+
* A TASTy file with no class file entry will be chosen over a class file entry. This can happen if we load
112+
* the Scala 2 library as it has one JAR containing the class files and one JAR containing the TASTy files.
113+
* As classpath orders are not guaranteed to be deterministic we might end up having the TASTy in a later classpath entry.
109114
*/
110115
private def mergeClassesAndSources(entries: scala.collection.Seq[ClassRepresentation]): Seq[ClassRepresentation] = {
111116
// based on the implementation from MergedClassPath
@@ -119,11 +124,18 @@ case class AggregateClassPath(aggregates: Seq[ClassPath]) extends ClassPath {
119124
if (indices.contains(name)) {
120125
val index = indices(name)
121126
val existing = mergedEntries(index)
122-
123-
if (existing.binary.isEmpty && entry.binary.isDefined)
124-
mergedEntries(index) = ClassAndSourceFilesEntry(entry.binary.get, existing.source.get)
125-
if (existing.source.isEmpty && entry.source.isDefined)
126-
mergedEntries(index) = ClassAndSourceFilesEntry(existing.binary.get, entry.source.get)
127+
(entry, existing) match
128+
case (entry: SourceFileEntry, existing: BinaryFileEntry) =>
129+
mergedEntries(index) = BinaryAndSourceFilesEntry(existing, entry)
130+
case (entry: BinaryFileEntry, existing: SourceFileEntry) =>
131+
mergedEntries(index) = BinaryAndSourceFilesEntry(entry, existing)
132+
case (entry: StandaloneTastyFileEntry, _: ClassFileEntry) =>
133+
// Here we do not create a TastyWithClassFileEntry because the TASTy and the classfile
134+
// come from different classpaths. These may not have the same TASTy UUID.
135+
mergedEntries(index) = entry
136+
case (entry: StandaloneTastyFileEntry, BinaryAndSourceFilesEntry(_: ClassFileEntry, sourceEntry)) =>
137+
mergedEntries(index) = BinaryAndSourceFilesEntry(entry, sourceEntry)
138+
case _ =>
127139
}
128140
else {
129141
indices(name) = count

compiler/src/dotty/tools/dotc/classpath/ClassPath.scala

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44
package dotty.tools.dotc.classpath
55

6+
import dotty.tools.dotc.classpath.FileUtils.isTasty
67
import dotty.tools.io.AbstractFile
78
import dotty.tools.io.ClassRepresentation
89

@@ -14,14 +15,6 @@ object ClassPathEntries {
1415
val empty = ClassPathEntries(Seq.empty, Seq.empty)
1516
}
1617

17-
trait ClassFileEntry extends ClassRepresentation {
18-
def file: AbstractFile
19-
}
20-
21-
trait SourceFileEntry extends ClassRepresentation {
22-
def file: AbstractFile
23-
}
24-
2518
case class PackageName(dottedString: String) {
2619
val dirPathTrailingSlashJar: String = FileUtils.dirPathInJar(dottedString) + "/"
2720

@@ -48,28 +41,50 @@ trait PackageEntry {
4841
def name: String
4942
}
5043

51-
private[dotty] case class ClassFileEntryImpl(file: AbstractFile) extends ClassFileEntry {
44+
/** A TASTy file or classfile */
45+
sealed trait BinaryFileEntry extends ClassRepresentation {
46+
def file: AbstractFile
5247
final def fileName: String = file.name
53-
def name: String = FileUtils.stripClassExtension(file.name) // class name
48+
final def name: String = FileUtils.stripClassExtension(file.name) // class name
49+
final def source: Option[AbstractFile] = None
50+
}
5451

52+
object BinaryFileEntry {
53+
def apply(file: AbstractFile): BinaryFileEntry =
54+
if file.isTasty then
55+
if file.resolveSiblingWithExtension("class") != null then TastyWithClassFileEntry(file)
56+
else StandaloneTastyFileEntry(file)
57+
else
58+
ClassFileEntry(file)
59+
}
60+
61+
/** A classfile or .sig that does not have an associated TASTy file */
62+
private[dotty] final case class ClassFileEntry(file: AbstractFile) extends BinaryFileEntry {
5563
def binary: Option[AbstractFile] = Some(file)
56-
def source: Option[AbstractFile] = None
5764
}
5865

59-
private[dotty] case class SourceFileEntryImpl(file: AbstractFile) extends SourceFileEntry {
66+
/** A TASTy file that has an associated class file */
67+
private[dotty] final case class TastyWithClassFileEntry(file: AbstractFile) extends BinaryFileEntry {
68+
def binary: Option[AbstractFile] = Some(file)
69+
}
70+
71+
/** A TASTy file that does not have an associated class file */
72+
private[dotty] final case class StandaloneTastyFileEntry(file: AbstractFile) extends BinaryFileEntry {
73+
def binary: Option[AbstractFile] = Some(file)
74+
}
75+
76+
private[dotty] final case class SourceFileEntry(file: AbstractFile) extends ClassRepresentation {
6077
final def fileName: String = file.name
6178
def name: String = FileUtils.stripSourceExtension(file.name)
62-
6379
def binary: Option[AbstractFile] = None
6480
def source: Option[AbstractFile] = Some(file)
6581
}
6682

67-
private[dotty] case class ClassAndSourceFilesEntry(classFile: AbstractFile, srcFile: AbstractFile) extends ClassRepresentation {
68-
final def fileName: String = classFile.name
69-
def name: String = FileUtils.stripClassExtension(classFile.name)
70-
71-
def binary: Option[AbstractFile] = Some(classFile)
72-
def source: Option[AbstractFile] = Some(srcFile)
83+
private[dotty] final case class BinaryAndSourceFilesEntry(binaryEntry: BinaryFileEntry, sourceEntry: SourceFileEntry) extends ClassRepresentation {
84+
final def fileName: String = binaryEntry.fileName
85+
def name: String = binaryEntry.name
86+
def binary: Option[AbstractFile] = binaryEntry.binary
87+
def source: Option[AbstractFile] = sourceEntry.source
7388
}
7489

7590
private[dotty] case class PackageEntryImpl(name: String) extends PackageEntry
@@ -81,5 +96,5 @@ private[dotty] trait NoSourcePaths {
8196

8297
private[dotty] trait NoClassPaths {
8398
def findClassFile(className: String): Option[AbstractFile] = None
84-
private[dotty] def classes(inPackage: PackageName): Seq[ClassFileEntry] = Seq.empty
99+
private[dotty] def classes(inPackage: PackageName): Seq[BinaryFileEntry] = Seq.empty
85100
}

compiler/src/dotty/tools/dotc/classpath/DirectoryClassPath.scala

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -183,12 +183,12 @@ final class JrtClassPath(fs: java.nio.file.FileSystem) extends ClassPath with No
183183
override private[dotty] def packages(inPackage: PackageName): Seq[PackageEntry] =
184184
packageToModuleBases.keysIterator.filter(pack => packageContains(inPackage.dottedString, pack)).map(PackageEntryImpl(_)).toVector
185185

186-
private[dotty] def classes(inPackage: PackageName): Seq[ClassFileEntry] =
186+
private[dotty] def classes(inPackage: PackageName): Seq[BinaryFileEntry] =
187187
if (inPackage.isRoot) Nil
188188
else
189189
packageToModuleBases.getOrElse(inPackage.dottedString, Nil).flatMap(x =>
190190
Files.list(x.resolve(inPackage.dirPathTrailingSlash)).iterator().asScala.filter(_.getFileName.toString.endsWith(".class"))).map(x =>
191-
ClassFileEntryImpl(x.toPlainFile)).toVector
191+
ClassFileEntry(x.toPlainFile)).toVector
192192

193193
override private[dotty] def list(inPackage: PackageName): ClassPathEntries =
194194
if (inPackage.isRoot) ClassPathEntries(packages(inPackage), Nil)
@@ -246,12 +246,12 @@ final class CtSymClassPath(ctSym: java.nio.file.Path, release: Int) extends Clas
246246
override private[dotty] def packages(inPackage: PackageName): Seq[PackageEntry] = {
247247
packageIndex.keysIterator.filter(pack => packageContains(inPackage.dottedString, pack)).map(PackageEntryImpl(_)).toVector
248248
}
249-
private[dotty] def classes(inPackage: PackageName): Seq[ClassFileEntry] = {
249+
private[dotty] def classes(inPackage: PackageName): Seq[BinaryFileEntry] = {
250250
if (inPackage.isRoot) Nil
251251
else {
252252
val sigFiles = packageIndex.getOrElse(inPackage.dottedString, Nil).iterator.flatMap(p =>
253253
Files.list(p).iterator.asScala.filter(_.getFileName.toString.endsWith(".sig")))
254-
sigFiles.map(f => ClassFileEntryImpl(f.toPlainFile)).toVector
254+
sigFiles.map(f => ClassFileEntry(f.toPlainFile)).toVector
255255
}
256256
}
257257

@@ -273,33 +273,35 @@ final class CtSymClassPath(ctSym: java.nio.file.Path, release: Int) extends Clas
273273
}
274274
}
275275

276-
case class DirectoryClassPath(dir: JFile) extends JFileDirectoryLookup[ClassFileEntryImpl] with NoSourcePaths {
277-
override def findClass(className: String): Option[ClassRepresentation] = findClassFile(className) map ClassFileEntryImpl.apply
276+
case class DirectoryClassPath(dir: JFile) extends JFileDirectoryLookup[BinaryFileEntry] with NoSourcePaths {
277+
override def findClass(className: String): Option[ClassRepresentation] =
278+
findClassFile(className).map(BinaryFileEntry(_))
278279

279280
def findClassFile(className: String): Option[AbstractFile] = {
280281
val relativePath = FileUtils.dirPath(className)
281282
val tastyFile = new JFile(dir, relativePath + ".tasty")
282283
if tastyFile.exists then Some(tastyFile.toPath.toPlainFile)
283284
else
284285
val classFile = new JFile(dir, relativePath + ".class")
285-
if classFile.exists then Some(classFile.toPath.toPlainFile)
286+
if classFile.exists then Some(classFile.toPath.toPlainFile)
286287
else None
287288
}
288289

289-
protected def createFileEntry(file: AbstractFile): ClassFileEntryImpl = ClassFileEntryImpl(file)
290+
protected def createFileEntry(file: AbstractFile): BinaryFileEntry = BinaryFileEntry(file)
291+
290292
protected def isMatchingFile(f: JFile): Boolean =
291293
f.isTasty || (f.isClass && f.classToTasty.isEmpty)
292294

293-
private[dotty] def classes(inPackage: PackageName): Seq[ClassFileEntry] = files(inPackage)
295+
private[dotty] def classes(inPackage: PackageName): Seq[BinaryFileEntry] = files(inPackage)
294296
}
295297

296-
case class DirectorySourcePath(dir: JFile) extends JFileDirectoryLookup[SourceFileEntryImpl] with NoClassPaths {
298+
case class DirectorySourcePath(dir: JFile) extends JFileDirectoryLookup[SourceFileEntry] with NoClassPaths {
297299
def asSourcePathString: String = asClassPathString
298300

299-
protected def createFileEntry(file: AbstractFile): SourceFileEntryImpl = SourceFileEntryImpl(file)
301+
protected def createFileEntry(file: AbstractFile): SourceFileEntry = SourceFileEntry(file)
300302
protected def isMatchingFile(f: JFile): Boolean = endsScalaOrJava(f.getName)
301303

302-
override def findClass(className: String): Option[ClassRepresentation] = findSourceFile(className) map SourceFileEntryImpl.apply
304+
override def findClass(className: String): Option[ClassRepresentation] = findSourceFile(className).map(SourceFileEntry(_))
303305

304306
private def findSourceFile(className: String): Option[AbstractFile] = {
305307
val relativePath = FileUtils.dirPath(className)

compiler/src/dotty/tools/dotc/classpath/VirtualDirectoryClassPath.scala

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import dotty.tools.io.{AbstractFile, VirtualDirectory}
77
import FileUtils.*
88
import java.net.{URI, URL}
99

10-
case class VirtualDirectoryClassPath(dir: VirtualDirectory) extends ClassPath with DirectoryLookup[ClassFileEntryImpl] with NoSourcePaths {
10+
case class VirtualDirectoryClassPath(dir: VirtualDirectory) extends ClassPath with DirectoryLookup[BinaryFileEntry] with NoSourcePaths {
1111
type F = AbstractFile
1212

1313
// From AbstractFileClassLoader
@@ -38,7 +38,8 @@ case class VirtualDirectoryClassPath(dir: VirtualDirectory) extends ClassPath wi
3838
def asURLs: Seq[URL] = Seq(new URI(dir.name).toURL)
3939
def asClassPathStrings: Seq[String] = Seq(dir.path)
4040

41-
override def findClass(className: String): Option[ClassRepresentation] = findClassFile(className) map ClassFileEntryImpl.apply
41+
override def findClass(className: String): Option[ClassRepresentation] =
42+
findClassFile(className).map(BinaryFileEntry(_))
4243

4344
def findClassFile(className: String): Option[AbstractFile] = {
4445
val pathSeq = FileUtils.dirPath(className).split(java.io.File.separator)
@@ -49,9 +50,10 @@ case class VirtualDirectoryClassPath(dir: VirtualDirectory) extends ClassPath wi
4950
.orElse(Option(lookupPath(parentDir)(pathSeq.last + ".class" :: Nil, directory = false)))
5051
}
5152

52-
private[dotty] def classes(inPackage: PackageName): Seq[ClassFileEntry] = files(inPackage)
53+
private[dotty] def classes(inPackage: PackageName): Seq[BinaryFileEntry] = files(inPackage)
54+
55+
protected def createFileEntry(file: AbstractFile): BinaryFileEntry = BinaryFileEntry(file)
5356

54-
protected def createFileEntry(file: AbstractFile): ClassFileEntryImpl = ClassFileEntryImpl(file)
5557
protected def isMatchingFile(f: AbstractFile): Boolean =
5658
f.isTasty || (f.isClass && f.classToTasty.isEmpty)
5759
}

compiler/src/dotty/tools/dotc/classpath/ZipAndJarFileLookupFactory.scala

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,23 @@ sealed trait ZipAndJarFileLookupFactory {
4141
*/
4242
object ZipAndJarClassPathFactory extends ZipAndJarFileLookupFactory {
4343
private case class ZipArchiveClassPath(zipFile: File, override val release: Option[String])
44-
extends ZipArchiveFileLookup[ClassFileEntryImpl]
44+
extends ZipArchiveFileLookup[BinaryFileEntry]
4545
with NoSourcePaths {
4646

4747
override def findClassFile(className: String): Option[AbstractFile] =
4848
findClass(className).map(_.file)
4949

5050
// This method is performance sensitive as it is used by SBT's ExtractDependencies phase.
51-
override def findClass(className: String): Option[ClassFileEntryImpl] = {
51+
override def findClass(className: String): Option[BinaryFileEntry] = {
5252
val (pkg, simpleClassName) = PackageNameUtils.separatePkgAndClassNames(className)
5353
val binaries = files(PackageName(pkg), simpleClassName + ".tasty", simpleClassName + ".class")
5454
binaries.find(_.file.isTasty).orElse(binaries.find(_.file.isClass))
5555
}
5656

57-
override private[dotty] def classes(inPackage: PackageName): Seq[ClassFileEntry] = files(inPackage)
57+
override private[dotty] def classes(inPackage: PackageName): Seq[BinaryFileEntry] = files(inPackage)
58+
59+
override protected def createFileEntry(file: FileZipArchive#Entry): BinaryFileEntry = BinaryFileEntry(file)
5860

59-
override protected def createFileEntry(file: FileZipArchive#Entry): ClassFileEntryImpl = ClassFileEntryImpl(file)
6061
override protected def isRequiredFileType(file: AbstractFile): Boolean =
6162
file.isTasty || (file.isClass && file.classToTasty.isEmpty)
6263
}
@@ -128,10 +129,10 @@ object ZipAndJarClassPathFactory extends ZipAndJarFileLookupFactory {
128129
subpackages.map(packageFile => PackageEntryImpl(inPackage.entryName(packageFile.name)))
129130
}
130131

131-
override private[dotty] def classes(inPackage: PackageName): Seq[ClassFileEntry] = cachedPackages.get(inPackage.dottedString) match {
132+
override private[dotty] def classes(inPackage: PackageName): Seq[BinaryFileEntry] = cachedPackages.get(inPackage.dottedString) match {
132133
case None => Seq.empty
133134
case Some(PackageFileInfo(pkg, _)) =>
134-
(for (file <- pkg if file.isClass) yield ClassFileEntryImpl(file)).toSeq
135+
(for (file <- pkg if file.isClass) yield ClassFileEntry(file)).toSeq
135136
}
136137

137138
override private[dotty] def hasPackage(pkg: PackageName) = cachedPackages.contains(pkg.dottedString)
@@ -162,7 +163,7 @@ object ZipAndJarClassPathFactory extends ZipAndJarFileLookupFactory {
162163
*/
163164
object ZipAndJarSourcePathFactory extends ZipAndJarFileLookupFactory {
164165
private case class ZipArchiveSourcePath(zipFile: File)
165-
extends ZipArchiveFileLookup[SourceFileEntryImpl]
166+
extends ZipArchiveFileLookup[SourceFileEntry]
166167
with NoClassPaths {
167168

168169
def release: Option[String] = None
@@ -171,7 +172,7 @@ object ZipAndJarSourcePathFactory extends ZipAndJarFileLookupFactory {
171172

172173
override private[dotty] def sources(inPackage: PackageName): Seq[SourceFileEntry] = files(inPackage)
173174

174-
override protected def createFileEntry(file: FileZipArchive#Entry): SourceFileEntryImpl = SourceFileEntryImpl(file)
175+
override protected def createFileEntry(file: FileZipArchive#Entry): SourceFileEntry = SourceFileEntry(file)
175176
override protected def isRequiredFileType(file: AbstractFile): Boolean = file.isScalaOrJavaSource
176177
}
177178

compiler/src/dotty/tools/io/AbstractFile.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,9 @@ abstract class AbstractFile extends Iterable[AbstractFile] {
258258
final def resolveSibling(name: String): AbstractFile | Null =
259259
container.lookupName(name, directory = false)
260260

261+
final def resolveSiblingWithExtension(extension: String): AbstractFile | Null =
262+
resolveSibling(name.stripSuffix(this.extension) + extension)
263+
261264
private def fileOrSubdirectoryNamed(name: String, isDir: Boolean): AbstractFile =
262265
lookupName(name, isDir) match {
263266
case null =>

compiler/src/dotty/tools/io/ClassPath.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ trait ClassPath {
2626

2727
final def hasPackage(pkg: String): Boolean = hasPackage(PackageName(pkg))
2828
final def packages(inPackage: String): Seq[PackageEntry] = packages(PackageName(inPackage))
29-
final def classes(inPackage: String): Seq[ClassFileEntry] = classes(PackageName(inPackage))
29+
final def classes(inPackage: String): Seq[BinaryFileEntry] = classes(PackageName(inPackage))
3030
final def sources(inPackage: String): Seq[SourceFileEntry] = sources(PackageName(inPackage))
3131
final def list(inPackage: String): ClassPathEntries = list(PackageName(inPackage))
3232

@@ -43,7 +43,7 @@ trait ClassPath {
4343

4444
private[dotty] def hasPackage(pkg: PackageName): Boolean
4545
private[dotty] def packages(inPackage: PackageName): Seq[PackageEntry]
46-
private[dotty] def classes(inPackage: PackageName): Seq[ClassFileEntry]
46+
private[dotty] def classes(inPackage: PackageName): Seq[BinaryFileEntry]
4747
private[dotty] def sources(inPackage: PackageName): Seq[SourceFileEntry]
4848

4949
/**

0 commit comments

Comments
 (0)