Skip to content

New command to resolve main class - used for run/debug code lenses in editors #345

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 5 commits into from
Jun 8, 2022
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
3 changes: 2 additions & 1 deletion server/src/main/kotlin/org/javacs/kt/CompilerClassPath.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import java.nio.file.Path
* and the compiler. Note that Kotlin sources are stored in SourcePath.
*/
class CompilerClassPath(private val config: CompilerConfiguration) : Closeable {
private val workspaceRoots = mutableSetOf<Path>()
val workspaceRoots = mutableSetOf<Path>()

private val javaSourcePath = mutableSetOf<Path>()
private val buildScriptClassPath = mutableSetOf<Path>()
val classPath = mutableSetOf<ClassPathEntry>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class KotlinLanguageServer : LanguageServer, LanguageClientAware, Closeable {

private val textDocuments = KotlinTextDocumentService(sourceFiles, sourcePath, config, tempDirectory, uriContentProvider, classPath)
private val workspaces = KotlinWorkspaceService(sourceFiles, sourcePath, classPath, textDocuments, config)
private val protocolExtensions = KotlinProtocolExtensionService(uriContentProvider, classPath)
private val protocolExtensions = KotlinProtocolExtensionService(uriContentProvider, classPath, sourcePath)

private lateinit var client: LanguageClient

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ package org.javacs.kt
import org.eclipse.lsp4j.*
import org.javacs.kt.util.AsyncExecutor
import org.javacs.kt.util.parseURI
import org.javacs.kt.resolve.resolveMain
import java.util.concurrent.CompletableFuture
import java.nio.file.Paths

class KotlinProtocolExtensionService(
private val uriContentProvider: URIContentProvider,
private val cp: CompilerClassPath
private val cp: CompilerClassPath,
private val sp: SourcePath
) : KotlinProtocolExtensions {
private val async = AsyncExecutor()

Expand All @@ -18,4 +21,22 @@ class KotlinProtocolExtensionService(
override fun buildOutputLocation(): CompletableFuture<String?> = async.compute {
cp.outputDirectory.absolutePath
}

override fun mainClass(textDocument: TextDocumentIdentifier): CompletableFuture<Map<String, Any?>> = async.compute {
val fileUri = parseURI(textDocument.uri)
val filePath = Paths.get(fileUri)

// we find the longest one in case both the root and submodule are included
val workspacePath = cp.workspaceRoots.filter {
filePath.startsWith(it)
}.map {
it.toString()
}.maxByOrNull(String::length) ?: ""

val compiledFile = sp.currentVersion(fileUri)

resolveMain(compiledFile) + mapOf(
"projectRoot" to workspacePath
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ interface KotlinProtocolExtensions {

@JsonRequest
fun buildOutputLocation(): CompletableFuture<String?>

@JsonRequest
fun mainClass(textDocument: TextDocumentIdentifier): CompletableFuture<Map<String, Any?>>
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import org.javacs.kt.KotlinTextDocumentService
import org.javacs.kt.position.extractRange
import org.javacs.kt.util.filePath
import org.javacs.kt.util.parseURI
import org.javacs.kt.resolve.resolveMain
import java.net.URI
import java.nio.file.Paths
import java.util.concurrent.CompletableFuture
Expand All @@ -29,7 +30,7 @@ class KotlinWorkspaceService(
) : WorkspaceService, LanguageClientAware {
private val gson = Gson()
private var languageClient: LanguageClient? = null

override fun connect(client: LanguageClient): Unit {
languageClient = client
}
Expand Down
3 changes: 2 additions & 1 deletion server/src/main/kotlin/org/javacs/kt/command/Commands.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.javacs.kt.command

const val JAVA_TO_KOTLIN_COMMAND = "convertJavaToKotlin"

val ALL_COMMANDS = listOf(
JAVA_TO_KOTLIN_COMMAND
JAVA_TO_KOTLIN_COMMAND,
)
62 changes: 62 additions & 0 deletions server/src/main/kotlin/org/javacs/kt/resolve/ResolveMain.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.javacs.kt.resolve

import org.jetbrains.kotlin.fileClasses.JvmFileClassUtil
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtObjectDeclaration
import org.javacs.kt.CompiledFile
import org.javacs.kt.position.range
import org.javacs.kt.util.partitionAroundLast
import com.intellij.openapi.util.TextRange


fun resolveMain(file: CompiledFile): Map<String,Any> {
val parsedFile = file.parse.copy() as KtFile

val mainFunction = findTopLevelMainFunction(parsedFile)
if(null != mainFunction) {
// the KtFiles name is weird. Full path. This causes the class to have full path in name as well. Correcting to top level only
parsedFile.name = parsedFile.name.partitionAroundLast("/").second.substring(1)

return mapOf("mainClass" to JvmFileClassUtil.getFileClassInfoNoResolve(parsedFile).facadeClassFqName.asString(),
"range" to range(file.content, mainFunction.second))
}

val companionMain = findCompanionObjectMain(parsedFile)
if(null != companionMain) {
return mapOf(
"mainClass" to (companionMain.first ?: ""),
"range" to range(file.content, companionMain.second)
)
}

return emptyMap()
}

// only one main method allowed top level in a file (so invalid syntax files will not show any main methods)
private fun findTopLevelMainFunction(file: KtFile): Pair<String?, TextRange>? = file.declarations.find {
it is KtNamedFunction && "main" == it.name
}?.let {
Pair(it.name, it.textRangeInParent)
}

// finds a top level class that contains a companion object with a main function inside
private fun findCompanionObjectMain(file: KtFile): Pair<String?, TextRange>? = file.declarations.flatMap { topLevelDeclaration ->
if(topLevelDeclaration is KtClass) {
topLevelDeclaration.companionObjects
} else {
emptyList<KtObjectDeclaration>()
}
}.flatMap { companionObject ->
companionObject.body?.children?.toList() ?: emptyList()
}.mapNotNull { companionObjectInternal ->
if(companionObjectInternal is KtNamedFunction && "main" == companionObjectInternal.name && companionObjectInternal.text.startsWith("@JvmStatic")) {
companionObjectInternal
} else {
null
}
}.firstOrNull()?.let {
// a little ugly, but because of success of the above, we know that "it" has 4 layers of parent objects (child of companion object body, companion object body, companion object, outer class)
Pair((it.parent.parent.parent.parent as KtClass).fqName?.toString(), it.textRange)
}
76 changes: 76 additions & 0 deletions server/src/test/kotlin/org/javacs/kt/ResolveMainTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package org.javacs.kt

import com.google.gson.Gson
import org.eclipse.lsp4j.ExecuteCommandParams
import org.eclipse.lsp4j.Position
import org.eclipse.lsp4j.Range
import org.eclipse.lsp4j.TextDocumentIdentifier
import org.junit.Test
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull

class NoMainResolve : SingleFileTestFixture("resolvemain", "NoMain.kt") {
@Test
fun `Should not find any main class info`() {
val root = testResourcesRoot().resolve(workspaceRoot)
val fileUri = root.resolve(file).toUri().toString()

val result = languageServer.getProtocolExtensionService().mainClass(TextDocumentIdentifier(fileUri)).get()

assertNotNull(result)
val mainInfo = result as Map<String, String>
assertNull(mainInfo["mainClass"])
assertEquals(root.toString(), mainInfo["projectRoot"])
}
}


class SimpleMainResolve : SingleFileTestFixture("resolvemain", "Simple.kt") {
@Test
fun `Should resolve correct main class of simple file`() {
val root = testResourcesRoot().resolve(workspaceRoot)
val fileUri = root.resolve(file).toUri().toString()

val result = languageServer.getProtocolExtensionService().mainClass(TextDocumentIdentifier(fileUri)).get()

assertNotNull(result)
val mainInfo = result as Map<String, Any>
assertEquals("test.SimpleKt", mainInfo["mainClass"])
assertEquals(Range(Position(2, 0), Position(4, 1)), mainInfo["range"])
assertEquals(root.toString(), mainInfo["projectRoot"])
}
}


class JvmNameAnnotationMainResolve : SingleFileTestFixture("resolvemain", "JvmNameAnnotation.kt") {
@Test
fun `Should resolve correct main class of file annotated with JvmName`() {
val root = testResourcesRoot().resolve(workspaceRoot)
val fileUri = root.resolve(file).toUri().toString()

val result = languageServer.getProtocolExtensionService().mainClass(TextDocumentIdentifier(fileUri)).get()

assertNotNull(result)
val mainInfo = result as Map<String, Any>
assertEquals("com.mypackage.name.Potato", mainInfo["mainClass"])
assertEquals(Range(Position(5, 0), Position(7, 1)), mainInfo["range"])
assertEquals(root.toString(), mainInfo["projectRoot"])
}
}

class CompanionObjectMainResolve : SingleFileTestFixture("resolvemain", "CompanionObject.kt") {
@Test
fun `Should resolve correct main class of main function inside companion object`() {
val root = testResourcesRoot().resolve(workspaceRoot)
val fileUri = root.resolve(file).toUri().toString()

val result = languageServer.getProtocolExtensionService().mainClass(TextDocumentIdentifier(fileUri)).get()

assertNotNull(result)
val mainInfo = result as Map<String, Any>
assertEquals("test.my.companion.SweetPotato", mainInfo["mainClass"])
assertEquals(Range(Position(8, 8), Position(11, 9)), mainInfo["range"])
assertEquals(root.toString(), mainInfo["projectRoot"])
}
}
14 changes: 14 additions & 0 deletions server/src/test/resources/resolvemain/CompanionObject.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package test.my.companion

val SOME_GLOBAL_CONSTANT = 42

fun multiplyByOne(num: Int) = num*1

class SweetPotato {
companion object {
@JvmStatic
fun main() {
println("42 multiplied by 1: ${multiplyByOne(42)}")
}
}
}
8 changes: 8 additions & 0 deletions server/src/test/resources/resolvemain/JvmNameAnnotation.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@JvmName("Potato")
package com.mypackage.name

val MY_CONSTANT = 1

fun main(args: Array<String>) {

}
3 changes: 3 additions & 0 deletions server/src/test/resources/resolvemain/NoMain.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package no.main.found.hopefully

fun multiplyByOne(num: Int) = num*1
5 changes: 5 additions & 0 deletions server/src/test/resources/resolvemain/Simple.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package test

fun main() {
println("Hello!")
}