Skip to content

Commit 73e8c14

Browse files
authored
Merge pull request #80 from sourcegraph/nsc/java-stdlib-hover
2 parents 225e711 + 2586043 commit 73e8c14

File tree

6 files changed

+386
-43
lines changed

6 files changed

+386
-43
lines changed

src/main/kotlin/lsifjava/ExternalDocs.kt

Lines changed: 97 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,145 @@
11
@file:Suppress("JAVA_MODULE_DOES_NOT_EXPORT_PACKAGE")
22
package lsifjava
33

4+
import com.sun.tools.javac.code.*
45
import com.sun.source.tree.*
56
import com.sun.source.util.DocTrees
67
import com.sun.source.util.JavacTask
78
import com.sun.source.util.TreePathScanner
8-
import com.sun.tools.javac.util.Context
99
import com.sun.tools.javac.api.JavacTool
1010
import com.sun.tools.javac.tree.JCTree.*
11+
import com.sun.tools.javac.util.Context
1112
import java.nio.charset.Charset
12-
import java.nio.file.Path
13+
import java.nio.file.*
1314
import javax.lang.model.element.Element
14-
import javax.tools.*
15+
import javax.tools.JavaFileObject
16+
import com.sun.tools.javac.file.JavacFileManager
17+
import javax.tools.StandardLocation
1518

1619
data class ExternalHoverMeta(val doc: String, val tree: Tree)
1720

