Skip to content

Support Java imports #190

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

Merged
merged 16 commits into from
Feb 22, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions server/src/main/kotlin/org/javacs/kt/CompiledFile.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class CompiledFile(
bindingContextOf(expression, scopeWithImports).getType(expression)

fun bindingContextOf(expression: KtExpression, scopeWithImports: LexicalScope): BindingContext =
classPath.compiler.compileExpression(expression, scopeWithImports, sourcePath, kind).first
classPath.compiler.compileKtExpression(expression, scopeWithImports, sourcePath, kind).first

private fun expandForType(cursor: Int, surroundingExpr: KtExpression): KtExpression {
val dotParent = surroundingExpr.parent as? KtDotQualifiedExpression
Expand Down Expand Up @@ -98,7 +98,7 @@ class CompiledFile(

val (surroundingContent, offset) = contentAndOffsetFromElement(psi, oldParent, asReference)
val padOffset = " ".repeat(offset)
val recompile = classPath.compiler.createFile(padOffset + surroundingContent, Paths.get("dummy.virtual" + if (isScript) ".kts" else ".kt"), kind)
val recompile = classPath.compiler.createKtFile(padOffset + surroundingContent, Paths.get("dummy.virtual" + if (isScript) ".kts" else ".kt"), kind)
return recompile.findElementAt(cursor)?.findParent<KtElement>()
}

Expand Down
61 changes: 33 additions & 28 deletions server/src/main/kotlin/org/javacs/kt/Compiler.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package org.javacs.kt

import org.jetbrains.kotlin.com.intellij.codeInsight.NullableNotNullManager
import org.jetbrains.kotlin.com.intellij.lang.Language
import org.jetbrains.kotlin.com.intellij.openapi.Disposable
import org.jetbrains.kotlin.com.intellij.openapi.util.Disposer
import org.jetbrains.kotlin.com.intellij.openapi.vfs.StandardFileSystems
import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFileManager
import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFileSystem
import org.jetbrains.kotlin.com.intellij.psi.PsiFile
import org.jetbrains.kotlin.com.intellij.psi.PsiFileFactory
import org.jetbrains.kotlin.com.intellij.mock.MockProject
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
Expand All @@ -14,6 +16,7 @@ import org.jetbrains.kotlin.cli.jvm.compiler.CliBindingTrace
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM
import org.jetbrains.kotlin.cli.jvm.config.addJavaSourceRoots
import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoots
import org.jetbrains.kotlin.cli.jvm.plugins.PluginCliParser
import org.jetbrains.kotlin.config.CommonConfigurationKeys
Expand All @@ -33,7 +36,7 @@ import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.BindingTraceContext
import org.jetbrains.kotlin.resolve.LazyTopDownAnalyzer
import org.jetbrains.kotlin.resolve.TopDownAnalysisMode.TopLevelDeclarations
import org.jetbrains.kotlin.resolve.TopDownAnalysisMode
import org.jetbrains.kotlin.resolve.calls.components.InferenceSession
import org.jetbrains.kotlin.resolve.calls.smartcasts.DataFlowInfo
import org.jetbrains.kotlin.resolve.extensions.ExtraImportsProviderExtension
Expand Down Expand Up @@ -84,6 +87,7 @@ private val GRADLE_DSL_DEPENDENCY_PATTERN = Regex("^gradle-(?:kotlin-dsl|core).*
* files and expressions.
*/
private class CompilationEnvironment(
javaSourcePath: Set<Path>,
classPath: Set<Path>
) : Closeable {
private val disposable = Disposer.newDisposable()
Expand All @@ -110,7 +114,9 @@ private class CompilationEnvironment(
put(CommonConfigurationKeys.LANGUAGE_VERSION_SETTINGS, languageVersionSettings)
put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, LoggingMessageCollector)
add(ComponentRegistrar.PLUGIN_COMPONENT_REGISTRARS, ScriptingCompilerConfigurationComponentRegistrar())

addJvmClasspathRoots(classPath.map { it.toFile() })
addJavaSourceRoots(javaSourcePath.map { it.toFile() })

// Setup script templates (e.g. used by Gradle's Kotlin DSL)
val scriptDefinitions: MutableList<ScriptDefinition> = mutableListOf(ScriptDefinition.getDefault(defaultJvmScriptingHostConfiguration))
Expand Down Expand Up @@ -187,13 +193,14 @@ private class CompilationEnvironment(
fun createContainer(sourcePath: Collection<KtFile>): Pair<ComponentProvider, BindingTraceContext> {
val trace = CliBindingTrace()
val container = TopDownAnalyzerFacadeForJVM.createContainer(
project = environment.project,
files = listOf(),
trace = trace,
configuration = environment.configuration,
packagePartProvider = environment::createPackagePartProvider,
// TODO FileBasedDeclarationProviderFactory keeps indices, re-use it across calls
declarationProviderFactory = { storageManager, _ -> FileBasedDeclarationProviderFactory(storageManager, sourcePath) })
project = environment.project,
files = sourcePath,
trace = trace,
configuration = environment.configuration,
packagePartProvider = environment::createPackagePartProvider,
// TODO FileBasedDeclarationProviderFactory keeps indices, re-use it across calls
declarationProviderFactory = ::FileBasedDeclarationProviderFactory
)
return Pair(container, trace)
}

Expand All @@ -217,12 +224,12 @@ enum class CompilationKind {
* Incrementally compiles files and expressions.
* The basic strategy for compiling one file at-a-time is outlined in OneFilePerformance.
*/
class Compiler(classPath: Set<Path>, buildScriptClassPath: Set<Path> = emptySet()) : Closeable {
class Compiler(javaSourcePath: Set<Path>, classPath: Set<Path>, buildScriptClassPath: Set<Path> = emptySet()) : Closeable {
private var closed = false
private val localFileSystem: VirtualFileSystem

private val defaultCompileEnvironment = CompilationEnvironment(classPath)
private val buildScriptCompileEnvironment = buildScriptClassPath.takeIf { it.isNotEmpty() }?.let(::CompilationEnvironment)
private val defaultCompileEnvironment = CompilationEnvironment(javaSourcePath, classPath)
private val buildScriptCompileEnvironment = buildScriptClassPath.takeIf { it.isNotEmpty() }?.let { CompilationEnvironment(emptySet(), it) }
private val compileLock = ReentrantLock() // TODO: Lock at file-level

companion object {
Expand All @@ -244,26 +251,25 @@ class Compiler(classPath: Set<Path>, buildScriptClassPath: Set<Path> = emptySet(
buildScriptCompileEnvironment?.updateConfiguration(config)
}

fun createFile(content: String, file: Path = Paths.get("dummy.virtual.kt"), kind: CompilationKind = CompilationKind.DEFAULT): KtFile {
fun createPsiFile(content: String, file: Path = Paths.get("dummy.virtual.kt"), language: Language = KotlinLanguage.INSTANCE, kind: CompilationKind = CompilationKind.DEFAULT): PsiFile {
assert(!content.contains('\r'))

val new = psiFileFactoryFor(kind).createFileFromText(file.toString(), KotlinLanguage.INSTANCE, content, true, false) as KtFile
val new = psiFileFactoryFor(kind).createFileFromText(file.toString(), language, content, true, false)
assert(new.virtualFile != null)

return new
}

fun createExpression(content: String, file: Path = Paths.get("dummy.virtual.kt"), kind: CompilationKind = CompilationKind.DEFAULT): KtExpression {
val property = parseDeclaration("val x = $content", file, kind) as KtProperty
fun createKtFile(content: String, file: Path = Paths.get("dummy.virtual.kt"), kind: CompilationKind = CompilationKind.DEFAULT): KtFile =
createPsiFile(content, file, language = KotlinLanguage.INSTANCE, kind = kind) as KtFile

fun createKtExpression(content: String, file: Path = Paths.get("dummy.virtual.kt"), kind: CompilationKind = CompilationKind.DEFAULT): KtExpression {
val property = createKtDeclaration("val x = $content", file, kind) as KtProperty
return property.initializer!!
}

fun createDeclaration(content: String, file: Path = Paths.get("dummy.virtual.kt"), kind: CompilationKind = CompilationKind.DEFAULT): KtDeclaration =
parseDeclaration(content, file, kind)

private fun parseDeclaration(content: String, file: Path, kind: CompilationKind = CompilationKind.DEFAULT): KtDeclaration {
val parse = createFile(content, file, kind)
fun createKtDeclaration(content: String, file: Path = Paths.get("dummy.virtual.kt"), kind: CompilationKind = CompilationKind.DEFAULT): KtDeclaration {
val parse = createKtFile(content, file, kind)
val declarations = parse.declarations

assert(declarations.size == 1) { "${declarations.size} declarations in $content" }
Expand All @@ -288,25 +294,24 @@ class Compiler(classPath: Set<Path>, buildScriptClassPath: Set<Path> = emptySet(
fun psiFileFactoryFor(kind: CompilationKind): PsiFileFactory =
PsiFileFactory.getInstance(compileEnvironmentFor(kind).environment.project)

fun compileFile(file: KtFile, sourcePath: Collection<KtFile>, kind: CompilationKind = CompilationKind.DEFAULT): Pair<BindingContext, ComponentProvider> =
compileFiles(listOf(file), sourcePath, kind)
fun compileKtFile(file: KtFile, sourcePath: Collection<KtFile>, kind: CompilationKind = CompilationKind.DEFAULT): Pair<BindingContext, ComponentProvider> =
compileKtFiles(listOf(file), sourcePath, kind)

fun compileFiles(files: Collection<KtFile>, sourcePath: Collection<KtFile>, kind: CompilationKind = CompilationKind.DEFAULT): Pair<BindingContext, ComponentProvider> {
fun compileKtFiles(files: Collection<KtFile>, sourcePath: Collection<KtFile>, kind: CompilationKind = CompilationKind.DEFAULT): Pair<BindingContext, ComponentProvider> {
if (kind == CompilationKind.BUILD_SCRIPT) {
// Print the (legacy) script template used by the compiled Kotlin DSL build file
files.forEach { LOG.debug { "$it -> ScriptDefinition: ${it.findScriptDefinition()?.asLegacyOrNull<KotlinScriptDefinition>()?.template?.simpleName}" } }
}

compileLock.withLock {
val (container, trace) = compileEnvironmentFor(kind).createContainer(sourcePath)
val topDownAnalyzer = container.get<LazyTopDownAnalyzer>()
topDownAnalyzer.analyzeDeclarations(TopLevelDeclarations, files)

val compileEnv = compileEnvironmentFor(kind)
val (container, trace) = compileEnv.createContainer(sourcePath)
container.get<LazyTopDownAnalyzer>().analyzeDeclarations(TopDownAnalysisMode.TopLevelDeclarations, files)
return Pair(trace.bindingContext, container)
}
}

fun compileExpression(expression: KtExpression, scopeWithImports: LexicalScope, sourcePath: Collection<KtFile>, kind: CompilationKind = CompilationKind.DEFAULT): Pair<BindingContext, ComponentProvider> {
fun compileKtExpression(expression: KtExpression, scopeWithImports: LexicalScope, sourcePath: Collection<KtFile>, kind: CompilationKind = CompilationKind.DEFAULT): Pair<BindingContext, ComponentProvider> {
try {
// Use same lock as 'compileFile' to avoid concurrency issues such as #42
compileLock.withLock {
Expand Down
107 changes: 75 additions & 32 deletions server/src/main/kotlin/org/javacs/kt/CompilerClassPath.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,70 @@ package org.javacs.kt

import org.javacs.kt.classpath.defaultClassPathResolver
import java.io.Closeable
import java.nio.file.Files
import java.nio.file.FileSystems
import java.nio.file.Path

/**
* Manages the class path (compiled JARs, etc), the Java source path
* and the compiler. Note that Kotlin sources are stored in SourcePath.
*/
class CompilerClassPath(private val config: CompilerConfiguration) : Closeable {
private val workspaceRoots = mutableSetOf<Path>()
private val javaSourcePath = mutableSetOf<Path>()
private val classPath = mutableSetOf<Path>()
private val buildScriptClassPath = mutableSetOf<Path>()
var compiler = Compiler(classPath, buildScriptClassPath)
var compiler = Compiler(javaSourcePath, classPath, buildScriptClassPath)
private set

init {
compiler.updateConfiguration(config)
}

private fun refresh(updateBuildScriptClassPath: Boolean = true) {
/** Updates and possibly reinstantiates the compiler using new paths. */
private fun refresh(
updateClassPath: Boolean = true,
updateBuildScriptClassPath: Boolean = true,
updateJavaSourcePath: Boolean = false
): Boolean {
// TODO: Fetch class path and build script class path concurrently (and asynchronously)
val resolver = defaultClassPathResolver(workspaceRoots)
var refreshCompiler = false
var refreshCompiler = updateJavaSourcePath

val newClassPath = resolver.classpathOrEmpty
if (newClassPath != classPath) {
syncClassPath(classPath, newClassPath)
refreshCompiler = true
if (updateClassPath) {
val newClassPath = resolver.classpathOrEmpty
if (newClassPath != classPath) {
syncPaths(classPath, newClassPath, "class path")
refreshCompiler = true
}
}

if (updateBuildScriptClassPath) {
LOG.info("Update build script path")
val newBuildScriptClassPath = resolver.buildScriptClasspathOrEmpty
if (newBuildScriptClassPath != buildScriptClassPath) {
syncClassPath(buildScriptClassPath, newBuildScriptClassPath)
syncPaths(buildScriptClassPath, newBuildScriptClassPath, "class path")
refreshCompiler = true
}
}

if (refreshCompiler) {
LOG.info("Reinstantiating compiler")
compiler.close()
compiler = Compiler(classPath, buildScriptClassPath)
compiler = Compiler(javaSourcePath, classPath, buildScriptClassPath)
updateCompilerConfiguration()
}

return refreshCompiler
}

private fun syncClassPath(dest: MutableSet<Path>, new: Set<Path>) {
/** Synchronizes the given two path sets and logs the differences. */
private fun syncPaths(dest: MutableSet<Path>, new: Set<Path>, name: String) {
val added = new - dest
val removed = dest - new

logAdded(added)
logRemoved(removed)
logAdded(added, name)
logRemoved(removed, name)

dest.removeAll(removed)
dest.addAll(added)
Expand All @@ -56,53 +75,77 @@ class CompilerClassPath(private val config: CompilerConfiguration) : Closeable {
compiler.updateConfiguration(config)
}

fun addWorkspaceRoot(root: Path) {
LOG.info("Searching for dependencies in workspace root {}", root)
fun addWorkspaceRoot(root: Path): Boolean {
LOG.info("Searching for dependencies and Java sources in workspace root {}", root)

workspaceRoots.add(root)
javaSourcePath.addAll(findJavaSourceFiles(root))

refresh()
return refresh(updateJavaSourcePath = true)
}

fun removeWorkspaceRoot(root: Path) {
LOG.info("Remove dependencies from workspace root {}", root)
fun removeWorkspaceRoot(root: Path): Boolean {
LOG.info("Removing dependencies and Java source path from workspace root {}", root)

workspaceRoots.remove(root)
javaSourcePath.removeAll(findJavaSourceFiles(root))

refresh()
return refresh(updateJavaSourcePath = true)
}

fun createdOnDisk(file: Path) {
changedOnDisk(file)
fun createdOnDisk(file: Path): Boolean {
if (isJavaSource(file)) {
javaSourcePath.add(file)
}
return changedOnDisk(file)
}

fun deletedOnDisk(file: Path) {
changedOnDisk(file)
fun deletedOnDisk(file: Path): Boolean {
if (isJavaSource(file)) {
javaSourcePath.remove(file)
}
return changedOnDisk(file)
}

fun changedOnDisk(file: Path) {
val name = file.fileName.toString()
if (name == "pom.xml" || name == "build.gradle" || name == "build.gradle.kts")
refresh(updateBuildScriptClassPath = false)
fun changedOnDisk(file: Path): Boolean {
val buildScript = isBuildScript(file)
val javaSource = isJavaSource(file)
if (buildScript || javaSource) {
return refresh(updateClassPath = buildScript, updateBuildScriptClassPath = false, updateJavaSourcePath = javaSource)
} else {
return false
}
}

private fun isJavaSource(file: Path): Boolean = file.fileName.toString().endsWith(".java")

private fun isBuildScript(file: Path): Boolean = file.fileName.toString().let { it == "pom.xml" || it == "build.gradle" || it == "build.gradle.kts" }

override fun close() {
compiler.close()
}
}

private fun logAdded(sources: Collection<Path>) {
private fun findJavaSourceFiles(root: Path): Set<Path> {
val sourceMatcher = FileSystems.getDefault().getPathMatcher("glob:*.java")
return SourceExclusions(root)
.walkIncluded()
.filter { sourceMatcher.matches(it.fileName) }
.toSet()
}

private fun logAdded(sources: Collection<Path>, name: String) {
when {
sources.isEmpty() -> return
sources.size > 5 -> LOG.info("Adding {} files to class path", sources.size)
else -> LOG.info("Adding {} to class path", sources)
sources.size > 5 -> LOG.info("Adding {} files to {}", sources.size, name)
else -> LOG.info("Adding {} to {}", sources, name)
}
}

private fun logRemoved(sources: Collection<Path>) {
private fun logRemoved(sources: Collection<Path>, name: String) {
when {
sources.isEmpty() -> return
sources.size > 5 -> LOG.info("Removing {} files from class path", sources.size)
else -> LOG.info("Removing {} from class path", sources)
sources.size > 5 -> LOG.info("Removing {} files from {}", sources.size, name)
else -> LOG.info("Removing {} from {}", sources, name)
}
}
5 changes: 4 additions & 1 deletion server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ class KotlinLanguageServer : LanguageServer, LanguageClientAware, Closeable {
val root = Paths.get(parseURI(params.rootUri))

sourceFiles.addWorkspaceRoot(root)
classPath.addWorkspaceRoot(root)
val refreshed = classPath.addWorkspaceRoot(root)
if (refreshed) {
sourcePath.refresh()
}
}

return completedFuture(InitializeResult(serverCapabilities))
Expand Down
Loading