Skip to content

Commit 0f8e84b

Browse files
Strum355olafurpg
authored andcommitted
Add support for Kotlin files in lsif-java.json builds
Previously, lsif-java.json builds only supported Java and Scala. This commits adds support for Kotlin files by using the new semanticdb-kotlinc compiler plugin in https://github.com/sourcegraph/lsif-kotlin The semanticdb-kotlinc compiler plugin is a new project that needs more work before it's ready for stable usage. This is just one milestone towards that goal.
1 parent d296b26 commit 0f8e84b

File tree

4 files changed

+193
-16
lines changed

4 files changed

+193
-16
lines changed

build.sbt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ lazy val V =
1818
def scala3 = "3.0.1"
1919
def metals = "0.10.6-M1"
2020
def scalameta = "4.4.26"
21+
def semanticdbKotlinc = "0.0.2"
2122
def testcontainers = "0.39.3"
2223
def requests = "0.6.5"
2324
}
@@ -156,6 +157,7 @@ lazy val cli = project
156157
"sbtSourcegraphVersion" ->
157158
com.sourcegraph.sbtsourcegraph.BuildInfo.version,
158159
"semanticdbVersion" -> V.scalameta,
160+
"semanticdbKotlincVersion" -> V.semanticdbKotlinc,
159161
"mtagsVersion" -> V.metals,
160162
"scala211" -> V.scala211,
161163
"scala212" -> V.scala212,
@@ -172,7 +174,8 @@ lazy val cli = project
172174
"org.scala-lang.modules" %% "scala-xml" % "1.3.0",
173175
"com.lihaoyi" %% "requests" % V.requests,
174176
"org.scalameta" %% "moped" % V.moped,
175-
"org.scalameta" %% "ascii-graphs" % "0.1.2"
177+
"org.scalameta" %% "ascii-graphs" % "0.1.2",
178+
"org.jetbrains.kotlin" % "kotlin-compiler-embeddable" % "1.5.21"
176179
),
177180
(Compile / resourceGenerators) +=
178181
Def

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

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import java.io.File
44
import java.nio.file.Path
55

66
import scala.concurrent.duration.Duration
7+
import scala.util.Try
78
import scala.xml.XML
89

9-
import com.sourcegraph.lsif_java.BuildInfo
1010
import coursier.Fetch
1111
import coursier.Repositories
1212
import coursier.Resolve
@@ -41,6 +41,37 @@ object Dependencies {
4141
Repositories.centralGcs
4242
)
4343

