Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improvement: fetch missing dependency sources #5819

Merged
merged 5 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion metals-bench/src/main/scala/bench/ClasspathFuzzBench.scala
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class ClasspathFuzzBench {
@BenchmarkMode(Array(Mode.SingleShotTime))
@OutputTimeUnit(TimeUnit.MILLISECONDS)
def run(): Seq[SymbolInformation] = {
symbols.search(query)
symbols.search(query, None)
}

}
2 changes: 1 addition & 1 deletion metals-bench/src/main/scala/bench/WorkspaceFuzzBench.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class WorkspaceFuzzBench {
@BenchmarkMode(Array(Mode.SingleShotTime))
@OutputTimeUnit(TimeUnit.MILLISECONDS)
def upper(): Seq[SymbolInformation] = {
symbols.search(query)
symbols.search(query, None)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,9 @@ final class DefinitionProvider(
else true
}

val dialect = scalaVersionSelector.dialectFromBuildTarget(path)
val locs = workspaceSearch
.searchExactFrom(ident.value, path, token)
.searchExactFrom(ident.value, path, token, dialect)

val reducedGuesses =
if (locs.size > 1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,14 @@ object ImportedBuild {
new JavacOptionsParams(ids)
)
sources <- conn.buildTargetSources(new SourcesParams(ids))
dependencySources <- conn.buildTargetDependencySources(
bspProvidedDependencySources <- conn.buildTargetDependencySources(
new DependencySourcesParams(ids)
)
dependencySources <- resolveMissingDependencySources(
bspProvidedDependencySources,
javacOptions,
scalacOptions,
)
wrappedSources <- conn.buildTargetWrappedSources(
new WrappedSourcesParams(ids)
)
Expand All @@ -98,6 +103,34 @@ object ImportedBuild {
)
}

private def resolveMissingDependencySources(
dependencySources: DependencySourcesResult,
javacOptions: JavacOptionsResult,
scalacOptions: ScalacOptionsResult,
)(implicit ec: ExecutionContext): Future[DependencySourcesResult] = Future {
val dependencySourcesItems = dependencySources.getItems().asScala.toList
val idsLookup = dependencySourcesItems.map(_.getTarget()).toSet
val classpaths = javacOptions
.getItems()
.asScala
.map(item => (item.getTarget(), item.getClasspath())) ++
scalacOptions
.getItems()
.asScala
.map(item => (item.getTarget(), item.getClasspath()))

val newItems =
classpaths.collect {
case (id, classpath) if !idsLookup(id) =>
val items = JarSourcesProvider.fetchSources(
classpath.asScala.filter(_.endsWith(".jar")).toSeq
)
new DependencySourcesItem(id, items.asJava)
}

new DependencySourcesResult((dependencySourcesItems ++ newItems).asJava)
}

def fromList(data: Seq[ImportedBuild]): ImportedBuild =
if (data.isEmpty) empty
else if (data.lengthCompare(1) == 0) data.head
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package scala.meta.internal.metals

import java.nio.file.Path

import scala.util.Success
import scala.util.Try
import scala.xml.XML

import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.internal.semver.SemVer
import scala.meta.io.AbsolutePath

import coursierapi.Dependency
import coursierapi.Fetch

object JarSourcesProvider {

private val sbtRegex = "sbt-(.*)".r

def fetchSources(jars: Seq[String]): Seq[String] = {
def sourcesPath(jar: String) = s"${jar.stripSuffix(".jar")}-sources.jar"

val (haveSources, toDownload) = jars.partition { jar =>
sourcesPath(jar).toAbsolutePathSafe.exists(_.exists)
}

val dependencies = toDownload.flatMap { jarPath =>
val pomPath = s"${jarPath.stripSuffix(".jar")}.pom"
val dependency =
for {
pom <- pomPath.toAbsolutePathSafe
if pom.exists
dependency <- getDependency(pom)
} yield dependency

dependency.orElse { jarPath.toAbsolutePathSafe.flatMap(sbtFallback) }
}.distinct

val fetchedSources =
dependencies.flatMap { dep =>
Try(fetchDependencySources(dep)).toEither match {
case Right(fetched) => fetched.map(_.toUri().toString())
case Left(error) =>
scribe.warn(
s"could not fetch dependency sources for $dep, error: $error"
)
None
}
}
val existingSources = haveSources.map(sourcesPath)
fetchedSources ++ existingSources

}

private def sbtFallback(jar: AbsolutePath): Option[Dependency] = {
val filename = jar.filename.stripSuffix(".jar")
filename match {
case sbtRegex(versionStr) if Try().isSuccess =>
kasiaMarek marked this conversation as resolved.
Show resolved Hide resolved
Try(SemVer.Version.fromString(versionStr)) match {
case Success(version) if version.toString == versionStr =>
kasiaMarek marked this conversation as resolved.
Show resolved Hide resolved
Some(Dependency.of("org.scala-sbt", "sbt", versionStr))
case _ => None
}
case _ => None
}
}

private def getDependency(pom: AbsolutePath) = {
val xml = XML.loadFile(pom.toFile)
val groupId = (xml \ "groupId").text
val version = (xml \ "version").text
val artifactId = (xml \ "artifactId").text
Option
.when(groupId.nonEmpty && version.nonEmpty && artifactId.nonEmpty) {
Dependency.of(groupId, artifactId, version)
}
.filterNot(dep => isSbtDap(dep) || isMetalsPlugin(dep))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we filter those out? This might be added manually to the build even by the user. There is no way to see if it was added on purpose or automatically. I think it's fine to leave it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't get sbt-debug-adapter to resolve correctly.

}

private def isSbtDap(dependency: Dependency) = {
dependency.getModule().getOrganization() == "ch.epfl.scala" &&
dependency.getModule().getName() == "sbt-debug-adapter" &&
dependency.getVersion() == BuildInfo.debugAdapterVersion
}

private def isMetalsPlugin(dependency: Dependency) = {
dependency.getModule().getOrganization() == "org.scalameta" &&
dependency.getModule().getName() == "sbt-metals" &&
dependency.getVersion() == BuildInfo.metalsVersion
}

private def fetchDependencySources(
dependency: Dependency
): List[Path] = {
Fetch
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot about it and I think we do it wrong in most places. Could we make sure we handle an issue if there is no internet available? Make sure it doesn't get stuck etc. Probably we should have some timeout and do it in a separate future

.create()
.withDependencies(dependency)
.addClassifiers("sources")
.fetchResult()
.getFiles()
.asScala
.map(_.toPath())
.toList
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -1677,7 +1677,8 @@ class MetalsLspService(
): Future[List[SymbolInformation]] =
indexingPromise.future.map { _ =>
val timer = new Timer(time)
val result = workspaceSymbols.search(params.getQuery, token).toList
val result =
workspaceSymbols.search(params.getQuery, token, currentDialect).toList
if (clientConfig.initialConfig.statistics.isWorkspaceSymbol) {
scribe.info(
s"time: found ${result.length} results for query '${params.getQuery}' in $timer"
Expand All @@ -1687,9 +1688,12 @@ class MetalsLspService(
}

def workspaceSymbol(query: String): Seq[SymbolInformation] = {
workspaceSymbols.search(query)
workspaceSymbols.search(query, currentDialect)
}

private def currentDialect =
focusedDocument().flatMap(scalaVersionSelector.dialectFromBuildTarget)

def indexSources(): Future[Unit] = Future {
indexer.indexWorkspaceSources(buildTargets.allWritableData)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import java.{util => ju}

import scala.collection.mutable

import scala.meta.Dialect
import scala.meta.dialects
import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.internal.mtags.GlobalSymbolIndex
import scala.meta.internal.mtags.Symbol
Expand Down Expand Up @@ -32,6 +34,7 @@ class WorkspaceSearchVisitor(
token: CancelChecker,
index: GlobalSymbolIndex,
saveClassFileToDisk: Boolean,
preferredDialect: Option[Dialect],
)(implicit rc: ReportContext)
extends SymbolSearchVisitor {
private val fromWorkspace = new ju.ArrayList[l.SymbolInformation]()
Expand Down Expand Up @@ -88,14 +91,21 @@ class WorkspaceSearchVisitor(
): Option[SymbolDefinition] = {
val nme = Classfile.name(filename)
val tpe = Symbol(Symbols.Global(pkg, Descriptor.Type(nme)))

val preferredDialects = preferredDialect match {
case Some(dialects.Scala213) =>
Set(dialects.Scala213, dialects.Scala213Source3)
case Some(dialects.Scala212) =>
Set(dialects.Scala212, dialects.Scala212Source3)
case opt => opt.toSet
}
val forTpe = index.definitions(tpe)
val defs = if (forTpe.isEmpty) {
val term = Symbol(Symbols.Global(pkg, Descriptor.Term(nme)))
index.definitions(term)
} else forTpe

defs.sortBy(_.path.toURI.toString).headOption
defs.sortBy { defn =>
(!preferredDialects(defn.dialect), defn.path.toURI.toString)
}.headOption
}
override def shouldVisitPackage(pkg: String): Boolean = true
override def visitWorkspaceSymbol(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import java.nio.file.Path
import scala.collection.concurrent.TrieMap
import scala.util.control.NonFatal

import scala.meta.Dialect
import scala.meta.internal.mtags.GlobalSymbolIndex
import scala.meta.internal.pc.InterruptException
import scala.meta.io.AbsolutePath
Expand Down Expand Up @@ -39,14 +40,21 @@ final class WorkspaceSymbolProvider(
var inDependencies: ClasspathSearch =
ClasspathSearch.empty

def search(query: String): Seq[l.SymbolInformation] = {
search(query, () => ())
def search(
query: String,
preferredDialect: Option[Dialect],
): Seq[l.SymbolInformation] = {
search(query, () => (), preferredDialect)
}

def search(query: String, token: CancelChecker): Seq[l.SymbolInformation] = {
def search(
query: String,
token: CancelChecker,
preferredDialect: Option[Dialect],
): Seq[l.SymbolInformation] = {
if (query.isEmpty) return Nil
try {
searchUnsafe(query, token)
searchUnsafe(query, token, preferredDialect)
} catch {
case InterruptException() =>
Nil
Expand All @@ -57,6 +65,7 @@ final class WorkspaceSymbolProvider(
queryString: String,
path: AbsolutePath,
token: CancelToken,
preferredDialect: Option[Dialect],
): Seq[l.SymbolInformation] = {
val query = WorkspaceSymbolQuery.exact(queryString)
val visistor =
Expand All @@ -66,6 +75,7 @@ final class WorkspaceSymbolProvider(
token,
index,
saveClassFileToDisk,
preferredDialect,
)
val targetId = buildTargets.inverseSources(path)
search(query, visistor, targetId)
Expand Down Expand Up @@ -205,6 +215,7 @@ final class WorkspaceSymbolProvider(
private def searchUnsafe(
textQuery: String,
token: CancelChecker,
preferredDialect: Option[Dialect],
): Seq[l.SymbolInformation] = {
val query = WorkspaceSymbolQuery.fromTextQuery(textQuery)
val visitor =
Expand All @@ -214,6 +225,7 @@ final class WorkspaceSymbolProvider(
token,
index,
saveClassFileToDisk,
preferredDialect,
)
search(query, visitor, None)
visitor.allResults()
Expand Down
31 changes: 30 additions & 1 deletion tests/slow/src/test/scala/tests/sbt/SbtServerSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ import scribe.writer.Writer
import tests.BaseImportSuite
import tests.SbtBuildLayout
import tests.SbtServerInitializer
import tests.ScriptsAssertions
import tests.TestSemanticTokens

/**
* Basic suite to ensure that a connection to sbt server can be made.
*/
class SbtServerSuite
extends BaseImportSuite("sbt-server", SbtServerInitializer) {
extends BaseImportSuite("sbt-server", SbtServerInitializer)
with ScriptsAssertions {

val preBspVersion = "1.3.13"
val supportedMetaBuildVersion = "1.6.0-M1"
Expand Down Expand Up @@ -497,4 +499,31 @@ class SbtServerSuite
} yield ()
}

test("build-sbt") {
cleanWorkspace()
for {
_ <- initialize(
s"""|/project/build.properties
|sbt.version=${V.sbtVersion}
|/build.sbt
|${SbtBuildLayout.commonSbtSettings}
|ThisBuild / scalaVersion := "${V.scala213}"
|val a = project.in(file("a"))
|/a/src/main/scala/a/A.scala
|package a
|object A {
| val a = 1
|}
|""".stripMargin
)
_ <- server.didOpen("build.sbt")
res <- definitionsAt(
"build.sbt",
s"ThisBuild / sc@@alaVersion := \"${V.scala213}\"",
)
_ = assert(res.length == 1)
_ = assertNoDiff(res.head.getUri().toAbsolutePath.filename, "Keys.scala")
} yield ()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ abstract class BaseWorkspaceSymbolSuite extends BaseSuite {
expected: String,
)(implicit loc: Location): Unit = {
test(query) {
val result = symbols.search(query)
val result = symbols.search(query, None)
val obtained =
if (result.length > 100) s"${result.length} results"
else {
Expand Down
Loading
Loading