diff --git a/plugins/kotlin/base/test/test/org/jetbrains/kotlin/idea/base/test/ModuleStructureSplitter.kt b/plugins/kotlin/base/test/test/org/jetbrains/kotlin/idea/base/test/ModuleStructureSplitter.kt new file mode 100644 index 000000000000..23499948632e --- /dev/null +++ b/plugins/kotlin/base/test/test/org/jetbrains/kotlin/idea/base/test/ModuleStructureSplitter.kt @@ -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> { + val lines = file.readLines() + return splitPerModule(lines) + } + + fun splitPerModule(lines: List): MutableMap> { + val result = mutableMapOf>() + 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 + } +} \ No newline at end of file diff --git a/plugins/kotlin/code-insight/kotlin.code-insight.k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/quickDoc/KotlinDocumentationTarget.kt b/plugins/kotlin/code-insight/kotlin.code-insight.k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/quickDoc/KotlinDocumentationTarget.kt index 4512fd01c00c..97f310bf3e13 100644 --- a/plugins/kotlin/code-insight/kotlin.code-insight.k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/quickDoc/KotlinDocumentationTarget.kt +++ b/plugins/kotlin/code-insight/kotlin.code-insight.k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/quickDoc/KotlinDocumentationTarget.kt @@ -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().mapNotNull { expectFunction -> + findKDoc(expectFunction.valueParameters[idx]) + }.firstOrNull()?.let { return it } + } + } + + return (symbol as? KtDeclarationSymbol)?.getExpectsForActual()?.mapNotNull { declarationSymbol -> findKDoc(declarationSymbol) }?.firstOrNull() } context(KtAnalysisSession) diff --git a/plugins/kotlin/fir/tests/test/org/jetbrains/kotlin/idea/fir/documentation/AbstractFirQuickDocMultiplatformTest.kt b/plugins/kotlin/fir/tests/test/org/jetbrains/kotlin/idea/fir/documentation/AbstractFirQuickDocMultiplatformTest.kt new file mode 100644 index 000000000000..9892bc96dc34 --- /dev/null +++ b/plugins/kotlin/fir/tests/test/org/jetbrains/kotlin/idea/fir/documentation/AbstractFirQuickDocMultiplatformTest.kt @@ -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) + } + } + } +} diff --git a/plugins/kotlin/fir/tests/test/org/jetbrains/kotlin/idea/fir/documentation/FirQuickDocMultiplatformTestGenerated.java b/plugins/kotlin/fir/tests/test/org/jetbrains/kotlin/idea/fir/documentation/FirQuickDocMultiplatformTestGenerated.java new file mode 100644 index 000000000000..b356e6b03da1 --- /dev/null +++ b/plugins/kotlin/fir/tests/test/org/jetbrains/kotlin/idea/fir/documentation/FirQuickDocMultiplatformTestGenerated.java @@ -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"); + } +} diff --git a/plugins/kotlin/idea/tests/testData/editor/quickDoc/multiplatform/OnActualClassWithConstructorFromExpect.kt b/plugins/kotlin/idea/tests/testData/editor/quickDoc/multiplatform/OnActualClassWithConstructorFromExpect.kt new file mode 100644 index 000000000000..33cbc408f591 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/editor/quickDoc/multiplatform/OnActualClassWithConstructorFromExpect.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 constructor() + + + +//INFO:
public actual constructor Foo()

Common Description

 Foo
\ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/editor/quickDoc/multiplatform/OnActualDeclarationWithInheritedDoc.kt b/plugins/kotlin/idea/tests/testData/editor/quickDoc/multiplatform/OnActualDeclarationWithInheritedDoc.kt new file mode 100644 index 000000000000..87f22f7deade --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/editor/quickDoc/multiplatform/OnActualDeclarationWithInheritedDoc.kt @@ -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 Foo + +//INFO:
public actual final class Foo

Doc for expected class Foo

 Foo.kt
diff --git a/plugins/kotlin/idea/tests/testData/editor/quickDoc/multiplatform/OnActualValueParameterDeclaration.kt b/plugins/kotlin/idea/tests/testData/editor/quickDoc/multiplatform/OnActualValueParameterDeclaration.kt new file mode 100644 index 000000000000..975a1acff702 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/editor/quickDoc/multiplatform/OnActualValueParameterDeclaration.kt @@ -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(param: String) {} + +//INFO:
value-parameter param: String

doc for expected param of function foo

diff --git a/plugins/kotlin/test-framework/test/org/jetbrains/kotlin/idea/test/KotlinLightMultiplatformCodeInsightFixtureTestCase.kt b/plugins/kotlin/test-framework/test/org/jetbrains/kotlin/idea/test/KotlinLightMultiplatformCodeInsightFixtureTestCase.kt new file mode 100644 index 000000000000..1e87dd390fcd --- /dev/null +++ b/plugins/kotlin/test-framework/test/org/jetbrains/kotlin/idea/test/KotlinLightMultiplatformCodeInsightFixtureTestCase.kt @@ -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() + + @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 +} diff --git a/plugins/kotlin/test-framework/test/org/jetbrains/kotlin/idea/test/KotlinMultiPlatformProjectDescriptor.kt b/plugins/kotlin/test-framework/test/org/jetbrains/kotlin/idea/test/KotlinMultiPlatformProjectDescriptor.kt new file mode 100644 index 000000000000..efb25e7ddded --- /dev/null +++ b/plugins/kotlin/test-framework/test/org/jetbrains/kotlin/idea/test/KotlinMultiPlatformProjectDescriptor.kt @@ -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) } + } +} diff --git a/plugins/kotlin/util/test-generator-fe10/test/org/jetbrains/kotlin/fe10/testGenerator/Fe10GenerateTests.kt b/plugins/kotlin/util/test-generator-fe10/test/org/jetbrains/kotlin/fe10/testGenerator/Fe10GenerateTests.kt index bd010cf46c78..94c26ef0e2f9 100644 --- a/plugins/kotlin/util/test-generator-fe10/test/org/jetbrains/kotlin/fe10/testGenerator/Fe10GenerateTests.kt +++ b/plugins/kotlin/util/test-generator-fe10/test/org/jetbrains/kotlin/fe10/testGenerator/Fe10GenerateTests.kt @@ -625,7 +625,7 @@ private fun assembleWorkspace(): TWorkspace = workspace { } testClass { - model("editor/quickDoc", pattern = Patterns.forRegex("""^([^_]+)\.(kt|java)$""")) + model("editor/quickDoc", pattern = Patterns.forRegex("""^([^_]+)\.(kt|java)$"""), isRecursive = false) } testClass { diff --git a/plugins/kotlin/util/test-generator-fir/test/org/jetbrains/kotlin/fir/testGenerator/FirGenerateTests.kt b/plugins/kotlin/util/test-generator-fir/test/org/jetbrains/kotlin/fir/testGenerator/FirGenerateTests.kt index 0492e40aa13c..e44053070539 100644 --- a/plugins/kotlin/util/test-generator-fir/test/org/jetbrains/kotlin/fir/testGenerator/FirGenerateTests.kt +++ b/plugins/kotlin/util/test-generator-fir/test/org/jetbrains/kotlin/fir/testGenerator/FirGenerateTests.kt @@ -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 { - 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 { + model("../../../idea/tests/testData/editor/quickDoc/multiplatform", pattern = Patterns.forRegex("""^([^_]+)\.(kt|java)$""")) } }