Skip to content

Commit e8dbcf1

Browse files
authored
Merge pull request #302 from adpi2/fix-metals-4532
Fix metals#4532: Handle empty jar files
2 parents 3b91680 + be26042 commit e8dbcf1

File tree

10 files changed

+81
-40
lines changed

10 files changed

+81
-40
lines changed

build.sbt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ lazy val tests = projectMatrix
8989
publish := {},
9090
// Test / javaOptions += "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=1044",
9191
Test / fork := true,
92+
Test / baseDirectory := (ThisBuild / baseDirectory).value / "tests",
9293
// do not use sbt logger, otherwise the output of a test only appears at the end of the suite
9394
Test / testOptions += Tests.Argument(TestFrameworks.MUnit, "+l"),
9495
Test / testOptions := (Test / testOptions)

core/src/main/scala/ch/epfl/scala/debugadapter/ClassSystem.scala

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,37 +8,38 @@ import java.nio.file.Path
88
import java.nio.file.Files
99
import java.net.URI
1010
import scala.collection.mutable
11-
import scala.util.control.NonFatal
1211
import java.util.Collections
12+
import scala.util.Try
1313

1414
sealed trait ClassSystem {
15-
def within[T](f: (FileSystem, Path) => T): Option[T]
15+
def name: String
16+
def within[T](f: (FileSystem, Path) => T): Try[T]
1617
def readBytes(path: String): Array[Byte] =
1718
within { (_, root) =>
1819
Files.readAllBytes(root.resolve(path))
1920
}.get
2021
}
2122

2223
final case class ClassJar(absolutePath: Path) extends ClassSystem {
23-
def within[T](f: (FileSystem, Path) => T): Option[T] =
24+
def name: String = absolutePath.toString
25+
def within[T](f: (FileSystem, Path) => T): Try[T] =
2426
IO.withinJarFile(absolutePath)(fs => f(fs, fs.getPath("/")))
2527
}
2628

2729
final case class ClassDirectory(absolutePath: Path) extends ClassSystem {
28-
def within[T](f: (FileSystem, Path) => T): Option[T] =
29-
Some(f(FileSystems.getDefault, absolutePath))
30+
def name: String = absolutePath.toString
31+
def within[T](f: (FileSystem, Path) => T): Try[T] = Try {
32+
f(FileSystems.getDefault, absolutePath)
33+
}
3034
}
3135

