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 3 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 @@ -13,7 +13,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
25 changes: 25 additions & 0 deletions server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ import org.eclipse.lsp4j.services.LanguageClientAware
import org.eclipse.lsp4j.jsonrpc.messages.Either
import org.javacs.kt.symbols.workspaceSymbols
import org.javacs.kt.command.JAVA_TO_KOTLIN_COMMAND
import org.javacs.kt.command.RESOLVE_MAIN
import org.javacs.kt.j2k.convertJavaToKotlin
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.util.AsyncExecutor
import org.javacs.kt.resolve.resolveMain
import java.net.URI
import java.nio.file.Paths
import java.util.concurrent.CompletableFuture
Expand All @@ -30,6 +33,8 @@ class KotlinWorkspaceService(
private val gson = Gson()
private var languageClient: LanguageClient? = null

private val async = AsyncExecutor()

override fun connect(client: LanguageClient): Unit {
languageClient = client
}
Expand All @@ -53,6 +58,26 @@ class KotlinWorkspaceService(
)
)))))
}

RESOLVE_MAIN -> {
val fileUri = parseURI(gson.fromJson(args[0] as JsonElement, String::class.java))
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)

return async.compute {
resolveMain(compiledFile) + mapOf(
"projectRoot" to workspacePath
)
}
}
}

return CompletableFuture.completedFuture(null)
Expand Down
5 changes: 4 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,9 @@
package org.javacs.kt.command

const val JAVA_TO_KOTLIN_COMMAND = "convertJavaToKotlin"
const val RESOLVE_MAIN = "resolveMain"

val ALL_COMMANDS = listOf(
JAVA_TO_KOTLIN_COMMAND
JAVA_TO_KOTLIN_COMMAND,
RESOLVE_MAIN
)
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/ResolveMainCommandTest.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.junit.Test
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.javacs.kt.command.RESOLVE_MAIN

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

val commandResult = languageServer.workspaceService.executeCommand(executeCommandParams).get()

assertNotNull(commandResult)
val mainInfo = commandResult 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 executeCommandParams = ExecuteCommandParams(RESOLVE_MAIN, listOf(Gson().toJsonTree(root.resolve(file).toUri().toString())))

val commandResult = languageServer.workspaceService.executeCommand(executeCommandParams).get()

assertNotNull(commandResult)
val mainInfo = commandResult 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 executeCommandParams = ExecuteCommandParams(RESOLVE_MAIN, listOf(Gson().toJsonTree(root.resolve(file).toUri().toString())))

val commandResult = languageServer.workspaceService.executeCommand(executeCommandParams).get()

assertNotNull(commandResult)
val mainInfo = commandResult 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 executeCommandParams = ExecuteCommandParams(RESOLVE_MAIN, listOf(Gson().toJsonTree(root.resolve(file).toUri().toString())))

val commandResult = languageServer.workspaceService.executeCommand(executeCommandParams).get()

assertNotNull(commandResult)
val mainInfo = commandResult 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!")
}