Skip to content

Commit 5f2f95f

Browse files
committed
.
1 parent 0981644 commit 5f2f95f

File tree

2 files changed

+58
-36
lines changed

2 files changed

+58
-36
lines changed

compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,10 @@ object SymbolLoaders {
335335
packageClass: ClassSymbol, fullPackageName: String,
336336
jarClasspath: ClassPath, fullClasspath: ClassPath,
337337
)(using Context): Unit =
338-
if jarClasspath.classes(fullPackageName).nonEmpty then
338+
val hasClasses = jarClasspath.classes(fullPackageName).nonEmpty
339+
val hasPackages = jarClasspath.packages(fullPackageName).nonEmpty
340+
341+
if hasClasses then
339342
// if the package contains classes in jarClasspath, the package is invalidated (or removed if there are no more classes in it)
340343
val packageVal = packageClass.sourceModule.asInstanceOf[TermSymbol]
341344
if packageClass.isRoot then
@@ -354,7 +357,12 @@ object SymbolLoaders {
354357
packageClass.info = new PackageLoader(packageVal, fullClasspath)
355358
else
356359
packageClass.owner.info.decls.openForMutations.unlink(packageVal)
357-
else
360+
361+
// Always process sub-packages, even when hasClasses is true.
362+
// This is needed when a package has BOTH classes AND sub-packages,
363+
// e.g. scala-parallel-collections adds both classes to scala.collection
364+
// and the new scala.collection.parallel sub-package.
365+
if hasPackages then
358366
for p <- jarClasspath.packages(fullPackageName) do
359367
val subPackageName = PackageNameUtils.separatePkgAndClassNames(p.name)._2.toTermName
360368
val subPackage = packageClass.info.decl(subPackageName).orElse:

repl/test/dotty/tools/repl/ScalaPackageJarTests.scala

Lines changed: 48 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,38 +18,39 @@ import org.junit.Assert.{assertEquals, assertTrue, assertFalse}
1818
class ScalaPackageJarTests extends ReplTest:
1919
import ScalaPackageJarTests.*
2020

21-
@Test def `i25058 load JAR with scala collection package classes` =
21+
@Test def `i25058 load JAR with scala collection parallel subpackage` =
2222
// Skip if javac is not available
2323
Assume.assumeTrue("javac not available", ToolProvider.getSystemJavaCompiler != null)
2424

25-
// Create a JAR with a class directly in scala.collection package
26-
// This tests the fix for #25058 where adding classes to existing scala.* packages
27-
// would fail because mergeNewEntries was skipping all scala.* packages
28-
val jarPath = createScalaCollectionJar()
25+
// Create a JAR that simulates scala-parallel-collections:
26+
// - Classes in scala.collection (e.g., ParIterable)
27+
// - Classes in scala.collection.parallel subpackage (e.g., CollectionConverters)
28+
// The bug was that when a package has BOTH classes AND subpackages,
29+
// only the classes were processed and subpackages were skipped.
30+
val jarPath = createScalaCollectionParallelJar()
2931
try
3032
initially {
3133
// First, access scala.collection to ensure it's loaded in the symbol table
3234
val state = run("scala.collection.mutable.ArrayBuffer.empty[Int]")
3335
storedOutput() // discard output
3436
state
3537
} andThen {
36-
// Load the JAR with a class in scala.collection
38+
// Load the JAR with classes in scala.collection AND scala.collection.parallel
3739
val state = run(s":jar $jarPath")
3840
val output = storedOutput()
3941
assertTrue(s"Expected success message, got: $output", output.contains("Added") && output.contains("to classpath"))
4042
state
4143
} andThen {
42-
// Import from scala.collection - this would fail before the fix
43-
// because mergeNewEntries was skipping all scala.* packages
44-
val state = run("import scala.collection.TestCollection")
44+
// Import from scala.collection.parallel - this is the key test for #25058
45+
val state = run("import scala.collection.parallel.TestParallel")
4546
val importOutput = storedOutput()
4647
// Should not have an error
4748
assertFalse(s"Import should succeed, got: $importOutput",
4849
importOutput.contains("Not Found Error") || importOutput.contains("not a member"))
4950
state
5051
} andThen {
5152
// Use the imported class
52-
run("TestCollection.getValue()")
53+
run("TestParallel.getValue()")
5354
val output = storedOutput()
5455
assertTrue(s"Expected value 42, got: $output", output.contains("42"))
5556
}
@@ -58,56 +59,69 @@ class ScalaPackageJarTests extends ReplTest:
5859

5960
object ScalaPackageJarTests:
6061

61-
/** Creates a JAR file containing a simple class directly in the scala.collection package.
62-
* This simulates what happens when a library adds classes to an existing scala.* package.
62+
/** Creates a JAR file simulating scala-parallel-collections structure:
63+
* - A class in scala.collection (TestParIterable)
64+
* - A class in scala.collection.parallel (TestParallel)
6365
*
64-
* The generated class is equivalent to:
65-
* {{{
66-
* package scala.collection;
67-
* public class TestCollection {
68-
* public static int getValue() { return 42; }
69-
* }
70-
* }}}
66+
* This is critical for testing #25058: the bug only manifests when
67+
* a JAR adds BOTH classes to an existing package (scala.collection)
68+
* AND a new subpackage (scala.collection.parallel).
7169
*/
72-
def createScalaCollectionJar(): String =
70+
def createScalaCollectionParallelJar(): String =
7371
val tempDir = Files.createTempDirectory("scala-pkg-test")
7472

75-
// Create package directory structure - using the existing scala.collection path
76-
val packageDir = tempDir.resolve("scala/collection")
77-
Files.createDirectories(packageDir)
73+
// Create package directory structures
74+
val collectionDir = tempDir.resolve("scala/collection")
75+
val parallelDir = tempDir.resolve("scala/collection/parallel")
76+
Files.createDirectories(parallelDir)
7877

79-
// Write Java source file
80-
val javaSource = packageDir.resolve("TestCollection.java")
81-
Files.writeString(javaSource,
78+
// Write Java source file in scala.collection (simulates ParIterable etc.)
79+
val collectionSource = collectionDir.resolve("TestParIterable.java")
80+
Files.writeString(collectionSource,
8281
"""|package scala.collection;
83-
|public class TestCollection {
82+
|public class TestParIterable {
83+
| public static int getCount() { return 100; }
84+
|}
85+
|""".stripMargin)
86+
87+
// Write Java source file in scala.collection.parallel (simulates CollectionConverters etc.)
88+
val parallelSource = parallelDir.resolve("TestParallel.java")
89+
Files.writeString(parallelSource,
90+
"""|package scala.collection.parallel;
91+
|public class TestParallel {
8492
| public static int getValue() { return 42; }
8593
|}
8694
|""".stripMargin)
8795

8896
// Compile with javac
8997
val compiler = ToolProvider.getSystemJavaCompiler
9098
val fileManager = compiler.getStandardFileManager(null, null, null)
91-
val compilationUnits = fileManager.getJavaFileObjects(javaSource.toFile)
99+
val compilationUnits = fileManager.getJavaFileObjects(collectionSource.toFile, parallelSource.toFile)
92100
val task = compiler.getTask(null, fileManager, null,
93101
java.util.Arrays.asList("-d", tempDir.toString), null, compilationUnits)
94102
val success = task.call()
95103
fileManager.close()
96104

97105
if !success then
98-
throw new RuntimeException("Failed to compile test class")
106+
throw new RuntimeException("Failed to compile test classes")
99107

100108
// Create JAR file
101-
val jarFile = tempDir.resolve("scala-collection-test.jar").toFile
109+
val jarFile = tempDir.resolve("scala-collection-parallel.jar").toFile
102110
val manifest = new Manifest()
103111
manifest.getMainAttributes.putValue("Manifest-Version", "1.0")
104112

105113
val jos = new JarOutputStream(new FileOutputStream(jarFile), manifest)
106114
try
107-
// Add the compiled class file
108-
val classFile = packageDir.resolve("TestCollection.class")
109-
jos.putNextEntry(new JarEntry("scala/collection/TestCollection.class"))
110-
jos.write(Files.readAllBytes(classFile))
115+
// Add class in scala.collection
116+
val collectionClass = collectionDir.resolve("TestParIterable.class")
117+
jos.putNextEntry(new JarEntry("scala/collection/TestParIterable.class"))
118+
jos.write(Files.readAllBytes(collectionClass))
119+
jos.closeEntry()
120+
121+
// Add class in scala.collection.parallel
122+
val parallelClass = parallelDir.resolve("TestParallel.class")
123+
jos.putNextEntry(new JarEntry("scala/collection/parallel/TestParallel.class"))
124+
jos.write(Files.readAllBytes(parallelClass))
111125
jos.closeEntry()
112126
finally
113127
jos.close()

0 commit comments

Comments
 (0)