From 0e68b8a906bc4d07e44856de10cd18e73d8353e7 Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Sat, 9 Apr 2022 23:03:56 +0200 Subject: [PATCH 01/46] Basic resolve main-class command for use with run code lenses (and more?) --- .../kotlin/org/javacs/kt/CompilerClassPath.kt | 3 +- .../org/javacs/kt/KotlinWorkspaceService.kt | 25 ++++++++ .../kotlin/org/javacs/kt/command/Commands.kt | 5 +- .../org/javacs/kt/resolve/ResolveMain.kt | 36 +++++++++++ .../org/javacs/kt/ResolveMainCommandTest.kt | 63 +++++++++++++++++++ .../resolvemain/JvmNameAnnotation.kt | 8 +++ .../src/test/resources/resolvemain/NoMain.kt | 3 + .../src/test/resources/resolvemain/Simple.kt | 5 ++ 8 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 server/src/main/kotlin/org/javacs/kt/resolve/ResolveMain.kt create mode 100644 server/src/test/kotlin/org/javacs/kt/ResolveMainCommandTest.kt create mode 100644 server/src/test/resources/resolvemain/JvmNameAnnotation.kt create mode 100644 server/src/test/resources/resolvemain/NoMain.kt create mode 100644 server/src/test/resources/resolvemain/Simple.kt diff --git a/server/src/main/kotlin/org/javacs/kt/CompilerClassPath.kt b/server/src/main/kotlin/org/javacs/kt/CompilerClassPath.kt index 9d3805e18..f26b47b9c 100644 --- a/server/src/main/kotlin/org/javacs/kt/CompilerClassPath.kt +++ b/server/src/main/kotlin/org/javacs/kt/CompilerClassPath.kt @@ -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() + val workspaceRoots = mutableSetOf() + private val javaSourcePath = mutableSetOf() private val buildScriptClassPath = mutableSetOf() val classPath = mutableSetOf() diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt b/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt index 85af3b1ad..704a4f3c5 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt @@ -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 @@ -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 } @@ -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) diff --git a/server/src/main/kotlin/org/javacs/kt/command/Commands.kt b/server/src/main/kotlin/org/javacs/kt/command/Commands.kt index 0b47ad7e5..a0fcf3760 100644 --- a/server/src/main/kotlin/org/javacs/kt/command/Commands.kt +++ b/server/src/main/kotlin/org/javacs/kt/command/Commands.kt @@ -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 ) diff --git a/server/src/main/kotlin/org/javacs/kt/resolve/ResolveMain.kt b/server/src/main/kotlin/org/javacs/kt/resolve/ResolveMain.kt new file mode 100644 index 000000000..d6be2d5c0 --- /dev/null +++ b/server/src/main/kotlin/org/javacs/kt/resolve/ResolveMain.kt @@ -0,0 +1,36 @@ +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.javacs.kt.CompiledFile +import org.javacs.kt.LOG +import org.javacs.kt.position.range +import org.javacs.kt.util.partitionAroundLast +import com.intellij.openapi.util.TextRange + + +fun resolveMain(file: CompiledFile): Map { + LOG.info("Resolving main function! yay") + + 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)) + } + + return emptyMap() +} + +// only one allowed (so invalid syntax files will not show any main methods) +private fun findTopLevelMainFunction(file: KtFile): Pair? = file.declarations.find { + // TODO: any validations on arguments + it is KtNamedFunction && "main" == it.name +}?.let { + // TODO: any better things to return? + Pair(it.name, it.textRangeInParent) +} diff --git a/server/src/test/kotlin/org/javacs/kt/ResolveMainCommandTest.kt b/server/src/test/kotlin/org/javacs/kt/ResolveMainCommandTest.kt new file mode 100644 index 000000000..241c9a3cf --- /dev/null +++ b/server/src/test/kotlin/org/javacs/kt/ResolveMainCommandTest.kt @@ -0,0 +1,63 @@ +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 + 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 + 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 + assertEquals("com.mypackage.name.Potato", mainInfo["mainClass"]) + assertEquals(Range(Position(5, 0), Position(7, 1)), mainInfo["range"]) + assertEquals(root.toString(), mainInfo["projectRoot"]) + } +} + + +// TODO: should we support inner companion object mains? diff --git a/server/src/test/resources/resolvemain/JvmNameAnnotation.kt b/server/src/test/resources/resolvemain/JvmNameAnnotation.kt new file mode 100644 index 000000000..04c03c434 --- /dev/null +++ b/server/src/test/resources/resolvemain/JvmNameAnnotation.kt @@ -0,0 +1,8 @@ +@JvmName("Potato") +package com.mypackage.name + +val MY_CONSTANT = 1 + +fun main(args: Array) { + +} diff --git a/server/src/test/resources/resolvemain/NoMain.kt b/server/src/test/resources/resolvemain/NoMain.kt new file mode 100644 index 000000000..64eb222b1 --- /dev/null +++ b/server/src/test/resources/resolvemain/NoMain.kt @@ -0,0 +1,3 @@ +package no.main.found.hopefully + +fun multiplyByOne(num: Int) = num*1 diff --git a/server/src/test/resources/resolvemain/Simple.kt b/server/src/test/resources/resolvemain/Simple.kt new file mode 100644 index 000000000..de8be317a --- /dev/null +++ b/server/src/test/resources/resolvemain/Simple.kt @@ -0,0 +1,5 @@ +package test + +fun main() { + println("Hello!") +} From 29d2c965d6085f6d60a0377400e6bcc116bea95f Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Sun, 10 Apr 2022 15:41:58 +0200 Subject: [PATCH 02/46] Work in progress companion object main function support. --- .../org/javacs/kt/resolve/ResolveMain.kt | 43 +++++++++++++++++-- .../org/javacs/kt/ResolveMainCommandTest.kt | 15 ++++++- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/server/src/main/kotlin/org/javacs/kt/resolve/ResolveMain.kt b/server/src/main/kotlin/org/javacs/kt/resolve/ResolveMain.kt index d6be2d5c0..91aac4e15 100644 --- a/server/src/main/kotlin/org/javacs/kt/resolve/ResolveMain.kt +++ b/server/src/main/kotlin/org/javacs/kt/resolve/ResolveMain.kt @@ -3,6 +3,11 @@ 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.jetbrains.kotlin.psi.psiUtil.anyDescendantOfType +import org.jetbrains.kotlin.psi.KtClassOrObject +import org.jetbrains.kotlin.container.topologicalSort import org.javacs.kt.CompiledFile import org.javacs.kt.LOG import org.javacs.kt.position.range @@ -11,9 +16,8 @@ import com.intellij.openapi.util.TextRange fun resolveMain(file: CompiledFile): Map { - LOG.info("Resolving main function! yay") - 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 @@ -22,6 +26,15 @@ fun resolveMain(file: CompiledFile): Map { return mapOf("mainClass" to JvmFileClassUtil.getFileClassInfoNoResolve(parsedFile).facadeClassFqName.asString(), "range" to range(file.content, mainFunction.second)) } + + val companionMain = findCompanionObjectMain(parsedFile) + if(null != companionMain) { + // TODO: any way we should handle the jvmname stuff here? + return mapOf( + "mainClass" to (companionMain.first ?: ""), + "range" to range(file.content, companionMain.second) + ) + } return emptyMap() } @@ -31,6 +44,30 @@ private fun findTopLevelMainFunction(file: KtFile): Pair? = // TODO: any validations on arguments it is KtNamedFunction && "main" == it.name }?.let { - // TODO: any better things to return? Pair(it.name, it.textRangeInParent) } + +// TODO: can this and the previous be merged in any way? or is this approach the cleanest? +// finds a top level class that contains a companion object with a main function inside +private fun findCompanionObjectMain(file: KtFile): Pair? = file.declarations.flatMap { topLevelDeclaration -> + if(topLevelDeclaration is KtClass) { + topLevelDeclaration.companionObjects + } else { + emptyList() + } +}.flatMap { companionObject -> + companionObject.body?.children?.toList() ?: emptyList() +}.mapNotNull { companionObjectInternal -> + if(companionObjectInternal is KtNamedFunction && "main" == companionObjectInternal.name) { // && companionObjectInternal.annotations.any { + // LOG.info("Annotation!! {}", it.name) + // "JvmStatic" == it.name + // } + 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) + // TODO: should we correct textRange start line manually? includes the JvmStatic annotation if present.. + Pair((it.parent.parent.parent.parent as KtClass).fqName?.toString(), it.textRange) +} diff --git a/server/src/test/kotlin/org/javacs/kt/ResolveMainCommandTest.kt b/server/src/test/kotlin/org/javacs/kt/ResolveMainCommandTest.kt index 241c9a3cf..68ab2a617 100644 --- a/server/src/test/kotlin/org/javacs/kt/ResolveMainCommandTest.kt +++ b/server/src/test/kotlin/org/javacs/kt/ResolveMainCommandTest.kt @@ -59,5 +59,18 @@ class JvmNameAnnotationMainResolve : SingleFileTestFixture("resolvemain", "JvmNa } } +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() -// TODO: should we support inner companion object mains? + assertNotNull(commandResult) + val mainInfo = commandResult as Map + assertEquals("test.my.companion.SweetPotato", mainInfo["mainClass"]) + assertEquals(Range(Position(9, 8), Position(11, 9)), mainInfo["range"]) + assertEquals(root.toString(), mainInfo["projectRoot"]) + } +} From cd015765fd6fecd0aba02e87e02dac4cc76c00b4 Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Sun, 10 Apr 2022 16:03:29 +0200 Subject: [PATCH 03/46] Clean up and finalized companion object main resolving. Also adding test resource that was not added earlier. --- .../kotlin/org/javacs/kt/resolve/ResolveMain.kt | 15 ++------------- .../org/javacs/kt/ResolveMainCommandTest.kt | 2 +- .../test/resources/resolvemain/CompanionObject.kt | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 14 deletions(-) create mode 100644 server/src/test/resources/resolvemain/CompanionObject.kt diff --git a/server/src/main/kotlin/org/javacs/kt/resolve/ResolveMain.kt b/server/src/main/kotlin/org/javacs/kt/resolve/ResolveMain.kt index 91aac4e15..b6282eae2 100644 --- a/server/src/main/kotlin/org/javacs/kt/resolve/ResolveMain.kt +++ b/server/src/main/kotlin/org/javacs/kt/resolve/ResolveMain.kt @@ -5,11 +5,7 @@ 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.jetbrains.kotlin.psi.psiUtil.anyDescendantOfType -import org.jetbrains.kotlin.psi.KtClassOrObject -import org.jetbrains.kotlin.container.topologicalSort import org.javacs.kt.CompiledFile -import org.javacs.kt.LOG import org.javacs.kt.position.range import org.javacs.kt.util.partitionAroundLast import com.intellij.openapi.util.TextRange @@ -29,7 +25,6 @@ fun resolveMain(file: CompiledFile): Map { val companionMain = findCompanionObjectMain(parsedFile) if(null != companionMain) { - // TODO: any way we should handle the jvmname stuff here? return mapOf( "mainClass" to (companionMain.first ?: ""), "range" to range(file.content, companionMain.second) @@ -39,15 +34,13 @@ fun resolveMain(file: CompiledFile): Map { return emptyMap() } -// only one allowed (so invalid syntax files will not show any main methods) +// 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? = file.declarations.find { - // TODO: any validations on arguments it is KtNamedFunction && "main" == it.name }?.let { Pair(it.name, it.textRangeInParent) } -// TODO: can this and the previous be merged in any way? or is this approach the cleanest? // finds a top level class that contains a companion object with a main function inside private fun findCompanionObjectMain(file: KtFile): Pair? = file.declarations.flatMap { topLevelDeclaration -> if(topLevelDeclaration is KtClass) { @@ -58,16 +51,12 @@ private fun findCompanionObjectMain(file: KtFile): Pair? = f }.flatMap { companionObject -> companionObject.body?.children?.toList() ?: emptyList() }.mapNotNull { companionObjectInternal -> - if(companionObjectInternal is KtNamedFunction && "main" == companionObjectInternal.name) { // && companionObjectInternal.annotations.any { - // LOG.info("Annotation!! {}", it.name) - // "JvmStatic" == it.name - // } + 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) - // TODO: should we correct textRange start line manually? includes the JvmStatic annotation if present.. Pair((it.parent.parent.parent.parent as KtClass).fqName?.toString(), it.textRange) } diff --git a/server/src/test/kotlin/org/javacs/kt/ResolveMainCommandTest.kt b/server/src/test/kotlin/org/javacs/kt/ResolveMainCommandTest.kt index 68ab2a617..e9997843d 100644 --- a/server/src/test/kotlin/org/javacs/kt/ResolveMainCommandTest.kt +++ b/server/src/test/kotlin/org/javacs/kt/ResolveMainCommandTest.kt @@ -70,7 +70,7 @@ class CompanionObjectMainResolve : SingleFileTestFixture("resolvemain", "Compani assertNotNull(commandResult) val mainInfo = commandResult as Map assertEquals("test.my.companion.SweetPotato", mainInfo["mainClass"]) - assertEquals(Range(Position(9, 8), Position(11, 9)), mainInfo["range"]) + assertEquals(Range(Position(8, 8), Position(11, 9)), mainInfo["range"]) assertEquals(root.toString(), mainInfo["projectRoot"]) } } diff --git a/server/src/test/resources/resolvemain/CompanionObject.kt b/server/src/test/resources/resolvemain/CompanionObject.kt new file mode 100644 index 000000000..b9a95def1 --- /dev/null +++ b/server/src/test/resources/resolvemain/CompanionObject.kt @@ -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)}") + } + } +} From 2aea2bfe50630ea686266ba70bd47438ad5e5986 Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Fri, 22 Apr 2022 18:16:44 +0200 Subject: [PATCH 04/46] Tests for verifying current implemention of abstract method quickfix --- .../kotlin/org/javacs/kt/CodeActionTest.kt | 75 +++++++++++++++++++ .../test/resources/codeactions/samefile.kt | 15 ++++ 2 files changed, 90 insertions(+) create mode 100644 server/src/test/kotlin/org/javacs/kt/CodeActionTest.kt create mode 100644 server/src/test/resources/codeactions/samefile.kt diff --git a/server/src/test/kotlin/org/javacs/kt/CodeActionTest.kt b/server/src/test/kotlin/org/javacs/kt/CodeActionTest.kt new file mode 100644 index 000000000..363c935b9 --- /dev/null +++ b/server/src/test/kotlin/org/javacs/kt/CodeActionTest.kt @@ -0,0 +1,75 @@ +package org.javacs.kt + +import org.eclipse.lsp4j.* +import org.javacs.kt.SingleFileTestFixture +import org.junit.Test +import org.junit.Assert.assertThat +import org.junit.Assert.fail +import org.hamcrest.Matchers.* + +// TODO: naming +class ImplementAbstractMembersQuickFixSameFileTest : SingleFileTestFixture("codeactions", "samefile.kt") { + + @Test + fun `should find no code actions`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 3, 1, 3, 22, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(0)) + } + + @Test + fun `should find one abstract method to implement`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 7, 1, 7, 14, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(1)) + val codeAction = codeActionResult[0].right + assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeAction.title, equalTo("Implement abstract functions")) + assertThat(codeAction.diagnostics, equalTo(listOf(diagnostics[0]))) + + val textEdit = codeAction.edit.changes + val key = workspaceRoot.resolve(file).toUri().toString() + assertThat(textEdit.containsKey(key), equalTo(true)) + assertThat(textEdit[key], hasSize(1)) + + val functionToImplementEdit = textEdit[key]?.get(0) + assertThat(functionToImplementEdit?.range, equalTo(range(7, 30, 7, 30))) + assertThat(functionToImplementEdit?.newText, equalTo("\n\n override fun test(input: String, otherInput: Int) { }")) + } + + @Test + fun `should find several abstract methods to implement`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 15, 1, 15, 21, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(1)) + val codeAction = codeActionResult[0].right + assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeAction.title, equalTo("Implement abstract functions")) + //assertThat(codeAction.diagnostics, equalTo(listOf(diagnostics[0]))) + + val textEdit = codeAction.edit.changes + val key = workspaceRoot.resolve(file).toUri().toString() + assertThat(textEdit.containsKey(key), equalTo(true)) + assertThat(textEdit[key], hasSize(2)) + + val firstFunctionToImplementEdit = textEdit[key]?.get(0) + assertThat(firstFunctionToImplementEdit?.range, equalTo(range(15, 49, 15, 49))) + assertThat(firstFunctionToImplementEdit?.newText, equalTo("\n\n override fun print() { }")) + + val secondFunctionToImplementEdit = textEdit[key]?.get(1) + assertThat(secondFunctionToImplementEdit?.range, equalTo(range(15, 49, 15, 49))) + assertThat(secondFunctionToImplementEdit?.newText, equalTo("\n\n override fun test(input: String, otherInput: Int) { }")) + } +} + + + diff --git a/server/src/test/resources/codeactions/samefile.kt b/server/src/test/resources/codeactions/samefile.kt new file mode 100644 index 000000000..946706364 --- /dev/null +++ b/server/src/test/resources/codeactions/samefile.kt @@ -0,0 +1,15 @@ +package test.kotlin.lsp + +interface MyInterface { + fun test(input: String, otherInput: Int) +} + +class MyClass : MyInterface { +} + + +abstract class CanPrint { + abstract fun print() +} + +class PrintableClass : CanPrint(), MyInterface {} From b89ec14e3b55475e2337101f74a2936dd67c5887 Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Sun, 24 Apr 2022 15:10:45 +0200 Subject: [PATCH 05/46] WIP. Import abstract method quick fix, external methods as well. External methods include Java standard library, Kotlin standard library and more. Some additional cleanup is needed. --- .../ImplementAbstractFunctionsQuickFix.kt | 84 ++++++++++++++++++- .../kotlin/org/javacs/kt/CodeActionTest.kt | 49 +++++++++++ .../implementabstract_standardlib.kt | 7 ++ 3 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 server/src/test/resources/codeactions/implementabstract_standardlib.kt diff --git a/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractFunctionsQuickFix.kt b/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractFunctionsQuickFix.kt index cb063951f..651036c1e 100644 --- a/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractFunctionsQuickFix.kt +++ b/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractFunctionsQuickFix.kt @@ -7,16 +7,24 @@ import org.javacs.kt.index.SymbolIndex import org.javacs.kt.position.offset import org.javacs.kt.position.position import org.javacs.kt.util.toPath +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.descriptors.isInterface +import org.jetbrains.kotlin.descriptors.Modality import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.KtClass import org.jetbrains.kotlin.psi.KtDeclaration import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.kotlin.psi.KtSuperTypeListEntry +import org.jetbrains.kotlin.psi.KtTypeArgumentList import org.jetbrains.kotlin.psi.psiUtil.containingClass import org.jetbrains.kotlin.psi.psiUtil.endOffset import org.jetbrains.kotlin.psi.psiUtil.isAbstract import org.jetbrains.kotlin.psi.psiUtil.startOffset import org.jetbrains.kotlin.resolve.diagnostics.Diagnostics +import org.jetbrains.kotlin.types.TypeProjection +import org.jetbrains.kotlin.types.typeUtil.asTypeProjection private const val DEFAULT_TAB_SIZE = 4 @@ -70,8 +78,13 @@ private fun getAbstractFunctionStubs(file: CompiledFile, kotlinClass: KtClass) = // For each of the super types used by this class kotlinClass.superTypeListEntries.mapNotNull { // Find the definition of this super type - val descriptor = file.referenceAtPoint(it.startOffset)?.second + val referenceAtPoint = file.referenceExpressionAtPoint(it.startOffset) + val descriptor = referenceAtPoint?.second val superClass = descriptor?.findPsi() + + val classDescriptor = descriptor as? ClassDescriptor + val superClassTypeArguments = getSuperClassTypeArguments(file, it) + // If the super class is abstract or an interface if (superClass is KtClass && (superClass.isAbstract() || superClass.isInterface())) { // Get the abstract functions of this super type that are currently not implemented by this class @@ -80,11 +93,27 @@ private fun getAbstractFunctionStubs(file: CompiledFile, kotlinClass: KtClass) = } // Get stubs for each function abstractFunctions.map { function -> getFunctionStub(function as KtNamedFunction) } + } else if(null != classDescriptor && (classDescriptor.kind.isInterface || classDescriptor.modality == Modality.ABSTRACT)) { + // TODO: refactor and prettify + classDescriptor.getMemberScope(superClassTypeArguments).getContributedDescriptors().filter { classMember -> + classMember is FunctionDescriptor && classMember.modality == Modality.ABSTRACT && !overridesDeclaration(kotlinClass, classMember) + }.map { function -> + createFunctionStub(function as FunctionDescriptor) + } } else { null } }.flatten() +// TODO: better name? +private fun getSuperClassTypeArguments(file: CompiledFile, superType: KtSuperTypeListEntry): List = superType.typeReference?.typeElement?.children?.filter { + it is KtTypeArgumentList +}?.flatMap { + (it as KtTypeArgumentList).arguments +}?.mapNotNull { + (file.referenceExpressionAtPoint(it?.startOffset ?: 0)?.second as? ClassDescriptor)?.defaultType?.asTypeProjection() +} ?: emptyList() + private fun isAbstractFunction(declaration: KtDeclaration): Boolean = declaration is KtNamedFunction && !declaration.hasBody() && (declaration.containingClass()?.isInterface() ?: false || declaration.hasModifier(KtTokens.ABSTRACT_KEYWORD)) @@ -103,6 +132,19 @@ private fun overridesDeclaration(kotlinClass: KtClass, declaration: KtDeclaratio } } +private fun overridesDeclaration(kotlinClass: KtClass, descriptor: FunctionDescriptor): Boolean = + kotlinClass.declarations.any { + if (it.name == descriptor.name.asString() && it.hasModifier(KtTokens.OVERRIDE_KEYWORD)) { + if (it is KtNamedFunction) { + parametersMatch(it, descriptor) + } else { + true + } + } else { + false + } + } + // Checks if two functions have matching parameters private fun parametersMatch(function: KtNamedFunction, functionDeclaration: KtNamedFunction): Boolean { if (function.valueParameters.size == functionDeclaration.valueParameters.size) { @@ -128,9 +170,49 @@ private fun parametersMatch(function: KtNamedFunction, functionDeclaration: KtNa return false } +private fun parametersMatch(function: KtNamedFunction, functionDescriptor: FunctionDescriptor): Boolean { + if (function.valueParameters.size == functionDescriptor.valueParameters.size) { + for (index in 0 until function.valueParameters.size) { + if (function.valueParameters[index].name != functionDescriptor.valueParameters[index].name.asString()) { + return false + } else if (function.valueParameters[index].typeReference?.name != functionDescriptor.valueParameters[index].type.toString()) { + // TODO: here we have exactly the old issue of type as above + return false + } + } + + if (function.typeParameters.size == functionDescriptor.typeParameters.size) { + for (index in 0 until function.typeParameters.size) { + if (function.typeParameters[index].variance != functionDescriptor.typeParameters[index].variance) { + return false + } + } + } + + return true + } + + return false +} + private fun getFunctionStub(function: KtNamedFunction): String = "override fun" + function.text.substringAfter("fun") + " { }" +private fun createFunctionStub(function: FunctionDescriptor): String { + // TODO: clean + val name = function.name + val arguments = function.valueParameters.map { argument -> + val argumentName = argument.name + // TODO: how to check if we actually should unwrap and make non-nullable? + val argumentType = argument.type.unwrap().makeNullableAsSpecified(false) + + "$argumentName: $argumentType" + }.joinToString(", ") + val returnType = function.returnType?.toString() + + return "override fun $name($arguments)${returnType?.takeIf { "Unit" != it }?.let { ": $it" } ?: ""} { }" +} + private fun getDeclarationPadding(file: CompiledFile, kotlinClass: KtClass): String { // If the class is not empty, the amount of padding is the same as the one in the last declaration of the class val paddingSize = if (kotlinClass.declarations.isNotEmpty()) { diff --git a/server/src/test/kotlin/org/javacs/kt/CodeActionTest.kt b/server/src/test/kotlin/org/javacs/kt/CodeActionTest.kt index 363c935b9..67d2e1902 100644 --- a/server/src/test/kotlin/org/javacs/kt/CodeActionTest.kt +++ b/server/src/test/kotlin/org/javacs/kt/CodeActionTest.kt @@ -72,4 +72,53 @@ class ImplementAbstractMembersQuickFixSameFileTest : SingleFileTestFixture("code } +// TODO: naming +class ImplementAbstractMembersQuickFixExternalLibraryTest : SingleFileTestFixture("codeactions", "implementabstract_standardlib.kt") { + @Test + fun `should find one abstract method from Runnable to implement`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 5, 1, 5, 15, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(1)) + val codeAction = codeActionResult[0].right + assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeAction.title, equalTo("Implement abstract functions")) + //assertThat(codeAction.diagnostics, equalTo(listOf(diagnostics[0]))) + + val textEdit = codeAction.edit.changes + val key = workspaceRoot.resolve(file).toUri().toString() + assertThat(textEdit.containsKey(key), equalTo(true)) + assertThat(textEdit[key], hasSize(1)) + + val functionToImplementEdit = textEdit[key]?.get(0) + assertThat(functionToImplementEdit?.range, equalTo(range(5, 28, 5, 28))) + assertThat(functionToImplementEdit?.newText, equalTo("\n\n override fun run() { }")) + } + + @Test + fun `should find one abstract method from Comparable to implement`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 7, 1, 7, 19, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(1)) + val codeAction = codeActionResult[0].right + assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeAction.title, equalTo("Implement abstract functions")) + //assertThat(codeAction.diagnostics, equalTo(listOf(diagnostics[0]))) + + val textEdit = codeAction.edit.changes + val key = workspaceRoot.resolve(file).toUri().toString() + assertThat(textEdit.containsKey(key), equalTo(true)) + assertThat(textEdit[key], hasSize(1)) + + val functionToImplementEdit = textEdit[key]?.get(0) + assertThat(functionToImplementEdit?.range, equalTo(range(7, 42, 7, 42))) + assertThat(functionToImplementEdit?.newText, equalTo("\n\n override fun compare(p0: String, p1: String): Int { }")) + } +} +// TODO: should we have tests for one method already being in place? and then trying to implement the rest? diff --git a/server/src/test/resources/codeactions/implementabstract_standardlib.kt b/server/src/test/resources/codeactions/implementabstract_standardlib.kt new file mode 100644 index 000000000..665eab2d2 --- /dev/null +++ b/server/src/test/resources/codeactions/implementabstract_standardlib.kt @@ -0,0 +1,7 @@ +package test.kotlin.lsp + +import java.util.Comparator + +class MyThread : Runnable {} + +class MyComperable : Comparator {} From f94a586c928a2d71e28654a2eea7aaa66d4ebbca Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Sun, 24 Apr 2022 17:14:00 +0200 Subject: [PATCH 06/46] Unified the old implementation and the new. Better nullability handling --- .../ImplementAbstractFunctionsQuickFix.kt | 96 +++++++------------ .../kotlin/org/javacs/kt/CodeActionTest.kt | 53 ++++++++-- ...efile.kt => implementabstract_samefile.kt} | 10 ++ 3 files changed, 91 insertions(+), 68 deletions(-) rename server/src/test/resources/codeactions/{samefile.kt => implementabstract_samefile.kt} (50%) diff --git a/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractFunctionsQuickFix.kt b/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractFunctionsQuickFix.kt index 651036c1e..40b452b72 100644 --- a/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractFunctionsQuickFix.kt +++ b/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractFunctionsQuickFix.kt @@ -6,8 +6,11 @@ import org.javacs.kt.CompiledFile import org.javacs.kt.index.SymbolIndex import org.javacs.kt.position.offset import org.javacs.kt.position.position +import org.javacs.kt.LOG import org.javacs.kt.util.toPath import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.ClassConstructorDescriptor +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor import org.jetbrains.kotlin.descriptors.FunctionDescriptor import org.jetbrains.kotlin.descriptors.isInterface import org.jetbrains.kotlin.descriptors.Modality @@ -16,8 +19,10 @@ import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.KtClass import org.jetbrains.kotlin.psi.KtDeclaration import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.kotlin.psi.KtSimpleNameExpression import org.jetbrains.kotlin.psi.KtSuperTypeListEntry import org.jetbrains.kotlin.psi.KtTypeArgumentList +import org.jetbrains.kotlin.psi.KtTypeReference import org.jetbrains.kotlin.psi.psiUtil.containingClass import org.jetbrains.kotlin.psi.psiUtil.endOffset import org.jetbrains.kotlin.psi.psiUtil.isAbstract @@ -36,6 +41,8 @@ class ImplementAbstractFunctionsQuickFix : QuickFix { val endCursor = offset(file.content, range.end) val kotlinDiagnostics = file.compile.diagnostics + //LOG.info("start: {}, end: {}, range {}, diagnostics {}", startCursor, endCursor, range, diagnostics) + // If the client side and the server side diagnostics contain a valid diagnostic for this range. if (diagnostic != null && anyDiagnosticMatch(kotlinDiagnostics, startCursor, endCursor)) { // Get the class with the missing functions @@ -80,24 +87,15 @@ private fun getAbstractFunctionStubs(file: CompiledFile, kotlinClass: KtClass) = // Find the definition of this super type val referenceAtPoint = file.referenceExpressionAtPoint(it.startOffset) val descriptor = referenceAtPoint?.second - val superClass = descriptor?.findPsi() - val classDescriptor = descriptor as? ClassDescriptor - val superClassTypeArguments = getSuperClassTypeArguments(file, it) + val classDescriptor = getClassDescriptor(descriptor) + val superClassTypeArguments = getSuperClassTypeArguments(file, it) // If the super class is abstract or an interface - if (superClass is KtClass && (superClass.isAbstract() || superClass.isInterface())) { - // Get the abstract functions of this super type that are currently not implemented by this class - val abstractFunctions = superClass.declarations.filter { - declaration -> isAbstractFunction(declaration) && !overridesDeclaration(kotlinClass, declaration) - } - // Get stubs for each function - abstractFunctions.map { function -> getFunctionStub(function as KtNamedFunction) } - } else if(null != classDescriptor && (classDescriptor.kind.isInterface || classDescriptor.modality == Modality.ABSTRACT)) { - // TODO: refactor and prettify + if(null != classDescriptor && (classDescriptor.kind.isInterface || classDescriptor.modality == Modality.ABSTRACT)) { classDescriptor.getMemberScope(superClassTypeArguments).getContributedDescriptors().filter { classMember -> - classMember is FunctionDescriptor && classMember.modality == Modality.ABSTRACT && !overridesDeclaration(kotlinClass, classMember) - }.map { function -> + classMember is FunctionDescriptor && classMember.modality == Modality.ABSTRACT && !overridesDeclaration(kotlinClass, classMember) + }.map { function -> createFunctionStub(function as FunctionDescriptor) } } else { @@ -105,6 +103,16 @@ private fun getAbstractFunctionStubs(file: CompiledFile, kotlinClass: KtClass) = } }.flatten() +// TODO: better name? +// interfaces are ClassDescriptors by default. When calling AbstractClass super methods, we get a ClassConstructorDescriptor +private fun getClassDescriptor(descriptor: DeclarationDescriptor?): ClassDescriptor? = if(descriptor is ClassDescriptor) { + descriptor +} else if(descriptor is ClassConstructorDescriptor) { + descriptor.containingDeclaration +} else { + null +} + // TODO: better name? private fun getSuperClassTypeArguments(file: CompiledFile, superType: KtSuperTypeListEntry): List = superType.typeReference?.typeElement?.children?.filter { it is KtTypeArgumentList @@ -113,25 +121,8 @@ private fun getSuperClassTypeArguments(file: CompiledFile, superType: KtSuperTyp }?.mapNotNull { (file.referenceExpressionAtPoint(it?.startOffset ?: 0)?.second as? ClassDescriptor)?.defaultType?.asTypeProjection() } ?: emptyList() - -private fun isAbstractFunction(declaration: KtDeclaration): Boolean = - declaration is KtNamedFunction && !declaration.hasBody() - && (declaration.containingClass()?.isInterface() ?: false || declaration.hasModifier(KtTokens.ABSTRACT_KEYWORD)) // Checks if the class overrides the given declaration -private fun overridesDeclaration(kotlinClass: KtClass, declaration: KtDeclaration): Boolean = - kotlinClass.declarations.any { - if (it.name == declaration.name && it.hasModifier(KtTokens.OVERRIDE_KEYWORD)) { - if (it is KtNamedFunction && declaration is KtNamedFunction) { - parametersMatch(it, declaration) - } else { - true - } - } else { - false - } - } - private fun overridesDeclaration(kotlinClass: KtClass, descriptor: FunctionDescriptor): Boolean = kotlinClass.declarations.any { if (it.name == descriptor.name.asString() && it.hasModifier(KtTokens.OVERRIDE_KEYWORD)) { @@ -146,37 +137,13 @@ private fun overridesDeclaration(kotlinClass: KtClass, descriptor: FunctionDescr } // Checks if two functions have matching parameters -private fun parametersMatch(function: KtNamedFunction, functionDeclaration: KtNamedFunction): Boolean { - if (function.valueParameters.size == functionDeclaration.valueParameters.size) { - for (index in 0 until function.valueParameters.size) { - if (function.valueParameters[index].name != functionDeclaration.valueParameters[index].name) { - return false - } else if (function.valueParameters[index].typeReference?.name != functionDeclaration.valueParameters[index].typeReference?.name) { - return false - } - } - - if (function.typeParameters.size == functionDeclaration.typeParameters.size) { - for (index in 0 until function.typeParameters.size) { - if (function.typeParameters[index].variance != functionDeclaration.typeParameters[index].variance) { - return false - } - } - } - - return true - } - - return false -} - private fun parametersMatch(function: KtNamedFunction, functionDescriptor: FunctionDescriptor): Boolean { if (function.valueParameters.size == functionDescriptor.valueParameters.size) { for (index in 0 until function.valueParameters.size) { if (function.valueParameters[index].name != functionDescriptor.valueParameters[index].name.asString()) { return false - } else if (function.valueParameters[index].typeReference?.name != functionDescriptor.valueParameters[index].type.toString()) { - // TODO: here we have exactly the old issue of type as above + } else if (getTypeNameFromTypeReference(function.valueParameters[index].typeReference) != functionDescriptor.valueParameters[index].type.toString()) { + // TODO: here we have exactly the old issue of type as above. Maybe a common method that can be called to get the correct one? or do we maybe have to check both null and not null types here? return false } } @@ -195,20 +162,25 @@ private fun parametersMatch(function: KtNamedFunction, functionDescriptor: Funct return false } -private fun getFunctionStub(function: KtNamedFunction): String = - "override fun" + function.text.substringAfter("fun") + " { }" +// typeReference.name is often null. This fetches the name directly from the KtSimpleNameExpression instead +private fun getTypeNameFromTypeReference(typeReference: KtTypeReference?): String? = typeReference?.typeElement?.children?.filter { + it is KtSimpleNameExpression +}?.map { + (it as KtSimpleNameExpression).getReferencedName() +}?.get(0) private fun createFunctionStub(function: FunctionDescriptor): String { // TODO: clean val name = function.name val arguments = function.valueParameters.map { argument -> val argumentName = argument.name - // TODO: how to check if we actually should unwrap and make non-nullable? - val argumentType = argument.type.unwrap().makeNullableAsSpecified(false) + // about argument.type: regular Kotlin types are marked T or T?, but types from Java are (T..T?) because nullability cannot be decided. + // Therefore we have to unpack in case we have the Java type. Fortunately, the Java types are not marked nullable, so we default to non nullable types. Let the user decide if they want nullable types instead. With this implementation Kotlin types also keeps their nullability + val argumentType = argument.type.unwrap().makeNullableAsSpecified(argument.type.isMarkedNullable) "$argumentName: $argumentType" }.joinToString(", ") - val returnType = function.returnType?.toString() + val returnType = function.returnType?.unwrap()?.makeNullableAsSpecified(function.returnType?.isMarkedNullable ?: false)?.toString() return "override fun $name($arguments)${returnType?.takeIf { "Unit" != it }?.let { ": $it" } ?: ""} { }" } diff --git a/server/src/test/kotlin/org/javacs/kt/CodeActionTest.kt b/server/src/test/kotlin/org/javacs/kt/CodeActionTest.kt index 67d2e1902..d591ac46a 100644 --- a/server/src/test/kotlin/org/javacs/kt/CodeActionTest.kt +++ b/server/src/test/kotlin/org/javacs/kt/CodeActionTest.kt @@ -7,8 +7,7 @@ import org.junit.Assert.assertThat import org.junit.Assert.fail import org.hamcrest.Matchers.* -// TODO: naming -class ImplementAbstractMembersQuickFixSameFileTest : SingleFileTestFixture("codeactions", "samefile.kt") { +class ImplementAbstractMembersQuickFixSameFileTest : SingleFileTestFixture("codeactions", "implementabstract_samefile.kt") { @Test fun `should find no code actions`() { @@ -69,10 +68,54 @@ class ImplementAbstractMembersQuickFixSameFileTest : SingleFileTestFixture("code assertThat(secondFunctionToImplementEdit?.range, equalTo(range(15, 49, 15, 49))) assertThat(secondFunctionToImplementEdit?.newText, equalTo("\n\n override fun test(input: String, otherInput: Int) { }")) } -} + @Test + fun `should find only one abstract method when the other one is already implemented`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 17, 1, 17, 26, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(1)) + val codeAction = codeActionResult[0].right + assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeAction.title, equalTo("Implement abstract functions")) + //assertThat(codeAction.diagnostics, equalTo(listOf(diagnostics[0]))) + + val textEdit = codeAction.edit.changes + val key = workspaceRoot.resolve(file).toUri().toString() + assertThat(textEdit.containsKey(key), equalTo(true)) + assertThat(textEdit[key], hasSize(1)) + + val functionToImplementEdit = textEdit[key]?.get(0) + assertThat(functionToImplementEdit?.range, equalTo(range(18, 57, 18, 57))) + assertThat(functionToImplementEdit?.newText, equalTo("\n\n override fun print() { }")) + } + + @Test + fun `should respect nullability of parameter and return value in abstract method`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 25, 1, 25, 16, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(1)) + val codeAction = codeActionResult[0].right + assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeAction.title, equalTo("Implement abstract functions")) + //assertThat(codeAction.diagnostics, equalTo(listOf(diagnostics[0]))) + + val textEdit = codeAction.edit.changes + val key = workspaceRoot.resolve(file).toUri().toString() + assertThat(textEdit.containsKey(key), equalTo(true)) + assertThat(textEdit[key], hasSize(1)) + + val functionToImplementEdit = textEdit[key]?.get(0) + assertThat(functionToImplementEdit?.range, equalTo(range(25, 48, 25, 48))) + assertThat(functionToImplementEdit?.newText, equalTo("\n\n override fun myMethod(myStr: String?): String? { }")) + } +} -// TODO: naming class ImplementAbstractMembersQuickFixExternalLibraryTest : SingleFileTestFixture("codeactions", "implementabstract_standardlib.kt") { @Test fun `should find one abstract method from Runnable to implement`() { @@ -120,5 +163,3 @@ class ImplementAbstractMembersQuickFixExternalLibraryTest : SingleFileTestFixtur assertThat(functionToImplementEdit?.newText, equalTo("\n\n override fun compare(p0: String, p1: String): Int { }")) } } - -// TODO: should we have tests for one method already being in place? and then trying to implement the rest? diff --git a/server/src/test/resources/codeactions/samefile.kt b/server/src/test/resources/codeactions/implementabstract_samefile.kt similarity index 50% rename from server/src/test/resources/codeactions/samefile.kt rename to server/src/test/resources/codeactions/implementabstract_samefile.kt index 946706364..b4d2eb3d5 100644 --- a/server/src/test/resources/codeactions/samefile.kt +++ b/server/src/test/resources/codeactions/implementabstract_samefile.kt @@ -13,3 +13,13 @@ abstract class CanPrint { } class PrintableClass : CanPrint(), MyInterface {} + +class OtherPrintableClass : CanPrint(), MyInterface { + override fun test(input: String, otherInput: Int) {} +} + +interface NullMethodAndReturn { + fun myMethod(myStr: T?): T? +} + +class NullClass : NullMethodAndReturn {} From 0e5ab967ca8681ab972653377da5db477c8b1abd Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Sun, 24 Apr 2022 20:45:57 +0200 Subject: [PATCH 07/46] Refactoring and minor cleanup. --- .../ImplementAbstractFunctionsQuickFix.kt | 41 +++++++++---------- .../kotlin/org/javacs/kt/CodeActionTest.kt | 9 +--- .../codeactions/implementabstract_samefile.kt | 2 +- 3 files changed, 22 insertions(+), 30 deletions(-) diff --git a/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractFunctionsQuickFix.kt b/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractFunctionsQuickFix.kt index 40b452b72..12910c87d 100644 --- a/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractFunctionsQuickFix.kt +++ b/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractFunctionsQuickFix.kt @@ -6,7 +6,6 @@ import org.javacs.kt.CompiledFile import org.javacs.kt.index.SymbolIndex import org.javacs.kt.position.offset import org.javacs.kt.position.position -import org.javacs.kt.LOG import org.javacs.kt.util.toPath import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.ClassConstructorDescriptor @@ -28,6 +27,7 @@ import org.jetbrains.kotlin.psi.psiUtil.endOffset import org.jetbrains.kotlin.psi.psiUtil.isAbstract import org.jetbrains.kotlin.psi.psiUtil.startOffset import org.jetbrains.kotlin.resolve.diagnostics.Diagnostics +import org.jetbrains.kotlin.types.KotlinType import org.jetbrains.kotlin.types.TypeProjection import org.jetbrains.kotlin.types.typeUtil.asTypeProjection @@ -40,8 +40,6 @@ class ImplementAbstractFunctionsQuickFix : QuickFix { val startCursor = offset(file.content, range.start) val endCursor = offset(file.content, range.end) val kotlinDiagnostics = file.compile.diagnostics - - //LOG.info("start: {}, end: {}, range {}, diagnostics {}", startCursor, endCursor, range, diagnostics) // If the client side and the server side diagnostics contain a valid diagnostic for this range. if (diagnostic != null && anyDiagnosticMatch(kotlinDiagnostics, startCursor, endCursor)) { @@ -89,10 +87,10 @@ private fun getAbstractFunctionStubs(file: CompiledFile, kotlinClass: KtClass) = val descriptor = referenceAtPoint?.second val classDescriptor = getClassDescriptor(descriptor) - val superClassTypeArguments = getSuperClassTypeArguments(file, it) // If the super class is abstract or an interface - if(null != classDescriptor && (classDescriptor.kind.isInterface || classDescriptor.modality == Modality.ABSTRACT)) { + if (null != classDescriptor && (classDescriptor.kind.isInterface || classDescriptor.modality == Modality.ABSTRACT)) { + val superClassTypeArguments = getSuperClassTypeProjections(file, it) classDescriptor.getMemberScope(superClassTypeArguments).getContributedDescriptors().filter { classMember -> classMember is FunctionDescriptor && classMember.modality == Modality.ABSTRACT && !overridesDeclaration(kotlinClass, classMember) }.map { function -> @@ -103,18 +101,16 @@ private fun getAbstractFunctionStubs(file: CompiledFile, kotlinClass: KtClass) = } }.flatten() -// TODO: better name? // interfaces are ClassDescriptors by default. When calling AbstractClass super methods, we get a ClassConstructorDescriptor -private fun getClassDescriptor(descriptor: DeclarationDescriptor?): ClassDescriptor? = if(descriptor is ClassDescriptor) { +private fun getClassDescriptor(descriptor: DeclarationDescriptor?): ClassDescriptor? = if (descriptor is ClassDescriptor) { descriptor -} else if(descriptor is ClassConstructorDescriptor) { +} else if (descriptor is ClassConstructorDescriptor) { descriptor.containingDeclaration } else { null } - -// TODO: better name? -private fun getSuperClassTypeArguments(file: CompiledFile, superType: KtSuperTypeListEntry): List = superType.typeReference?.typeElement?.children?.filter { + +private fun getSuperClassTypeProjections(file: CompiledFile, superType: KtSuperTypeListEntry): List = superType.typeReference?.typeElement?.children?.filter { it is KtTypeArgumentList }?.flatMap { (it as KtTypeArgumentList).arguments @@ -142,8 +138,9 @@ private fun parametersMatch(function: KtNamedFunction, functionDescriptor: Funct for (index in 0 until function.valueParameters.size) { if (function.valueParameters[index].name != functionDescriptor.valueParameters[index].name.asString()) { return false - } else if (getTypeNameFromTypeReference(function.valueParameters[index].typeReference) != functionDescriptor.valueParameters[index].type.toString()) { - // TODO: here we have exactly the old issue of type as above. Maybe a common method that can be called to get the correct one? or do we maybe have to check both null and not null types here? + } else if (function.valueParameters[index].typeReference?.typeName() != functionDescriptor.valueParameters[index].type.unwrappedType().toString()) { + // Note: Since we treat Java overrides as non nullable by default, the above test will fail when the user has made the type nullable. + // TODO: look into this return false } } @@ -162,29 +159,29 @@ private fun parametersMatch(function: KtNamedFunction, functionDescriptor: Funct return false } -// typeReference.name is often null. This fetches the name directly from the KtSimpleNameExpression instead -private fun getTypeNameFromTypeReference(typeReference: KtTypeReference?): String? = typeReference?.typeElement?.children?.filter { +private fun KtTypeReference.typeName(): String? = this.name ?: this.typeElement?.children?.filter { it is KtSimpleNameExpression }?.map { (it as KtSimpleNameExpression).getReferencedName() -}?.get(0) +}?.firstOrNull() private fun createFunctionStub(function: FunctionDescriptor): String { - // TODO: clean val name = function.name val arguments = function.valueParameters.map { argument -> val argumentName = argument.name - // about argument.type: regular Kotlin types are marked T or T?, but types from Java are (T..T?) because nullability cannot be decided. - // Therefore we have to unpack in case we have the Java type. Fortunately, the Java types are not marked nullable, so we default to non nullable types. Let the user decide if they want nullable types instead. With this implementation Kotlin types also keeps their nullability - val argumentType = argument.type.unwrap().makeNullableAsSpecified(argument.type.isMarkedNullable) + val argumentType = argument.type.unwrappedType() "$argumentName: $argumentType" }.joinToString(", ") - val returnType = function.returnType?.unwrap()?.makeNullableAsSpecified(function.returnType?.isMarkedNullable ?: false)?.toString() + val returnType = function.returnType?.unwrappedType()?.toString()?.takeIf { "Unit" != it } - return "override fun $name($arguments)${returnType?.takeIf { "Unit" != it }?.let { ": $it" } ?: ""} { }" + return "override fun $name($arguments)${returnType?.let { ": $it" } ?: ""} { }" } +// about types: regular Kotlin types are marked T or T?, but types from Java are (T..T?) because nullability cannot be decided. +// Therefore we have to unpack in case we have the Java type. Fortunately, the Java types are not marked nullable, so we default to non nullable types. Let the user decide if they want nullable types instead. With this implementation Kotlin types also keeps their nullability +private fun KotlinType.unwrappedType(): KotlinType = this.unwrap().makeNullableAsSpecified(this.isMarkedNullable) + private fun getDeclarationPadding(file: CompiledFile, kotlinClass: KtClass): String { // If the class is not empty, the amount of padding is the same as the one in the last declaration of the class val paddingSize = if (kotlinClass.declarations.isNotEmpty()) { diff --git a/server/src/test/kotlin/org/javacs/kt/CodeActionTest.kt b/server/src/test/kotlin/org/javacs/kt/CodeActionTest.kt index d591ac46a..49c92b1a5 100644 --- a/server/src/test/kotlin/org/javacs/kt/CodeActionTest.kt +++ b/server/src/test/kotlin/org/javacs/kt/CodeActionTest.kt @@ -4,8 +4,8 @@ import org.eclipse.lsp4j.* import org.javacs.kt.SingleFileTestFixture import org.junit.Test import org.junit.Assert.assertThat -import org.junit.Assert.fail -import org.hamcrest.Matchers.* +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.hasSize class ImplementAbstractMembersQuickFixSameFileTest : SingleFileTestFixture("codeactions", "implementabstract_samefile.kt") { @@ -53,7 +53,6 @@ class ImplementAbstractMembersQuickFixSameFileTest : SingleFileTestFixture("code val codeAction = codeActionResult[0].right assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) assertThat(codeAction.title, equalTo("Implement abstract functions")) - //assertThat(codeAction.diagnostics, equalTo(listOf(diagnostics[0]))) val textEdit = codeAction.edit.changes val key = workspaceRoot.resolve(file).toUri().toString() @@ -80,7 +79,6 @@ class ImplementAbstractMembersQuickFixSameFileTest : SingleFileTestFixture("code val codeAction = codeActionResult[0].right assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) assertThat(codeAction.title, equalTo("Implement abstract functions")) - //assertThat(codeAction.diagnostics, equalTo(listOf(diagnostics[0]))) val textEdit = codeAction.edit.changes val key = workspaceRoot.resolve(file).toUri().toString() @@ -103,7 +101,6 @@ class ImplementAbstractMembersQuickFixSameFileTest : SingleFileTestFixture("code val codeAction = codeActionResult[0].right assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) assertThat(codeAction.title, equalTo("Implement abstract functions")) - //assertThat(codeAction.diagnostics, equalTo(listOf(diagnostics[0]))) val textEdit = codeAction.edit.changes val key = workspaceRoot.resolve(file).toUri().toString() @@ -128,7 +125,6 @@ class ImplementAbstractMembersQuickFixExternalLibraryTest : SingleFileTestFixtur val codeAction = codeActionResult[0].right assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) assertThat(codeAction.title, equalTo("Implement abstract functions")) - //assertThat(codeAction.diagnostics, equalTo(listOf(diagnostics[0]))) val textEdit = codeAction.edit.changes val key = workspaceRoot.resolve(file).toUri().toString() @@ -151,7 +147,6 @@ class ImplementAbstractMembersQuickFixExternalLibraryTest : SingleFileTestFixtur val codeAction = codeActionResult[0].right assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) assertThat(codeAction.title, equalTo("Implement abstract functions")) - //assertThat(codeAction.diagnostics, equalTo(listOf(diagnostics[0]))) val textEdit = codeAction.edit.changes val key = workspaceRoot.resolve(file).toUri().toString() diff --git a/server/src/test/resources/codeactions/implementabstract_samefile.kt b/server/src/test/resources/codeactions/implementabstract_samefile.kt index b4d2eb3d5..6e322092d 100644 --- a/server/src/test/resources/codeactions/implementabstract_samefile.kt +++ b/server/src/test/resources/codeactions/implementabstract_samefile.kt @@ -19,7 +19,7 @@ class OtherPrintableClass : CanPrint(), MyInterface { } interface NullMethodAndReturn { - fun myMethod(myStr: T?): T? + fun myMethod(myStr: T?): T? } class NullClass : NullMethodAndReturn {} From d9d7b6a36b971a2096b530687fd47499d6e359bd Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Mon, 25 Apr 2022 17:19:25 +0200 Subject: [PATCH 08/46] Moved main resolve logic to protocol extensions instead of commands --- .../org/javacs/kt/KotlinLanguageServer.kt | 2 +- .../kt/KotlinProtocolExtensionService.kt | 23 ++++++++++++- .../org/javacs/kt/KotlinProtocolExtensions.kt | 3 ++ .../org/javacs/kt/KotlinWorkspaceService.kt | 26 +------------- .../kotlin/org/javacs/kt/command/Commands.kt | 2 -- ...eMainCommandTest.kt => ResolveMainTest.kt} | 34 +++++++++---------- 6 files changed, 44 insertions(+), 46 deletions(-) rename server/src/test/kotlin/org/javacs/kt/{ResolveMainCommandTest.kt => ResolveMainTest.kt} (59%) diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt b/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt index 7f2017359..b3a417bd7 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt @@ -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 diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensionService.kt b/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensionService.kt index cba9d5d6d..4fa37acf7 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensionService.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensionService.kt @@ -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() @@ -18,4 +21,22 @@ class KotlinProtocolExtensionService( override fun buildOutputLocation(): CompletableFuture = async.compute { cp.outputDirectory.absolutePath } + + override fun mainClass(textDocument: TextDocumentIdentifier): CompletableFuture> = 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 + ) + } } diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensions.kt b/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensions.kt index b6338eaf5..808ba0dce 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensions.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensions.kt @@ -12,4 +12,7 @@ interface KotlinProtocolExtensions { @JsonRequest fun buildOutputLocation(): CompletableFuture + + @JsonRequest + fun mainClass(textDocument: TextDocumentIdentifier): CompletableFuture> } diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt b/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt index 704a4f3c5..470853cc4 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt @@ -8,13 +8,11 @@ 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 @@ -32,9 +30,7 @@ class KotlinWorkspaceService( ) : WorkspaceService, LanguageClientAware { private val gson = Gson() private var languageClient: LanguageClient? = null - - private val async = AsyncExecutor() - + override fun connect(client: LanguageClient): Unit { languageClient = client } @@ -58,26 +54,6 @@ 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) diff --git a/server/src/main/kotlin/org/javacs/kt/command/Commands.kt b/server/src/main/kotlin/org/javacs/kt/command/Commands.kt index a0fcf3760..6d7feb7e0 100644 --- a/server/src/main/kotlin/org/javacs/kt/command/Commands.kt +++ b/server/src/main/kotlin/org/javacs/kt/command/Commands.kt @@ -1,9 +1,7 @@ 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, - RESOLVE_MAIN ) diff --git a/server/src/test/kotlin/org/javacs/kt/ResolveMainCommandTest.kt b/server/src/test/kotlin/org/javacs/kt/ResolveMainTest.kt similarity index 59% rename from server/src/test/kotlin/org/javacs/kt/ResolveMainCommandTest.kt rename to server/src/test/kotlin/org/javacs/kt/ResolveMainTest.kt index e9997843d..65ca7d5fc 100644 --- a/server/src/test/kotlin/org/javacs/kt/ResolveMainCommandTest.kt +++ b/server/src/test/kotlin/org/javacs/kt/ResolveMainTest.kt @@ -4,22 +4,22 @@ 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 -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 fileUri = root.resolve(file).toUri().toString() - val commandResult = languageServer.workspaceService.executeCommand(executeCommandParams).get() + val result = languageServer.getProtocolExtensionService().mainClass(TextDocumentIdentifier(fileUri)).get() - assertNotNull(commandResult) - val mainInfo = commandResult as Map + assertNotNull(result) + val mainInfo = result as Map assertNull(mainInfo["mainClass"]) assertEquals(root.toString(), mainInfo["projectRoot"]) } @@ -30,12 +30,12 @@ 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 fileUri = root.resolve(file).toUri().toString() - val commandResult = languageServer.workspaceService.executeCommand(executeCommandParams).get() + val result = languageServer.getProtocolExtensionService().mainClass(TextDocumentIdentifier(fileUri)).get() - assertNotNull(commandResult) - val mainInfo = commandResult as Map + assertNotNull(result) + val mainInfo = result as Map assertEquals("test.SimpleKt", mainInfo["mainClass"]) assertEquals(Range(Position(2, 0), Position(4, 1)), mainInfo["range"]) assertEquals(root.toString(), mainInfo["projectRoot"]) @@ -47,12 +47,12 @@ class JvmNameAnnotationMainResolve : SingleFileTestFixture("resolvemain", "JvmNa @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 fileUri = root.resolve(file).toUri().toString() - val commandResult = languageServer.workspaceService.executeCommand(executeCommandParams).get() + val result = languageServer.getProtocolExtensionService().mainClass(TextDocumentIdentifier(fileUri)).get() - assertNotNull(commandResult) - val mainInfo = commandResult as Map + assertNotNull(result) + val mainInfo = result as Map assertEquals("com.mypackage.name.Potato", mainInfo["mainClass"]) assertEquals(Range(Position(5, 0), Position(7, 1)), mainInfo["range"]) assertEquals(root.toString(), mainInfo["projectRoot"]) @@ -63,12 +63,12 @@ class CompanionObjectMainResolve : SingleFileTestFixture("resolvemain", "Compani @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 fileUri = root.resolve(file).toUri().toString() - val commandResult = languageServer.workspaceService.executeCommand(executeCommandParams).get() + val result = languageServer.getProtocolExtensionService().mainClass(TextDocumentIdentifier(fileUri)).get() - assertNotNull(commandResult) - val mainInfo = commandResult as Map + assertNotNull(result) + val mainInfo = result as Map assertEquals("test.my.companion.SweetPotato", mainInfo["mainClass"]) assertEquals(Range(Position(8, 8), Position(11, 9)), mainInfo["range"]) assertEquals(root.toString(), mainInfo["projectRoot"]) From ce9311f84873b35b043e1925c79f236fd10f6d75 Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Mon, 25 Apr 2022 20:48:47 +0200 Subject: [PATCH 09/46] Unified test files of quickfix tests --- .../kotlin/org/javacs/kt/CodeActionTest.kt | 160 ------------- .../kotlin/org/javacs/kt/QuickFixesTest.kt | 224 +++++++++++++++--- .../samefile.kt} | 0 .../standardlib.kt} | 0 4 files changed, 188 insertions(+), 196 deletions(-) delete mode 100644 server/src/test/kotlin/org/javacs/kt/CodeActionTest.kt rename server/src/test/resources/{codeactions/implementabstract_samefile.kt => quickfixes/samefile.kt} (100%) rename server/src/test/resources/{codeactions/implementabstract_standardlib.kt => quickfixes/standardlib.kt} (100%) diff --git a/server/src/test/kotlin/org/javacs/kt/CodeActionTest.kt b/server/src/test/kotlin/org/javacs/kt/CodeActionTest.kt deleted file mode 100644 index 49c92b1a5..000000000 --- a/server/src/test/kotlin/org/javacs/kt/CodeActionTest.kt +++ /dev/null @@ -1,160 +0,0 @@ -package org.javacs.kt - -import org.eclipse.lsp4j.* -import org.javacs.kt.SingleFileTestFixture -import org.junit.Test -import org.junit.Assert.assertThat -import org.hamcrest.Matchers.equalTo -import org.hamcrest.Matchers.hasSize - -class ImplementAbstractMembersQuickFixSameFileTest : SingleFileTestFixture("codeactions", "implementabstract_samefile.kt") { - - @Test - fun `should find no code actions`() { - val only = listOf(CodeActionKind.QuickFix) - val codeActionParams = codeActionParams(file, 3, 1, 3, 22, diagnostics, only) - - val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() - - assertThat(codeActionResult, hasSize(0)) - } - - @Test - fun `should find one abstract method to implement`() { - val only = listOf(CodeActionKind.QuickFix) - val codeActionParams = codeActionParams(file, 7, 1, 7, 14, diagnostics, only) - - val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() - - assertThat(codeActionResult, hasSize(1)) - val codeAction = codeActionResult[0].right - assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) - assertThat(codeAction.title, equalTo("Implement abstract functions")) - assertThat(codeAction.diagnostics, equalTo(listOf(diagnostics[0]))) - - val textEdit = codeAction.edit.changes - val key = workspaceRoot.resolve(file).toUri().toString() - assertThat(textEdit.containsKey(key), equalTo(true)) - assertThat(textEdit[key], hasSize(1)) - - val functionToImplementEdit = textEdit[key]?.get(0) - assertThat(functionToImplementEdit?.range, equalTo(range(7, 30, 7, 30))) - assertThat(functionToImplementEdit?.newText, equalTo("\n\n override fun test(input: String, otherInput: Int) { }")) - } - - @Test - fun `should find several abstract methods to implement`() { - val only = listOf(CodeActionKind.QuickFix) - val codeActionParams = codeActionParams(file, 15, 1, 15, 21, diagnostics, only) - - val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() - - assertThat(codeActionResult, hasSize(1)) - val codeAction = codeActionResult[0].right - assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) - assertThat(codeAction.title, equalTo("Implement abstract functions")) - - val textEdit = codeAction.edit.changes - val key = workspaceRoot.resolve(file).toUri().toString() - assertThat(textEdit.containsKey(key), equalTo(true)) - assertThat(textEdit[key], hasSize(2)) - - val firstFunctionToImplementEdit = textEdit[key]?.get(0) - assertThat(firstFunctionToImplementEdit?.range, equalTo(range(15, 49, 15, 49))) - assertThat(firstFunctionToImplementEdit?.newText, equalTo("\n\n override fun print() { }")) - - val secondFunctionToImplementEdit = textEdit[key]?.get(1) - assertThat(secondFunctionToImplementEdit?.range, equalTo(range(15, 49, 15, 49))) - assertThat(secondFunctionToImplementEdit?.newText, equalTo("\n\n override fun test(input: String, otherInput: Int) { }")) - } - - @Test - fun `should find only one abstract method when the other one is already implemented`() { - val only = listOf(CodeActionKind.QuickFix) - val codeActionParams = codeActionParams(file, 17, 1, 17, 26, diagnostics, only) - - val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() - - assertThat(codeActionResult, hasSize(1)) - val codeAction = codeActionResult[0].right - assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) - assertThat(codeAction.title, equalTo("Implement abstract functions")) - - val textEdit = codeAction.edit.changes - val key = workspaceRoot.resolve(file).toUri().toString() - assertThat(textEdit.containsKey(key), equalTo(true)) - assertThat(textEdit[key], hasSize(1)) - - val functionToImplementEdit = textEdit[key]?.get(0) - assertThat(functionToImplementEdit?.range, equalTo(range(18, 57, 18, 57))) - assertThat(functionToImplementEdit?.newText, equalTo("\n\n override fun print() { }")) - } - - @Test - fun `should respect nullability of parameter and return value in abstract method`() { - val only = listOf(CodeActionKind.QuickFix) - val codeActionParams = codeActionParams(file, 25, 1, 25, 16, diagnostics, only) - - val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() - - assertThat(codeActionResult, hasSize(1)) - val codeAction = codeActionResult[0].right - assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) - assertThat(codeAction.title, equalTo("Implement abstract functions")) - - val textEdit = codeAction.edit.changes - val key = workspaceRoot.resolve(file).toUri().toString() - assertThat(textEdit.containsKey(key), equalTo(true)) - assertThat(textEdit[key], hasSize(1)) - - val functionToImplementEdit = textEdit[key]?.get(0) - assertThat(functionToImplementEdit?.range, equalTo(range(25, 48, 25, 48))) - assertThat(functionToImplementEdit?.newText, equalTo("\n\n override fun myMethod(myStr: String?): String? { }")) - } -} - -class ImplementAbstractMembersQuickFixExternalLibraryTest : SingleFileTestFixture("codeactions", "implementabstract_standardlib.kt") { - @Test - fun `should find one abstract method from Runnable to implement`() { - val only = listOf(CodeActionKind.QuickFix) - val codeActionParams = codeActionParams(file, 5, 1, 5, 15, diagnostics, only) - - val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() - - assertThat(codeActionResult, hasSize(1)) - val codeAction = codeActionResult[0].right - assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) - assertThat(codeAction.title, equalTo("Implement abstract functions")) - - val textEdit = codeAction.edit.changes - val key = workspaceRoot.resolve(file).toUri().toString() - assertThat(textEdit.containsKey(key), equalTo(true)) - assertThat(textEdit[key], hasSize(1)) - - val functionToImplementEdit = textEdit[key]?.get(0) - assertThat(functionToImplementEdit?.range, equalTo(range(5, 28, 5, 28))) - assertThat(functionToImplementEdit?.newText, equalTo("\n\n override fun run() { }")) - } - - @Test - fun `should find one abstract method from Comparable to implement`() { - val only = listOf(CodeActionKind.QuickFix) - val codeActionParams = codeActionParams(file, 7, 1, 7, 19, diagnostics, only) - - val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() - - assertThat(codeActionResult, hasSize(1)) - val codeAction = codeActionResult[0].right - assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) - assertThat(codeAction.title, equalTo("Implement abstract functions")) - - val textEdit = codeAction.edit.changes - val key = workspaceRoot.resolve(file).toUri().toString() - assertThat(textEdit.containsKey(key), equalTo(true)) - assertThat(textEdit[key], hasSize(1)) - - val functionToImplementEdit = textEdit[key]?.get(0) - assertThat(functionToImplementEdit?.range, equalTo(range(7, 42, 7, 42))) - assertThat(functionToImplementEdit?.newText, equalTo("\n\n override fun compare(p0: String, p1: String): Int { }")) - } -} diff --git a/server/src/test/kotlin/org/javacs/kt/QuickFixesTest.kt b/server/src/test/kotlin/org/javacs/kt/QuickFixesTest.kt index 229a01859..4a514b89d 100644 --- a/server/src/test/kotlin/org/javacs/kt/QuickFixesTest.kt +++ b/server/src/test/kotlin/org/javacs/kt/QuickFixesTest.kt @@ -3,8 +3,9 @@ package org.javacs.kt import org.eclipse.lsp4j.CodeActionKind import org.eclipse.lsp4j.Diagnostic import org.eclipse.lsp4j.jsonrpc.messages.Either -import org.hamcrest.Matchers -import org.junit.Assert +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.hasSize +import org.junit.Assert.assertThat import org.junit.Test class ImplementAbstractFunctionsQuickFixTest : SingleFileTestFixture("quickfixes", "SomeSubclass.kt") { @@ -16,27 +17,27 @@ class ImplementAbstractFunctionsQuickFixTest : SingleFileTestFixture("quickfixes val codeActions = languageServer.textDocumentService.codeAction(codeActionParams).get() - Assert.assertThat(codeActions.size, Matchers.equalTo(1)) - Assert.assertThat(codeActions[0].right.kind, Matchers.equalTo(CodeActionKind.QuickFix)) - Assert.assertThat(codeActions[0].right.diagnostics.size, Matchers.equalTo(1)) - Assert.assertThat(codeActions[0].right.diagnostics[0], Matchers.equalTo(diagnostic)) - Assert.assertThat(codeActions[0].right.edit.changes.size, Matchers.equalTo(1)) - Assert.assertThat(codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.size, Matchers.equalTo(2)) - Assert.assertThat( + assertThat(codeActions.size, equalTo(1)) + assertThat(codeActions[0].right.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeActions[0].right.diagnostics.size, equalTo(1)) + assertThat(codeActions[0].right.diagnostics[0], equalTo(diagnostic)) + assertThat(codeActions[0].right.edit.changes.size, equalTo(1)) + assertThat(codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.size, equalTo(2)) + assertThat( codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.get(0)?.range, - Matchers.equalTo(range(3, 55, 3, 55)) + equalTo(range(3, 55, 3, 55)) ) - Assert.assertThat( + assertThat( codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.get(0)?.newText, - Matchers.equalTo(System.lineSeparator() + System.lineSeparator() + " override fun someSuperMethod(someParameter: String): Int { }") + equalTo(System.lineSeparator() + System.lineSeparator() + " override fun someSuperMethod(someParameter: String): Int { }") ) - Assert.assertThat( + assertThat( codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.get(1)?.range, - Matchers.equalTo(range(3, 55, 3, 55)) + equalTo(range(3, 55, 3, 55)) ) - Assert.assertThat( + assertThat( codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.get(1)?.newText, - Matchers.equalTo(System.lineSeparator() + System.lineSeparator() + " override fun someInterfaceMethod() { }") + equalTo(System.lineSeparator() + System.lineSeparator() + " override fun someInterfaceMethod() { }") ) } @@ -48,19 +49,19 @@ class ImplementAbstractFunctionsQuickFixTest : SingleFileTestFixture("quickfixes val codeActions = languageServer.textDocumentService.codeAction(codeActionParams).get() - Assert.assertThat(codeActions.size, Matchers.equalTo(1)) - Assert.assertThat(codeActions[0].right.kind, Matchers.equalTo(CodeActionKind.QuickFix)) - Assert.assertThat(codeActions[0].right.diagnostics.size, Matchers.equalTo(1)) - Assert.assertThat(codeActions[0].right.diagnostics[0], Matchers.equalTo(diagnostic)) - Assert.assertThat(codeActions[0].right.edit.changes.size, Matchers.equalTo(1)) - Assert.assertThat(codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.size, Matchers.equalTo(1)) - Assert.assertThat( + assertThat(codeActions.size, equalTo(1)) + assertThat(codeActions[0].right.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeActions[0].right.diagnostics.size, equalTo(1)) + assertThat(codeActions[0].right.diagnostics[0], equalTo(diagnostic)) + assertThat(codeActions[0].right.edit.changes.size, equalTo(1)) + assertThat(codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.size, equalTo(1)) + assertThat( codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.get(0)?.range, - Matchers.equalTo(range(7, 74, 7, 74)) + equalTo(range(7, 74, 7, 74)) ) - Assert.assertThat( + assertThat( codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.get(0)?.newText, - Matchers.equalTo(System.lineSeparator() + System.lineSeparator() + " override fun someInterfaceMethod() { }") + equalTo(System.lineSeparator() + System.lineSeparator() + " override fun someInterfaceMethod() { }") ) } @@ -72,19 +73,170 @@ class ImplementAbstractFunctionsQuickFixTest : SingleFileTestFixture("quickfixes val codeActions = languageServer.textDocumentService.codeAction(codeActionParams).get() - Assert.assertThat(codeActions.size, Matchers.equalTo(1)) - Assert.assertThat(codeActions[0].right.kind, Matchers.equalTo(CodeActionKind.QuickFix)) - Assert.assertThat(codeActions[0].right.diagnostics.size, Matchers.equalTo(1)) - Assert.assertThat(codeActions[0].right.diagnostics[0], Matchers.equalTo(diagnostic)) - Assert.assertThat(codeActions[0].right.edit.changes.size, Matchers.equalTo(1)) - Assert.assertThat(codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.size, Matchers.equalTo(1)) - Assert.assertThat( + assertThat(codeActions.size, equalTo(1)) + assertThat(codeActions[0].right.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeActions[0].right.diagnostics.size, equalTo(1)) + assertThat(codeActions[0].right.diagnostics[0], equalTo(diagnostic)) + assertThat(codeActions[0].right.edit.changes.size, equalTo(1)) + assertThat(codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.size, equalTo(1)) + assertThat( codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.get(0)?.range, - Matchers.equalTo(range(11, 43, 11, 43)) + equalTo(range(11, 43, 11, 43)) ) - Assert.assertThat( + assertThat( codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.get(0)?.newText, - Matchers.equalTo(System.lineSeparator() + System.lineSeparator() + " override fun someSuperMethod(someParameter: String): Int { }") + equalTo(System.lineSeparator() + System.lineSeparator() + " override fun someSuperMethod(someParameter: String): Int { }") ) } } + +class ImplementAbstractFunctionsQuickFixSameFileTest : SingleFileTestFixture("quickfixes", "samefile.kt") { + @Test + fun `should find no code actions`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 3, 1, 3, 22, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(0)) + } + + @Test + fun `should find one abstract method to implement`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 7, 1, 7, 14, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(1)) + val codeAction = codeActionResult[0].right + assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeAction.title, equalTo("Implement abstract functions")) + assertThat(codeAction.diagnostics, equalTo(listOf(diagnostics[0]))) + + val textEdit = codeAction.edit.changes + val key = workspaceRoot.resolve(file).toUri().toString() + assertThat(textEdit.containsKey(key), equalTo(true)) + assertThat(textEdit[key], hasSize(1)) + + val functionToImplementEdit = textEdit[key]?.get(0) + assertThat(functionToImplementEdit?.range, equalTo(range(7, 30, 7, 30))) + assertThat(functionToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun test(input: String, otherInput: Int) { }")) + } + + @Test + fun `should find several abstract methods to implement`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 15, 1, 15, 21, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(1)) + val codeAction = codeActionResult[0].right + assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeAction.title, equalTo("Implement abstract functions")) + + val textEdit = codeAction.edit.changes + val key = workspaceRoot.resolve(file).toUri().toString() + assertThat(textEdit.containsKey(key), equalTo(true)) + assertThat(textEdit[key], hasSize(2)) + + val firstFunctionToImplementEdit = textEdit[key]?.get(0) + assertThat(firstFunctionToImplementEdit?.range, equalTo(range(15, 49, 15, 49))) + assertThat(firstFunctionToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun print() { }")) + + val secondFunctionToImplementEdit = textEdit[key]?.get(1) + assertThat(secondFunctionToImplementEdit?.range, equalTo(range(15, 49, 15, 49))) + assertThat(secondFunctionToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun test(input: String, otherInput: Int) { }")) + } + + @Test + fun `should find only one abstract method when the other one is already implemented`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 17, 1, 17, 26, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(1)) + val codeAction = codeActionResult[0].right + assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeAction.title, equalTo("Implement abstract functions")) + + val textEdit = codeAction.edit.changes + val key = workspaceRoot.resolve(file).toUri().toString() + assertThat(textEdit.containsKey(key), equalTo(true)) + assertThat(textEdit[key], hasSize(1)) + + val functionToImplementEdit = textEdit[key]?.get(0) + assertThat(functionToImplementEdit?.range, equalTo(range(18, 57, 18, 57))) + assertThat(functionToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun print() { }")) + } + + @Test + fun `should respect nullability of parameter and return value in abstract method`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 25, 1, 25, 16, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(1)) + val codeAction = codeActionResult[0].right + assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeAction.title, equalTo("Implement abstract functions")) + + val textEdit = codeAction.edit.changes + val key = workspaceRoot.resolve(file).toUri().toString() + assertThat(textEdit.containsKey(key), equalTo(true)) + assertThat(textEdit[key], hasSize(1)) + + val functionToImplementEdit = textEdit[key]?.get(0) + assertThat(functionToImplementEdit?.range, equalTo(range(25, 48, 25, 48))) + assertThat(functionToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun myMethod(myStr: String?): String? { }")) + } +} + +class ImplementAbstractFunctionsQuickFixExternalLibraryTest : SingleFileTestFixture("quickfixes", "standardlib.kt") { + @Test + fun `should find one abstract method from Runnable to implement`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 5, 1, 5, 15, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(1)) + val codeAction = codeActionResult[0].right + assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeAction.title, equalTo("Implement abstract functions")) + + val textEdit = codeAction.edit.changes + val key = workspaceRoot.resolve(file).toUri().toString() + assertThat(textEdit.containsKey(key), equalTo(true)) + assertThat(textEdit[key], hasSize(1)) + + val functionToImplementEdit = textEdit[key]?.get(0) + assertThat(functionToImplementEdit?.range, equalTo(range(5, 28, 5, 28))) + assertThat(functionToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun run() { }")) + } + + @Test + fun `should find one abstract method from Comparable to implement`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 7, 1, 7, 19, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(1)) + val codeAction = codeActionResult[0].right + assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeAction.title, equalTo("Implement abstract functions")) + + val textEdit = codeAction.edit.changes + val key = workspaceRoot.resolve(file).toUri().toString() + assertThat(textEdit.containsKey(key), equalTo(true)) + assertThat(textEdit[key], hasSize(1)) + + val functionToImplementEdit = textEdit[key]?.get(0) + assertThat(functionToImplementEdit?.range, equalTo(range(7, 42, 7, 42))) + assertThat(functionToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun compare(p0: String, p1: String): Int { }")) + } +} diff --git a/server/src/test/resources/codeactions/implementabstract_samefile.kt b/server/src/test/resources/quickfixes/samefile.kt similarity index 100% rename from server/src/test/resources/codeactions/implementabstract_samefile.kt rename to server/src/test/resources/quickfixes/samefile.kt diff --git a/server/src/test/resources/codeactions/implementabstract_standardlib.kt b/server/src/test/resources/quickfixes/standardlib.kt similarity index 100% rename from server/src/test/resources/codeactions/implementabstract_standardlib.kt rename to server/src/test/resources/quickfixes/standardlib.kt From 322fb60be038c20dfa11cec8db90a47d48be0e2f Mon Sep 17 00:00:00 2001 From: fwcd Date: Fri, 29 Apr 2022 01:25:36 +0200 Subject: [PATCH 10/46] Trigger push CI only on main branch --- .github/workflows/build.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 43e51914d..7afdab91c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,10 @@ name: Build -on: [ push, pull_request ] + +on: + push: + branches: + - main + pull_request: jobs: build: From efe32bbec633a895228aa7f6d27213562e379755 Mon Sep 17 00:00:00 2001 From: fwcd Date: Wed, 8 Jun 2022 16:21:32 +0200 Subject: [PATCH 11/46] Add grammar development extension for VSCode --- .vscode/launch.json | 8 ++++++++ grammars/vscode-grammar-dev/.vscodeignore | 4 ++++ grammars/vscode-grammar-dev/README.md | 11 ++++++++++ grammars/vscode-grammar-dev/package.json | 25 +++++++++++++++++++++++ 4 files changed, 48 insertions(+) create mode 100644 grammars/vscode-grammar-dev/.vscodeignore create mode 100644 grammars/vscode-grammar-dev/README.md create mode 100644 grammars/vscode-grammar-dev/package.json diff --git a/.vscode/launch.json b/.vscode/launch.json index 0e7ab461c..f26ad1cdb 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -30,6 +30,14 @@ "request": "attach", "hostName": "localhost", "port": 5005 + }, + { + "type": "extensionHost", + "name": "Run Grammar Dev Extension", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}/grammars/vscode-grammar-dev" + ] } ] } diff --git a/grammars/vscode-grammar-dev/.vscodeignore b/grammars/vscode-grammar-dev/.vscodeignore new file mode 100644 index 000000000..f369b5e55 --- /dev/null +++ b/grammars/vscode-grammar-dev/.vscodeignore @@ -0,0 +1,4 @@ +.vscode/** +.vscode-test/** +.gitignore +vsc-extension-quickstart.md diff --git a/grammars/vscode-grammar-dev/README.md b/grammars/vscode-grammar-dev/README.md new file mode 100644 index 000000000..5de31e33f --- /dev/null +++ b/grammars/vscode-grammar-dev/README.md @@ -0,0 +1,11 @@ +# Grammar Development Extension for VSCode + +A small VSCode extension for development of the Kotlin grammar. + +> Note: The actual Kotlin support for VSCode, including the language client, is located in the [`vscode-kotlin` repository](https://github.com/fwcd/vscode-kotlin). + +## Usage + +The most convenient way to run the extension is to simply use the debug configuration `Run Grammar Dev` in this repo. + +> Note: VSCode might show a warning that grammar paths are located outside of the extension folder. This message can be ignored, since the extension is only indended for development anyway. diff --git a/grammars/vscode-grammar-dev/package.json b/grammars/vscode-grammar-dev/package.json new file mode 100644 index 000000000..a0fa617f5 --- /dev/null +++ b/grammars/vscode-grammar-dev/package.json @@ -0,0 +1,25 @@ +{ + "name": "kotlin-grammar-dev", + "displayName": "Kotlin Grammar Development", + "description": "Grammar development extension for Kotlin", + "version": "0.0.1", + "engines": { + "vscode": "^1.67.0" + }, + "categories": [ + "Programming Languages" + ], + "contributes": { + "languages": [{ + "id": "kotlin", + "aliases": ["Kotlin", "kotlin"], + "extensions": [".kt",".kts"], + "configuration": "../kotlin.configuration.json" + }], + "grammars": [{ + "language": "kotlin", + "scopeName": "source.kotlin", + "path": "../Kotlin.tmLanguage.json" + }] + } +} From 29539764266f0bdc8222316f39b1ad1132852868 Mon Sep 17 00:00:00 2001 From: fwcd Date: Wed, 8 Jun 2022 16:25:19 +0200 Subject: [PATCH 12/46] Disable main Kotlin extension for grammar development --- .vscode/launch.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/launch.json b/.vscode/launch.json index f26ad1cdb..523d944fc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -36,6 +36,7 @@ "name": "Run Grammar Dev Extension", "request": "launch", "args": [ + "--disable-extension=fwcd.kotlin", "--extensionDevelopmentPath=${workspaceFolder}/grammars/vscode-grammar-dev" ] } From dbd2bb7c5779ada3cf74d7d19c96f0024067f71f Mon Sep 17 00:00:00 2001 From: fwcd Date: Wed, 8 Jun 2022 16:30:52 +0200 Subject: [PATCH 13/46] Fix string interpolation punctuation highlighting --- grammars/Kotlin.tmLanguage.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/grammars/Kotlin.tmLanguage.json b/grammars/Kotlin.tmLanguage.json index 2689ca4e3..28a5c0849 100644 --- a/grammars/Kotlin.tmLanguage.json +++ b/grammars/Kotlin.tmLanguage.json @@ -475,10 +475,14 @@ "end": "(\\})", "name": "meta.template.expression.kotlin", "beginCaptures": { - "1": "punctuation.definition.template-expression.begin" + "1": { + "name": "punctuation.definition.template-expression.begin" + } }, "endCaptures": { - "1": "punctuation.definition.template-expression.begin" + "1": { + "name": "punctuation.definition.template-expression.end" + } }, "patterns": [ { From 5dab99536c87e677534f1ecc6dcff7fe69d01f0f Mon Sep 17 00:00:00 2001 From: fwcd Date: Wed, 8 Jun 2022 16:34:15 +0200 Subject: [PATCH 14/46] Reference JSON schema in Kotlin grammar --- grammars/Kotlin.tmLanguage.json | 1 + 1 file changed, 1 insertion(+) diff --git a/grammars/Kotlin.tmLanguage.json b/grammars/Kotlin.tmLanguage.json index 28a5c0849..23c3a4876 100644 --- a/grammars/Kotlin.tmLanguage.json +++ b/grammars/Kotlin.tmLanguage.json @@ -1,4 +1,5 @@ { + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", "name": "Kotlin", "scopeName": "source.kotlin", "patterns": [ From 092e44e47b642be58fb47825815e762bf0b2ac7a Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Wed, 8 Jun 2022 19:00:39 +0200 Subject: [PATCH 15/46] Implement abstract members, now includes abstract variables --- .../org/javacs/kt/codeaction/CodeAction.kt | 4 +- ...kt => ImplementAbstractMembersQuickFix.kt} | 27 +++++- .../kotlin/org/javacs/kt/QuickFixesTest.kt | 92 +++++++++++++++++-- .../src/test/resources/quickfixes/samefile.kt | 14 +++ .../test/resources/quickfixes/standardlib.kt | 2 + 5 files changed, 123 insertions(+), 16 deletions(-) rename server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/{ImplementAbstractFunctionsQuickFix.kt => ImplementAbstractMembersQuickFix.kt} (88%) diff --git a/server/src/main/kotlin/org/javacs/kt/codeaction/CodeAction.kt b/server/src/main/kotlin/org/javacs/kt/codeaction/CodeAction.kt index 1adc4f991..7f9bdbdcc 100644 --- a/server/src/main/kotlin/org/javacs/kt/codeaction/CodeAction.kt +++ b/server/src/main/kotlin/org/javacs/kt/codeaction/CodeAction.kt @@ -3,14 +3,14 @@ package org.javacs.kt.codeaction import org.eclipse.lsp4j.* import org.eclipse.lsp4j.jsonrpc.messages.Either import org.javacs.kt.CompiledFile -import org.javacs.kt.codeaction.quickfix.ImplementAbstractFunctionsQuickFix +import org.javacs.kt.codeaction.quickfix.ImplementAbstractMembersQuickFix import org.javacs.kt.codeaction.quickfix.AddMissingImportsQuickFix import org.javacs.kt.command.JAVA_TO_KOTLIN_COMMAND import org.javacs.kt.util.toPath import org.javacs.kt.index.SymbolIndex val QUICK_FIXES = listOf( - ImplementAbstractFunctionsQuickFix(), + ImplementAbstractMembersQuickFix(), AddMissingImportsQuickFix() ) diff --git a/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractFunctionsQuickFix.kt b/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractMembersQuickFix.kt similarity index 88% rename from server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractFunctionsQuickFix.kt rename to server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractMembersQuickFix.kt index 12910c87d..c1f9be1d2 100644 --- a/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractFunctionsQuickFix.kt +++ b/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractMembersQuickFix.kt @@ -11,6 +11,7 @@ import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.ClassConstructorDescriptor import org.jetbrains.kotlin.descriptors.DeclarationDescriptor import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.descriptors.PropertyDescriptor import org.jetbrains.kotlin.descriptors.isInterface import org.jetbrains.kotlin.descriptors.Modality import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi @@ -33,7 +34,7 @@ import org.jetbrains.kotlin.types.typeUtil.asTypeProjection private const val DEFAULT_TAB_SIZE = 4 -class ImplementAbstractFunctionsQuickFix : QuickFix { +class ImplementAbstractMembersQuickFix : QuickFix { override fun compute(file: CompiledFile, index: SymbolIndex, range: Range, diagnostics: List): List> { val diagnostic = findDiagnosticMatch(diagnostics, range) @@ -64,7 +65,7 @@ class ImplementAbstractFunctionsQuickFix : QuickFix { val codeAction = CodeAction() codeAction.edit = WorkspaceEdit(mapOf(uri to textEdits)) codeAction.kind = CodeActionKind.QuickFix - codeAction.title = "Implement abstract functions" + codeAction.title = "Implement abstract members" codeAction.diagnostics = listOf(diagnostic) return listOf(Either.forRight(codeAction)) } @@ -92,9 +93,15 @@ private fun getAbstractFunctionStubs(file: CompiledFile, kotlinClass: KtClass) = if (null != classDescriptor && (classDescriptor.kind.isInterface || classDescriptor.modality == Modality.ABSTRACT)) { val superClassTypeArguments = getSuperClassTypeProjections(file, it) classDescriptor.getMemberScope(superClassTypeArguments).getContributedDescriptors().filter { classMember -> - classMember is FunctionDescriptor && classMember.modality == Modality.ABSTRACT && !overridesDeclaration(kotlinClass, classMember) - }.map { function -> - createFunctionStub(function as FunctionDescriptor) + (classMember is FunctionDescriptor && classMember.modality == Modality.ABSTRACT && !overridesDeclaration(kotlinClass, classMember)) || (classMember is PropertyDescriptor && classMember.modality == Modality.ABSTRACT && !overridesDeclaration(kotlinClass, classMember)) + }.mapNotNull { member -> + if(member is FunctionDescriptor) { + createFunctionStub(member) + } else if(member is PropertyDescriptor) { + createVariableStub(member) + } else { + null + } } } else { null @@ -132,6 +139,11 @@ private fun overridesDeclaration(kotlinClass: KtClass, descriptor: FunctionDescr } } +private fun overridesDeclaration(kotlinClass: KtClass, descriptor: PropertyDescriptor): Boolean = + kotlinClass.declarations.any { + it.name == descriptor.name.asString() && it.hasModifier(KtTokens.OVERRIDE_KEYWORD) + } + // Checks if two functions have matching parameters private fun parametersMatch(function: KtNamedFunction, functionDescriptor: FunctionDescriptor): Boolean { if (function.valueParameters.size == functionDescriptor.valueParameters.size) { @@ -178,6 +190,11 @@ private fun createFunctionStub(function: FunctionDescriptor): String { return "override fun $name($arguments)${returnType?.let { ": $it" } ?: ""} { }" } +private fun createVariableStub(variable: PropertyDescriptor): String { + val variableType = variable.returnType?.unwrappedType()?.toString()?.takeIf { "Unit" != it } + return "override val ${variable.name}${variableType?.let { ": $it" } ?: ""} = TODO(\"SET VALUE\")" +} + // about types: regular Kotlin types are marked T or T?, but types from Java are (T..T?) because nullability cannot be decided. // Therefore we have to unpack in case we have the Java type. Fortunately, the Java types are not marked nullable, so we default to non nullable types. Let the user decide if they want nullable types instead. With this implementation Kotlin types also keeps their nullability private fun KotlinType.unwrappedType(): KotlinType = this.unwrap().makeNullableAsSpecified(this.isMarkedNullable) diff --git a/server/src/test/kotlin/org/javacs/kt/QuickFixesTest.kt b/server/src/test/kotlin/org/javacs/kt/QuickFixesTest.kt index 4a514b89d..f763df54a 100644 --- a/server/src/test/kotlin/org/javacs/kt/QuickFixesTest.kt +++ b/server/src/test/kotlin/org/javacs/kt/QuickFixesTest.kt @@ -8,7 +8,7 @@ import org.hamcrest.Matchers.hasSize import org.junit.Assert.assertThat import org.junit.Test -class ImplementAbstractFunctionsQuickFixTest : SingleFileTestFixture("quickfixes", "SomeSubclass.kt") { +class ImplementAbstractMembersQuickFixTest : SingleFileTestFixture("quickfixes", "SomeSubclass.kt") { @Test fun `gets workspace edit for all abstract methods when none are implemented`() { val diagnostic = Diagnostic(range(3, 1, 3, 19), "") @@ -90,7 +90,7 @@ class ImplementAbstractFunctionsQuickFixTest : SingleFileTestFixture("quickfixes } } -class ImplementAbstractFunctionsQuickFixSameFileTest : SingleFileTestFixture("quickfixes", "samefile.kt") { +class ImplementAbstractMembersQuickFixSameFileTest : SingleFileTestFixture("quickfixes", "samefile.kt") { @Test fun `should find no code actions`() { val only = listOf(CodeActionKind.QuickFix) @@ -111,7 +111,7 @@ class ImplementAbstractFunctionsQuickFixSameFileTest : SingleFileTestFixture("qu assertThat(codeActionResult, hasSize(1)) val codeAction = codeActionResult[0].right assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) - assertThat(codeAction.title, equalTo("Implement abstract functions")) + assertThat(codeAction.title, equalTo("Implement abstract members")) assertThat(codeAction.diagnostics, equalTo(listOf(diagnostics[0]))) val textEdit = codeAction.edit.changes @@ -134,7 +134,7 @@ class ImplementAbstractFunctionsQuickFixSameFileTest : SingleFileTestFixture("qu assertThat(codeActionResult, hasSize(1)) val codeAction = codeActionResult[0].right assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) - assertThat(codeAction.title, equalTo("Implement abstract functions")) + assertThat(codeAction.title, equalTo("Implement abstract members")) val textEdit = codeAction.edit.changes val key = workspaceRoot.resolve(file).toUri().toString() @@ -160,7 +160,7 @@ class ImplementAbstractFunctionsQuickFixSameFileTest : SingleFileTestFixture("qu assertThat(codeActionResult, hasSize(1)) val codeAction = codeActionResult[0].right assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) - assertThat(codeAction.title, equalTo("Implement abstract functions")) + assertThat(codeAction.title, equalTo("Implement abstract members")) val textEdit = codeAction.edit.changes val key = workspaceRoot.resolve(file).toUri().toString() @@ -182,7 +182,7 @@ class ImplementAbstractFunctionsQuickFixSameFileTest : SingleFileTestFixture("qu assertThat(codeActionResult, hasSize(1)) val codeAction = codeActionResult[0].right assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) - assertThat(codeAction.title, equalTo("Implement abstract functions")) + assertThat(codeAction.title, equalTo("Implement abstract members")) val textEdit = codeAction.edit.changes val key = workspaceRoot.resolve(file).toUri().toString() @@ -193,9 +193,57 @@ class ImplementAbstractFunctionsQuickFixSameFileTest : SingleFileTestFixture("qu assertThat(functionToImplementEdit?.range, equalTo(range(25, 48, 25, 48))) assertThat(functionToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun myMethod(myStr: String?): String? { }")) } + + @Test + fun `should find abstract variable and function`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 35, 1, 35, 18, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(1)) + val codeAction = codeActionResult[0].right + assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeAction.title, equalTo("Implement abstract members")) + + val textEdit = codeAction.edit.changes + val key = workspaceRoot.resolve(file).toUri().toString() + assertThat(textEdit.containsKey(key), equalTo(true)) + assertThat(textEdit[key], hasSize(2)) + + val firstMemberToImplementEdit = textEdit[key]?.get(0) + assertThat(firstMemberToImplementEdit?.range, equalTo(range(35, 35, 35, 35))) + assertThat(firstMemberToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override val name: String = TODO(\"SET VALUE\")")) + + val secondMemberToImplementEdit = textEdit[key]?.get(1) + assertThat(secondMemberToImplementEdit?.range, equalTo(range(35, 35, 35, 35))) + assertThat(secondMemberToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun myFun() { }")) + } + + @Test + fun `should find abstract function when variable is already implemented`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 37, 1, 37, 17, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(1)) + val codeAction = codeActionResult[0].right + assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeAction.title, equalTo("Implement abstract members")) + + val textEdit = codeAction.edit.changes + val key = workspaceRoot.resolve(file).toUri().toString() + assertThat(textEdit.containsKey(key), equalTo(true)) + assertThat(textEdit[key], hasSize(1)) + + val memberToImplementEdit = textEdit[key]?.get(0) + assertThat(memberToImplementEdit?.range, equalTo(range(38, 31, 38, 31))) + assertThat(memberToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun myFun() { }")) + } } -class ImplementAbstractFunctionsQuickFixExternalLibraryTest : SingleFileTestFixture("quickfixes", "standardlib.kt") { +class ImplementAbstractMembersQuickFixExternalLibraryTest : SingleFileTestFixture("quickfixes", "standardlib.kt") { @Test fun `should find one abstract method from Runnable to implement`() { val only = listOf(CodeActionKind.QuickFix) @@ -206,7 +254,7 @@ class ImplementAbstractFunctionsQuickFixExternalLibraryTest : SingleFileTestFixt assertThat(codeActionResult, hasSize(1)) val codeAction = codeActionResult[0].right assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) - assertThat(codeAction.title, equalTo("Implement abstract functions")) + assertThat(codeAction.title, equalTo("Implement abstract members")) val textEdit = codeAction.edit.changes val key = workspaceRoot.resolve(file).toUri().toString() @@ -228,7 +276,7 @@ class ImplementAbstractFunctionsQuickFixExternalLibraryTest : SingleFileTestFixt assertThat(codeActionResult, hasSize(1)) val codeAction = codeActionResult[0].right assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) - assertThat(codeAction.title, equalTo("Implement abstract functions")) + assertThat(codeAction.title, equalTo("Implement abstract members")) val textEdit = codeAction.edit.changes val key = workspaceRoot.resolve(file).toUri().toString() @@ -239,4 +287,30 @@ class ImplementAbstractFunctionsQuickFixExternalLibraryTest : SingleFileTestFixt assertThat(functionToImplementEdit?.range, equalTo(range(7, 42, 7, 42))) assertThat(functionToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun compare(p0: String, p1: String): Int { }")) } + + @Test + fun `should find abstract members for AbstractList`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 9, 1, 9, 13, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(1)) + val codeAction = codeActionResult[0].right + assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeAction.title, equalTo("Implement abstract members")) + + val textEdit = codeAction.edit.changes + val key = workspaceRoot.resolve(file).toUri().toString() + assertThat(textEdit.containsKey(key), equalTo(true)) + assertThat(textEdit[key], hasSize(2)) + + val firstMemberToImplementEdit = textEdit[key]?.get(0) + assertThat(firstMemberToImplementEdit?.range, equalTo(range(9, 40, 9, 40))) + assertThat(firstMemberToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override val size: Int = TODO(\"SET VALUE\")")) + + val secondMemberToImplementEdit = textEdit[key]?.get(1) + assertThat(secondMemberToImplementEdit?.range, equalTo(range(9, 40, 9, 40))) + assertThat(secondMemberToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun get(index: Int): String { }")) + } } diff --git a/server/src/test/resources/quickfixes/samefile.kt b/server/src/test/resources/quickfixes/samefile.kt index 6e322092d..dfbdfa90a 100644 --- a/server/src/test/resources/quickfixes/samefile.kt +++ b/server/src/test/resources/quickfixes/samefile.kt @@ -23,3 +23,17 @@ interface NullMethodAndReturn { } class NullClass : NullMethodAndReturn {} + +abstract class MyAbstract { + val otherValToTestAbstractOverride = 1 + + abstract val name: String + + abstract fun myFun() +} + +class MyImplClass : MyAbstract() {} + +class My2ndClass : MyAbstract() { + override val name = "Nils" +} diff --git a/server/src/test/resources/quickfixes/standardlib.kt b/server/src/test/resources/quickfixes/standardlib.kt index 665eab2d2..bb5acb08d 100644 --- a/server/src/test/resources/quickfixes/standardlib.kt +++ b/server/src/test/resources/quickfixes/standardlib.kt @@ -5,3 +5,5 @@ import java.util.Comparator class MyThread : Runnable {} class MyComperable : Comparator {} + +class MyList : AbstractList() {} From d23bab1714aa4d1e6222d1a6e998d0c22a7d43de Mon Sep 17 00:00:00 2001 From: fwcd Date: Wed, 8 Jun 2022 23:03:24 +0200 Subject: [PATCH 16/46] Suppress unchecked cast warnings in ResolveMainTest --- .../test/kotlin/org/javacs/kt/ResolveMainTest.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/server/src/test/kotlin/org/javacs/kt/ResolveMainTest.kt b/server/src/test/kotlin/org/javacs/kt/ResolveMainTest.kt index 65ca7d5fc..af67a8f77 100644 --- a/server/src/test/kotlin/org/javacs/kt/ResolveMainTest.kt +++ b/server/src/test/kotlin/org/javacs/kt/ResolveMainTest.kt @@ -15,10 +15,10 @@ class NoMainResolve : SingleFileTestFixture("resolvemain", "NoMain.kt") { 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) + @Suppress("UNCHECKED_CAST") val mainInfo = result as Map assertNull(mainInfo["mainClass"]) assertEquals(root.toString(), mainInfo["projectRoot"]) @@ -31,10 +31,11 @@ class SimpleMainResolve : SingleFileTestFixture("resolvemain", "Simple.kt") { 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) + @Suppress("UNCHECKED_CAST") val mainInfo = result as Map assertEquals("test.SimpleKt", mainInfo["mainClass"]) assertEquals(Range(Position(2, 0), Position(4, 1)), mainInfo["range"]) @@ -48,10 +49,11 @@ class JvmNameAnnotationMainResolve : SingleFileTestFixture("resolvemain", "JvmNa 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) + @Suppress("UNCHECKED_CAST") val mainInfo = result as Map assertEquals("com.mypackage.name.Potato", mainInfo["mainClass"]) assertEquals(Range(Position(5, 0), Position(7, 1)), mainInfo["range"]) @@ -64,10 +66,11 @@ class CompanionObjectMainResolve : SingleFileTestFixture("resolvemain", "Compani 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) + @Suppress("UNCHECKED_CAST") val mainInfo = result as Map assertEquals("test.my.companion.SweetPotato", mainInfo["mainClass"]) assertEquals(Range(Position(8, 8), Position(11, 9)), mainInfo["range"]) From 29f0a9fdf7075f07642d2e49ed000e5ca6f2d2e0 Mon Sep 17 00:00:00 2001 From: fwcd Date: Wed, 8 Jun 2022 23:10:40 +0200 Subject: [PATCH 17/46] Reformat ResolveMain with some smaller stylistic changes --- .../org/javacs/kt/resolve/ResolveMain.kt | 53 ++++++++++--------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/server/src/main/kotlin/org/javacs/kt/resolve/ResolveMain.kt b/server/src/main/kotlin/org/javacs/kt/resolve/ResolveMain.kt index b6282eae2..4a52fa50f 100644 --- a/server/src/main/kotlin/org/javacs/kt/resolve/ResolveMain.kt +++ b/server/src/main/kotlin/org/javacs/kt/resolve/ResolveMain.kt @@ -10,27 +10,26 @@ import org.javacs.kt.position.range import org.javacs.kt.util.partitionAroundLast import com.intellij.openapi.util.TextRange - fun resolveMain(file: CompiledFile): Map { val parsedFile = file.parse.copy() as KtFile - - val mainFunction = findTopLevelMainFunction(parsedFile) - if(null != mainFunction) { + + findTopLevelMainFunction(parsedFile)?.let { 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)) + return mapOf( + "mainClass" to JvmFileClassUtil.getFileClassInfoNoResolve(parsedFile).facadeClassFqName.asString(), + "range" to range(file.content, mainFunction.second) + ) } - val companionMain = findCompanionObjectMain(parsedFile) - if(null != companionMain) { + findCompanionObjectMain(parsedFile)?.let { companionMain -> return mapOf( "mainClass" to (companionMain.first ?: ""), "range" to range(file.content, companionMain.second) ) } - + return emptyMap() } @@ -42,21 +41,25 @@ private fun findTopLevelMainFunction(file: KtFile): Pair? = } // finds a top level class that contains a companion object with a main function inside -private fun findCompanionObjectMain(file: KtFile): Pair? = file.declarations.flatMap { topLevelDeclaration -> - if(topLevelDeclaration is KtClass) { - topLevelDeclaration.companionObjects - } else { - emptyList() +private fun findCompanionObjectMain(file: KtFile): Pair? = file.declarations + .flatMap { topLevelDeclaration -> + if (topLevelDeclaration is KtClass) { + topLevelDeclaration.companionObjects + } else { + emptyList() + } } -}.flatMap { companionObject -> - companionObject.body?.children?.toList() ?: emptyList() -}.mapNotNull { companionObjectInternal -> - if(companionObjectInternal is KtNamedFunction && "main" == companionObjectInternal.name && companionObjectInternal.text.startsWith("@JvmStatic")) { - companionObjectInternal - } else { - null + .flatMap { companionObject -> + companionObject.body?.children?.toList() ?: emptyList() + } + .mapNotNull { companionObjectInternal -> + companionObjectInternal.takeIf { + companionObjectInternal is KtNamedFunction + && "main" == companionObjectInternal.name + && companionObjectInternal.text.startsWith("@JvmStatic") + } + } + .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) } -}.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) -} From e7d136d0c0d69e9cf9c4ba8a93ba7298cd639992 Mon Sep 17 00:00:00 2001 From: fwcd Date: Wed, 8 Jun 2022 23:39:57 +0200 Subject: [PATCH 18/46] Update version back to 1.3.1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e8355f897..4f20d2796 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=1.4.0 +projectVersion=1.3.1 kotlinVersion=1.6.10 exposedVersion=0.37.3 lsp4jVersion=0.12.0 From b015f9a51275ab4d4795e7303d02d27a5764a9e2 Mon Sep 17 00:00:00 2001 From: fwcd Date: Wed, 8 Jun 2022 23:42:40 +0200 Subject: [PATCH 19/46] Bump version to 1.3.2 --- CHANGELOG.md | 6 ++++++ gradle.properties | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1160aa16..636f21b25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to the language server will be documented in this file. Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. +## [1.3.1] +- Add support for run/debug code lenses +- Add definition lookup support for JDT symbols +- Add quick fix for implementing abstract functions +- Add experimental JDT.LS integration + ## [1.3.0] - Bump to Kotlin 1.6 - Support JDK 17 diff --git a/gradle.properties b/gradle.properties index 4f20d2796..e0aa80117 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=1.3.1 +projectVersion=1.3.2 kotlinVersion=1.6.10 exposedVersion=0.37.3 lsp4jVersion=0.12.0 From 1fe596fc4a508430486e711524980342fa220f40 Mon Sep 17 00:00:00 2001 From: FW <30873659+fwcd@users.noreply.github.com> Date: Wed, 8 Jun 2022 23:54:36 +0200 Subject: [PATCH 20/46] Highlight note in `vscode-grammar-dev` README --- grammars/vscode-grammar-dev/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/grammars/vscode-grammar-dev/README.md b/grammars/vscode-grammar-dev/README.md index 5de31e33f..f9ba45b0e 100644 --- a/grammars/vscode-grammar-dev/README.md +++ b/grammars/vscode-grammar-dev/README.md @@ -2,10 +2,10 @@ A small VSCode extension for development of the Kotlin grammar. -> Note: The actual Kotlin support for VSCode, including the language client, is located in the [`vscode-kotlin` repository](https://github.com/fwcd/vscode-kotlin). +> **Note**: The actual Kotlin support for VSCode, including the language client, is located in the [`vscode-kotlin` repository](https://github.com/fwcd/vscode-kotlin). ## Usage The most convenient way to run the extension is to simply use the debug configuration `Run Grammar Dev` in this repo. -> Note: VSCode might show a warning that grammar paths are located outside of the extension folder. This message can be ignored, since the extension is only indended for development anyway. +> **Note**: VSCode might show a warning that grammar paths are located outside of the extension folder. This message can be ignored, since the extension is only indended for development anyway. From ec0c9c5946acbff3be8aa9766c77e8f6e6a37ece Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Thu, 9 Jun 2022 17:05:49 +0200 Subject: [PATCH 21/46] Prettify code based upon PR comment --- .../quickfix/ImplementAbstractMembersQuickFix.kt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractMembersQuickFix.kt b/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractMembersQuickFix.kt index c1f9be1d2..0c2d33b34 100644 --- a/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractMembersQuickFix.kt +++ b/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractMembersQuickFix.kt @@ -95,12 +95,10 @@ private fun getAbstractFunctionStubs(file: CompiledFile, kotlinClass: KtClass) = classDescriptor.getMemberScope(superClassTypeArguments).getContributedDescriptors().filter { classMember -> (classMember is FunctionDescriptor && classMember.modality == Modality.ABSTRACT && !overridesDeclaration(kotlinClass, classMember)) || (classMember is PropertyDescriptor && classMember.modality == Modality.ABSTRACT && !overridesDeclaration(kotlinClass, classMember)) }.mapNotNull { member -> - if(member is FunctionDescriptor) { - createFunctionStub(member) - } else if(member is PropertyDescriptor) { - createVariableStub(member) - } else { - null + when (member) { + is FunctionDescriptor -> createFunctionStub(member) + is PropertyDescriptor -> createVariableStub(member) + else -> null } } } else { From de63e05a8156a2f8b42b6d0b25c945b8b1d5a690 Mon Sep 17 00:00:00 2001 From: fwcd Date: Sat, 25 Jun 2022 04:02:01 +0200 Subject: [PATCH 22/46] Add packaging badges to README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 4d7840357..93fa64a2c 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,10 @@ Any editor conforming to LSP is supported, including [VSCode](https://github.com * See [Kotlin Debug Adapter](https://github.com/fwcd/kotlin-debug-adapter) for debugging support on JVM * See [tree-sitter-kotlin](https://github.com/fwcd/tree-sitter-kotlin) for an experimental [Tree-Sitter](https://tree-sitter.github.io/tree-sitter/) grammar +## Packaging + +[![Packaging status](https://repology.org/badge/vertical-allrepos/kotlin-language-server.svg)](https://repology.org/project/kotlin-language-server/versions) + ## This repository needs your help! [The original author](https://github.com/georgewfraser) created this project while he was considering using Kotlin in his work. He ended up deciding not to and is not really using Kotlin these days though this is a pretty fully-functional language server that just needs someone to use it every day for a while and iron out the last few pesky bugs. From d8c7bab010f674749d12b771f407375137bbf148 Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Mon, 27 Jun 2022 18:24:31 +0200 Subject: [PATCH 23/46] Fix issue where nothing happens on empty body for implement abstract members quick fix --- .../ImplementAbstractMembersQuickFix.kt | 14 +++++++-- .../kotlin/org/javacs/kt/QuickFixesTest.kt | 30 +++++++++++++++++++ .../src/test/resources/quickfixes/samefile.kt | 8 +++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractMembersQuickFix.kt b/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractMembersQuickFix.kt index 0c2d33b34..769cd14b1 100644 --- a/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractMembersQuickFix.kt +++ b/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractMembersQuickFix.kt @@ -53,14 +53,17 @@ class ImplementAbstractMembersQuickFix : QuickFix { val uri = file.parse.toPath().toUri().toString() // Get the padding to be introduced before the function declarations val padding = getDeclarationPadding(file, kotlinClass) + // Get the location where the new code will be placed val newFunctionStartPosition = getNewFunctionStartPosition(file, kotlinClass) + val bodyAppendBeginning = listOf(TextEdit(Range(newFunctionStartPosition, newFunctionStartPosition), "{")).takeIf { kotlinClass.hasNoBody() } ?: emptyList() + val bodyAppendEnd = listOf(TextEdit(Range(newFunctionStartPosition, newFunctionStartPosition), System.lineSeparator() + "}")).takeIf { kotlinClass.hasNoBody() } ?: emptyList() - val textEdits = functionsToImplement.map { + val textEdits = bodyAppendBeginning + functionsToImplement.map { // We leave two new lines before the function is inserted val newText = System.lineSeparator() + System.lineSeparator() + padding + it TextEdit(Range(newFunctionStartPosition, newFunctionStartPosition), newText) - } + } + bodyAppendEnd val codeAction = CodeAction() codeAction.edit = WorkspaceEdit(mapOf(uri to textEdits)) @@ -221,6 +224,11 @@ private fun getNewFunctionStartPosition(file: CompiledFile, kotlinClass: KtClass if (body != null) { position(file.content, body.startOffset + 1) } else { - null + // function has no body. We have to create one. New position is right after entire kotlin class text (with space) + val newPosCorrectLine = position(file.content, kotlinClass.startOffset + 1) + newPosCorrectLine.character = (kotlinClass.text.length + 2) + newPosCorrectLine } } + +private fun KtClass.hasNoBody() = null == this.body diff --git a/server/src/test/kotlin/org/javacs/kt/QuickFixesTest.kt b/server/src/test/kotlin/org/javacs/kt/QuickFixesTest.kt index f763df54a..4a73190a3 100644 --- a/server/src/test/kotlin/org/javacs/kt/QuickFixesTest.kt +++ b/server/src/test/kotlin/org/javacs/kt/QuickFixesTest.kt @@ -241,6 +241,36 @@ class ImplementAbstractMembersQuickFixSameFileTest : SingleFileTestFixture("quic assertThat(memberToImplementEdit?.range, equalTo(range(38, 31, 38, 31))) assertThat(memberToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun myFun() { }")) } + + @Test + fun `should find abstract members when class has no body (square brackets)`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 47, 1, 47, 12, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(1)) + val codeAction = codeActionResult[0].right + assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeAction.title, equalTo("Implement abstract members")) + + val textEdit = codeAction.edit.changes + val key = workspaceRoot.resolve(file).toUri().toString() + assertThat(textEdit.containsKey(key), equalTo(true)) + assertThat(textEdit[key], hasSize(3)) + + val firstMemberToImplementEdit = textEdit[key]?.get(0) + assertThat(firstMemberToImplementEdit?.range, equalTo(range(47, 23, 47, 23))) + assertThat(firstMemberToImplementEdit?.newText, equalTo("{")) + + val secondMemberToImplementEdit = textEdit[key]?.get(1) + assertThat(secondMemberToImplementEdit?.range, equalTo(range(47, 23, 47, 23))) + assertThat(secondMemberToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun behaviour() { }")) + + val thirdMemberToImplementEdit = textEdit[key]?.get(2) + assertThat(thirdMemberToImplementEdit?.range, equalTo(range(47, 23, 47, 23))) + assertThat(thirdMemberToImplementEdit?.newText, equalTo(System.lineSeparator() + "}")) + } } class ImplementAbstractMembersQuickFixExternalLibraryTest : SingleFileTestFixture("quickfixes", "standardlib.kt") { diff --git a/server/src/test/resources/quickfixes/samefile.kt b/server/src/test/resources/quickfixes/samefile.kt index dfbdfa90a..3d17c623e 100644 --- a/server/src/test/resources/quickfixes/samefile.kt +++ b/server/src/test/resources/quickfixes/samefile.kt @@ -37,3 +37,11 @@ class MyImplClass : MyAbstract() {} class My2ndClass : MyAbstract() { override val name = "Nils" } + + +// defect GH-366, part of the solution +interface IThing { + fun behaviour +} + +class Thing : IThing From 8ee1628b3191e8ed0c687a32b44d96c8790c4464 Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Mon, 27 Jun 2022 18:28:37 +0200 Subject: [PATCH 24/46] Consistency change based upon what the class is currently doing --- .../ImplementAbstractMembersQuickFix.kt | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractMembersQuickFix.kt b/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractMembersQuickFix.kt index 769cd14b1..7603e6f43 100644 --- a/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractMembersQuickFix.kt +++ b/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractMembersQuickFix.kt @@ -44,25 +44,25 @@ class ImplementAbstractMembersQuickFix : QuickFix { // If the client side and the server side diagnostics contain a valid diagnostic for this range. if (diagnostic != null && anyDiagnosticMatch(kotlinDiagnostics, startCursor, endCursor)) { - // Get the class with the missing functions + // Get the class with the missing members val kotlinClass = file.parseAtPoint(startCursor) if (kotlinClass is KtClass) { // Get the functions that need to be implemented - val functionsToImplement = getAbstractFunctionStubs(file, kotlinClass) + val membersToImplement = getAbstractMembersStubs(file, kotlinClass) val uri = file.parse.toPath().toUri().toString() - // Get the padding to be introduced before the function declarations + // Get the padding to be introduced before the member declarations val padding = getDeclarationPadding(file, kotlinClass) // Get the location where the new code will be placed - val newFunctionStartPosition = getNewFunctionStartPosition(file, kotlinClass) - val bodyAppendBeginning = listOf(TextEdit(Range(newFunctionStartPosition, newFunctionStartPosition), "{")).takeIf { kotlinClass.hasNoBody() } ?: emptyList() - val bodyAppendEnd = listOf(TextEdit(Range(newFunctionStartPosition, newFunctionStartPosition), System.lineSeparator() + "}")).takeIf { kotlinClass.hasNoBody() } ?: emptyList() + val newMembersStartPosition = getNewMembersStartPosition(file, kotlinClass) + val bodyAppendBeginning = listOf(TextEdit(Range(newMembersStartPosition, newMembersStartPosition), "{")).takeIf { kotlinClass.hasNoBody() } ?: emptyList() + val bodyAppendEnd = listOf(TextEdit(Range(newMembersStartPosition, newMembersStartPosition), System.lineSeparator() + "}")).takeIf { kotlinClass.hasNoBody() } ?: emptyList() - val textEdits = bodyAppendBeginning + functionsToImplement.map { - // We leave two new lines before the function is inserted + val textEdits = bodyAppendBeginning + membersToImplement.map { + // We leave two new lines before the member is inserted val newText = System.lineSeparator() + System.lineSeparator() + padding + it - TextEdit(Range(newFunctionStartPosition, newFunctionStartPosition), newText) + TextEdit(Range(newMembersStartPosition, newMembersStartPosition), newText) } + bodyAppendEnd val codeAction = CodeAction() @@ -83,7 +83,7 @@ fun findDiagnosticMatch(diagnostics: List, range: Range) = private fun anyDiagnosticMatch(diagnostics: Diagnostics, startCursor: Int, endCursor: Int) = diagnostics.any { diagnosticMatch(it, startCursor, endCursor, hashSetOf("ABSTRACT_MEMBER_NOT_IMPLEMENTED", "ABSTRACT_CLASS_MEMBER_NOT_IMPLEMENTED")) } -private fun getAbstractFunctionStubs(file: CompiledFile, kotlinClass: KtClass) = +private fun getAbstractMembersStubs(file: CompiledFile, kotlinClass: KtClass) = // For each of the super types used by this class kotlinClass.superTypeListEntries.mapNotNull { // Find the definition of this super type @@ -214,12 +214,12 @@ private fun getDeclarationPadding(file: CompiledFile, kotlinClass: KtClass): Str return " ".repeat(paddingSize) } -private fun getNewFunctionStartPosition(file: CompiledFile, kotlinClass: KtClass): Position? = - // If the class is not empty, the new function will be put right after the last declaration +private fun getNewMembersStartPosition(file: CompiledFile, kotlinClass: KtClass): Position? = + // If the class is not empty, the new member will be put right after the last declaration if (kotlinClass.declarations.isNotEmpty()) { val lastFunctionEndOffset = kotlinClass.declarations.last().endOffset position(file.content, lastFunctionEndOffset) - } else { // Otherwise, the function is put at the beginning of the class + } else { // Otherwise, the member is put at the beginning of the class val body = kotlinClass.body if (body != null) { position(file.content, body.startOffset + 1) From ffd78c9064c887330fadd20ec5fea8b6c049011f Mon Sep 17 00:00:00 2001 From: Omar El Halabi Date: Sun, 3 Jul 2022 23:44:19 +0200 Subject: [PATCH 25/46] Add test for Maven workspace --- .../kotlin/org/javacs/kt/ClassPathTest.kt | 13 ++++++++ .../src/test/resources/mavenWorkspace/pom.xml | 18 ++++++++++ .../src/main/java/com/example/App.java | 10 ++++++ .../src/test/java/com/example/AppTest.java | 33 +++++++++++++++++++ 4 files changed, 74 insertions(+) create mode 100644 server/src/test/resources/mavenWorkspace/pom.xml create mode 100644 server/src/test/resources/mavenWorkspace/src/main/java/com/example/App.java create mode 100644 server/src/test/resources/mavenWorkspace/src/test/java/com/example/AppTest.java diff --git a/server/src/test/kotlin/org/javacs/kt/ClassPathTest.kt b/server/src/test/kotlin/org/javacs/kt/ClassPathTest.kt index a118efdb5..386db010a 100644 --- a/server/src/test/kotlin/org/javacs/kt/ClassPathTest.kt +++ b/server/src/test/kotlin/org/javacs/kt/ClassPathTest.kt @@ -28,6 +28,19 @@ class ClassPathTest { assertThat(classPath, hasItem(containsString("junit"))) } + @Test fun `find maven classpath`() { + val workspaceRoot = testResourcesRoot().resolve("mavenWorkspace") + val buildFile = workspaceRoot.resolve("pom.xml") + + assertTrue(Files.exists(buildFile)) + + val resolvers = defaultClassPathResolver(listOf(workspaceRoot)) + print(resolvers) + val classPath = resolvers.classpathOrEmpty.map { it.toString() } + + assertThat(classPath, hasItem(containsString("junit"))) + } + @Test fun `find kotlin stdlib`() { assertThat(findKotlinStdlib(), notNullValue()) } diff --git a/server/src/test/resources/mavenWorkspace/pom.xml b/server/src/test/resources/mavenWorkspace/pom.xml new file mode 100644 index 000000000..8e47afbee --- /dev/null +++ b/server/src/test/resources/mavenWorkspace/pom.xml @@ -0,0 +1,18 @@ + + 4.0.0 + com.example + test-project + jar + 1.0-SNAPSHOT + test-project + http://maven.apache.org + + + junit + junit + 3.8.1 + test + + + diff --git a/server/src/test/resources/mavenWorkspace/src/main/java/com/example/App.java b/server/src/test/resources/mavenWorkspace/src/main/java/com/example/App.java new file mode 100644 index 000000000..daa6d0859 --- /dev/null +++ b/server/src/test/resources/mavenWorkspace/src/main/java/com/example/App.java @@ -0,0 +1,10 @@ +package com.example; + +/** + * Hello world! + */ +public class App { + public static void main(String[] args) { + System.out.println("Hello World!"); + } +} diff --git a/server/src/test/resources/mavenWorkspace/src/test/java/com/example/AppTest.java b/server/src/test/resources/mavenWorkspace/src/test/java/com/example/AppTest.java new file mode 100644 index 000000000..0ac8f7895 --- /dev/null +++ b/server/src/test/resources/mavenWorkspace/src/test/java/com/example/AppTest.java @@ -0,0 +1,33 @@ +package com.example; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Unit test for simple App. + */ +public class AppTest extends TestCase { + /** + * Create the test case + * + * @param testName name of the test case + */ + public AppTest(String testName) { + super(testName); + } + + /** + * @return the suite of tests being tested + */ + public static Test suite() { + return new TestSuite(AppTest.class); + } + + /** + * Rigorous Test :-) + */ + public void testApp() { + assertTrue(true); + } +} From b0f06ded2ea61b0fc201272c149934ffca9aa68e Mon Sep 17 00:00:00 2001 From: Omar El Halabi Date: Tue, 19 Jul 2022 11:42:31 +0300 Subject: [PATCH 26/46] Upgrade lsp4j version to 0.14.0 --- gradle.properties | 2 +- .../javacs/kt/KotlinTextDocumentService.kt | 1 + .../org/javacs/kt/KotlinWorkspaceService.kt | 11 ++++------ .../kotlin/org/javacs/kt/symbols/Symbols.kt | 20 ++++++++++--------- .../org/javacs/kt/WorkspaceSymbolsTest.kt | 3 +-- 5 files changed, 18 insertions(+), 19 deletions(-) diff --git a/gradle.properties b/gradle.properties index e0aa80117..68abee22c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ projectVersion=1.3.2 kotlinVersion=1.6.10 exposedVersion=0.37.3 -lsp4jVersion=0.12.0 +lsp4jVersion=0.14.0 javaVersion=11 diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinTextDocumentService.kt b/server/src/main/kotlin/org/javacs/kt/KotlinTextDocumentService.kt index d74d84540..1b483632a 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinTextDocumentService.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinTextDocumentService.kt @@ -156,6 +156,7 @@ class KotlinTextDocumentService( TODO("not implemented") } + @Suppress("DEPRECATION") override fun documentSymbol(params: DocumentSymbolParams): CompletableFuture>> = async.compute { LOG.info("Find symbols in {}", describeURI(params.textDocument.uri)) diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt b/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt index 470853cc4..766f76a5a 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt @@ -1,6 +1,5 @@ package org.javacs.kt -import com.intellij.openapi.project.Project import org.eclipse.lsp4j.* import org.eclipse.lsp4j.services.WorkspaceService import org.eclipse.lsp4j.services.LanguageClient @@ -9,12 +8,9 @@ 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.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.resolve.resolveMain -import java.net.URI import java.nio.file.Paths import java.util.concurrent.CompletableFuture import com.google.gson.JsonElement @@ -30,7 +26,7 @@ class KotlinWorkspaceService( ) : WorkspaceService, LanguageClientAware { private val gson = Gson() private var languageClient: LanguageClient? = null - + override fun connect(client: LanguageClient): Unit { languageClient = client } @@ -143,10 +139,11 @@ class KotlinWorkspaceService( LOG.info("Updated configuration: {}", settings) } - override fun symbol(params: WorkspaceSymbolParams): CompletableFuture> { + @Suppress("DEPRECATION") + override fun symbol(params: WorkspaceSymbolParams): CompletableFuture, List>> { val result = workspaceSymbols(params.query, sp) - return CompletableFuture.completedFuture(result) + return CompletableFuture.completedFuture(Either.forRight(result)) } override fun didChangeWorkspaceFolders(params: DidChangeWorkspaceFoldersParams) { diff --git a/server/src/main/kotlin/org/javacs/kt/symbols/Symbols.kt b/server/src/main/kotlin/org/javacs/kt/symbols/Symbols.kt index 8255992aa..cee4dbe42 100644 --- a/server/src/main/kotlin/org/javacs/kt/symbols/Symbols.kt +++ b/server/src/main/kotlin/org/javacs/kt/symbols/Symbols.kt @@ -1,10 +1,13 @@ +@file:Suppress("DEPRECATION") + package org.javacs.kt.symbols import com.intellij.psi.PsiElement -import org.eclipse.lsp4j.Location import org.eclipse.lsp4j.SymbolInformation import org.eclipse.lsp4j.SymbolKind import org.eclipse.lsp4j.DocumentSymbol +import org.eclipse.lsp4j.WorkspaceSymbol +import org.eclipse.lsp4j.WorkspaceSymbolLocation import org.eclipse.lsp4j.jsonrpc.messages.Either import org.javacs.kt.SourcePath import org.javacs.kt.position.range @@ -15,7 +18,7 @@ import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.psiUtil.parents fun documentSymbols(file: KtFile): List> = - doDocumentSymbols(file).map { Either.forRight(it) } + doDocumentSymbols(file).map { Either.forRight(it) } private fun doDocumentSymbols(element: PsiElement): List { val children = element.children.flatMap(::doDocumentSymbols) @@ -30,10 +33,10 @@ private fun doDocumentSymbols(element: PsiElement): List { } ?: children } -fun workspaceSymbols(query: String, sp: SourcePath): List = +fun workspaceSymbols(query: String, sp: SourcePath): List = doWorkspaceSymbols(sp) .filter { containsCharactersInOrder(it.name!!, query, false) } - .mapNotNull(::symbolInformation) + .mapNotNull(::workspaceSymbol) .toList() private fun doWorkspaceSymbols(sp: SourcePath): Sequence = @@ -53,10 +56,10 @@ private fun pickImportantElements(node: PsiElement, includeLocals: Boolean): KtN else -> null } -private fun symbolInformation(d: KtNamedDeclaration): SymbolInformation? { +private fun workspaceSymbol(d: KtNamedDeclaration): WorkspaceSymbol? { val name = d.name ?: return null - return SymbolInformation(name, symbolKind(d), symbolLocation(d), symbolContainer(d)) + return WorkspaceSymbol(name, symbolKind(d), Either.forRight(workspaceLocation(d)), symbolContainer(d)) } private fun symbolKind(d: KtNamedDeclaration): SymbolKind = @@ -70,12 +73,11 @@ private fun symbolKind(d: KtNamedDeclaration): SymbolKind = else -> throw IllegalArgumentException("Unexpected symbol $d") } -private fun symbolLocation(d: KtNamedDeclaration): Location { +private fun workspaceLocation(d: KtNamedDeclaration): WorkspaceSymbolLocation { val file = d.containingFile val uri = file.toPath().toUri().toString() - val range = range(file.text, d.textRange) - return Location(uri, range) + return WorkspaceSymbolLocation(uri) } private fun symbolContainer(d: KtNamedDeclaration): String? = diff --git a/server/src/test/kotlin/org/javacs/kt/WorkspaceSymbolsTest.kt b/server/src/test/kotlin/org/javacs/kt/WorkspaceSymbolsTest.kt index 80e70bc1d..5c4ebf2fa 100644 --- a/server/src/test/kotlin/org/javacs/kt/WorkspaceSymbolsTest.kt +++ b/server/src/test/kotlin/org/javacs/kt/WorkspaceSymbolsTest.kt @@ -5,12 +5,11 @@ import org.eclipse.lsp4j.WorkspaceSymbolParams import org.hamcrest.Matchers.hasItem import org.hamcrest.Matchers.not import org.junit.Assert.assertThat -import org.junit.Before import org.junit.Test class WorkspaceSymbolsTest : SingleFileTestFixture("symbols", "DocumentSymbols.kt") { @Test fun `find symbols in OtherFileSymbols`() { - val found = languageServer.workspaceService.symbol(WorkspaceSymbolParams("")).get() + val found = languageServer.workspaceService.symbol(WorkspaceSymbolParams("")).get().right val byKind = found.groupBy({ it.kind }, { it.name }) val all = found.map { it.name }.toList() From 4e0260ab9d7d04f70e78f6a75c15fa559b8557c1 Mon Sep 17 00:00:00 2001 From: Omar El Halabi Date: Tue, 19 Jul 2022 12:04:23 +0300 Subject: [PATCH 27/46] Upgrade Gradle version to 7.5 --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 84d1f85fd..8049c684f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 79cbf3a8e86fa121561d0875288f102f7912cdb1 Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Mon, 1 Aug 2022 16:11:06 +0200 Subject: [PATCH 28/46] Resolve kotlin multiplatform dependencies for gradle, basic functionality --- .../resources/projectClassPathFinder.gradle | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/shared/src/main/resources/projectClassPathFinder.gradle b/shared/src/main/resources/projectClassPathFinder.gradle index 7ef0a56b3..e9c957aa6 100644 --- a/shared/src/main/resources/projectClassPathFinder.gradle +++ b/shared/src/main/resources/projectClassPathFinder.gradle @@ -58,6 +58,23 @@ allprojects { project -> } } } + + + // handle kotlin multiplatform style dependencies if any + def kotlinExtension = project.extensions.findByName("kotlin") + if(kotlinExtension) { + def kotlinSourceSets = kotlinExtension.sourceSets + + // Print the list of all dependencies jar files. + kotlinSourceSets.forEach { + // fetch the jar files from the current source set config (identified by their displayname, e.g, backendMain) + def dependencyLocations = configurations["${it.displayName}ImplementationDependenciesMetadata"].files + + dependencyLocations.each { + System.out.println "kotlin-lsp-gradle $it" + } + } + } } } From 5fb055a5d06efc80e47de23bc57e725561471552 Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Mon, 1 Aug 2022 17:46:28 +0200 Subject: [PATCH 29/46] Improved kmp dependency resolution solution that works without any hazzle --- .../src/main/resources/projectClassPathFinder.gradle | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/shared/src/main/resources/projectClassPathFinder.gradle b/shared/src/main/resources/projectClassPathFinder.gradle index e9c957aa6..7fce53927 100644 --- a/shared/src/main/resources/projectClassPathFinder.gradle +++ b/shared/src/main/resources/projectClassPathFinder.gradle @@ -62,15 +62,13 @@ allprojects { project -> // handle kotlin multiplatform style dependencies if any def kotlinExtension = project.extensions.findByName("kotlin") - if(kotlinExtension) { + if(kotlinExtension && kotlinExtension.hasProperty("targets")) { def kotlinSourceSets = kotlinExtension.sourceSets // Print the list of all dependencies jar files. - kotlinSourceSets.forEach { - // fetch the jar files from the current source set config (identified by their displayname, e.g, backendMain) - def dependencyLocations = configurations["${it.displayName}ImplementationDependenciesMetadata"].files - - dependencyLocations.each { + kotlinExtension.targets.names.each { + def classpath = configurations["${it}CompileClasspath"] + classpath.files.each { System.out.println "kotlin-lsp-gradle $it" } } From 5d49b80fe3007e335e89a1c00a569af3886da45a Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Tue, 2 Aug 2022 20:10:40 +0200 Subject: [PATCH 30/46] Skeleton of new general override members implementation. #359 --- .../kt/KotlinProtocolExtensionService.kt | 10 +++ .../org/javacs/kt/KotlinProtocolExtensions.kt | 5 ++ .../kt/overridemembers/OverrideMembers.kt | 11 +++ .../org/javacs/kt/OverrideMemberTest.kt | 77 +++++++++++++++++++ .../overridemember/OverrideMembers.kt | 21 +++++ 5 files changed, 124 insertions(+) create mode 100644 server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt create mode 100644 server/src/test/kotlin/org/javacs/kt/OverrideMemberTest.kt create mode 100644 server/src/test/resources/overridemember/OverrideMembers.kt diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensionService.kt b/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensionService.kt index 4fa37acf7..131a39375 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensionService.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensionService.kt @@ -4,6 +4,8 @@ import org.eclipse.lsp4j.* import org.javacs.kt.util.AsyncExecutor import org.javacs.kt.util.parseURI import org.javacs.kt.resolve.resolveMain +import org.javacs.kt.position.offset +import org.javacs.kt.listOverridableMembers import java.util.concurrent.CompletableFuture import java.nio.file.Paths @@ -39,4 +41,12 @@ class KotlinProtocolExtensionService( "projectRoot" to workspacePath ) } + + override fun overrideMember(position: TextDocumentPositionParams): CompletableFuture> = async.compute { + val fileUri = parseURI(position.textDocument.uri) + val compiledFile = sp.currentVersion(fileUri) + val cursorOffset = offset(compiledFile.content, position.position) + + listOverridableMembers(compiledFile, cursorOffset) + } } diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensions.kt b/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensions.kt index 808ba0dce..a599f11f4 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensions.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensions.kt @@ -15,4 +15,9 @@ interface KotlinProtocolExtensions { @JsonRequest fun mainClass(textDocument: TextDocumentIdentifier): CompletableFuture> + + // TODO: what is the best return value in this case? CodeAction? + // TODO: should the naming be something like listOverrideableMembers? or something similar instead? + @JsonRequest + fun overrideMember(position: TextDocumentPositionParams): CompletableFuture> } diff --git a/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt b/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt new file mode 100644 index 000000000..59afa5a6d --- /dev/null +++ b/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt @@ -0,0 +1,11 @@ +package org.javacs.kt + +import org.eclipse.lsp4j.Range +import org.eclipse.lsp4j.CodeAction + + +fun listOverridableMembers(file: CompiledFile, cursor: Int): List { + // TODO: implement + + return listOf() +} diff --git a/server/src/test/kotlin/org/javacs/kt/OverrideMemberTest.kt b/server/src/test/kotlin/org/javacs/kt/OverrideMemberTest.kt new file mode 100644 index 000000000..658bab353 --- /dev/null +++ b/server/src/test/kotlin/org/javacs/kt/OverrideMemberTest.kt @@ -0,0 +1,77 @@ +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.eclipse.lsp4j.TextDocumentPositionParams +import org.junit.Test +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.hasSize +import org.junit.Assert.assertThat + +// TODO: what should the title be? just the signature? or the name of the member? should we separate between methods and variables? + +class OverrideMemberTest : SingleFileTestFixture("overridemember", "OverrideMembers.kt") { + + val root = testResourcesRoot().resolve(workspaceRoot) + val fileUri = root.resolve(file).toUri().toString() + + @Test + fun `should show all overrides for class`() { + val result = languageServer.getProtocolExtensionService().overrideMember(TextDocumentPositionParams(TextDocumentIdentifier(fileUri), position(9, 8))).get() + + assertThat(result, hasSize(2)) + + val firstCodeAction = result[0] + assertThat(firstCodeAction.title, equalTo("text")) + + val firstTextEdit = firstCodeAction.edit.changes + assertThat(firstTextEdit.containsKey(fileUri), equalTo(true)) + assertThat(firstTextEdit[fileUri], hasSize(1)) + + val memberToImplementEdit = firstTextEdit[fileUri]?.get(0) + assertThat(memberToImplementEdit?.range, equalTo(range(9, 32, 9, 32))) + assertThat(memberToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override val text: String = ???")) + + + val secondCodeAction = result[1] + assertThat(secondCodeAction.title, equalTo("print")) + + val secondTextEdit = secondCodeAction.edit.changes + assertThat(secondTextEdit.containsKey(fileUri), equalTo(true)) + assertThat(secondTextEdit[fileUri], hasSize(1)) + + val functionToImplementEdit = secondTextEdit[fileUri]?.get(0) + assertThat(functionToImplementEdit?.range, equalTo(range(9, 32, 9, 32))) + assertThat(functionToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun print() {}")) + } + + @Test + fun `should show one override for class where other alternatives are already implemented`() { + val result = languageServer.getProtocolExtensionService().overrideMember(TextDocumentPositionParams(TextDocumentIdentifier(fileUri), position(11, 8))).get() + + assertThat(result, hasSize(1)) + + val codeAction = result[0] + assertThat(codeAction.title, equalTo("print")) + + val textEdit = codeAction.edit.changes + assertThat(textEdit.containsKey(fileUri), equalTo(true)) + assertThat(textEdit[fileUri], hasSize(1)) + + val functionToImplementEdit = textEdit[fileUri]?.get(0) + assertThat(functionToImplementEdit?.range, equalTo(range(12, 57, 12, 57))) + assertThat(functionToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun print() {}")) + } + + @Test + fun `should show NO overrides for class where all other alternatives are already implemented`() { + val result = languageServer.getProtocolExtensionService().overrideMember(TextDocumentPositionParams(TextDocumentIdentifier(fileUri), position(15, 8))).get() + + assertThat(result, hasSize(0)) + } + + // TODO: test for kotlin sdk and jdk classes to verify that it works +} diff --git a/server/src/test/resources/overridemember/OverrideMembers.kt b/server/src/test/resources/overridemember/OverrideMembers.kt new file mode 100644 index 000000000..2772b6338 --- /dev/null +++ b/server/src/test/resources/overridemember/OverrideMembers.kt @@ -0,0 +1,21 @@ +interface Printable { + val text: String + + fun print() { + println("not implemented yet yo") + } +} + +class MyPrintable: Printable {} + +class OtherPrintable: Printable { + override val text: String = "you had me at lasagna" +} + +class CompletePrintable: Printable { + override val text: String = "something something something darkside" + + override fun print() { + println("not implemented yet yo") + } +} From 5207d575ce6710e98e75997fccf9537002ae38db Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Wed, 3 Aug 2022 19:14:25 +0200 Subject: [PATCH 31/46] Implement most of the general override functionality. Stubborn equals method --- .../kt/KotlinProtocolExtensionService.kt | 2 +- .../ImplementAbstractMembersQuickFix.kt | 132 +------- .../kt/overridemembers/OverrideMembers.kt | 298 +++++++++++++++++- .../org/javacs/kt/OverrideMemberTest.kt | 95 ++++-- .../overridemember/OverrideMembers.kt | 16 + 5 files changed, 380 insertions(+), 163 deletions(-) diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensionService.kt b/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensionService.kt index 131a39375..76428a510 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensionService.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensionService.kt @@ -5,7 +5,7 @@ import org.javacs.kt.util.AsyncExecutor import org.javacs.kt.util.parseURI import org.javacs.kt.resolve.resolveMain import org.javacs.kt.position.offset -import org.javacs.kt.listOverridableMembers +import org.javacs.kt.overridemembers.listOverridableMembers import java.util.concurrent.CompletableFuture import java.nio.file.Paths diff --git a/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractMembersQuickFix.kt b/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractMembersQuickFix.kt index 7603e6f43..131208aa2 100644 --- a/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractMembersQuickFix.kt +++ b/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractMembersQuickFix.kt @@ -7,6 +7,14 @@ import org.javacs.kt.index.SymbolIndex import org.javacs.kt.position.offset import org.javacs.kt.position.position import org.javacs.kt.util.toPath +import org.javacs.kt.overridemembers.createFunctionStub +import org.javacs.kt.overridemembers.createVariableStub +import org.javacs.kt.overridemembers.getClassDescriptor +import org.javacs.kt.overridemembers.getDeclarationPadding +import org.javacs.kt.overridemembers.getNewMembersStartPosition +import org.javacs.kt.overridemembers.getSuperClassTypeProjections +import org.javacs.kt.overridemembers.hasNoBody +import org.javacs.kt.overridemembers.overridesDeclaration import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.ClassConstructorDescriptor import org.jetbrains.kotlin.descriptors.DeclarationDescriptor @@ -108,127 +116,3 @@ private fun getAbstractMembersStubs(file: CompiledFile, kotlinClass: KtClass) = null } }.flatten() - -// interfaces are ClassDescriptors by default. When calling AbstractClass super methods, we get a ClassConstructorDescriptor -private fun getClassDescriptor(descriptor: DeclarationDescriptor?): ClassDescriptor? = if (descriptor is ClassDescriptor) { - descriptor -} else if (descriptor is ClassConstructorDescriptor) { - descriptor.containingDeclaration -} else { - null -} - -private fun getSuperClassTypeProjections(file: CompiledFile, superType: KtSuperTypeListEntry): List = superType.typeReference?.typeElement?.children?.filter { - it is KtTypeArgumentList -}?.flatMap { - (it as KtTypeArgumentList).arguments -}?.mapNotNull { - (file.referenceExpressionAtPoint(it?.startOffset ?: 0)?.second as? ClassDescriptor)?.defaultType?.asTypeProjection() -} ?: emptyList() - -// Checks if the class overrides the given declaration -private fun overridesDeclaration(kotlinClass: KtClass, descriptor: FunctionDescriptor): Boolean = - kotlinClass.declarations.any { - if (it.name == descriptor.name.asString() && it.hasModifier(KtTokens.OVERRIDE_KEYWORD)) { - if (it is KtNamedFunction) { - parametersMatch(it, descriptor) - } else { - true - } - } else { - false - } - } - -private fun overridesDeclaration(kotlinClass: KtClass, descriptor: PropertyDescriptor): Boolean = - kotlinClass.declarations.any { - it.name == descriptor.name.asString() && it.hasModifier(KtTokens.OVERRIDE_KEYWORD) - } - -// Checks if two functions have matching parameters -private fun parametersMatch(function: KtNamedFunction, functionDescriptor: FunctionDescriptor): Boolean { - if (function.valueParameters.size == functionDescriptor.valueParameters.size) { - for (index in 0 until function.valueParameters.size) { - if (function.valueParameters[index].name != functionDescriptor.valueParameters[index].name.asString()) { - return false - } else if (function.valueParameters[index].typeReference?.typeName() != functionDescriptor.valueParameters[index].type.unwrappedType().toString()) { - // Note: Since we treat Java overrides as non nullable by default, the above test will fail when the user has made the type nullable. - // TODO: look into this - return false - } - } - - if (function.typeParameters.size == functionDescriptor.typeParameters.size) { - for (index in 0 until function.typeParameters.size) { - if (function.typeParameters[index].variance != functionDescriptor.typeParameters[index].variance) { - return false - } - } - } - - return true - } - - return false -} - -private fun KtTypeReference.typeName(): String? = this.name ?: this.typeElement?.children?.filter { - it is KtSimpleNameExpression -}?.map { - (it as KtSimpleNameExpression).getReferencedName() -}?.firstOrNull() - -private fun createFunctionStub(function: FunctionDescriptor): String { - val name = function.name - val arguments = function.valueParameters.map { argument -> - val argumentName = argument.name - val argumentType = argument.type.unwrappedType() - - "$argumentName: $argumentType" - }.joinToString(", ") - val returnType = function.returnType?.unwrappedType()?.toString()?.takeIf { "Unit" != it } - - return "override fun $name($arguments)${returnType?.let { ": $it" } ?: ""} { }" -} - -private fun createVariableStub(variable: PropertyDescriptor): String { - val variableType = variable.returnType?.unwrappedType()?.toString()?.takeIf { "Unit" != it } - return "override val ${variable.name}${variableType?.let { ": $it" } ?: ""} = TODO(\"SET VALUE\")" -} - -// about types: regular Kotlin types are marked T or T?, but types from Java are (T..T?) because nullability cannot be decided. -// Therefore we have to unpack in case we have the Java type. Fortunately, the Java types are not marked nullable, so we default to non nullable types. Let the user decide if they want nullable types instead. With this implementation Kotlin types also keeps their nullability -private fun KotlinType.unwrappedType(): KotlinType = this.unwrap().makeNullableAsSpecified(this.isMarkedNullable) - -private fun getDeclarationPadding(file: CompiledFile, kotlinClass: KtClass): String { - // If the class is not empty, the amount of padding is the same as the one in the last declaration of the class - val paddingSize = if (kotlinClass.declarations.isNotEmpty()) { - val lastFunctionStartOffset = kotlinClass.declarations.last().startOffset - position(file.content, lastFunctionStartOffset).character - } else { - // Otherwise, we just use a default tab size in addition to any existing padding - // on the class itself (note that the class could be inside another class, for example) - position(file.content, kotlinClass.startOffset).character + DEFAULT_TAB_SIZE - } - - return " ".repeat(paddingSize) -} - -private fun getNewMembersStartPosition(file: CompiledFile, kotlinClass: KtClass): Position? = - // If the class is not empty, the new member will be put right after the last declaration - if (kotlinClass.declarations.isNotEmpty()) { - val lastFunctionEndOffset = kotlinClass.declarations.last().endOffset - position(file.content, lastFunctionEndOffset) - } else { // Otherwise, the member is put at the beginning of the class - val body = kotlinClass.body - if (body != null) { - position(file.content, body.startOffset + 1) - } else { - // function has no body. We have to create one. New position is right after entire kotlin class text (with space) - val newPosCorrectLine = position(file.content, kotlinClass.startOffset + 1) - newPosCorrectLine.character = (kotlinClass.text.length + 2) - newPosCorrectLine - } - } - -private fun KtClass.hasNoBody() = null == this.body diff --git a/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt b/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt index 59afa5a6d..dd3734b37 100644 --- a/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt +++ b/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt @@ -1,11 +1,301 @@ -package org.javacs.kt +package org.javacs.kt.overridemembers -import org.eclipse.lsp4j.Range import org.eclipse.lsp4j.CodeAction +import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.Range +import org.eclipse.lsp4j.TextEdit +import org.eclipse.lsp4j.WorkspaceEdit +import org.javacs.kt.CompiledFile +import org.javacs.kt.util.toPath +import org.javacs.kt.LOG +import org.javacs.kt.position.position +import org.jetbrains.kotlin.psi.KtClass +import org.jetbrains.kotlin.psi.KtTypeArgumentList +import org.jetbrains.kotlin.psi.KtSuperTypeListEntry +import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.kotlin.psi.KtTypeReference +import org.jetbrains.kotlin.psi.KtSimpleNameExpression +import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.descriptors.ClassConstructorDescriptor +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.descriptors.isInterface +import org.jetbrains.kotlin.descriptors.PropertyDescriptor +import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi +import org.jetbrains.kotlin.psi.psiUtil.endOffset +import org.jetbrains.kotlin.psi.psiUtil.isAbstract +import org.jetbrains.kotlin.psi.psiUtil.startOffset +import org.jetbrains.kotlin.types.TypeProjection +import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.types.typeUtil.asTypeProjection +// TODO: see where this should ideally be placed +private const val DEFAULT_TAB_SIZE = 4 fun listOverridableMembers(file: CompiledFile, cursor: Int): List { - // TODO: implement + val kotlinClass = file.parseAtPoint(cursor) + + if (kotlinClass is KtClass) { + return createOverrideAlternatives(file, kotlinClass) + } - return listOf() + return emptyList() } + +private fun createOverrideAlternatives(file: CompiledFile, kotlinClass: KtClass): List { + // Get the functions that need to be implemented + val membersToImplement = getUnimplementedMembersStubs(file, kotlinClass) + + val uri = file.parse.toPath().toUri().toString() + + // Get the padding to be introduced before the member declarations + val padding = getDeclarationPadding(file, kotlinClass) + + // Get the location where the new code will be placed + val newMembersStartPosition = getNewMembersStartPosition(file, kotlinClass) + // TODO: if both used here and in the abstract member stuff.... should we put it in a method both can use? + // TODO: how should this be implemented? + // val bodyAppendBeginning = + // listOf(TextEdit(Range(newMembersStartPosition, newMembersStartPosition), "{")).takeIf { + // kotlinClass.hasNoBody() + // } + // ?: emptyList() + // val bodyAppendEnd = + // listOf( + // TextEdit( + // Range(newMembersStartPosition, newMembersStartPosition), + // System.lineSeparator() + "}") + // ) + // .takeIf { kotlinClass.hasNoBody() } + // ?: emptyList() + + LOG.info("Members: {}", membersToImplement) + + // loop through the memberstoimplement and create code actions + return membersToImplement.map { member -> + val newText = System.lineSeparator() + System.lineSeparator() + padding + member + val textEdit = TextEdit(Range(newMembersStartPosition, newMembersStartPosition), newText) + + // TODO: how should we get the name of the property? if needed? + val codeAction = CodeAction() + codeAction.edit = WorkspaceEdit(mapOf(uri to listOf(textEdit))) + codeAction.title = member + + codeAction + } +} + +// TODO: any way can repeat less code between this and the getAbstractMembersStubs in the ImplementAbstractMembersQuickfix? +private fun getUnimplementedMembersStubs(file: CompiledFile, kotlinClass: KtClass): List = + // For each of the super types used by this class + kotlinClass + .superTypeListEntries + .mapNotNull { + // Find the definition of this super type + val referenceAtPoint = file.referenceExpressionAtPoint(it.startOffset) + val descriptor = referenceAtPoint?.second + val classDescriptor = getClassDescriptor(descriptor) + + // If the super class is abstract, interface or just plain open + if (null != classDescriptor && classDescriptor.canBeExtended() + ) { + val superClassTypeArguments = getSuperClassTypeProjections(file, it) + classDescriptor + .getMemberScope(superClassTypeArguments) + .getContributedDescriptors() + .filter { classMember -> + (classMember is FunctionDescriptor && + classMember.canBeOverriden() && + !overridesDeclaration(kotlinClass, classMember)) || + (classMember is PropertyDescriptor && + classMember.canBeOverriden() && + !overridesDeclaration(kotlinClass, classMember)) + } + .mapNotNull { member -> + when (member) { + is FunctionDescriptor -> createFunctionStub(member) + is PropertyDescriptor -> createVariableStub(member) + else -> null + } + } + } else { + null + } + } + .flatten() + +private fun ClassDescriptor.canBeExtended() = this.kind.isInterface || + this.modality == Modality.ABSTRACT || + this.modality == Modality.OPEN + +private fun FunctionDescriptor.canBeOverriden() = Modality.ABSTRACT == this.modality || Modality.OPEN == this.modality + +private fun PropertyDescriptor.canBeOverriden() = Modality.ABSTRACT == this.modality || Modality.OPEN == this.modality + +// interfaces are ClassDescriptors by default. When calling AbstractClass super methods, we get a ClassConstructorDescriptor +fun getClassDescriptor(descriptor: DeclarationDescriptor?): ClassDescriptor? = + if (descriptor is ClassDescriptor) { + descriptor + } else if (descriptor is ClassConstructorDescriptor) { + descriptor.containingDeclaration + } else { + null + } + +fun getSuperClassTypeProjections( + file: CompiledFile, + superType: KtSuperTypeListEntry +): List = + superType + .typeReference + ?.typeElement + ?.children + ?.filter { it is KtTypeArgumentList } + ?.flatMap { (it as KtTypeArgumentList).arguments } + ?.mapNotNull { + (file.referenceExpressionAtPoint(it?.startOffset ?: 0)?.second as? + ClassDescriptor) + ?.defaultType?.asTypeProjection() + } + ?: emptyList() + +// Checks if the class overrides the given declaration +fun overridesDeclaration(kotlinClass: KtClass, descriptor: FunctionDescriptor): Boolean = + kotlinClass.declarations.any { + if (it.name == descriptor.name.asString() && it.hasModifier(KtTokens.OVERRIDE_KEYWORD) + ) { + if (it is KtNamedFunction) { + parametersMatch(it, descriptor) + } else { + true + } + } else { + false + } + } + +fun overridesDeclaration(kotlinClass: KtClass, descriptor: PropertyDescriptor): Boolean = + kotlinClass.declarations.any { + it.name == descriptor.name.asString() && it.hasModifier(KtTokens.OVERRIDE_KEYWORD) + } + +// Checks if two functions have matching parameters +private fun parametersMatch( + function: KtNamedFunction, + functionDescriptor: FunctionDescriptor +): Boolean { + if (function.valueParameters.size == functionDescriptor.valueParameters.size) { + for (index in 0 until function.valueParameters.size) { + LOG.info("Method: {} {} - {} {}", function.valueParameters[index].name, functionDescriptor.valueParameters[index].name.asString(), function.valueParameters[index].typeReference?.typeName(), functionDescriptor.valueParameters[index] + .type + .unwrappedType() + .toString()) + if (function.valueParameters[index].name != + functionDescriptor.valueParameters[index].name.asString() + ) { + return false + } else if (function.valueParameters[index].typeReference?.typeName() != + functionDescriptor.valueParameters[index] + .type + .unwrappedType() + .toString() + ) { + // Note: Since we treat Java overrides as non nullable by default, the above test + // will fail when the user has made the type nullable. + // TODO: look into this + // TODO: look into the weird issue with equals... + return false + } + } + + if (function.typeParameters.size == functionDescriptor.typeParameters.size) { + for (index in 0 until function.typeParameters.size) { + if (function.typeParameters[index].variance != + functionDescriptor.typeParameters[index].variance + ) { + return false + } + } + } + + return true + } + + return false +} + +private fun KtTypeReference.typeName(): String? = + this.name + ?: this.typeElement + ?.children + ?.filter { it is KtSimpleNameExpression } + ?.map { (it as KtSimpleNameExpression).getReferencedName() } + ?.firstOrNull() + +fun createFunctionStub(function: FunctionDescriptor): String { + val name = function.name + val arguments = + function.valueParameters + .map { argument -> + val argumentName = argument.name + val argumentType = argument.type.unwrappedType() + + "$argumentName: $argumentType" + } + .joinToString(", ") + val returnType = function.returnType?.unwrappedType()?.toString()?.takeIf { "Unit" != it } + + return "override fun $name($arguments)${returnType?.let { ": $it" } ?: ""} { }" +} + +fun createVariableStub(variable: PropertyDescriptor): String { + val variableType = variable.returnType?.unwrappedType()?.toString()?.takeIf { "Unit" != it } + return "override val ${variable.name}${variableType?.let { ": $it" } ?: ""} = TODO(\"SET VALUE\")" +} + +// about types: regular Kotlin types are marked T or T?, but types from Java are (T..T?) because +// nullability cannot be decided. +// Therefore we have to unpack in case we have the Java type. Fortunately, the Java types are not +// marked nullable, so we default to non nullable types. Let the user decide if they want nullable +// types instead. With this implementation Kotlin types also keeps their nullability +private fun KotlinType.unwrappedType(): KotlinType = + this.unwrap().makeNullableAsSpecified(this.isMarkedNullable) + +fun getDeclarationPadding(file: CompiledFile, kotlinClass: KtClass): String { + // If the class is not empty, the amount of padding is the same as the one in the last + // declaration of the class + val paddingSize = + if (kotlinClass.declarations.isNotEmpty()) { + val lastFunctionStartOffset = kotlinClass.declarations.last().startOffset + position(file.content, lastFunctionStartOffset).character + } else { + // Otherwise, we just use a default tab size in addition to any existing padding + // on the class itself (note that the class could be inside another class, for + // example) + position(file.content, kotlinClass.startOffset).character + DEFAULT_TAB_SIZE + } + + return " ".repeat(paddingSize) +} + +fun getNewMembersStartPosition(file: CompiledFile, kotlinClass: KtClass): Position? = + // If the class is not empty, the new member will be put right after the last declaration + if (kotlinClass.declarations.isNotEmpty()) { + val lastFunctionEndOffset = kotlinClass.declarations.last().endOffset + position(file.content, lastFunctionEndOffset) + } else { // Otherwise, the member is put at the beginning of the class + val body = kotlinClass.body + if (body != null) { + position(file.content, body.startOffset + 1) + } else { + // function has no body. We have to create one. New position is right after entire + // kotlin class text (with space) + val newPosCorrectLine = position(file.content, kotlinClass.startOffset + 1) + newPosCorrectLine.character = (kotlinClass.text.length + 2) + newPosCorrectLine + } + } + +fun KtClass.hasNoBody() = null == this.body diff --git a/server/src/test/kotlin/org/javacs/kt/OverrideMemberTest.kt b/server/src/test/kotlin/org/javacs/kt/OverrideMemberTest.kt index 658bab353..358755aab 100644 --- a/server/src/test/kotlin/org/javacs/kt/OverrideMemberTest.kt +++ b/server/src/test/kotlin/org/javacs/kt/OverrideMemberTest.kt @@ -7,11 +7,14 @@ import org.eclipse.lsp4j.Range import org.eclipse.lsp4j.TextDocumentIdentifier import org.eclipse.lsp4j.TextDocumentPositionParams import org.junit.Test +import org.hamcrest.core.Every.everyItem +import org.hamcrest.Matchers.containsInAnyOrder import org.hamcrest.Matchers.equalTo import org.hamcrest.Matchers.hasSize import org.junit.Assert.assertThat // TODO: what should the title be? just the signature? or the name of the member? should we separate between methods and variables? +// easiest is probably just to show the signatures? class OverrideMemberTest : SingleFileTestFixture("overridemember", "OverrideMembers.kt") { @@ -21,49 +24,50 @@ class OverrideMemberTest : SingleFileTestFixture("overridemember", "OverrideMemb @Test fun `should show all overrides for class`() { val result = languageServer.getProtocolExtensionService().overrideMember(TextDocumentPositionParams(TextDocumentIdentifier(fileUri), position(9, 8))).get() - - assertThat(result, hasSize(2)) - - val firstCodeAction = result[0] - assertThat(firstCodeAction.title, equalTo("text")) - - val firstTextEdit = firstCodeAction.edit.changes - assertThat(firstTextEdit.containsKey(fileUri), equalTo(true)) - assertThat(firstTextEdit[fileUri], hasSize(1)) - - val memberToImplementEdit = firstTextEdit[fileUri]?.get(0) - assertThat(memberToImplementEdit?.range, equalTo(range(9, 32, 9, 32))) - assertThat(memberToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override val text: String = ???")) + val titles = result.map { it.title } + val edits = result.flatMap { it.edit.changes[fileUri]!! } + val newTexts = edits.map { it.newText } + val ranges = edits.map { it.range } - val secondCodeAction = result[1] - assertThat(secondCodeAction.title, equalTo("print")) + assertThat(titles, containsInAnyOrder("override val text: String = TODO(\"SET VALUE\")", + "override fun print() { }", + "override fun equals(other: Any?): Boolean { }", + "override fun hashCode(): Int { }", + "override fun toString(): String { }")) - val secondTextEdit = secondCodeAction.edit.changes - assertThat(secondTextEdit.containsKey(fileUri), equalTo(true)) - assertThat(secondTextEdit[fileUri], hasSize(1)) + val padding = System.lineSeparator() + System.lineSeparator() + " " + assertThat(newTexts, containsInAnyOrder(padding + "override val text: String = TODO(\"SET VALUE\")", + padding + "override fun print() { }", + padding + "override fun equals(other: Any?): Boolean { }", + padding + "override fun hashCode(): Int { }", + padding + "override fun toString(): String { }")) - val functionToImplementEdit = secondTextEdit[fileUri]?.get(0) - assertThat(functionToImplementEdit?.range, equalTo(range(9, 32, 9, 32))) - assertThat(functionToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun print() {}")) + + assertThat(ranges, everyItem(equalTo(range(9, 31, 9, 31)))) } @Test - fun `should show one override for class where other alternatives are already implemented`() { + fun `should show one less override for class where one member is already implemented`() { val result = languageServer.getProtocolExtensionService().overrideMember(TextDocumentPositionParams(TextDocumentIdentifier(fileUri), position(11, 8))).get() - - assertThat(result, hasSize(1)) - - val codeAction = result[0] - assertThat(codeAction.title, equalTo("print")) - - val textEdit = codeAction.edit.changes - assertThat(textEdit.containsKey(fileUri), equalTo(true)) - assertThat(textEdit[fileUri], hasSize(1)) + + val titles = result.map { it.title } + val edits = result.flatMap { it.edit.changes[fileUri]!! } + val newTexts = edits.map { it.newText } + val ranges = edits.map { it.range } + + assertThat(titles, containsInAnyOrder("override fun print() { }", + "override fun equals(other: Any?): Boolean { }", + "override fun hashCode(): Int { }", + "override fun toString(): String { }")) + + val padding = System.lineSeparator() + System.lineSeparator() + " " + assertThat(newTexts, containsInAnyOrder(padding + "override fun print() { }", + padding + "override fun equals(other: Any?): Boolean { }", + padding + "override fun hashCode(): Int { }", + padding + "override fun toString(): String { }")) - val functionToImplementEdit = textEdit[fileUri]?.get(0) - assertThat(functionToImplementEdit?.range, equalTo(range(12, 57, 12, 57))) - assertThat(functionToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun print() {}")) + assertThat(ranges, everyItem(equalTo(range(12, 56, 12, 56)))) } @Test @@ -73,5 +77,28 @@ class OverrideMemberTest : SingleFileTestFixture("overridemember", "OverrideMemb assertThat(result, hasSize(0)) } + @Test + fun `should find method in open class`() { + val result = languageServer.getProtocolExtensionService().overrideMember(TextDocumentPositionParams(TextDocumentIdentifier(fileUri), position(37, 8))).get() + + val titles = result.map { it.title } + val edits = result.flatMap { it.edit.changes[fileUri]!! } + val newTexts = edits.map { it.newText } + val ranges = edits.map { it.range } + + assertThat(titles, containsInAnyOrder("override fun numOpenDoorsWithName(input: String): Int { }", + "override fun equals(other: Any?): Boolean { }", + "override fun hashCode(): Int { }", + "override fun toString(): String { }")) + + val padding = System.lineSeparator() + System.lineSeparator() + " " + assertThat(newTexts, containsInAnyOrder(padding + "override fun numOpenDoorsWithName(input: String): Int { }", + padding + "override fun equals(other: Any?): Boolean { }", + padding + "override fun hashCode(): Int { }", + padding + "override fun toString(): String { }")) + + assertThat(ranges, everyItem(equalTo(range(37, 25, 37, 25)))) + } + // TODO: test for kotlin sdk and jdk classes to verify that it works } diff --git a/server/src/test/resources/overridemember/OverrideMembers.kt b/server/src/test/resources/overridemember/OverrideMembers.kt index 2772b6338..1d3434321 100644 --- a/server/src/test/resources/overridemember/OverrideMembers.kt +++ b/server/src/test/resources/overridemember/OverrideMembers.kt @@ -15,7 +15,23 @@ class OtherPrintable: Printable { class CompletePrintable: Printable { override val text: String = "something something something darkside" + override fun equals(other: Any?): Boolean { return true } + + override fun hashCode(): Int { return 1 } + + override fun toString(): String { + return "something something complete" + } + override fun print() { println("not implemented yet yo") } } + +open class MyOpen { + open fun numOpenDoorsWithName(input: String): Int { + return 2 + } +} + +class Closed: MyOpen() {} From 52f80e927720ec05d4f383be72ad702df1643615 Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Wed, 3 Aug 2022 19:33:23 +0200 Subject: [PATCH 32/46] Fix weird issue with overriding equals --- .../javacs/kt/overridemembers/OverrideMembers.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt b/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt index dd3734b37..6dfef58c6 100644 --- a/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt +++ b/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt @@ -26,6 +26,7 @@ import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi import org.jetbrains.kotlin.psi.psiUtil.endOffset import org.jetbrains.kotlin.psi.psiUtil.isAbstract import org.jetbrains.kotlin.psi.psiUtil.startOffset +import org.jetbrains.kotlin.psi.psiUtil.unwrapNullability import org.jetbrains.kotlin.types.TypeProjection import org.jetbrains.kotlin.types.KotlinType import org.jetbrains.kotlin.lexer.KtTokens @@ -188,24 +189,26 @@ private fun parametersMatch( ): Boolean { if (function.valueParameters.size == functionDescriptor.valueParameters.size) { for (index in 0 until function.valueParameters.size) { - LOG.info("Method: {} {} - {} {}", function.valueParameters[index].name, functionDescriptor.valueParameters[index].name.asString(), function.valueParameters[index].typeReference?.typeName(), functionDescriptor.valueParameters[index] + LOG.info("Method: {} {} - {} {}", function.valueParameters[index].name, functionDescriptor.valueParameters[index].name.asString(), function.valueParameters[index].typeReference?.typeElement?.unwrapNullability()?.name, functionDescriptor.valueParameters[index] .type .unwrappedType() .toString()) if (function.valueParameters[index].name != - functionDescriptor.valueParameters[index].name.asString() + functionDescriptor.valueParameters[index].name.asString() ) { return false } else if (function.valueParameters[index].typeReference?.typeName() != functionDescriptor.valueParameters[index] .type .unwrappedType() - .toString() + .toString() && function.valueParameters[index].typeReference?.typeName() != null ) { + // Any and Any? seems to be null for Kt* psi objects for some reason? At least for equals + // TODO: look further into this + // Note: Since we treat Java overrides as non nullable by default, the above test // will fail when the user has made the type nullable. // TODO: look into this - // TODO: look into the weird issue with equals... return false } } @@ -213,7 +216,7 @@ private fun parametersMatch( if (function.typeParameters.size == functionDescriptor.typeParameters.size) { for (index in 0 until function.typeParameters.size) { if (function.typeParameters[index].variance != - functionDescriptor.typeParameters[index].variance + functionDescriptor.typeParameters[index].variance ) { return false } From 095ddaf3091d585d4bffaa97d44128b2cd82fee6 Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Wed, 3 Aug 2022 20:07:26 +0200 Subject: [PATCH 33/46] Add testcase for jdk superclass and fix minor issues --- .../org/javacs/kt/KotlinProtocolExtensions.kt | 2 - .../kt/overridemembers/OverrideMembers.kt | 27 ++--------- .../org/javacs/kt/OverrideMemberTest.kt | 47 ++++++++++++++++++- .../overridemember/OverrideMembers.kt | 2 + 4 files changed, 51 insertions(+), 27 deletions(-) diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensions.kt b/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensions.kt index a599f11f4..b29c4b247 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensions.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensions.kt @@ -16,8 +16,6 @@ interface KotlinProtocolExtensions { @JsonRequest fun mainClass(textDocument: TextDocumentIdentifier): CompletableFuture> - // TODO: what is the best return value in this case? CodeAction? - // TODO: should the naming be something like listOverrideableMembers? or something similar instead? @JsonRequest fun overrideMember(position: TextDocumentPositionParams): CompletableFuture> } diff --git a/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt b/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt index 6dfef58c6..f0f74053b 100644 --- a/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt +++ b/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt @@ -18,6 +18,7 @@ import org.jetbrains.kotlin.psi.KtSimpleNameExpression import org.jetbrains.kotlin.descriptors.Modality import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.descriptors.DescriptorVisibilities import org.jetbrains.kotlin.descriptors.ClassConstructorDescriptor import org.jetbrains.kotlin.descriptors.FunctionDescriptor import org.jetbrains.kotlin.descriptors.isInterface @@ -56,30 +57,12 @@ private fun createOverrideAlternatives(file: CompiledFile, kotlinClass: KtClass) // Get the location where the new code will be placed val newMembersStartPosition = getNewMembersStartPosition(file, kotlinClass) - // TODO: if both used here and in the abstract member stuff.... should we put it in a method both can use? - // TODO: how should this be implemented? - // val bodyAppendBeginning = - // listOf(TextEdit(Range(newMembersStartPosition, newMembersStartPosition), "{")).takeIf { - // kotlinClass.hasNoBody() - // } - // ?: emptyList() - // val bodyAppendEnd = - // listOf( - // TextEdit( - // Range(newMembersStartPosition, newMembersStartPosition), - // System.lineSeparator() + "}") - // ) - // .takeIf { kotlinClass.hasNoBody() } - // ?: emptyList() - - LOG.info("Members: {}", membersToImplement) // loop through the memberstoimplement and create code actions return membersToImplement.map { member -> val newText = System.lineSeparator() + System.lineSeparator() + padding + member val textEdit = TextEdit(Range(newMembersStartPosition, newMembersStartPosition), newText) - // TODO: how should we get the name of the property? if needed? val codeAction = CodeAction() codeAction.edit = WorkspaceEdit(mapOf(uri to listOf(textEdit))) codeAction.title = member @@ -131,9 +114,9 @@ private fun ClassDescriptor.canBeExtended() = this.kind.isInterface || this.modality == Modality.ABSTRACT || this.modality == Modality.OPEN -private fun FunctionDescriptor.canBeOverriden() = Modality.ABSTRACT == this.modality || Modality.OPEN == this.modality +private fun FunctionDescriptor.canBeOverriden() = (Modality.ABSTRACT == this.modality || Modality.OPEN == this.modality) && Modality.FINAL != this.modality && this.visibility != DescriptorVisibilities.PRIVATE && this.visibility != DescriptorVisibilities.PROTECTED -private fun PropertyDescriptor.canBeOverriden() = Modality.ABSTRACT == this.modality || Modality.OPEN == this.modality +private fun PropertyDescriptor.canBeOverriden() = (Modality.ABSTRACT == this.modality || Modality.OPEN == this.modality) && Modality.FINAL != this.modality && this.visibility != DescriptorVisibilities.PRIVATE && this.visibility != DescriptorVisibilities.PROTECTED // interfaces are ClassDescriptors by default. When calling AbstractClass super methods, we get a ClassConstructorDescriptor fun getClassDescriptor(descriptor: DeclarationDescriptor?): ClassDescriptor? = @@ -189,10 +172,6 @@ private fun parametersMatch( ): Boolean { if (function.valueParameters.size == functionDescriptor.valueParameters.size) { for (index in 0 until function.valueParameters.size) { - LOG.info("Method: {} {} - {} {}", function.valueParameters[index].name, functionDescriptor.valueParameters[index].name.asString(), function.valueParameters[index].typeReference?.typeElement?.unwrapNullability()?.name, functionDescriptor.valueParameters[index] - .type - .unwrappedType() - .toString()) if (function.valueParameters[index].name != functionDescriptor.valueParameters[index].name.asString() ) { diff --git a/server/src/test/kotlin/org/javacs/kt/OverrideMemberTest.kt b/server/src/test/kotlin/org/javacs/kt/OverrideMemberTest.kt index 358755aab..4813bc694 100644 --- a/server/src/test/kotlin/org/javacs/kt/OverrideMemberTest.kt +++ b/server/src/test/kotlin/org/javacs/kt/OverrideMemberTest.kt @@ -100,5 +100,50 @@ class OverrideMemberTest : SingleFileTestFixture("overridemember", "OverrideMemb assertThat(ranges, everyItem(equalTo(range(37, 25, 37, 25)))) } - // TODO: test for kotlin sdk and jdk classes to verify that it works + @Test + fun `should find members in jdk object`() { + val result = languageServer.getProtocolExtensionService().overrideMember(TextDocumentPositionParams(TextDocumentIdentifier(fileUri), position(39, 9))).get() + + val titles = result.map { it.title } + val edits = result.flatMap { it.edit.changes[fileUri]!! } + val newTexts = edits.map { it.newText } + val ranges = edits.map { it.range } + + assertThat(titles, containsInAnyOrder("override fun equals(other: Any?): Boolean { }", + "override fun hashCode(): Int { }", + "override fun toString(): String { }", + "override fun run() { }", + "override fun clone(): Any { }", + "override fun start() { }", + "override fun interrupt() { }", + "override fun isInterrupted(): Boolean { }", + "override fun countStackFrames(): Int { }", + "override fun getContextClassLoader(): ClassLoader { }", + "override fun setContextClassLoader(cl: ClassLoader) { }", + "override fun getStackTrace(): (Array<(StackTraceElement..StackTraceElement?)>..Array) { }", + "override fun getId(): Long { }", + "override fun getState(): State { }", + "override fun getUncaughtExceptionHandler(): UncaughtExceptionHandler { }", + "override fun setUncaughtExceptionHandler(eh: UncaughtExceptionHandler) { }")) + + val padding = System.lineSeparator() + System.lineSeparator() + " " + assertThat(newTexts, containsInAnyOrder(padding + "override fun equals(other: Any?): Boolean { }", + padding + "override fun hashCode(): Int { }", + padding + "override fun toString(): String { }", + padding + "override fun run() { }", + padding + "override fun clone(): Any { }", + padding + "override fun start() { }", + padding + "override fun interrupt() { }", + padding + "override fun isInterrupted(): Boolean { }", + padding + "override fun countStackFrames(): Int { }", + padding + "override fun getContextClassLoader(): ClassLoader { }", + padding + "override fun setContextClassLoader(cl: ClassLoader) { }", + padding + "override fun getStackTrace(): (Array<(StackTraceElement..StackTraceElement?)>..Array) { }", + padding + "override fun getId(): Long { }", + padding + "override fun getState(): State { }", + padding + "override fun getUncaughtExceptionHandler(): UncaughtExceptionHandler { }", + padding + "override fun setUncaughtExceptionHandler(eh: UncaughtExceptionHandler) { }")) + + assertThat(ranges, everyItem(equalTo(range(39, 25, 39, 25)))) + } } diff --git a/server/src/test/resources/overridemember/OverrideMembers.kt b/server/src/test/resources/overridemember/OverrideMembers.kt index 1d3434321..4e6b14465 100644 --- a/server/src/test/resources/overridemember/OverrideMembers.kt +++ b/server/src/test/resources/overridemember/OverrideMembers.kt @@ -35,3 +35,5 @@ open class MyOpen { } class Closed: MyOpen() {} + +class MyThread: Thread {} From 7cec062e38d798a0dd192e593fb76501ea538a52 Mon Sep 17 00:00:00 2001 From: fwcd Date: Thu, 4 Aug 2022 05:25:26 +0200 Subject: [PATCH 34/46] Upgrade Gradle wrapper --- gradle/wrapper/gradle-wrapper.jar | Bin 59536 -> 60756 bytes gradlew | 6 ++++++ gradlew.bat | 14 ++++++++------ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180f2ae8848c63b8b4dea2cb829da983f2fa..249e5832f090a2944b7473328c07c9755baa3196 100644 GIT binary patch delta 10158 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 delta 9041 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 diff --git a/gradlew b/gradlew index 1b6c78733..a69d9cb6c 100755 --- a/gradlew +++ b/gradlew @@ -205,6 +205,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index ac1b06f93..53a6b238d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal From 965f844e6dffa5ecdae7edb597fcf953cebe63a0 Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Thu, 4 Aug 2022 17:05:25 +0200 Subject: [PATCH 35/46] Minor tweaks and indentation fix --- .../kt/overridemembers/OverrideMembers.kt | 71 +++++++++---------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt b/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt index f0f74053b..152baf602 100644 --- a/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt +++ b/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt @@ -7,7 +7,6 @@ import org.eclipse.lsp4j.TextEdit import org.eclipse.lsp4j.WorkspaceEdit import org.javacs.kt.CompiledFile import org.javacs.kt.util.toPath -import org.javacs.kt.LOG import org.javacs.kt.position.position import org.jetbrains.kotlin.psi.KtClass import org.jetbrains.kotlin.psi.KtTypeArgumentList @@ -73,42 +72,42 @@ private fun createOverrideAlternatives(file: CompiledFile, kotlinClass: KtClass) // TODO: any way can repeat less code between this and the getAbstractMembersStubs in the ImplementAbstractMembersQuickfix? private fun getUnimplementedMembersStubs(file: CompiledFile, kotlinClass: KtClass): List = - // For each of the super types used by this class - kotlinClass - .superTypeListEntries - .mapNotNull { - // Find the definition of this super type - val referenceAtPoint = file.referenceExpressionAtPoint(it.startOffset) - val descriptor = referenceAtPoint?.second - val classDescriptor = getClassDescriptor(descriptor) - - // If the super class is abstract, interface or just plain open - if (null != classDescriptor && classDescriptor.canBeExtended() - ) { - val superClassTypeArguments = getSuperClassTypeProjections(file, it) - classDescriptor - .getMemberScope(superClassTypeArguments) - .getContributedDescriptors() - .filter { classMember -> - (classMember is FunctionDescriptor && - classMember.canBeOverriden() && - !overridesDeclaration(kotlinClass, classMember)) || - (classMember is PropertyDescriptor && - classMember.canBeOverriden() && - !overridesDeclaration(kotlinClass, classMember)) - } - .mapNotNull { member -> - when (member) { - is FunctionDescriptor -> createFunctionStub(member) - is PropertyDescriptor -> createVariableStub(member) - else -> null - } - } - } else { - null + // For each of the super types used by this class + // TODO: does not seem to handle the implicit Any and Object super types that well. Need to find out if that is easily solvable. Finds the methods from them if any super class or interface is present + kotlinClass + .superTypeListEntries + .mapNotNull { + // Find the definition of this super type + val referenceAtPoint = file.referenceExpressionAtPoint(it.startOffset) + val descriptor = referenceAtPoint?.second + val classDescriptor = getClassDescriptor(descriptor) + + // If the super class is abstract, interface or just plain open + if (null != classDescriptor && classDescriptor.canBeExtended()) { + val superClassTypeArguments = getSuperClassTypeProjections(file, it) + classDescriptor + .getMemberScope(superClassTypeArguments) + .getContributedDescriptors() + .filter { classMember -> + (classMember is FunctionDescriptor && + classMember.canBeOverriden() && + !overridesDeclaration(kotlinClass, classMember)) || + (classMember is PropertyDescriptor && + classMember.canBeOverriden() && + !overridesDeclaration(kotlinClass, classMember)) } - } - .flatten() + .mapNotNull { member -> + when (member) { + is FunctionDescriptor -> createFunctionStub(member) + is PropertyDescriptor -> createVariableStub(member) + else -> null + } + } + } else { + null + } + } + .flatten() private fun ClassDescriptor.canBeExtended() = this.kind.isInterface || this.modality == Modality.ABSTRACT || From 74136b442e38574b3f7eecd4b9d318f1a8bbdce6 Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Thu, 4 Aug 2022 17:43:17 +0200 Subject: [PATCH 36/46] Removed todo comment --- server/src/test/kotlin/org/javacs/kt/OverrideMemberTest.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/server/src/test/kotlin/org/javacs/kt/OverrideMemberTest.kt b/server/src/test/kotlin/org/javacs/kt/OverrideMemberTest.kt index 4813bc694..e2f6c1f18 100644 --- a/server/src/test/kotlin/org/javacs/kt/OverrideMemberTest.kt +++ b/server/src/test/kotlin/org/javacs/kt/OverrideMemberTest.kt @@ -13,9 +13,6 @@ import org.hamcrest.Matchers.equalTo import org.hamcrest.Matchers.hasSize import org.junit.Assert.assertThat -// TODO: what should the title be? just the signature? or the name of the member? should we separate between methods and variables? -// easiest is probably just to show the signatures? - class OverrideMemberTest : SingleFileTestFixture("overridemember", "OverrideMembers.kt") { val root = testResourcesRoot().resolve(workspaceRoot) From d5e4a22183c5e4c492b61a8dd58b2cb207d896fe Mon Sep 17 00:00:00 2001 From: Omar El Halabi Date: Fri, 5 Aug 2022 21:16:16 +0200 Subject: [PATCH 37/46] Depend on Kotlin's implementation of parsing JVM target --- .../kotlin/org/javacs/kt/compiler/Compiler.kt | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/server/src/main/kotlin/org/javacs/kt/compiler/Compiler.kt b/server/src/main/kotlin/org/javacs/kt/compiler/Compiler.kt index 207ae70e0..7704ab4cb 100644 --- a/server/src/main/kotlin/org/javacs/kt/compiler/Compiler.kt +++ b/server/src/main/kotlin/org/javacs/kt/compiler/Compiler.kt @@ -382,25 +382,10 @@ private class CompilationEnvironment( } fun updateConfiguration(config: CompilerConfiguration) { - jvmTargetFrom(config.jvm.target) + JvmTarget.fromString(config.jvm.target) ?.let { environment.configuration.put(JVMConfigurationKeys.JVM_TARGET, it) } } - private fun jvmTargetFrom(target: String): JvmTarget? = when (target) { - // See https://github.com/JetBrains/kotlin/blob/master/compiler/config.jvm/src/org/jetbrains/kotlin/config/JvmTarget.kt - "default" -> JvmTarget.DEFAULT - "1.6" -> JvmTarget.JVM_1_6 - "1.8" -> JvmTarget.JVM_1_8 - "9" -> JvmTarget.JVM_9 - "10" -> JvmTarget.JVM_10 - "11" -> JvmTarget.JVM_11 - "12" -> JvmTarget.JVM_12 - "13" -> JvmTarget.JVM_13 - "14" -> JvmTarget.JVM_14 - "15" -> JvmTarget.JVM_15 - else -> null - } - fun createContainer(sourcePath: Collection): Pair { val trace = CliBindingTrace() val container = TopDownAnalyzerFacadeForJVM.createContainer( From e3b1836a50422a844592d2a16a51976ff717da44 Mon Sep 17 00:00:00 2001 From: Omar El Halabi Date: Sat, 6 Aug 2022 12:10:04 +0200 Subject: [PATCH 38/46] Upgrade lsp4j version to 0.15.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 68abee22c..1c44847c2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ projectVersion=1.3.2 kotlinVersion=1.6.10 exposedVersion=0.37.3 -lsp4jVersion=0.14.0 +lsp4jVersion=0.15.0 javaVersion=11 From 0a9d0b8ddb7123a6bddad54085d48cd45b5a91a4 Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Wed, 10 Aug 2022 16:35:32 +0200 Subject: [PATCH 39/46] Support different kotlin/kotlinc install directory setups in backup classpath resolver. --- .../javacs/kt/classpath/BackupClassPathResolver.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/shared/src/main/kotlin/org/javacs/kt/classpath/BackupClassPathResolver.kt b/shared/src/main/kotlin/org/javacs/kt/classpath/BackupClassPathResolver.kt index c14175225..65cd5b8d9 100644 --- a/shared/src/main/kotlin/org/javacs/kt/classpath/BackupClassPathResolver.kt +++ b/shared/src/main/kotlin/org/javacs/kt/classpath/BackupClassPathResolver.kt @@ -53,8 +53,16 @@ private fun findKotlinCliCompilerLibrary(name: String): Path? = findCommandOnPath("kotlinc") ?.toRealPath() ?.parent // bin - ?.parent // libexec - ?.resolve("lib") + ?.parent // libexec or "top-level" dir + ?.let { + // either in libexec or a top-level directory (that may contain libexec, or just a lib-directory directly) + val possibleLibDir = it.resolve("lib") + if (Files.exists(possibleLibDir)) { + possibleLibDir + } else { + it.resolve("libexec").resolve("lib") + } + } ?.takeIf { Files.exists(it) } ?.let(Files::list) ?.filter { it.fileName.toString() == "$name.jar" } From c5c4f519481b2e20a0750a785b418cc5ade51043 Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Fri, 12 Aug 2022 21:42:15 +0200 Subject: [PATCH 40/46] Remove unused variable --- .../kt/codeaction/quickfix/ImplementAbstractMembersQuickFix.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractMembersQuickFix.kt b/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractMembersQuickFix.kt index 131208aa2..e4b0a6b11 100644 --- a/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractMembersQuickFix.kt +++ b/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractMembersQuickFix.kt @@ -40,8 +40,6 @@ import org.jetbrains.kotlin.types.KotlinType import org.jetbrains.kotlin.types.TypeProjection import org.jetbrains.kotlin.types.typeUtil.asTypeProjection -private const val DEFAULT_TAB_SIZE = 4 - class ImplementAbstractMembersQuickFix : QuickFix { override fun compute(file: CompiledFile, index: SymbolIndex, range: Range, diagnostics: List): List> { val diagnostic = findDiagnosticMatch(diagnostics, range) From ed1d4adb58968172424b3c17d6b77155e420dbea Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Fri, 12 Aug 2022 21:45:29 +0200 Subject: [PATCH 41/46] Simplify code based upon PR comments --- .../kt/overridemembers/OverrideMembers.kt | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt b/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt index 152baf602..cd3e66ead 100644 --- a/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt +++ b/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt @@ -22,6 +22,7 @@ import org.jetbrains.kotlin.descriptors.ClassConstructorDescriptor import org.jetbrains.kotlin.descriptors.FunctionDescriptor import org.jetbrains.kotlin.descriptors.isInterface import org.jetbrains.kotlin.descriptors.PropertyDescriptor +import org.jetbrains.kotlin.descriptors.MemberDescriptor import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi import org.jetbrains.kotlin.psi.psiUtil.endOffset import org.jetbrains.kotlin.psi.psiUtil.isAbstract @@ -112,11 +113,9 @@ private fun getUnimplementedMembersStubs(file: CompiledFile, kotlinClass: KtClas private fun ClassDescriptor.canBeExtended() = this.kind.isInterface || this.modality == Modality.ABSTRACT || this.modality == Modality.OPEN - -private fun FunctionDescriptor.canBeOverriden() = (Modality.ABSTRACT == this.modality || Modality.OPEN == this.modality) && Modality.FINAL != this.modality && this.visibility != DescriptorVisibilities.PRIVATE && this.visibility != DescriptorVisibilities.PROTECTED - -private fun PropertyDescriptor.canBeOverriden() = (Modality.ABSTRACT == this.modality || Modality.OPEN == this.modality) && Modality.FINAL != this.modality && this.visibility != DescriptorVisibilities.PRIVATE && this.visibility != DescriptorVisibilities.PROTECTED +private fun MemberDescriptor.canBeOverriden() = (Modality.ABSTRACT == this.modality || Modality.OPEN == this.modality) && Modality.FINAL != this.modality && this.visibility != DescriptorVisibilities.PRIVATE && this.visibility != DescriptorVisibilities.PROTECTED + // interfaces are ClassDescriptors by default. When calling AbstractClass super methods, we get a ClassConstructorDescriptor fun getClassDescriptor(descriptor: DeclarationDescriptor?): ClassDescriptor? = if (descriptor is ClassDescriptor) { @@ -145,24 +144,18 @@ fun getSuperClassTypeProjections( ?: emptyList() // Checks if the class overrides the given declaration -fun overridesDeclaration(kotlinClass: KtClass, descriptor: FunctionDescriptor): Boolean = - kotlinClass.declarations.any { - if (it.name == descriptor.name.asString() && it.hasModifier(KtTokens.OVERRIDE_KEYWORD) - ) { - if (it is KtNamedFunction) { - parametersMatch(it, descriptor) - } else { - true - } - } else { - false - } +fun overridesDeclaration(kotlinClass: KtClass, descriptor: MemberDescriptor): Boolean = + when (descriptor) { + is FunctionDescriptor -> kotlinClass.declarations.any { + it.name == descriptor.name.asString() + && it.hasModifier(KtTokens.OVERRIDE_KEYWORD) + && ((it as? KtNamedFunction)?.let { parametersMatch(it, descriptor) } ?: true) } - -fun overridesDeclaration(kotlinClass: KtClass, descriptor: PropertyDescriptor): Boolean = - kotlinClass.declarations.any { + is PropertyDescriptor -> kotlinClass.declarations.any { it.name == descriptor.name.asString() && it.hasModifier(KtTokens.OVERRIDE_KEYWORD) } + else -> false + } // Checks if two functions have matching parameters private fun parametersMatch( From 596c6a3268e2fa6d9cbeb13daa551e03af464e54 Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Sat, 13 Aug 2022 09:56:14 +0200 Subject: [PATCH 42/46] Simplify expression based upon earlier refactoring using MemberDescriptor --- .../org/javacs/kt/overridemembers/OverrideMembers.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt b/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt index cd3e66ead..e404d94a9 100644 --- a/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt +++ b/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt @@ -90,12 +90,9 @@ private fun getUnimplementedMembersStubs(file: CompiledFile, kotlinClass: KtClas .getMemberScope(superClassTypeArguments) .getContributedDescriptors() .filter { classMember -> - (classMember is FunctionDescriptor && + classMember is MemberDescriptor && classMember.canBeOverriden() && - !overridesDeclaration(kotlinClass, classMember)) || - (classMember is PropertyDescriptor && - classMember.canBeOverriden() && - !overridesDeclaration(kotlinClass, classMember)) + !overridesDeclaration(kotlinClass, classMember) } .mapNotNull { member -> when (member) { From 4219851cc29ea72a8d998638e7fd2848b21e529a Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Wed, 17 Aug 2022 16:13:41 +0200 Subject: [PATCH 43/46] Resolve kotlin-stdlib.jar from snap install --- .../org/javacs/kt/classpath/BackupClassPathResolver.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/shared/src/main/kotlin/org/javacs/kt/classpath/BackupClassPathResolver.kt b/shared/src/main/kotlin/org/javacs/kt/classpath/BackupClassPathResolver.kt index 65cd5b8d9..b3b0eb0db 100644 --- a/shared/src/main/kotlin/org/javacs/kt/classpath/BackupClassPathResolver.kt +++ b/shared/src/main/kotlin/org/javacs/kt/classpath/BackupClassPathResolver.kt @@ -18,6 +18,7 @@ object BackupClassPathResolver : ClassPathResolver { fun findKotlinStdlib(): Path? = findLocalArtifact("org.jetbrains.kotlin", "kotlin-stdlib") ?: findKotlinCliCompilerLibrary("kotlin-stdlib") + ?: findAlternativeLibraryLocation("kotlin-stdlib") private fun findLocalArtifact(group: String, artifact: String) = tryResolving("$artifact using Maven") { tryFindingLocalArtifactUsing(group, artifact, findLocalArtifactDirUsingMaven(group, artifact)) } @@ -70,6 +71,11 @@ private fun findKotlinCliCompilerLibrary(name: String): Path? = ?.orElse(null) +// alternative library locations like for snap +// (can probably just use elvis operator and multiple similar expressions for other install directories) +private fun findAlternativeLibraryLocation(name: String): Path? = + Paths.get("/snap/kotlin/current/lib/${name}.jar").existsOrNull() + private fun Path.existsOrNull() = if (Files.exists(this)) this else null From 4435b417f6d61dde02b34061625d91252ea09d25 Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Tue, 23 Aug 2022 10:55:14 +0200 Subject: [PATCH 44/46] Fix startup issue due to method always throwing unsupportedoperationexception --- .../src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt b/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt index b3a417bd7..96eb61565 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt @@ -6,6 +6,7 @@ import org.eclipse.lsp4j.jsonrpc.services.JsonDelegate import org.eclipse.lsp4j.services.LanguageClient import org.eclipse.lsp4j.services.LanguageClientAware import org.eclipse.lsp4j.services.LanguageServer +import org.eclipse.lsp4j.services.NotebookDocumentService import org.javacs.kt.command.ALL_COMMANDS import org.javacs.kt.externalsources.* import org.javacs.kt.util.AsyncExecutor @@ -162,4 +163,10 @@ class KotlinLanguageServer : LanguageServer, LanguageClientAware, Closeable { } override fun exit() {} + + // Fixed in https://github.com/eclipse/lsp4j/commit/04b0c6112f0a94140e22b8b15bb5a90d5a0ed851 + // Causes issue in lsp 0.15 + override fun getNotebookDocumentService(): NotebookDocumentService? { + return null; + } } From 6022088afb3a5206871bd65e629262ef26f7abe1 Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Tue, 30 Aug 2022 20:08:04 +0200 Subject: [PATCH 45/46] Fix issue where implement abstract members code action doesn't show up if you don't mark the entire text. Happens in editors that are not VSCode --- .../org/javacs/kt/codeaction/quickfix/QuickFix.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/QuickFix.kt b/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/QuickFix.kt index a7048ddf4..0115eaacd 100644 --- a/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/QuickFix.kt +++ b/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/QuickFix.kt @@ -16,10 +16,18 @@ interface QuickFix { } fun diagnosticMatch(diagnostic: Diagnostic, range: Range, diagnosticTypes: Set): Boolean = - diagnostic.range.equals(range) && diagnosticTypes.contains(diagnostic.code.left) + isDiagnosticInRange(diagnostic, range) && diagnosticTypes.contains(diagnostic.code.left) + +// for a diagnostic to be in range the lines should be the same, and +// the input character range should be within the bounds of the diagnostics range. +private fun isDiagnosticInRange(diagnostic: Diagnostic, range: Range): Boolean { + val diagnosticRange = diagnostic.range + return diagnosticRange.start.line == range.start.line && diagnosticRange.end.line == range.end.line && + diagnosticRange.start.character <= range.start.character && diagnosticRange.end.character >= range.end.character +} fun diagnosticMatch(diagnostic: KotlinDiagnostic, startCursor: Int, endCursor: Int, diagnosticTypes: Set): Boolean = - diagnostic.textRanges.any { it.startOffset == startCursor && it.endOffset == endCursor } && diagnosticTypes.contains(diagnostic.factory.name) + diagnostic.textRanges.any { it.startOffset <= startCursor && it.endOffset >= endCursor } && diagnosticTypes.contains(diagnostic.factory.name) fun findDiagnosticMatch(diagnostics: List, range: Range, diagnosticTypes: Set) = diagnostics.find { diagnosticMatch(it, range, diagnosticTypes) } From a3c040e297602b8cc705c327f8a79713a0fbe17d Mon Sep 17 00:00:00 2001 From: Marie Katrine Ekeberg Date: Wed, 31 Aug 2022 17:49:17 +0200 Subject: [PATCH 46/46] Change private helper function into an extension function --- .../org/javacs/kt/codeaction/quickfix/QuickFix.kt | 11 ++--------- .../src/main/kotlin/org/javacs/kt/util/RangeUtils.kt | 8 ++++++++ 2 files changed, 10 insertions(+), 9 deletions(-) create mode 100644 server/src/main/kotlin/org/javacs/kt/util/RangeUtils.kt diff --git a/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/QuickFix.kt b/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/QuickFix.kt index 0115eaacd..89526bb0a 100644 --- a/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/QuickFix.kt +++ b/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/QuickFix.kt @@ -7,6 +7,7 @@ import org.eclipse.lsp4j.Range import org.eclipse.lsp4j.jsonrpc.messages.Either import org.javacs.kt.CompiledFile import org.javacs.kt.index.SymbolIndex +import org.javacs.kt.util.isSubrangeOf import org.jetbrains.kotlin.resolve.diagnostics.Diagnostics import org.jetbrains.kotlin.diagnostics.Diagnostic as KotlinDiagnostic @@ -16,15 +17,7 @@ interface QuickFix { } fun diagnosticMatch(diagnostic: Diagnostic, range: Range, diagnosticTypes: Set): Boolean = - isDiagnosticInRange(diagnostic, range) && diagnosticTypes.contains(diagnostic.code.left) - -// for a diagnostic to be in range the lines should be the same, and -// the input character range should be within the bounds of the diagnostics range. -private fun isDiagnosticInRange(diagnostic: Diagnostic, range: Range): Boolean { - val diagnosticRange = diagnostic.range - return diagnosticRange.start.line == range.start.line && diagnosticRange.end.line == range.end.line && - diagnosticRange.start.character <= range.start.character && diagnosticRange.end.character >= range.end.character -} + range.isSubrangeOf(diagnostic.range) && diagnosticTypes.contains(diagnostic.code.left) fun diagnosticMatch(diagnostic: KotlinDiagnostic, startCursor: Int, endCursor: Int, diagnosticTypes: Set): Boolean = diagnostic.textRanges.any { it.startOffset <= startCursor && it.endOffset >= endCursor } && diagnosticTypes.contains(diagnostic.factory.name) diff --git a/server/src/main/kotlin/org/javacs/kt/util/RangeUtils.kt b/server/src/main/kotlin/org/javacs/kt/util/RangeUtils.kt new file mode 100644 index 000000000..01820c80d --- /dev/null +++ b/server/src/main/kotlin/org/javacs/kt/util/RangeUtils.kt @@ -0,0 +1,8 @@ +package org.javacs.kt.util + +import org.eclipse.lsp4j.Range + +// checks if the current range is within the other range (same lines, within the character bounds) +fun Range.isSubrangeOf(otherRange: Range): Boolean = + otherRange.start.line == this.start.line && otherRange.end.line == this.end.line && + otherRange.start.character <= this.start.character && otherRange.end.character >= this.end.character