18-
class ExternalDocs(private val docPaths: List<Path>) {
21+
private val emptyFileManager = SourceFileManager(emptySet())
1922

23+
class ExternalDocs(private val docPaths: List<Path>, private val diagnosticListener: CountingDiagnosticListener) {
24+
// we cache compilation unit trees here to reduce the number of times we have to invoke the parser
2025
private val fileCache = HashMap<String, Pair<JavacTask, CompilationUnitTree>?>()
2126

22-
private val fileManager: StandardJavaFileManager by lazy {
23-
val manager = ToolProvider.getSystemJavaCompiler()
24-
.getStandardFileManager(null, null, Charset.defaultCharset())
27+
private val fileManager = let {
28+
val manager = JavacFileManager(Context(), false, Charset.defaultCharset())
2529
manager.setLocation(StandardLocation.SOURCE_PATH, docPaths.map { it.toFile() })
2630
manager
2731
}
2832

33+
private val jdkFileManager = JDK8CompatFileManager(fileManager)
34+
35+
/**
36+
* Returns hover metadata for the tree section that matches the given element in the class file
37+
* associated with <code>containerClass</code>, which is expected to be the fully qualified name of the class file
38+
*/
2939
fun findDocForElement(containerClass: String, javac: JavacTool, element: Element): ExternalHoverMeta? {
3040
val context = DocumentIndexer.SimpleContext()
3141

32-
val (task, compUnit) = fileCache.getOrPut(containerClass) { analyzeFileFromJar(containerClass, context, javac) }
33-
?: return null
42+
val (task, compUnit) = fileCache.getOrPut(containerClass) {
43+
val fileObject = findFileFromJars(containerClass) ?: return@getOrPut null
44+
parseFileObject(fileObject, context, javac)
45+
} ?: return null
3446

3547
return DocExtractionVisitor(task, element).scan(compUnit, null)
3648
}
3749

38-
private fun analyzeFileFromJar(containerClass: String, context: Context, javac: JavacTool): Pair<JavacTask, CompilationUnitTree>? {
39-
val file = fileManager.getJavaFileForInput(StandardLocation.SOURCE_PATH, containerClass, JavaFileObject.Kind.SOURCE)
40-
?: return null
41-
val task = javac.getTask(NoopWriter, fileManager, CountingDiagnosticListener.NullWriter, listOf(), listOf(), listOf(file), context)
42-
val compUnit = task.parse().iterator().next()
43-
val analyzeResult = runCatching { task.analyze() }
44-
analyzeResult.getOrNull() ?: run {
45-
//println("${file.name} threw exception")
46-
return null
47-
}
50+
private fun findFileFromJars(containerClass: String): JavaFileObject? =
51+
fileManager.getJavaFileForInput(StandardLocation.SOURCE_PATH, containerClass, JavaFileObject.Kind.SOURCE)
52+
?: jdkFileManager.getJavaFileForInput(containerClass)
53+
54+
/**
55+
* Performs basic parsing of the external dependency JavaFileObject and returns a (javac task, compilation unit) pair.
56+
* For reasons beyond my knowing (and many a staring at the debugger), an empty file manager
57+
*/
58+
private fun parseFileObject(file: JavaFileObject, context: Context, javac: JavacTool): Pair<JavacTask, CompilationUnitTree>? {
59+
val task = javac.getTask(NoopWriter, emptyFileManager, diagnosticListener, listOf(), listOf(), listOf(file), context)
60+
val compUnit = task.parse().iterator().apply {
61+
// bail out of the enclosing function
62+
if(!hasNext()) return null
63+
}.next()
4864
return Pair(task, compUnit)
4965
}
50-
51-
private class DocExtractionVisitor(task: JavacTask, private val element: Element): TreePathScanner<ExternalHoverMeta?, Unit?>() {
52-
private val docs: DocTrees = DocTrees.instance(task)
53-
54-
override fun visitMethod(node: MethodTree?, p: Unit?): ExternalHoverMeta? {
55-
(node as JCMethodDecl).sym ?: return null
5666

57-
if(node.sym.toString() == element.toString()) {
67+
private class DocExtractionVisitor(
68+
val task: JavacTask,
69+
private val element: Element,
70+
private val docs: DocTrees = DocTrees.instance(task)
71+
): TreePathScanner<ExternalHoverMeta?, Unit?>() {
72+
// Basic flag to indicate if this DocExtractionVisitor has visited its assigned class decl yet.
73+
// If set to false, we want to create a new DocExtractionVisitor for the class decl we are visiting.
74+
// This way we can keep track of the owning class decl for methods/variables that are otherwise only
75+
// available with fully resolved symbols, which we don't get with non-full-fat parsing+analyzing
76+
private var new: Boolean = true
77+
private lateinit var classDecl: ClassTree
78+
79+
override fun visitMethod(node: MethodTree, p: Unit?): ExternalHoverMeta? {
80+
if(element !is Symbol.MethodSymbol) return null
81+
82+
if(element.owner.simpleName.toString() != classDecl.simpleName.toString())
83+
return null
84+
85+
if(element.name.toString() != node.name.toString()) return null
86+
87+
if(element.name.toString() != "<init>" &&
88+
element.returnType.toString() != node.returnType.toString()) return null
89+
90+
val paramsEqual = element.params.size == node.parameters.size &&
91+
element.params.foldIndexed(true) { i, acc, sym ->
92+
val paramType = (node.parameters[i] as JCVariableDecl).vartype
93+
val defTypeName = when(paramType) {
94+
is JCPrimitiveTypeTree -> paramType.toString()
95+
is JCTypeApply -> paramType.clazz.toString()
96+
is JCIdent -> paramType.toString()
97+
else -> {
98+
println("param type wasn't JCPrimitiveTypeTree|JCTypeApply|JCIdent, but ${paramType::class.java}")
99+
return null
100+
}
101+
}
102+
acc && sym.type.tsym.simpleName.toString() == defTypeName
103+
}
104+
105+
if(paramsEqual) {
58106
val doc = docs.getDocComment(currentPath) ?: return null
59-
return ExternalHoverMeta(doc, node)
107+
return ExternalHoverMeta(doc.trim(), node)
60108
}
61109

62110
return null
63111
}
64112

65-
override fun visitVariable(node: VariableTree?, p: Unit?): ExternalHoverMeta? {
66-
(node as JCVariableDecl).sym ?: return null
113+
override fun visitVariable(node: VariableTree, p: Unit?): ExternalHoverMeta? {
114+
if(element !is Symbol.VarSymbol) return null
67115

68-
if(node.sym.toString() == element.toString()) {
116+
if(element.owner.simpleName.toString() != classDecl.simpleName.toString())
117+
return null
69118

119+
if(element.name.toString() == node.name.toString()) {
120+
val doc = docs.getDocComment(currentPath) ?: return null
121+
return ExternalHoverMeta(doc.trim(), node)
70122
}
123+
71124
// filter to instance variables
72125
return null
73126
}
74127

75-
override fun visitClass(node: ClassTree?, p: Unit?): ExternalHoverMeta? {
76-
(node as JCClassDecl).sym ?: return null
77-
78-
if(node.sym.toString() == element.toString()) {
128+
override fun visitClass(node: ClassTree, p: Unit?): ExternalHoverMeta? {
129+
// we need this logic here to stop calling scan on the same ClassTree infinitely, but we also
130+
// want to start a new visitor for each nested class decl
131+
if(!new) {
132+
return DocExtractionVisitor(task, element, docs).scan(node, null)
133+
}
134+
new = false
135+
classDecl = node
136+
137+
if(element !is Symbol.ClassSymbol) return super.visitClass(node, p)
138+
139+
// Assumption: no class-like name conflicts within a single class file
140+
if((node as JCClassDecl).name.toString() == element.simpleName.toString()) {
79141
val doc = docs.getDocComment(currentPath) ?: return super.visitClass(node, p)
80-
return ExternalHoverMeta(doc, node)
142+
return ExternalHoverMeta(doc.trim(), node)
81143
}
82144

83145
return super.visitClass(node, p)

src/main/kotlin/lsifjava/FileCollector.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ fun buildIndexerMap(
2727
val sourceVersions = buildToolInterface.javaSourceVersions
2828

2929
// TODO(nsc) where to move this
30-
val externalDocManager = ExternalDocs(buildToolInterface.sourcesList)
30+
val externalDocManager = ExternalDocs(buildToolInterface.sourcesList, javacDiagListener)
3131

3232
val fileBuildInfo = Channel<FileBuildInfo>()
3333

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
@file:Suppress("JAVA_MODULE_DOES_NOT_EXPORT_PACKAGE")
2+
package lsifjava
3+
4+
import com.sun.tools.javac.util.Context
5+
import java.io.IOException
6+
import java.nio.charset.Charset
7+
import java.nio.file.*
8+
import javax.tools.*
9+
10+
// hard-coded list for convenience. Sorry, George :)
11+
private val JDK_MODULES = listOf(
12+
"java.activation",
13+
"java.base",
14+
"java.compiler",
15+
"java.corba",
16+
"java.datatransfer",
17+
"java.desktop",
18+
"java.instrument",
19+
"java.jnlp",
20+
"java.logging",
21+
"java.management",
22+
"java.management.rmi",
23+
"java.naming",
24+
"java.net.http",
25+
"java.prefs",
26+
"java.rmi",
27+
"java.scripting",
28+
"java.se",
29+
"java.se.ee",
30+
"java.security.jgss",
31+
"java.security.sasl",
32+
"java.smartcardio",
33+
"java.sql",
34+
"java.sql.rowset",
35+
"java.transaction",
36+
"java.transaction.xa",
37+
"java.xml",
38+
"java.xml.bind",
39+
"java.xml.crypto",
40+
"java.xml.ws",
41+
"java.xml.ws.annotation",
42+
"javafx.base",
43+
"javafx.controls",
44+
"javafx.fxml",
45+
"javafx.graphics",
46+
"javafx.media",
47+
"javafx.swing",
48+
"javafx.web",
49+
"jdk.accessibility",
50+
"jdk.aot",
51+
"jdk.attach",
52+
"jdk.charsets",
53+
"jdk.compiler",
54+
"jdk.crypto.cryptoki",
55+
"jdk.crypto.ec",
56+
"jdk.dynalink",
57+
"jdk.editpad",
58+
"jdk.hotspot.agent",
59+
"jdk.httpserver",
60+
"jdk.incubator.httpclient",
61+
"jdk.internal.ed",
62+
"jdk.internal.jvmstat",
63+
"jdk.internal.le",
64+
"jdk.internal.opt",
65+
"jdk.internal.vm.ci",
66+
"jdk.internal.vm.compiler",
67+
"jdk.internal.vm.compiler.management",
68+
"jdk.jartool",
69+
"jdk.javadoc",
70+
"jdk.jcmd",
71+
"jdk.jconsole",
72+
"jdk.jdeps",
73+
"jdk.jdi",
74+
"jdk.jdwp.agent",
75+
"jdk.jfr",
76+
"jdk.jlink",
77+
"jdk.jshell",
78+
"jdk.jsobject",
79+
"jdk.jstatd",
80+
"jdk.localedata",
81+
"jdk.management",
82+
"jdk.management.agent",
83+
"jdk.management.cmm",
84+
"jdk.management.jfr",
85+
"jdk.management.resource",
86+
"jdk.naming.dns",
87+
"jdk.naming.rmi",
88+
"jdk.net",
89+
"jdk.pack",
90+
"jdk.packager.services",
91+
"jdk.rmic",
92+
"jdk.scripting.nashorn",
93+
"jdk.scripting.nashorn.shell",
94+
"jdk.sctp",
95+
"jdk.security.auth",
96+
"jdk.security.jgss",
97+
"jdk.snmp",
98+
"jdk.unsupported",
99+
"jdk.unsupported.desktop",
100+
"jdk.xml.dom",
101+
"jdk.zipfs",
102+
)
103+
104+
/**
105+
* FileManager that falls back to JavacPathFileManager for Java 8.
106+
* Java 8 StandardJavaFileManager doesn't have the <code>setLocationFromPaths</code>
107+
* method, instead it only has <code>setLocation</code> which requires an
108+
* <code>Iterable<? extends File></code>, which would cause an UnsupportedException
109+
* when trying to turn the ZipFile from the in-memory FileSystem into a File.
110+
* Because JavacPathFileManager doesn't exist beyond Java 8 (9?) and we build with 14+,
111+
* the symbol resolver would fail for that symbol, hence we create an instance via
112+
* reflection if the Java version is 8. God I hate this.
113+
*/
114+
class JDK8CompatFileManager(manager: StandardJavaFileManager): ForwardingJavaFileManager<JavaFileManager>(getFileManager(manager)) {
115+
companion object {
116+
/**
117+
* returns a different JavaFileManager based on the current java version, with either SOURCE_PATH or MODULE_SOURCE_PATH set to "/"
118+
* denoting the root of the JAR/ZIP file that this file manager is for. If JDK 8, we instantiate a <code>com.sun.tools.javac.nio.JavacPathFileManager</code>
119+
* via reflection, else we use the passed file manager. See the class doc for the reason why.
120+
*/
121+
private fun getFileManager(fileManager: StandardJavaFileManager): JavaFileManager {
122+
var java8Manager: JavaFileManager? = null
123+
124+
if(javaVersion == 8) {
125+
java8Manager = Class.forName("com.sun.tools.javac.nio.JavacPathFileManager")
126+
.constructors[0]
127+
.newInstance(Context(), false, Charset.defaultCharset())
128+
as JavaFileManager?
129+
}
130+
131+
srcZip()?.also {
132+
if(javaVersion > 8) {
133+
fileManager.setLocationFromPaths(StandardLocation.MODULE_SOURCE_PATH, setOf(it.getPath("/")))
134+
} else {
135+
java8Manager!!::class.java
136+
.getMethod("setDefaultFileSystem", FileSystem::class.java)
137+
.invoke(java8Manager, it)
138+
java8Manager::class.java
139+
.getMethod("setLocation", JavaFileManager.Location::class.java, Iterable::class.java)
140+
.invoke(java8Manager, StandardLocation.SOURCE_PATH, setOf(it.getPath("/")))
141+
}
142+
}
143+
144+
return java8Manager ?: fileManager
145+
}
146+
147+
private fun srcZip(): FileSystem? {
148+
val srcZip = findSrcZip() ?: return null
149+
return try {
150+
FileSystems.newFileSystem(srcZip, ExternalDocs::class.java.classLoader)
151+
} catch (e: IOException) {
152+
throw RuntimeException(e)
153+
}
154+
}
155+
156+
private fun findSrcZip(): Path? {
157+
val javaHome = JavaHomeHelper.javaHome() ?: return null
158+
val locations = arrayOf("lib/src.zip", "src.zip")
159+
for (rel in locations) {
160+
val abs = javaHome.resolve(rel)
161+
if (Files.exists(abs)) {
162+
return abs
163+
}
164+
}
165+
return null
166+
}
167+
}
168+
169+
/**
170+
* Searches for a class either by module or not depending on the current java version.
171+
*/
172+
fun getJavaFileForInput(containerClass: String): JavaFileObject? {
173+
if(javaVersion == 8) {
174+
return this.getJavaFileForInput(StandardLocation.SOURCE_PATH, containerClass, JavaFileObject.Kind.SOURCE)
175+
} else {
176+
for(module in JDK_MODULES) {
177+
val moduleLocation = this.getLocationForModule(StandardLocation.MODULE_SOURCE_PATH, module) ?: continue
178+
this.getJavaFileForInput(moduleLocation, containerClass, JavaFileObject.Kind.SOURCE)?.run {
179+
return this
180+
}
181+
}
182+
}
183+
184+
return null
185+
}
186+
}

0 commit comments

Comments
 (0)