44+
/**
45+
* Attempts to find the "common definitions" JAR for a potentially
46+
* MultiPlatform Project. We only support JVM for now, native and JS are not
47+
* supported. If it ends with '-jvm', we search for a JAR with the classifier
48+
* truncated. If it does not end with -jvm, we search for a JAR with the
49+
* -common classifier. This is non-exhaustive, and the classifiers are
50+
* completely arbitrary.
51+
*/
52+
def kotlinMPPCommon(
53+
group: String,
54+
artifact: String,
55+
version: String
56+
): Option[Path] =
57+
Try {
58+
val task = Fetch[Task](Cache.default)
59+
.withClassifiers(Set(Classifier.sources))
60+
.addRepositories(defaultExtraRepositories: _*)
61+
62+
if (artifact.endsWith("-jvm")) {
63+
val dependency = Dependencies
64+
.parseDependency(s"$group:${artifact.stripSuffix("-jvm")}:$version")
65+
val result = task.addDependencies(dependency).runResult()
66+
return Some(result.files.head.toPath)
67+
}
68+
69+
val dependency = Dependencies
70+
.parseDependency(s"$group:$artifact-common:$version")
71+
val result = task.addDependencies(dependency).runResult()
72+
result.files.head.toPath
73+
}.toOption
74+
4475
def resolveDependencies(
4576
dependencies: List[String],
4677
transitive: Boolean = true

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

Lines changed: 156 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.sourcegraph.lsif_java.buildtools
22

3-
import java.io.File
4-
import java.io.IOException
3+
import java.io.{File, FileOutputStream, IOException}
54
import java.net.URLClassLoader
65
import java.nio.file.FileSystems
76
import java.nio.file.FileVisitResult
@@ -17,17 +16,14 @@ import java.util.Collections
1716
import java.util.Optional
1817
import java.util.ServiceLoader
1918
import java.util.concurrent.TimeUnit
20-
2119
import scala.collection.mutable.ListBuffer
2220
import scala.jdk.CollectionConverters._
2321
import scala.util.Failure
2422
import scala.util.Success
2523
import scala.util.Try
2624
import scala.util.control.NonFatal
27-
2825
import scala.meta.pc.PresentationCompiler
2926
import scala.meta.pc.PresentationCompilerConfig
30-
3127
import com.sourcegraph.io.DeleteVisitor
3228
import com.sourcegraph.lsif_java.BuildInfo
3329
import com.sourcegraph.lsif_java.Dependencies
@@ -46,10 +42,23 @@ import moped.macros.ClassShape
4642
import moped.parsers.JsonParser
4743
import moped.reporters.Diagnostic
4844
import moped.reporters.Input
45+
import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments
46+
import org.jetbrains.kotlin.cli.common.arguments.ParseCommandLineArgumentsKt.parseCommandLineArguments
47+
import org.jetbrains.kotlin.cli.common.messages.{
48+
CompilerMessageSeverity,
49+
CompilerMessageSourceLocation,
50+
MessageCollector,
51+
MessageRenderer
52+
}
53+
import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
54+
import org.jetbrains.kotlin.config.Services
4955
import os.CommandResult
5056
import os.ProcessOutput.Readlines
5157
import os.SubprocessException
5258

59+
import java.util.jar.JarFile
60+
import scala.language.postfixOps
61+
5362
/**
5463
* A custom build tool that is specifically made for lsif-java.
5564
*
@@ -72,9 +81,12 @@ class LsifBuildTool(index: IndexCommand) extends BuildTool("LSIF", index) {
7281
private val scalaPattern = FileSystems
7382
.getDefault
7483
.getPathMatcher("glob:**.scala")
84+
private val kotlinPattern = FileSystems
85+
.getDefault
86+
.getPathMatcher("glob:**.kt")
7587
private val allPatterns = FileSystems
7688
.getDefault
77-
.getPathMatcher("glob:**.{java,scala}")
89+
.getPathMatcher("glob:**.{java,scala,kt}")
7890
private val moduleInfo = Paths.get("module-info.java")
7991

8092
override def usedInCurrentDirectory(): Boolean =
@@ -139,20 +151,22 @@ class LsifBuildTool(index: IndexCommand) extends BuildTool("LSIF", index) {
139151
throw new NoSuchFileException(sourceroot.toString)
140152
}
141153
val allSourceFiles = collectAllSourceFiles(sourceroot)
142-
val javaFiles = allSourceFiles.filter(path => javaPattern.matches(path))
143-
val scalaFiles = allSourceFiles.filter(path => scalaPattern.matches(path))
144-
if (javaFiles.isEmpty && scalaFiles.isEmpty) {
154+
val javaFiles = allSourceFiles.filter(javaPattern.matches)
155+
val scalaFiles = allSourceFiles.filter(scalaPattern.matches)
156+
val kotlinFiles = allSourceFiles.filter(kotlinPattern.matches)
157+
if (javaFiles.isEmpty && scalaFiles.isEmpty && kotlinFiles.isEmpty) {
145158
index
146159
.app
147160
.warning(
148-
s"doing nothing, no files matching pattern '$sourceroot/**.{java,scala}'"
161+
s"doing nothing, no files matching pattern '$sourceroot/**.{java,scala,kt}'"
149162
)
150163
return CommandResult(0, Nil)
151164
}
165+
152166
val errors = ListBuffer.empty[Try[Unit]]
153-
compileJavaFiles(tmp, deps, config, javaFiles)
154-
.recover(e => errors.addOne(Failure(e)))
155-
compileScalaFiles(deps, scalaFiles).recover(e => errors.addOne(Failure(e)))
167+
errors += compileJavaFiles(tmp, deps, config, javaFiles)
168+
errors += compileScalaFiles(deps, scalaFiles)
169+
errors += compileKotlinFiles(deps, config, kotlinFiles)
156170
if (index.cleanup) {
157171
Files.walkFileTree(tmp, new DeleteVisitor)
158172
}
@@ -168,14 +182,142 @@ class LsifBuildTool(index: IndexCommand) extends BuildTool("LSIF", index) {
168182
.info(
169183
"Some SemanticDB files got generated even if there were compile errors. " +
170184
"In most cases, this means that lsif-java managed to index everything " +
171-
"except the locations that had compile errors and you can ignore the compile errors." +
185+
"except the locations that had compile errors and you can ignore the compile errors.\n" +
172186
errors.mkString("\n")
173187
)
174188
}
175189
CommandResult(0, Nil)
176190
}
177191
}
178192

193+
private def compileKotlinFiles(
194+
deps: Dependencies,
195+
config: Config,
196+
allKotlinFiles: List[Path]
197+
): Try[Unit] = {
198+
if (allKotlinFiles.isEmpty)
199+
return Success()
200+
val filesPaths = allKotlinFiles.map(_.toString)
201+
202+
val plugin =
203+
Dependencies
204+
.resolveDependencies(
205+
List(
206+
s"com.sourcegraph:semanticdb-kotlinc:${BuildInfo.semanticdbKotlincVersion}"
207+
),
208+
transitive = false
209+
)
210+
.classpath
211+
.head
212+
213+
val self = config.dependencies.head
214+
val commonKotlinFiles: List[Path] =
215+
Dependencies
216+
.kotlinMPPCommon(self.groupId, self.artifactId, self.version) match {
217+
case Some(common) =>
218+
val outdir = Files.createTempDirectory("lsifjava-kotlin")
219+
val file = common.toFile
220+
val basename = file
221+
.getName
222+
.substring(0, file.getName.lastIndexOf("."))
223+
val newFiles = ListBuffer[Path]()
224+
val jar = new JarFile(file)
225+
val enu = jar.entries
226+
while (enu.hasMoreElements) {
227+
val entry = enu.nextElement
228+
val entryPath =
229+
if (entry.getName.startsWith(basename))
230+
entry.getName.substring(basename.length)
231+
else
232+
entry.getName
233+
234+
if (entry.isDirectory) {
235+
new File(outdir.toString, entryPath).mkdirs
236+
} else if (entry.getName.endsWith(".kt")) {
237+
val newFile = new File(outdir.toString, entryPath)
238+
newFiles.addOne(newFile.toPath)
239+
val istream = jar.getInputStream(entry)
240+
val ostream = new FileOutputStream(newFile)
241+
Iterator
242+
.continually(istream.read)
243+
.takeWhile(-1 !=)
244+
.foreach(ostream.write)
245+
ostream.close()
246+
istream.close()
247+
}
248+
}
249+
newFiles.toList
250+
case None =>
251+
List[Path]()
252+
}
253+
254+
val kargs: K2JVMCompilerArguments = new K2JVMCompilerArguments()
255+
val args = ListBuffer[String](
256+
"-nowarn",
257+
"-no-reflect",
258+
"-no-stdlib",
259+
"-Xmulti-platform",
260+
"-Xno-check-actual",
261+
"-Xopt-in=kotlin.RequiresOptIn",
262+
"-Xopt-in=kotlin.ExperimentalUnsignedTypes",
263+
"-Xopt-in=kotlin.ExperimentalStdlibApi",
264+
"-Xopt-in=kotlin.ExperimentalMultiplatform",
265+
"-Xopt-in=kotlin.contracts.ExperimentalContracts",
266+
"-Xallow-kotlin-package",
267+
s"-Xplugin=$plugin",
268+
"-P",
269+
s"plugin:semanticdb-kotlinc:sourceroot=$sourceroot",
270+
"-P",
271+
s"plugin:semanticdb-kotlinc:targetroot=$targetroot",
272+
"-classpath",
273+
deps.classpathSyntax
274+
)
275+
276+
if (commonKotlinFiles.nonEmpty) {
277+
args +=
278+
s"-Xcommon-sources=${commonKotlinFiles.map(_.toAbsolutePath.toString).mkString(",")}"
279+
}
280+
281+
args ++= filesPaths ++ commonKotlinFiles.map(_.toAbsolutePath.toString)
282+
283+
parseCommandLineArguments(args.asJava, kargs)
284+
285+
if (true)
286+
println(s"ARGS ${args.mkString(" ")}")
287+
288+
val exit = new K2JVMCompiler().exec(
289+
new MessageCollector {
290+
private val errors = new util.LinkedList[String]
291+
override def clear(): Unit = errors.clear()
292+
293+
override def hasErrors: Boolean = !errors.isEmpty
294+
295+
override def report(
296+
compilerMessageSeverity: CompilerMessageSeverity,
297+
s: String,
298+
compilerMessageSourceLocation: CompilerMessageSourceLocation
299+
): Unit = {
300+
if (
301+
s.endsWith("without a body must be abstract") ||
302+
s.endsWith("must have a body")
303+
)
304+
return // we get these when indexing the stdlib, no other solution found yet
305+
val msg = MessageRenderer
306+
.PLAIN_FULL_PATHS
307+
.render(compilerMessageSeverity, s, compilerMessageSourceLocation)
308+
System.err.println(msg)
309+
errors.push(msg)
310+
}
311+
},
312+
Services.EMPTY,
313+
kargs
314+
)
315+
if (exit.getCode == 0)
316+
Success(())
317+
else
318+
Failure(new Exception(exit.toString))
319+
}
320+
179321
private def compileScalaFiles(
180322
deps: Dependencies,
181323
allScalaFiles: List[Path]

semanticdb-java/src/main/protobuf/semanticdb.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ enum Language {
3333
UNKNOWN_LANGUAGE = 0;
3434
SCALA = 1;
3535
JAVA = 2;
36+
KOTLIN =3;
3637
}
3738

3839
message Range {

0 commit comments

Comments
 (0)