Skip to content

Commit d026593

Browse files
committed
Add Scala support.
Previously, lsif-java wasn't able to index Scala projects. This commit add support to - render Scala method/class signatures using Scala syntax - compile Scala sources in package repositories (Maven Central libraries)
1 parent fbfd0bc commit d026593

File tree

80 files changed

+8874
-150
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

80 files changed

+8874
-150
lines changed

build.sbt

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ lazy val V =
1212
val bloop = "1.4.7"
1313
val bsp = "2.0.0-M13"
1414
val moped = "0.1.10"
15-
def scala213 = "2.13.4"
16-
def scala212 = "2.12.12"
17-
def scalameta = "4.4.8"
15+
def scala213 = "2.13.6"
16+
def scala212 = "2.12.14"
17+
def scala211 = "2.11.12"
18+
def scala3 = "3.0.1"
19+
def metals = "0.10.5"
20+
def scalameta = "4.4.25"
1821
def testcontainers = "0.39.3"
1922
def requests = "0.6.5"
2023
}
@@ -143,13 +146,19 @@ lazy val cli = project
143146
Seq[BuildInfoKey](
144147
version,
145148
scalaVersion,
149+
"mtags" -> V.metals,
150+
"scala211" -> V.scala211,
151+
"scala212" -> V.scala212,
152+
"scala213" -> V.scala213,
153+
"scala3" -> V.scala3,
146154
"bloopVersion" -> V.bloop,
147155
"bspVersion" -> V.bsp
148156
),
149157
buildInfoPackage := "com.sourcegraph.lsif_java",
150158
libraryDependencies ++=
151159
List(
152160
"io.get-coursier" %% "coursier" % V.coursier,
161+
"org.scalameta" % "mtags-interfaces" % V.metals,
153162
"org.scala-lang.modules" %% "scala-xml" % "1.3.0",
154163
"com.lihaoyi" %% "requests" % V.requests,
155164
"org.scalameta" %% "moped" % V.moped,
@@ -385,6 +394,7 @@ lazy val testSettings = List(
385394
libraryDependencies ++=
386395
List(
387396
"org.scalameta" %% "munit" % "0.7.27",
397+
"org.scalameta" %% "mtags" % V.metals cross CrossVersion.full,
388398
"com.dimafeng" %% "testcontainers-scala-munit" % V.testcontainers,
389399
"com.dimafeng" %% "testcontainers-scala-postgresql" % V.testcontainers,
390400
"org.scalameta" %% "moped-testkit" % V.moped,

lsif-java/src/main/scala/com/sourcegraph/lsif_java/SemanticdbPrinters.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ object SemanticdbPrinters {
9797
if (sig.isEmpty)
9898
" " + info.getDisplayName
9999
else
100-
" " + sig.replace('\n', ' ')
100+
" " + sig.trim.replace('\n', ' ')
101101
case _ =>
102102
""
103103
}

lsif-java/src/main/scala/com/sourcegraph/lsif_java/buildtools/LsifBuildTool.scala

Lines changed: 177 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,34 @@ package com.sourcegraph.lsif_java.buildtools
22

33
import java.io.File
44
import java.io.IOException
5+
import java.net.URLClassLoader
56
import java.nio.file.FileSystems
67
import java.nio.file.FileVisitResult
78
import java.nio.file.Files
89
import java.nio.file.NoSuchFileException
910
import java.nio.file.Path
1011
import java.nio.file.Paths
1112
import java.nio.file.SimpleFileVisitor
13+
import java.nio.file.StandardOpenOption
1214
import java.nio.file.attribute.BasicFileAttributes
15+
import java.util.ServiceLoader
1316

1417
import scala.collection.mutable.ListBuffer
1518
import scala.jdk.CollectionConverters._
19+
import scala.util.Failure
20+
import scala.util.Success
21+
import scala.util.Try
1622
import scala.util.control.NonFatal
1723

24+
import scala.meta.pc.PresentationCompiler
25+
1826
import com.sourcegraph.io.DeleteVisitor
27+
import com.sourcegraph.lsif_java.BuildInfo
1928
import com.sourcegraph.lsif_java.Dependencies
2029
import com.sourcegraph.lsif_java.Embedded
2130
import com.sourcegraph.lsif_java.commands.IndexCommand
31+
import com.sourcegraph.semanticdb_javac.Semanticdb.TextDocument
32+
import com.sourcegraph.semanticdb_javac.Semanticdb.TextDocuments
2233
import moped.json.DecodingContext
2334
import moped.json.ErrorResult
2435
import moped.json.JsonCodec
@@ -32,6 +43,7 @@ import moped.reporters.Diagnostic
3243
import moped.reporters.Input
3344
import os.CommandResult
3445
import os.ProcessOutput.Readlines
46+
import os.SubprocessException
3547

3648
/**
3749
* A custom build tool that is specifically made for lsif-java.
@@ -49,6 +61,16 @@ import os.ProcessOutput.Readlines
4961
*/
5062
class LsifBuildTool(index: IndexCommand) extends BuildTool("LSIF", index) {
5163

64+
private val javaPattern = FileSystems
65+
.getDefault
66+
.getPathMatcher("glob:**.java")
67+
private val scalaPattern = FileSystems
68+
.getDefault
69+
.getPathMatcher("glob:**.scala")
70+
private val allPatterns = FileSystems
71+
.getDefault
72+
.getPathMatcher("glob:**.{java,scala}")
73+
private val moduleInfo = Paths.get("module-info.java")
5274
protected def defaultTargetroot: Path = Paths.get("target")
5375
private def configFile =
5476
index.workingDirectory.resolve(LsifBuildTool.ConfigFileName)
@@ -96,40 +118,168 @@ class LsifBuildTool(index: IndexCommand) extends BuildTool("LSIF", index) {
96118
Files.createDirectories(tmp)
97119
Files.createDirectories(targetroot)
98120
val deps = Dependencies.resolveDependencies(config.dependencies.map(_.repr))
99-
val semanticdbJar = Embedded.semanticdbJar(tmp)
100-
val coursier = Embedded.coursier(tmp)
101-
val actualClasspath = deps.classpath :+ semanticdbJar
102-
val argsfile = targetroot.resolve("javacopts.txt")
103121
val sourceroot = index.workingDirectory
104122
if (!Files.isDirectory(sourceroot)) {
105123
throw new NoSuchFileException(sourceroot.toString())
106124
}
107-
val allJavaFiles = collectAllJavaFiles(sourceroot)
108-
val javaFiles = allJavaFiles
109-
.filterNot(_.endsWith(moduleInfo))
110-
.map(_.toString())
111-
val moduleInfos = allJavaFiles.filter(_.endsWith(moduleInfo))
112-
if (javaFiles.isEmpty) {
125+
val allSourceFiles = collectAllSourceFiles(sourceroot)
126+
val javaFiles = allSourceFiles.filter(path => javaPattern.matches(path))
127+
val scalaFiles = allSourceFiles.filter(path => scalaPattern.matches(path))
128+
if (javaFiles.isEmpty && scalaFiles.isEmpty) {
113129
index
114130
.app
115131
.warning(
116-
s"doing nothing, no files matching pattern '$sourceroot/**.java'"
132+
s"doing nothing, no files matching pattern '$sourceroot/**.{java,scala}'"
117133
)
118134
return CommandResult(0, Nil)
119135
}
120-
def generatedDir(name: String): String = {
121-
Files.createDirectory(tmp.resolve(name)).toString()
136+
val errors = ListBuffer.empty[Try[Unit]]
137+
errors += compileJavaFiles(tmp, deps, config, javaFiles)
138+
errors += compileScalaFiles(deps, scalaFiles)
139+
if (index.cleanup) {
140+
Files.walkFileTree(tmp, new DeleteVisitor)
141+
}
142+
val isSemanticdbGenerated = Files
143+
.isDirectory(targetroot.resolve("META-INF"))
144+
if (errors.nonEmpty && !isSemanticdbGenerated) {
145+
CommandResult(1, Nil)
146+
} else {
147+
if (isSemanticdbGenerated) {
148+
index
149+
.app
150+
.reporter
151+
.info(
152+
"Some SemanticDB files got generated even if there were compile errors. " +
153+
"In most cases, this means that lsif-java managed to index everything " +
154+
"except the locations that had compile errors and you can ignore the compile errors."
155+
)
156+
}
157+
CommandResult(0, Nil)
158+
}
159+
}
160+
161+
private def compileScalaFiles(
162+
deps: Dependencies,
163+
allScalaFiles: List[Path]
164+
): Try[Unit] =
165+
Try {
166+
withScalaPresentationCompiler(deps) { compiler =>
167+
allScalaFiles.foreach { path =>
168+
try compileScalaFile(compiler, path)
169+
catch {
170+
case NonFatal(e) =>
171+
// We want to try and index as much as possible so we don't fail the entire
172+
// compilation even if a single file fails to compile.
173+
index.app.reporter.log(Diagnostic.exception(e))
174+
}
175+
}
176+
}
177+
}
178+
179+
private def compileScalaFile(
180+
compiler: PresentationCompiler,
181+
path: Path
182+
): Unit = {
183+
val input = Input.path(path)
184+
val textDocument = TextDocument
185+
.parseFrom(compiler.semanticdbTextDocument(path.toUri, input.text).get())
186+
.toBuilder
187+
.setUri(sourceroot.relativize(path).iterator().asScala.mkString("/"))
188+
val textDocuments = TextDocuments
189+
.newBuilder()
190+
.addDocuments(textDocument)
191+
.build()
192+
val relpath = sourceroot
193+
.relativize(path)
194+
.resolveSibling(path.getFileName.toString + ".semanticdb")
195+
val out = targetroot
196+
.resolve("META-INF")
197+
.resolve("semanticdb")
198+
.resolve(relpath)
199+
Files.createDirectories(out.getParent)
200+
Files.write(
201+
out,
202+
textDocuments.toByteArray,
203+
StandardOpenOption.TRUNCATE_EXISTING,
204+
StandardOpenOption.CREATE
205+
)
206+
}
207+
208+
private def withScalaPresentationCompiler[T](
209+
deps: Dependencies
210+
)(fn: PresentationCompiler => T): T = {
211+
val scalaVersion = deps
212+
.classpath
213+
.headOption
214+
.flatMap(jar => ScalaVersion.inferFromJar(jar))
215+
.getOrElse {
216+
throw new IllegalArgumentException(
217+
s"failed to infer the Scala version from the dependencies: " +
218+
pprint.PPrinter.BlackWhite.tokenize(deps.classpath).mkString
219+
)
220+
}
221+
val mtags = Dependencies.resolveDependencies(
222+
List(s"org.scalameta:mtags_${scalaVersion}:${BuildInfo.mtags}")
223+
)
224+
val scalaLibrary = mtags
225+
.classpath
226+
.filter(_.getFileName.toString.contains("scala-library"))
227+
val parent = new ScalaCompilerClassLoader(this.getClass.getClassLoader)
228+
229+
val jars = mtags.classpath.map(_.toUri.toURL).toArray
230+
val classloader = new URLClassLoader(jars, parent)
231+
val compilers = ServiceLoader
232+
.load(classOf[PresentationCompiler], classloader)
233+
.iterator()
234+
if (compilers.hasNext) {
235+
val classpath = deps.classpath ++ scalaLibrary
236+
val argsfile = targetroot.resolve("javacopts.txt")
237+
Files.createDirectories(argsfile.getParent)
238+
Files.write(
239+
argsfile,
240+
List("-classpath", classpath.mkString(File.pathSeparator)).asJava,
241+
StandardOpenOption.CREATE,
242+
StandardOpenOption.TRUNCATE_EXISTING
243+
)
244+
val compiler = compilers
245+
.next()
246+
.newInstance("lsif-java", classpath.asJava, List[String]().asJava)
247+
try {
248+
fn(compiler)
249+
} finally {
250+
compiler.shutdown()
251+
}
252+
} else {
253+
throw new IllegalArgumentException(
254+
s"failed to load mtags presentation compiler for Scala version $scalaVersion"
255+
)
122256
}
257+
}
258+
259+
private def compileJavaFiles(
260+
tmp: Path,
261+
deps: Dependencies,
262+
config: Config,
263+
allJavaFiles: List[Path]
264+
): Try[Unit] = {
265+
val (moduleInfos, javaFiles) = allJavaFiles
266+
.partition(_.endsWith(moduleInfo))
267+
if (javaFiles.isEmpty)
268+
return Success(())
269+
val semanticdbJar = Embedded.semanticdbJar(tmp)
270+
val coursier = Embedded.coursier(tmp)
271+
val actualClasspath = deps.classpath :+ semanticdbJar
272+
val argsfile = targetroot.resolve("javacopts.txt")
123273
val arguments = ListBuffer.empty[String]
124274
arguments += "-encoding"
125275
arguments += "utf8"
126276
arguments += "-nowarn"
127277
arguments += "-d"
128-
arguments += generatedDir("d")
278+
arguments += generatedDir(tmp, "d")
129279
arguments += "-s"
130-
arguments += generatedDir("s")
280+
arguments += generatedDir(tmp, "s")
131281
arguments += "-h"
132-
arguments += generatedDir("h")
282+
arguments += generatedDir(tmp, "h")
133283
arguments += "-classpath"
134284
arguments += actualClasspath.mkString(File.pathSeparator)
135285
arguments +=
@@ -142,7 +292,7 @@ class LsifBuildTool(index: IndexCommand) extends BuildTool("LSIF", index) {
142292
arguments += "--module-source-path"
143293
arguments += sourceroot.toString
144294
} else {
145-
arguments ++= javaFiles
295+
arguments ++= javaFiles.map(_.toString)
146296
}
147297
val quotedArguments = arguments.map(a => "\"" + a + "\"")
148298
Files.write(argsfile, quotedArguments.asJava)
@@ -169,37 +319,18 @@ class LsifBuildTool(index: IndexCommand) extends BuildTool("LSIF", index) {
169319
cwd = os.Path(sourceroot),
170320
check = false
171321
)
172-
if (index.cleanup) {
173-
Files.walkFileTree(tmp, new DeleteVisitor)
174-
}
175-
val isSemanticdbGenerated = Files
176-
.isDirectory(targetroot.resolve("META-INF"))
177-
if (result.exitCode != 0 && !isSemanticdbGenerated) {
178-
result
179-
} else {
180-
if (isSemanticdbGenerated) {
181-
index
182-
.app
183-
.reporter
184-
.info(
185-
"Some SemanticDB files got generated even if there were compile errors. " +
186-
"In most cases, this means that lsif-java managed to index everything " +
187-
"except the locations that had compile errors and you can ignore the compile errors."
188-
)
189-
}
190-
CommandResult(0, Nil)
191-
}
322+
if (result.exitCode == 0)
323+
Success(())
324+
else
325+
Failure(SubprocessException(result))
192326
}
193327

194328
private def clean(): Unit = {
195329
Files.walkFileTree(targetroot, new DeleteVisitor)
196330
}
197331

198-
private val moduleInfo = Paths.get("module-info.java")
199-
200332
/** Recursively collects all Java files in the working directory */
201-
private def collectAllJavaFiles(dir: Path): List[Path] = {
202-
val javaPattern = FileSystems.getDefault.getPathMatcher("glob:**.java")
333+
private def collectAllSourceFiles(dir: Path): List[Path] = {
203334
val buf = ListBuffer.empty[Path]
204335
Files.walkFileTree(
205336
dir,
@@ -217,7 +348,7 @@ class LsifBuildTool(index: IndexCommand) extends BuildTool("LSIF", index) {
217348
file: Path,
218349
attrs: BasicFileAttributes
219350
): FileVisitResult = {
220-
if (javaPattern.matches(file)) {
351+
if (allPatterns.matches(file)) {
221352
buf += file
222353
}
223354
FileVisitResult.CONTINUE
@@ -231,6 +362,10 @@ class LsifBuildTool(index: IndexCommand) extends BuildTool("LSIF", index) {
231362
buf.toList
232363
}
233364

365+
private def generatedDir(tmp: Path, name: String): String = {
366+
Files.createDirectory(tmp.resolve(name)).toString()
367+
}
368+
234369
/**
235370
* Gets parsed from "junit:junit:4.13.1" strings inside lsif-java.json files.
236371
*/
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.sourcegraph.lsif_java.buildtools
2+
// Copied from: https://github.com/scalameta/metals/blob/3c83447ec658f87fdccbfb3f0a39fca1cec4ef6e/metals/src/main/scala/scala/meta/internal/metals/PresentationCompilerClassLoader.scala
3+
4+
/**
5+
* ClassLoader that is used to reflectively invoke the Scala compiler.
6+
*
7+
* The Scala compiler is compiled against the exact Scala versions of the
8+
* compiler while lsif-java is only compiled with Scala 2.13. In order to
9+
* communicate between lsif-java and multiple versions of the compiler, this
10+
* classloader shares a subset of Java classes that appear in method signatures
11+
* of the `scala.meta.pc.PresentationCompiler` class.
12+
*/
13+
class ScalaCompilerClassLoader(parent: ClassLoader) extends ClassLoader(null) {
14+
override def findClass(name: String): Class[_] = {
15+
val isShared =
16+
name.startsWith("org.eclipse.lsp4j") || name.startsWith("javax.") ||
17+
name.startsWith("com.google.gson") || name.startsWith("scala.meta.pc")
18+
if (isShared) {
19+
parent.loadClass(name)
20+
} else {
21+
throw new ClassNotFoundException(name)
22+
}
23+
}
24+
}

0 commit comments

Comments
 (0)