[kotlin] k2: quick doc on actual declaration retrieved from expect declaration

- test framework to set up light multiplatform tests in one file (KTIJ-28551)

^KTIJ-27961 fixed

GitOrigin-RevId: 8b997886588a64069ccbc35c517fdfddb4ddb246
This commit is contained in:
Anna Kozlova
2024-03-28 18:18:25 +00:00
committed by intellij-monorepo-bot
parent 48d5ff6111
commit 7e87a9ace0
11 changed files with 382 additions and 3 deletions
@@ -0,0 +1,48 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.base.test
import java.io.File
data class TestFile(val relativePath: String, val text: String, val isMain: Boolean)
object ModuleStructureSplitter {
private const val PLATFORM_PREFIX = "// PLATFORM:"
private const val FILE_PATH_PREFIX = "// FILE:"
private const val IS_MAIN = "// MAIN"
fun splitPerModule(file: File): MutableMap<String, MutableList<TestFile>> {
val lines = file.readLines()
return splitPerModule(lines)
}
fun splitPerModule(lines: List<String>): MutableMap<String, MutableList<TestFile>> {
val result = mutableMapOf<String, MutableList<TestFile>>()
var currentPlatform: String? = null
var currentFileName: String? = null
var currentText: String? = null
var isMainFile = false
fun dumpFile(currentPlatform: String?, currentFileName: String?, currentText: String?) {
if (currentText != null && currentFileName != null && currentPlatform != null) {
result.getOrPut(currentPlatform) { mutableListOf() }.add(TestFile(currentFileName, currentText, isMainFile))
}
}
lines.forEach { line ->
if (line == IS_MAIN) {
isMainFile = true
} else if (line.startsWith(PLATFORM_PREFIX)) {
dumpFile(currentPlatform, currentFileName, currentText)
currentPlatform = line.substringAfter(PLATFORM_PREFIX).trim()
} else if (line.startsWith(FILE_PATH_PREFIX)) {
isMainFile = false
currentFileName = line.substringAfter(FILE_PATH_PREFIX).trim()
currentText = ""
} else {
currentText += line + "\n"
}
}
dumpFile(currentPlatform, currentFileName, currentText)
return result
}
}
@@ -32,6 +32,7 @@ import org.jetbrains.kotlin.idea.kdoc.KDocRenderer.createHighlightingManager
import org.jetbrains.kotlin.idea.kdoc.KDocRenderer.generateJavadoc
import org.jetbrains.kotlin.idea.kdoc.KDocRenderer.renderKDoc
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.idea.search.ExpectActualSupport
import org.jetbrains.kotlin.kdoc.psi.api.KDoc
import org.jetbrains.kotlin.kdoc.psi.impl.KDocSection
import org.jetbrains.kotlin.lexer.KtTokens
@@ -304,7 +305,18 @@ private fun findKDoc(symbol: KtSymbol): KDocContent? {
}
}
}
return null
if (symbol is KtValueParameterSymbol) {
val containingSymbol = symbol.getContainingSymbol() as? KtFunctionSymbol
if (containingSymbol != null) {
val idx = containingSymbol.valueParameters.indexOf(symbol)
containingSymbol.getExpectsForActual().filterIsInstance<KtFunctionSymbol>().mapNotNull { expectFunction ->
findKDoc(expectFunction.valueParameters[idx])
}.firstOrNull()?.let { return it }
}
}
return (symbol as? KtDeclarationSymbol)?.getExpectsForActual()?.mapNotNull { declarationSymbol -> findKDoc(declarationSymbol) }?.firstOrNull()
}
context(KtAnalysisSession)
@@ -0,0 +1,56 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.fir.documentation
import com.intellij.lang.documentation.ide.IdeDocumentationTargetProvider
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.VfsUtilCore
import com.intellij.platform.backend.documentation.impl.computeDocumentationBlocking
import com.intellij.platform.testFramework.core.FileComparisonFailedError
import org.jetbrains.kotlin.idea.base.test.InTextDirectivesUtils
import org.jetbrains.kotlin.idea.editor.quickDoc.AbstractQuickDocProviderTest.wrapToFileComparisonFailure
import org.jetbrains.kotlin.idea.test.KotlinLightMultiplatformCodeInsightFixtureTestCase
import java.io.File
abstract class AbstractFirQuickDocMultiplatformTest: KotlinLightMultiplatformCodeInsightFixtureTestCase() {
override fun isFirPlugin(): Boolean = true
fun getDoc(): String? {
val target =
IdeDocumentationTargetProvider.getInstance(project).documentationTargets(editor, file, editor.caretModel.offset).firstOrNull()
?: return null
return computeDocumentationBlocking(target.createPointer())?.html
}
fun doTest(path: String) {
val virtualFile = configureModuleStructure(path)
require(virtualFile != null)
myFixture.configureFromExistingVirtualFile(virtualFile)
val element = myFixture.getFile().findElementAt(myFixture.editor.caretModel.offset)
assertNotNull("Can't find element at caret in file: $path", element)
var info = getDoc()
if (info != null) {
info = StringUtil.convertLineSeparators(info)
}
val testDataFile = File(path)
val textData = FileUtil.loadFile(testDataFile, true)
val directives =
InTextDirectivesUtils.findLinesWithPrefixesRemoved(textData, false, true, "INFO:")
if (directives.isEmpty()) {
throw FileComparisonFailedError(
"'// INFO:' directive was expected",
textData,
"$textData\n\n//INFO: $info",
testDataFile.absolutePath
)
}
else {
val expectedInfo = directives.joinToString("\n")
if (expectedInfo != info) {
wrapToFileComparisonFailure(info, path, textData)
}
}
}
}
@@ -0,0 +1,40 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.fir.documentation;
import com.intellij.testFramework.TestDataPath;
import org.jetbrains.kotlin.idea.test.JUnit3RunnerWithInners;
import org.jetbrains.kotlin.idea.test.KotlinTestUtils;
import org.jetbrains.kotlin.test.TestMetadata;
import org.jetbrains.kotlin.idea.base.test.TestRoot;
import org.junit.runner.RunWith;
/**
* This class is generated by {@link org.jetbrains.kotlin.testGenerator.generator.TestGenerator}.
* DO NOT MODIFY MANUALLY.
*/
@SuppressWarnings("all")
@TestRoot("fir/tests")
@TestDataPath("$CONTENT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("../../idea/tests/testData/editor/quickDoc/multiplatform")
public class FirQuickDocMultiplatformTestGenerated extends AbstractFirQuickDocMultiplatformTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
@TestMetadata("OnActualClassWithConstructorFromExpect.kt")
public void testOnActualClassWithConstructorFromExpect() throws Exception {
runTest("../../idea/tests/testData/editor/quickDoc/multiplatform/OnActualClassWithConstructorFromExpect.kt");
}
@TestMetadata("OnActualDeclarationWithInheritedDoc.kt")
public void testOnActualDeclarationWithInheritedDoc() throws Exception {
runTest("../../idea/tests/testData/editor/quickDoc/multiplatform/OnActualDeclarationWithInheritedDoc.kt");
}
@TestMetadata("OnActualValueParameterDeclaration.kt")
public void testOnActualValueParameterDeclaration() throws Exception {
runTest("../../idea/tests/testData/editor/quickDoc/multiplatform/OnActualValueParameterDeclaration.kt");
}
}
@@ -0,0 +1,15 @@
// PLATFORM: Common
// FILE: Foo.kt
/**
* @constructor Common Description
*/
expect class Foo()
// PLATFORM: Jvm
// FILE: Foo.kt
// MAIN
actual class Foo actual co<caret>nstructor()
//INFO: <div class='definition'><pre><span style="color:#000080;font-weight:bold;">public</span> <span style="color:#000080;font-weight:bold;">actual</span> <span style="color:#000080;font-weight:bold;">constructor</span> <span style="color:#000000;">Foo</span>()</pre></div><div class='content'><p style='margin-top:0;padding-top:0;'>Common Description</p></div><table class='sections'></table><div class='bottom'><icon src="/org/jetbrains/kotlin/idea/icons/classKotlin.svg"/>&nbsp;<a href="psi_element://Foo"><code><span style="color:#000000;">Foo</span></code></a><br/></div>
@@ -0,0 +1,13 @@
// PLATFORM: Common
// FILE: Foo.kt
/**
* Doc for expected class Foo
*/
expect class Foo
// PLATFORM: Jvm
// FILE: Foo.kt
// MAIN
actual class F<caret>oo
//INFO: <div class='definition'><pre><span style="color:#000080;font-weight:bold;">public</span> <span style="color:#000080;font-weight:bold;">actual</span> <span style="color:#000080;font-weight:bold;">final</span> <span style="color:#000080;font-weight:bold;">class</span> <span style="color:#000000;">Foo</span></pre></div><div class='content'><p style='margin-top:0;padding-top:0;'>Doc for expected class Foo</p></div><table class='sections'></table><div class='bottom'><icon src="/org/jetbrains/kotlin/idea/icons/kotlin_file.svg"/>&nbsp;Foo.kt<br/></div>
@@ -0,0 +1,15 @@
// PLATFORM: Common
// FILE: Foo.kt
/**
* Doc for expected class Foo
*
* @param param doc for expected `param` of function `foo`
*/
expect fun foo(param: String)
// PLATFORM: Jvm
// FILE: Foo.kt
// MAIN
actual fun foo(pa<caret>ram: String) {}
//INFO: <div class='definition'><pre><span style="color:#000080;font-weight:bold;">value-parameter</span> <span style="color:#000000;">param</span><span style="">: </span><span style="color:#000000;">String</span></pre></div><div class='content'><p style='margin-top:0;padding-top:0;'>doc for expected <code><span style="">param</span></code> of function <code><span style="">foo</span></code></p></div><table class='sections'></table>
@@ -0,0 +1,75 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.test
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.testFramework.*
import org.jetbrains.kotlin.idea.base.test.ModuleStructureSplitter
import org.jetbrains.kotlin.idea.framework.KotlinSdkType
import org.jetbrains.kotlin.idea.test.util.slashedPath
import java.io.File
abstract class KotlinLightMultiplatformCodeInsightFixtureTestCase : KotlinLightCodeInsightFixtureTestCaseBase() {
private val exceptions = ArrayList<Throwable>()
@Deprecated("Migrate to 'testDataDirectory'.", ReplaceWith("testDataDirectory"))
final override fun getTestDataPath(): String = testDataDirectory.slashedPath
open val testDataDirectory: File by lazy {
File(TestMetadataUtil.getTestDataPath(javaClass))
}
/**
* Configures the module structure based on the given file.
*
* File is expected to have the following structure:
* // PLATFORM: (Common|Jvm|Js) [org.jetbrains.kotlin.idea.test.KotlinMultiPlatformProjectDescriptor.PlatformDescriptor]
* files
*
* // FILE: relativePath.kt (relative to the platform's source root)
* // MAIN (if this file should be configured in the editor)
* file content
*
* Each file is added to the test project's platform module.
*
* Returns file which was marked as `MAIN` or null
*/
fun configureModuleStructure(abstractFilePath: String): VirtualFile? {
val map = ModuleStructureSplitter.splitPerModule(File(abstractFilePath))
var currentFile: VirtualFile? = null
map.forEach { (platform, files) ->
val platformDescriptor = when (platform) {
"Common" -> KotlinMultiPlatformProjectDescriptor.PlatformDescriptor.COMMON
"Jvm" -> KotlinMultiPlatformProjectDescriptor.PlatformDescriptor.JVM
"Js" -> KotlinMultiPlatformProjectDescriptor.PlatformDescriptor.JS
else -> null
}
if (platformDescriptor != null) {
for (testFile in files) {
val virtualFile = VfsTestUtil.createFile(platformDescriptor.sourceRoot()!!, testFile.relativePath, testFile.text)
if (testFile.isMain) {
currentFile = virtualFile
}
myFixture.configureFromExistingVirtualFile(virtualFile)
}
}
}
return currentFile
}
override fun tearDown() {
runAll(
{ KotlinMultiPlatformProjectDescriptor.cleanupSourceRoots() },
{ KotlinSdkType.removeKotlinSdkInTests() },
{ super.tearDown() },
)
if (exceptions.isNotEmpty()) {
exceptions.forEach { it.printStackTrace() }
throw AssertionError("Exceptions in other threads happened")
}
}
override fun getProjectDescriptor(): LightProjectDescriptor = KotlinMultiPlatformProjectDescriptor
}
@@ -0,0 +1,101 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.test
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.*
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.ex.temp.TempFileSystem
import com.intellij.pom.java.LanguageLevel
import com.intellij.testFramework.IdeaTestUtil
import com.intellij.testFramework.IndexingTestUtil
import org.jetbrains.jps.model.java.JavaSourceRootType
import org.jetbrains.kotlin.platform.TargetPlatform
import org.jetbrains.kotlin.platform.js.JsPlatforms
import org.jetbrains.kotlin.platform.jvm.JvmPlatforms
/**
* The project is created with three modules: Common, Jvm -> Common, Js -> Common.
*
* Currently, no libraries are added.
*/
object KotlinMultiPlatformProjectDescriptor : KotlinLightProjectDescriptor() {
enum class PlatformDescriptor(
val moduleName: String,
val sourceRootName: String? = null
) {
COMMON("Common", sourceRootName = "src_common"),
JVM("Jvm", sourceRootName = "src_jvm"),
JS("Js", sourceRootName = "src_js");
fun sourceRoot(): VirtualFile? = findRoot(sourceRootName)
private fun findRoot(rootName: String?): VirtualFile? =
if (rootName == null) null
else TempFileSystem.getInstance().findFileByPath("/${rootName}")
?: throw IllegalStateException("Cannot find temp:///${rootName}")
}
override fun getSdk(): Sdk = IdeaTestUtil.getMockJdk9()
override fun setUpProject(project: Project, handler: SetupHandler) {
super.setUpProject(project, handler)
runWriteAction {
val common = makeModule(project, PlatformDescriptor.COMMON)
val jvm = makeModule(project, PlatformDescriptor.JVM)
ModuleRootModificationUtil.addDependency(jvm, common)
val js = makeModule(project, PlatformDescriptor.JS)
ModuleRootModificationUtil.addDependency(js, common)
}
IndexingTestUtil.waitUntilIndexesAreReady(project)
}
private fun makeModule(project: Project, descriptor: PlatformDescriptor): Module {
val path = "${FileUtil.getTempDirectory()}/${descriptor.moduleName}.iml"
val module = createModule(project, path)
ModuleRootModificationUtil.updateModel(module) { configureModule(module, it, descriptor) }
return module
}
private fun configureModule(module: Module, model: ModifiableRootModel, descriptor: PlatformDescriptor) {
model.getModuleExtension(LanguageLevelModuleExtension::class.java).languageLevel = LanguageLevel.HIGHEST
model.sdk = sdk
if (descriptor.sourceRootName != null) {
val sourceRoot = createSourceRoot(module, descriptor.sourceRootName)
model.addContentEntry(sourceRoot).addSourceFolder(sourceRoot, JavaSourceRootType.SOURCE)
}
when (descriptor) {
PlatformDescriptor.JVM -> {
module.createMultiplatformFacetM3(JvmPlatforms.jvm8, false, listOf("Common"), listOf(descriptor.sourceRoot()!!.path))
}
PlatformDescriptor.JS ->
module.createMultiplatformFacetM3(JsPlatforms.defaultJsPlatform, false, listOf("Common"), listOf(descriptor.sourceRoot()!!.path))
PlatformDescriptor.COMMON ->
module.createMultiplatformFacetM3(
TargetPlatform(
setOf(
JvmPlatforms.jvm8.single(),
JsPlatforms.defaultJsPlatform.single()
)
), false, emptyList(), listOf(descriptor.sourceRoot()!!.path)
)
}
}
fun cleanupSourceRoots() = runWriteAction {
PlatformDescriptor.entries.asSequence()
.map { it.sourceRoot() }
.filterNotNull()
.flatMap { it.children.asSequence() }
.forEach { it.delete(this) }
}
}
@@ -625,7 +625,7 @@ private fun assembleWorkspace(): TWorkspace = workspace {
}
testClass<AbstractQuickDocProviderTest> {
model("editor/quickDoc", pattern = Patterns.forRegex("""^([^_]+)\.(kt|java)$"""))
model("editor/quickDoc", pattern = Patterns.forRegex("""^([^_]+)\.(kt|java)$"""), isRecursive = false)
}
testClass<AbstractSafeDeleteTest> {
@@ -20,6 +20,7 @@ import org.jetbrains.kotlin.idea.fir.completion.test.handlers.*
import org.jetbrains.kotlin.idea.fir.completion.wheigher.AbstractHighLevelWeigherTest
import org.jetbrains.kotlin.idea.fir.copyPaste.AbstractFirLiteralKotlinToKotlinCopyPasteTest
import org.jetbrains.kotlin.idea.fir.copyPaste.AbstractFirLiteralTextToKotlinCopyPasteTest
import org.jetbrains.kotlin.idea.fir.documentation.AbstractFirQuickDocMultiplatformTest
import org.jetbrains.kotlin.idea.fir.documentation.AbstractFirQuickDocTest
import org.jetbrains.kotlin.idea.fir.externalAnnotations.AbstractK2ExternalAnnotationTest
import org.jetbrains.kotlin.idea.fir.findUsages.*
@@ -374,7 +375,10 @@ private fun assembleWorkspace(): TWorkspace = workspace {
testGroup("fir/tests") {
testClass<AbstractFirQuickDocTest> {
model("../../../idea/tests/testData/editor/quickDoc", pattern = Patterns.forRegex("""^([^_]+)\.(kt|java)$"""))
model("../../../idea/tests/testData/editor/quickDoc", pattern = Patterns.forRegex("""^([^_]+)\.(kt|java)$"""), isRecursive = false)
}
testClass<AbstractFirQuickDocMultiplatformTest> {
model("../../../idea/tests/testData/editor/quickDoc/multiplatform", pattern = Patterns.forRegex("""^([^_]+)\.(kt|java)$"""))
}
}