diff --git a/metals/src/main/scala/scala/meta/internal/implementation/ImplementationProvider.scala b/metals/src/main/scala/scala/meta/internal/implementation/ImplementationProvider.scala index 5e0a84afec6..57a30646891 100644 --- a/metals/src/main/scala/scala/meta/internal/implementation/ImplementationProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/implementation/ImplementationProvider.scala @@ -396,7 +396,8 @@ final class ImplementationProvider( } private def semanticdbForJarFile(fileSource: AbsolutePath) = { - val dialect = ScalaVersions.dialectForDependencyJar(fileSource.filename) + val dialect = + ScalaVersions.dialectForDependencyJar(fileSource, buildTargets) FileIO.slurp(fileSource, StandardCharsets.UTF_8) val textDocument = Mtags.index(fileSource, dialect) textDocument diff --git a/metals/src/main/scala/scala/meta/internal/metals/BuildTargets.scala b/metals/src/main/scala/scala/meta/internal/metals/BuildTargets.scala index 9226f663cff..476c37a6ba6 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/BuildTargets.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/BuildTargets.scala @@ -430,14 +430,31 @@ final class BuildTargets private ( * in other capacity than as a fallback, since both source jar * and normal jar might not be in the same directory. * - * @param sourceJarPath path to the nromaljar + * @param sourceJarPath path to the normal jar * @return path to the source jar for that jar */ private def sourceJarPathFallback( sourceJarPath: AbsolutePath - ): Option[AbsolutePath] = { - val fallback = sourceJarPath.parent.resolve( - sourceJarPath.filename.replace(".jar", "-sources.jar") + ): Option[AbsolutePath] = + findInTheSameDirectoryWithReplace(sourceJarPath, ".jar", "-sources.jar") + + /** + * Try to resolve path to normal jar from source jar, + * should only be used as fallback, since jar and source jar + * do not have to be in the same directory. + */ + private def jarFromSourceJarFallback( + jarPath: AbsolutePath + ): Option[AbsolutePath] = + findInTheSameDirectoryWithReplace(jarPath, "-sources.jar", ".jar") + + private def findInTheSameDirectoryWithReplace( + path: AbsolutePath, + fromPattern: String, + toPattern: String, + ) = { + val fallback = path.parent.resolve( + path.filename.replace(fromPattern, toPattern) ) if (fallback.exists) Some(fallback) else None @@ -578,7 +595,7 @@ final class BuildTargets private ( jar: AbsolutePath, ): Option[AbsolutePath] = { data - .fromOptions(_.findSourceJarOf(jar, Some(id))) + .fromOptions(_.findConnectedArtifact(jar, Some(id))) .orElse(sourceJarPathFallback(jar)) } @@ -586,10 +603,21 @@ final class BuildTargets private ( jar: AbsolutePath ): Option[AbsolutePath] = { data - .fromOptions(_.findSourceJarOf(jar, targetId = None)) + .fromOptions(_.findConnectedArtifact(jar, targetId = None)) .orElse(sourceJarPathFallback(jar)) } + def findJarFor( + id: BuildTargetIdentifier, + sourceJar: AbsolutePath, + ): Option[AbsolutePath] = { + data + .fromOptions( + _.findConnectedArtifact(sourceJar, Some(id), classifier = null) + ) + .orElse(jarFromSourceJarFallback(sourceJar)) + } + def inverseDependencySource( sourceJar: AbsolutePath ): collection.Set[BuildTargetIdentifier] = { diff --git a/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala b/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala index d9d8dcfa93b..90922ae0f92 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/Indexer.scala @@ -415,7 +415,7 @@ case class Indexer(indexProviders: IndexProviders)(implicit rc: ReportContext) { case Right(zip) => scribe.debug(s"Indexing JDK sources from $zip") usedJars += zip - val dialect = ScalaVersions.dialectForDependencyJar(zip.filename) + val dialect = ScalaVersions.dialectForDependencyJar(zip, buildTargets) sharedIndices.jvmTypeHierarchy.getTypeHierarchy(zip) match { case Some(overrides) => definitionIndex.addIndexedSourceJar(zip, Nil, dialect) @@ -540,7 +540,7 @@ case class Indexer(indexProviders: IndexProviders)(implicit rc: ReportContext) { * @param path JAR path */ private def addSourceJarSymbols(path: AbsolutePath): Unit = { - val dialect = ScalaVersions.dialectForDependencyJar(path.filename) + val dialect = ScalaVersions.dialectForDependencyJar(path, buildTargets) tables.jarSymbols.getTopLevels(path) match { case Some(toplevels) => tables.jarSymbols.getTypeHierarchy(path) match { diff --git a/metals/src/main/scala/scala/meta/internal/metals/ScalaVersions.scala b/metals/src/main/scala/scala/meta/internal/metals/ScalaVersions.scala index 4fac1a21a94..bcae4c1e9c0 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ScalaVersions.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ScalaVersions.scala @@ -1,8 +1,15 @@ package scala.meta.internal.metals +import java.io.UncheckedIOException + +import scala.collection.concurrent.TrieMap + import scala.meta.Dialect import scala.meta.dialects._ +import scala.meta.internal.io.FileIO +import scala.meta.internal.metals.MetalsEnrichments._ import scala.meta.internal.semver.SemVer +import scala.meta.io.AbsolutePath class ScalaVersions( deprecatedScalaVersions: Seq[String], @@ -13,6 +20,8 @@ class ScalaVersions( scala3: String, ) { + private val jarScalaVersionIndex = TrieMap[String, String]() + def isScala3Milestone(version: String): Boolean = version.startsWith("3.0.0-M") || version.startsWith("3.0.0-RC") @@ -150,7 +159,9 @@ class ScalaVersions( } private val scalaVersionRegex = - raw"(_)(\d)(\.\d{1,2})?(\.\d(-(RC|M)\d)?)?".r + raw"_(\d)(\.\d{1,2})?(\.\d(-(RC|M)\d)?)?".r + private val scalaLibraryRegex = + raw"scala-library-(\d)(\.\d{1,2})(\.\d(-(RC|M)\d)?)".r /** * Extract scala binary version from dependency jar name. @@ -160,35 +171,66 @@ class ScalaVersions( * `some-library_2.13-4.5.0` -> 2.13 * `some-library_2.13-2.11` -> 2.13 */ - def scalaBinaryVersionFromJarName(filename: String): String = { + def scalaBinaryVersionFromJarName(filename: String): Option[String] = { val dropEnding = filename .stripSuffix(".jar") - scalaVersionRegex - .findAllMatchIn(dropEnding) - .toList + List(scalaLibraryRegex, scalaVersionRegex) + .flatMap(_.findAllMatchIn(dropEnding).toList) .flatMap { m => - val hasUnderscorePrefix = Option(m.group(1)).isDefined - val major = m.group(2) - val minor = Option(m.group(3)).getOrElse("") - val ending = Option(m.group(4)).getOrElse("") + val major = m.group(1) + val minor = Option(m.group(2)).getOrElse("") + val ending = Option(m.group(3)).getOrElse("") val version = s"$major$minor$ending" if (isSupportedScalaBinaryVersion(version)) - Some(version -> hasUnderscorePrefix) + Some(version) else None } - .sortBy(_._2)(Ordering.Boolean.reverse) .headOption - .map { case (version, _) => scalaBinaryVersionFromFullVersion(version) } - .getOrElse("2.13") + .map(scalaBinaryVersionFromFullVersion) } - def dialectForDependencyJar(filename: String): Dialect = - dialectForScalaVersion( - scalaBinaryVersionFromJarName(filename), - includeSource3 = true, - ) + def dialectForDependencyJar( + jar: AbsolutePath, + buildTargets: BuildTargets, + ): Dialect = { + lazy val buildTargetAndScalaVersion = + buildTargets + .inverseDependencySource(jar) + .flatMap(id => buildTargets.scalaTarget(id)) + .map(target => (target.scalaBinaryVersion, target.id)) + .toList + .sortBy(_._1) + .headOption + + def fromTastyExistance = { + val fromTasty = buildTargetAndScalaVersion + .flatMap { case (_, id) => buildTargets.findJarFor(id, jar) } + .flatMap( + FileIO.withJarFileSystem(_, create = false) { root => + try { + root.listRecursive + .find(f => f.isFile && f.filename.endsWith(".tasty")) + .map(_ => "3") + .orElse(Some("2.13")) + } catch { + case _: UncheckedIOException => None + } + } + ) + fromTasty.foreach(jarScalaVersionIndex.put(jar.toURI.toString(), _)) + fromTasty + } + + val scalaVersion = + scalaBinaryVersionFromJarName(jar.toNIO.getFileName().toString()) + .orElse(jarScalaVersionIndex.get(jar.toURI.toString())) + .orElse(fromTastyExistance) + .orElse(buildTargetAndScalaVersion.map(_._1)) + .getOrElse("2.13") + dialectForScalaVersion(scalaVersion, includeSource3 = true) + } } diff --git a/metals/src/main/scala/scala/meta/internal/metals/StandaloneSymbolSearch.scala b/metals/src/main/scala/scala/meta/internal/metals/StandaloneSymbolSearch.scala index c9f257dabe1..77834b033cf 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/StandaloneSymbolSearch.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/StandaloneSymbolSearch.scala @@ -44,7 +44,10 @@ class StandaloneSymbolSearch( private val index = OnDemandSymbolIndex.empty() sources.foreach(s => - index.addSourceJar(s, ScalaVersions.dialectForDependencyJar(s.filename)) + index.addSourceJar( + s, + ScalaVersions.dialectForDependencyJar(s, buildTargets), + ) ) private val docs = new Docstrings(index) diff --git a/metals/src/main/scala/scala/meta/internal/metals/TargetData.scala b/metals/src/main/scala/scala/meta/internal/metals/TargetData.scala index b3ee4f757f5..bb868f60409 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/TargetData.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/TargetData.scala @@ -206,9 +206,10 @@ final class TargetData { } } - def findSourceJarOf( + def findConnectedArtifact( jar: AbsolutePath, targetId: Option[BuildTargetIdentifier], + classifier: String = "sources", ): Option[AbsolutePath] = { val jarUri = jar.toURI.toString() def depModules: Iterator[MavenDependencyModule] = targetId match { @@ -228,10 +229,10 @@ final class TargetData { module <- depModules artifacts = module.getArtifacts().asScala if artifacts.exists(artifact => isUriEqual(artifact.getUri(), jarUri)) - sourceJar <- artifacts.find(_.getClassifier() == "sources") - sourceJarPath = sourceJar.getUri().toAbsolutePath - if sourceJarPath.exists - } yield sourceJarPath + foundJar <- artifacts.find(_.getClassifier() == classifier) + foundJarPath = foundJar.getUri().toAbsolutePath + if foundJarPath.exists + } yield foundJarPath allFound.headOption } diff --git a/metals/src/main/scala/scala/meta/internal/tvp/MetalsTreeViewProvider.scala b/metals/src/main/scala/scala/meta/internal/tvp/MetalsTreeViewProvider.scala index 7ffb0429971..dc53d989fed 100644 --- a/metals/src/main/scala/scala/meta/internal/tvp/MetalsTreeViewProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/tvp/MetalsTreeViewProvider.scala @@ -132,9 +132,8 @@ class MetalsTreeViewProvider( val dialect = if (path.isJarFileSystem) { path.jarPath - .map { p => - ScalaVersions.dialectForDependencyJar(p.filename) - } + .flatMap(p => ScalaVersions.scalaBinaryVersionFromJarName(p.filename)) + .map(ScalaVersions.dialectForScalaVersion(_, includeSource3 = true)) .getOrElse(dialects.Scala3) } else dialectFromWorkspace val input = path.toInput @@ -264,7 +263,7 @@ class FolderTreeViewProvider( valueTooltip = _.toString, toplevels = () => buildTargets.allSourceJars.filter(_.exists), loadSymbols = (path, symbol) => { - val dialect = ScalaVersions.dialectForDependencyJar(path.filename) + val dialect = ScalaVersions.dialectForDependencyJar(path, buildTargets) classpath.jarSymbols(path, symbol, dialect) }, toplevelIcon = "package", diff --git a/tests/unit/src/main/scala/tests/MetalsTestEnrichments.scala b/tests/unit/src/main/scala/tests/MetalsTestEnrichments.scala index e66107e6e66..cb0835fbd04 100644 --- a/tests/unit/src/main/scala/tests/MetalsTestEnrichments.scala +++ b/tests/unit/src/main/scala/tests/MetalsTestEnrichments.scala @@ -106,7 +106,11 @@ object MetalsTestEnrichments { } libraries.foreach( _.sources.entries.foreach { s => - val dialect = ScalaVersions.dialectForDependencyJar(s.filename) + val version = ScalaVersions + .scalaBinaryVersionFromJarName(s.filename) + .getOrElse("2.13") + val dialect = + ScalaVersions.dialectForScalaVersion(version, includeSource3 = true) wsp.index.addSourceJar(s, dialect) } ) diff --git a/tests/unit/src/test/scala/tests/DefinitionSuite.scala b/tests/unit/src/test/scala/tests/DefinitionSuite.scala index a9f838bd8de..3a2b2f98d59 100644 --- a/tests/unit/src/test/scala/tests/DefinitionSuite.scala +++ b/tests/unit/src/test/scala/tests/DefinitionSuite.scala @@ -45,9 +45,15 @@ abstract class DefinitionSuiteBase( // Step 2. Index dependency sources index.addSourceJar(JdkSources().right.get, dialect) input.dependencySources.entries.foreach { jar => + val scalaVersion = ScalaVersions + .scalaBinaryVersionFromJarName(jar.filename) + .getOrElse("2.13") index.addSourceJar( jar, - ScalaVersions.dialectForDependencyJar(jar.filename), + ScalaVersions.dialectForScalaVersion( + scalaVersion, + includeSource3 = true, + ), ) } diff --git a/tests/unit/src/test/scala/tests/ScalaVersionsSuite.scala b/tests/unit/src/test/scala/tests/ScalaVersionsSuite.scala index 86777b76a1f..57c0740793c 100644 --- a/tests/unit/src/test/scala/tests/ScalaVersionsSuite.scala +++ b/tests/unit/src/test/scala/tests/ScalaVersionsSuite.scala @@ -316,7 +316,7 @@ class ScalaVersionsSuite extends BaseSuite { checkJar("tested-3.0-sources.jar", "2.13") def checkJar(jar: String, expected: String): Unit = test(jar) { - val out = ScalaVersions.scalaBinaryVersionFromJarName(jar) + val out = ScalaVersions.scalaBinaryVersionFromJarName(jar).getOrElse("2.13") assertEquals(out, expected, jar) }