Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 63 additions & 6 deletions metals/src/main/scala/scala/meta/internal/metals/Indexer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -248,12 +248,16 @@ case class Indexer(indexProviders: IndexProviders, mbtBuild: () => MbtBuild)(
)
progress.message =
s"indexing ${buildTool.importedBuild.dependencyModules.getItems().size()} dependencies"
if (indexProviders.clientConfig.definitionIndexStrategy().isClasspath) {
usedJars ++= indexDependencyModules(
buildTool.importedBuild.dependencyModules,
progress,
)
}
val isClasspathIndexing =
indexProviders.clientConfig.definitionIndexStrategy().isClasspath
val indexedDependencyModules =
if (isClasspathIndexing)
indexDependencyModules(
buildTool.importedBuild.dependencyModules,
progress,
)
else Set.empty[AbsolutePath]
usedJars ++= indexedDependencyModules
if (shouldFallbackToFileMbt) {
val build = MbtBuild.fromWorkspace(indexProviders.folder)
indexDependencyModules(build.asBspModules, progress)
Expand All @@ -263,6 +267,13 @@ case class Indexer(indexProviders: IndexProviders, mbtBuild: () => MbtBuild)(
buildTool.importedBuild.dependencySources,
progress,
)
// If no dependency modules are found, index normal classpath jars
if (indexedDependencyModules.isEmpty && isClasspathIndexing) {
usedJars ++= indexClasspathJarsFallback(
buildTool.data,
progress,
)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
scribe.debug(s"indexed ${usedJars.size} dependency source jars")
}

Expand Down Expand Up @@ -534,6 +545,52 @@ case class Indexer(indexProviders: IndexProviders, mbtBuild: () => MbtBuild)(
usedJars.toSet
}

/**
* Index classpath jars if none are included in the dependency modules
*/
private def indexClasspathJarsFallback(
data: TargetData,
progress: TaskProgress,
): Set[AbsolutePath] = {
val usedJars = mutable.HashSet.empty[AbsolutePath]
val isVisited = new ju.HashSet[AbsolutePath]()
scribe.info("Dependency modules empty, falling back to classpath jars")
for {
targetId <- data.allBuildTargetIds
jars <- data.targetJarClasspath(targetId).toList
jar <- jars
if jar.isJar && !isVisited.contains(jar)
} {
progress.progress = progress.progress + 1
isVisited.add(jar)
usedJars += jar

val sourcesJarName = jar.filename.stripSuffix(".jar") + "-sources.jar"
val sources = data.sourceJarNameToJarFile.get(sourcesJarName)

if (sources.isEmpty) {
scribe.warn(s"sources jar not found for $jar")
}

val jarName = jar.filename.stripSuffix(".jar")
// coordinates are not used anywhere currently in definitionIndex
val coordinates = MavenCoordinates("unknown", jarName, "unknown")

val dependencyModule = DependencyModule(coordinates, jar, sources)
val dialect = buildTargets
.scalaTarget(targetId)
.map(scalaTarget =>
ScalaVersions.dialectForScalaVersion(
scalaTarget.scalaVersion,
includeSource3 = true,
)
)
.getOrElse(Scala213)
definitionIndex.addDependencyModule(dependencyModule, dialect)
}
usedJars.toSet
}

private def processDependencyPath(
path: AbsolutePath,
target: b.BuildTargetIdentifier,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,8 @@ class BuildServerConnection private[metals] (
def isDependencySourcesSupported: Boolean =
capabilities.getDependencySourcesProvider()

// Scala CLI breaks when we try to use the `buildTarget/dependencyModules` request
def isDependencyModulesSupported: Boolean =
capabilities.getDependencyModulesProvider() && !isScalaCLI
capabilities.getDependencyModulesProvider()

def supportsSyncMethod: Boolean =
initialConnection.syncModes.isDefined
Expand Down
21 changes: 20 additions & 1 deletion tests/unit/src/main/scala/bill/Bill.scala
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ object Bill {
val capabilities = new BuildServerCapabilities
capabilities.setCompileProvider(new CompileProvider(languages))
capabilities.setCanReload(true)
capabilities.setDependencyModulesProvider(true)
new InitializeBuildResult("Bill", "1.0", "2.0.0-M2", capabilities)
}.logError("initialize").asJava
}
Expand Down Expand Up @@ -401,9 +402,27 @@ object Bill {
Future.successful(new ScalaMainClassesResult(List.empty.asJava)).asJava
}

// Returns true when tests request a nonempty DependencyModulesResult
// whose per-target module lists are empty (classpath fallback path).
def hasEmptyDependencyModuleLists(): Boolean = {
Files.isRegularFile(
workspace.resolve("bill-empty-dependency-module-lists")
)
}

override def buildTargetDependencyModules(
params: DependencyModulesParams
): CompletableFuture[DependencyModulesResult] = ???
): CompletableFuture[DependencyModulesResult] = {
CompletableFuture.completedFuture {
if (hasEmptyDependencyModuleLists()) {
val item =
new DependencyModulesItem(target.getId, Collections.emptyList())
new DependencyModulesResult(Collections.singletonList(item))
} else {
new DependencyModulesResult(Collections.emptyList())
}
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

override def debugSessionStart(
params: DebugSessionParams
Expand Down
71 changes: 71 additions & 0 deletions tests/unit/src/test/scala/tests/BillLspSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -313,4 +313,75 @@ class BillLspSuite extends BaseLspSuite("bill") {
_ = assertEquals(compileReport.getStatusCode(), StatusCode.OK)
} yield ()
}

test("definition-without-dependency-modules") {
cleanWorkspace()
Bill.installWorkspace(workspace)
for {
_ <- initialize(
"""|/src/com/App.scala
|object App {
| val list: List[Int] = List(1, 2, 3)
|}
|""".stripMargin
)
_ <- server.didOpen("src/com/App.scala")
_ = assertNoDiff(client.workspaceDiagnostics, "")
locations <- server.definition(
"src/com/App.scala",
"""|object App {
| val list: Li@@st[Int] = List(1, 2, 3)
|}
|""".stripMargin,
workspace,
)
_ = assert(
locations.nonEmpty,
"Expected definition location for List but got none. " +
"This tests the fallback to classpath jars when dependency modules are empty.",
)
uri = locations.head.getUri()
_ = assert(
uri.contains("scala-library"),
s"Expected definition to be in scala-library, but got: $uri",
)
} yield ()
}

test("definition-with-empty-dependency-module-lists") {
cleanWorkspace()
Bill.installWorkspace(workspace)
for {
_ <- initialize(
"""|/src/com/App.scala
|object App {
| val list: List[Int] = List(1, 2, 3)
|}
|/bill-empty-dependency-module-lists
|true
|""".stripMargin
)
_ <- server.didOpen("src/com/App.scala")
_ = assertNoDiff(client.workspaceDiagnostics, "")
locations <- server.definition(
"src/com/App.scala",
"""|object App {
| val list: Li@@st[Int] = List(1, 2, 3)
|}
|""".stripMargin,
workspace,
)
_ = assert(
locations.nonEmpty,
"Expected definition location for List but got none. " +
"This tests classpath jar fallback when DependencyModulesResult " +
"has items but empty module lists.",
)
uri = locations.head.getUri()
_ = assert(
uri.contains("scala-library"),
s"Expected definition to be in scala-library, but got: $uri",
)
} yield ()
}
}
Loading