3236
final case class JavaRuntimeSystem(classLoader: ClassLoader, javaHome: Path) extends ClassSystem {
37+
def name: String = javaHome.toString
3338
def fileSystem: FileSystem =
3439
JavaRuntimeSystem.getFileSystem(classLoader, javaHome)
3540

36-
def within[T](f: (FileSystem, Path) => T): Option[T] = {
37-
try {
38-
Some(f(fileSystem, fileSystem.getPath("/modules")))
39-
} catch {
40-
case NonFatal(_) => None
41-
}
41+
def within[T](f: (FileSystem, Path) => T): Try[T] = Try {
42+
f(fileSystem, fileSystem.getPath("/modules"))
4243
}
4344
}
4445

core/src/main/scala/ch/epfl/scala/debugadapter/SourceEntry.scala

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@ package ch.epfl.scala.debugadapter
22

33
import java.nio.file.Path
44

5-
sealed trait SourceEntry
6-
final case class SourceDirectory(directory: Path) extends SourceEntry
7-
final case class SourceJar(jar: Path) extends SourceEntry
8-
final case class StandaloneSourceFile(absolutePath: Path, relativePath: String) extends SourceEntry
5+
sealed trait SourceEntry {
6+
def name: String
7+
}
8+
final case class SourceDirectory(directory: Path) extends SourceEntry {
9+
def name: String = directory.toString
10+
}
11+
final case class SourceJar(jar: Path) extends SourceEntry {
12+
def name: String = jar.toString
13+
}
14+
final case class StandaloneSourceFile(absolutePath: Path, relativePath: String) extends SourceEntry {
15+
def name: String = relativePath.toString
16+
}

core/src/main/scala/ch/epfl/scala/debugadapter/internal/ClassEntryLookUp.scala

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import ClassEntryLookUp.readSourceContent
1212
import scala.util.matching.Regex
1313
import ch.epfl.scala.debugadapter.internal.scalasig.ScalaSig
1414
import ch.epfl.scala.debugadapter.internal.scalasig.Decompiler
15+
import ch.epfl.scala.debugadapter.internal.ScalaExtension.*
16+
import scala.util.Try
1517

1618
private case class SourceLine(uri: URI, lineNumber: Int)
1719

@@ -60,7 +62,9 @@ private class ClassEntryLookUp(
6062
sourceUriToClassFiles(sourceUri)
6163
.groupBy(_.classSystem)
6264
.foreach { case (classSystem, classFiles) =>
63-
classSystem.within((_, root) => loadLineNumbers(root, classFiles, sourceUri))
65+
classSystem
66+
.within((_, root) => loadLineNumbers(root, classFiles, sourceUri))
67+
.warnFailure(logger, s"Cannot load line numbers in ${classSystem.name}")
6468
}
6569
}
6670

@@ -117,7 +121,7 @@ private class ClassEntryLookUp(
117121
}
118122

119123
def getSourceContent(sourceUri: URI): Option[String] =
120-
sourceUriToSourceFile.get(sourceUri).flatMap(readSourceContent)
124+
sourceUriToSourceFile.get(sourceUri).flatMap(readSourceContent(_, logger))
121125

122126
def getSourceFile(fqcn: String): Option[URI] =
123127
classNameToSourceFile.get(fqcn).map(_.uri)
@@ -161,7 +165,7 @@ private object ClassEntryLookUp {
161165
new ClassEntryLookUp(entry, Map.empty, Map.empty, Map.empty, Map.empty, Seq.empty, Seq.empty, logger)
162166

163167
private[internal] def apply(entry: ClassEntry, logger: Logger): ClassEntryLookUp = {
164-
val sourceFiles = entry.sourceEntries.flatMap(SourceEntryLookUp.getAllSourceFiles)
168+
val sourceFiles = entry.sourceEntries.flatMap(SourceEntryLookUp.getAllSourceFiles(_, logger))
165169
ClassEntryLookUp(entry, sourceFiles, logger)
166170
}
167171

@@ -175,6 +179,7 @@ private object ClassEntryLookUp {
175179
val classFiles = entry.classSystems.flatMap { classSystem =>
176180
classSystem
177181
.within(readAllClassFiles(classSystem))
182+
.warnFailure(logger, s"Cannot list the class files in ${classSystem.name}")
178183
.getOrElse(Vector.empty)
179184
}
180185

@@ -235,7 +240,7 @@ private object ClassEntryLookUp {
235240
// so we try to find the right package declaration in each file
236241
// it would be very unfortunate that 2 sources file with the same name
237242
// declare the same package.
238-
manySourceFiles.filter(s => findPackage(s, classFile.fullPackage)) match {
243+
manySourceFiles.filter(s => findPackage(s, classFile.fullPackage, logger)) match {
239244
case sourceFile :: Nil =>
240245
recordSourceFile(sourceFile)
241246
case _ =>
@@ -325,36 +330,35 @@ private object ClassEntryLookUp {
325330

326331
private def findPackage(
327332
sourceFile: SourceFile,
328-
fullPackage: String
333+
fullPackage: String,
334+
logger: Logger
329335
): Boolean = {
330336
// for "a.b.c" it returns Seq("a.b.c", "b.c", "c")
331337
// so that we can match on "package a.b.c" or "package b.c" or "package c"
332338
val nestedPackages = fullPackage.split('.').foldLeft(Seq.empty[String]) { (nestedParts, newPart) =>
333339
nestedParts.map(outer => s"$outer.$newPart") :+ newPart
334340
}
335-
val sourceContent = readSourceContent(sourceFile)
341+
val sourceContent = readSourceContent(sourceFile, logger)
336342
nestedPackages.exists { `package` =>
337343
val quotedPackage = Regex.quote(`package`)
338344
val matcher = s"package\\s+(object\\s+)?$quotedPackage(\\{|:|;|\\s+)".r
339345
sourceContent.exists(matcher.findFirstIn(_).isDefined)
340346
}
341347
}
342348

343-
private def readSourceContent(sourceFile: SourceFile): Option[String] = {
349+
private def readSourceContent(sourceFile: SourceFile, logger: Logger): Option[String] = {
344350
withinSourceEntry(sourceFile.entry) { root =>
345351
val sourcePath = root.resolve(sourceFile.relativePath)
346352
new String(Files.readAllBytes(sourcePath))
347353
}
354+
.warnFailure(logger, s"Cannot read content of ${sourceFile.uri}")
348355
}
349356

350-
private def withinSourceEntry[T](
351-
sourceEntry: SourceEntry
352-
)(f: Path => T): Option[T] = {
357+
private def withinSourceEntry[T](sourceEntry: SourceEntry)(f: Path => T): Try[T] = {
353358
sourceEntry match {
354359
case SourceJar(jar) => IO.withinJarFile(jar)(fs => f(fs.getPath("/")))
355-
case SourceDirectory(dir) => Some(f(dir))
356-
case StandaloneSourceFile(absolutePath, _) =>
357-
Some(f(absolutePath.getParent))
360+
case SourceDirectory(dir) => Try(f(dir))
361+
case StandaloneSourceFile(absolutePath, _) => Try(f(absolutePath.getParent))
358362
}
359363
}
360364
}

core/src/main/scala/ch/epfl/scala/debugadapter/internal/IO.scala

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,19 @@ import java.nio.file.FileSystem
55
import java.nio.file.FileSystems
66
import java.nio.file.Path
77
import java.util
8+
import scala.util.Try
89
import scala.util.control.NonFatal
10+
import scala.util.Failure
11+
import scala.util.Success
912

1013
private[debugadapter] object IO {
11-
def withinJarFile[T](absolutePath: Path)(f: FileSystem => T): Option[T] = {
14+
def withinJarFile[T](absolutePath: Path)(f: FileSystem => T): Try[T] = try {
1215
val uri = URI.create(s"jar:${absolutePath.toUri}")
13-
val fileSystem =
14-
FileSystems.newFileSystem(uri, new util.HashMap[String, Any])
15-
try Some(f(fileSystem))
16-
catch {
17-
case NonFatal(e) => None
18-
} finally fileSystem.close()
16+
val fileSystem = FileSystems.newFileSystem(uri, new util.HashMap[String, Any])
17+
try Success(f(fileSystem))
18+
finally fileSystem.close()
19+
} catch {
20+
case NonFatal(exception) => Failure(exception)
21+
case zipError: util.zip.ZipError => Failure(zipError)
1922
}
2023
}

core/src/main/scala/ch/epfl/scala/debugadapter/internal/SourceEntryLookUp.scala

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import java.nio.file.Path
1010
import java.nio.file.Files
1111
import scala.jdk.CollectionConverters.*
1212
import java.net.URI
13+
import ch.epfl.scala.debugadapter.Logger
14+
import ch.epfl.scala.debugadapter.internal.ScalaExtension.*
1315

1416
private case class SourceFile(
1517
entry: SourceEntry,
@@ -21,12 +23,15 @@ private case class SourceFile(
2123
}
2224

2325
private object SourceEntryLookUp {
24-
def getAllSourceFiles(entry: SourceEntry): Seq[SourceFile] = {
26+
def getAllSourceFiles(entry: SourceEntry, logger: Logger): Seq[SourceFile] = {
2527
entry match {
2628
case SourceJar(jar) =>
27-
IO.withinJarFile(jar) { fileSystem =>
28-
getAllSourceFiles(entry, fileSystem, fileSystem.getPath("/")).toVector
29-
}.getOrElse(Vector.empty)
29+
IO
30+
.withinJarFile(jar) { fileSystem =>
31+
getAllSourceFiles(entry, fileSystem, fileSystem.getPath("/")).toVector
32+
}
33+
.warnFailure(logger, s"Cannot list the source files in ${entry.name}")
34+
.getOrElse(Vector.empty)
3035
case SourceDirectory(directory) =>
3136
getAllSourceFiles(entry, FileSystems.getDefault, directory).toSeq
3237
case StandaloneSourceFile(absolutePath, relativePath) =>

core/src/main/scala/ch/epfl/scala/debugadapter/internal/SourceLookUpProvider.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ private[debugadapter] object SourceLookUpProvider {
8282
val sourceFilesByEntry = parrallelEntries
8383
.flatMap(_.sourceEntries)
8484
.distinct
85-
.map(entry => entry -> SourceEntryLookUp.getAllSourceFiles(entry))
85+
.map(entry => entry -> SourceEntryLookUp.getAllSourceFiles(entry, logger))
8686
.toMap
8787
val allLookUps = parrallelEntries
8888
.map(entry => ClassEntryLookUp(entry, entry.sourceEntries.flatMap(sourceFilesByEntry.apply), logger))

tests/input/class-entry/empty-sources.jar

Whitespace-only changes.

tests/input/class-entry/empty.jar

2.15 KB
Binary file not shown.

tests/src/test/scala/ch/epfl/scala/debugadapter/internal/ClassEntryLookUpStats.scala

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import ch.epfl.scala.debugadapter.ClassEntry
88
import ch.epfl.scala.debugadapter.testfmk.NoopLogger
99
import munit.FunSuite
1010
import scala.concurrent.duration.*
11+
import ch.epfl.scala.debugadapter.Library
12+
import java.nio.file.Paths
13+
import ch.epfl.scala.debugadapter.SourceJar
1114

1215
/**
1316
* This is a test class that also
@@ -17,6 +20,14 @@ class ClassEntryLookUpStats extends FunSuite {
1720
override def munitTimeout: Duration = 120.seconds
1821
private val jvmCache = JvmCache().withDefaultIndex
1922

23+
test("empty source jar") {
24+
val entry = getClassEntry("empty")
25+
printAndCheck("empty", entry)(
26+
classCount => assertEquals(classCount, 0),
27+
orphanCount => assertEquals(orphanCount, 0)
28+
)
29+
}
30+
2031
test("adopt:1.8.0-292") {
2132
printAndCheck("adopt:1.8.0-292")(
2233
classCount => assert(classCount > 0),
@@ -208,4 +219,12 @@ class ClassEntryLookUpStats extends FunSuite {
208219
classCountAssertion(classCount)
209220
orphanAssertion(orphanClassCount)
210221
}
222+
223+
private def getClassEntry(name: String): ClassEntry = {
224+
val directory = Paths.get("input/class-entry").toAbsolutePath
225+
val classJar = directory.resolve(s"$name.jar")
226+
val sourceJar = directory.resolve(s"$name-sources.jar")
227+
val sourceEntry = SourceJar(sourceJar)
228+
Library(name, "", classJar, Seq(sourceEntry))
229+
}
211230
}

0 commit comments

Comments
 (0)