From fcf649cc7593ef6a3ad4fe11ed76a554e05efc08 Mon Sep 17 00:00:00 2001 From: Sebastian Sellmair Date: Fri, 16 Aug 2024 13:55:08 +0200 Subject: [PATCH] [kotlin] K2: Implement 'CreateActualForExpectLocalQuickFix' to fix missing actuals See: ^FL-28916 GitOrigin-RevId: 9c14adc27d853261d505ac2126cbc8a40a48d544 --- .../messages/KotlinBundle.properties | 2 + ...LevelQuickFixMultiModuleTestGenerated.java | 10 + .../kotlin.code-insight.inspections.k2.iml | 5 + .../KotlinNoActualForExpectInspection.kt | 227 +++++++++++++++++- .../core/overrideImplement/KtClassMember.kt | 5 +- ...kotlin.gradle.gradle-java.tests.shared.iml | 1 + .../QuickFixMultiModuleTestGenerated.java | 10 + .../createActual/functionK2/header/header.kt | 5 + .../functionK2/header/header.kt.after | 5 + .../functionK2/jvm/header.testModule_JVM.kt | 1 + .../jvm/header.testModule_JVM.kt.after | 4 + .../createActual/propertyK2/header/header.kt | 5 + .../propertyK2/jvm/header.testModule_JVM.kt | 1 + .../jvm/header.testModule_JVM.kt.after | 4 + .../test-framework/kotlin.test-framework.iml | 1 + .../idea/multiplatform/multiPlatformSetup.kt | 26 +- 16 files changed, 299 insertions(+), 13 deletions(-) create mode 100644 plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/functionK2/header/header.kt create mode 100644 plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/functionK2/header/header.kt.after create mode 100644 plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/functionK2/jvm/header.testModule_JVM.kt create mode 100644 plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/functionK2/jvm/header.testModule_JVM.kt.after create mode 100644 plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/propertyK2/header/header.kt create mode 100644 plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/propertyK2/jvm/header.testModule_JVM.kt create mode 100644 plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/propertyK2/jvm/header.testModule_JVM.kt.after diff --git a/plugins/kotlin/base/resources/resources-en/messages/KotlinBundle.properties b/plugins/kotlin/base/resources/resources-en/messages/KotlinBundle.properties index f6508795652a..c9e3b0ac75c3 100644 --- a/plugins/kotlin/base/resources/resources-en/messages/KotlinBundle.properties +++ b/plugins/kotlin/base/resources/resources-en/messages/KotlinBundle.properties @@ -1069,6 +1069,8 @@ add.0.constructor.to.1=Add {0} constructor to ''{1}'' text.secondary=secondary text.primary=primary add.method.0.to.1=Add method ''{0}'' to ''{1}'' +create.actual=Create actual +create.actual.in.0=Create actual in ''{0}'' create.actual.0.for.module.1.2=Create actual {0} for module {1} ({2}) create.expected.0.in.common.module.1=Create expected {0} in common module {1} repair.actual.members=Repair actual members diff --git a/plugins/kotlin/code-insight/fixes-k2/tests/test/org/jetbrains/kotlin/idea/k2/codeinsight/fixes/HighLevelQuickFixMultiModuleTestGenerated.java b/plugins/kotlin/code-insight/fixes-k2/tests/test/org/jetbrains/kotlin/idea/k2/codeinsight/fixes/HighLevelQuickFixMultiModuleTestGenerated.java index 281e5e51571b..ee292b672596 100644 --- a/plugins/kotlin/code-insight/fixes-k2/tests/test/org/jetbrains/kotlin/idea/k2/codeinsight/fixes/HighLevelQuickFixMultiModuleTestGenerated.java +++ b/plugins/kotlin/code-insight/fixes-k2/tests/test/org/jetbrains/kotlin/idea/k2/codeinsight/fixes/HighLevelQuickFixMultiModuleTestGenerated.java @@ -650,6 +650,11 @@ public abstract class HighLevelQuickFixMultiModuleTestGenerated extends Abstract runTest("../../../idea/tests/testData/multiModuleQuickFix/createActual/function/"); } + @TestMetadata("functionK2") + public void testFunctionK2() throws Exception { + runTest("../../../idea/tests/testData/multiModuleQuickFix/createActual/functionK2/"); + } + @TestMetadata("functionSameFile") public void testFunctionSameFile() throws Exception { runTest("../../../idea/tests/testData/multiModuleQuickFix/createActual/functionSameFile/"); @@ -700,6 +705,11 @@ public abstract class HighLevelQuickFixMultiModuleTestGenerated extends Abstract runTest("../../../idea/tests/testData/multiModuleQuickFix/createActual/property/"); } + @TestMetadata("propertyK2") + public void testPropertyK2() throws Exception { + runTest("../../../idea/tests/testData/multiModuleQuickFix/createActual/propertyK2/"); + } + @TestMetadata("sealed") public void testSealed() throws Exception { runTest("../../../idea/tests/testData/multiModuleQuickFix/createActual/sealed/"); diff --git a/plugins/kotlin/code-insight/inspections-k2/kotlin.code-insight.inspections.k2.iml b/plugins/kotlin/code-insight/inspections-k2/kotlin.code-insight.inspections.k2.iml index a44b2090b72e..81dea258a4cc 100644 --- a/plugins/kotlin/code-insight/inspections-k2/kotlin.code-insight.inspections.k2.iml +++ b/plugins/kotlin/code-insight/inspections-k2/kotlin.code-insight.inspections.k2.iml @@ -62,5 +62,10 @@ + + + + + \ No newline at end of file diff --git a/plugins/kotlin/code-insight/inspections-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/inspections/KotlinNoActualForExpectInspection.kt b/plugins/kotlin/code-insight/inspections-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/inspections/KotlinNoActualForExpectInspection.kt index 3d6c1903ac26..3def5ab6944f 100644 --- a/plugins/kotlin/code-insight/inspections-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/inspections/KotlinNoActualForExpectInspection.kt +++ b/plugins/kotlin/code-insight/inspections-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/inspections/KotlinNoActualForExpectInspection.kt @@ -1,28 +1,51 @@ // 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.k2.codeinsight.inspections -import com.intellij.codeInspection.LocalInspectionToolSession -import com.intellij.codeInspection.ProblemsHolder +import com.intellij.codeInspection.* +import com.intellij.modcommand.* +import com.intellij.openapi.application.* import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleManager +import com.intellij.openapi.project.DumbService +import com.intellij.openapi.project.Project +import com.intellij.openapi.project.rootManager import com.intellij.openapi.util.Key import com.intellij.openapi.util.getOrCreateUserData -import com.intellij.psi.PsiElementVisitor +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.findOrCreateDirectory +import com.intellij.psi.* import com.intellij.psi.PsiElementVisitor.EMPTY_VISITOR import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.util.childrenOfType import com.intellij.psi.util.findParentOfType +import com.intellij.util.concurrency.annotations.RequiresWriteLock +import org.jetbrains.kotlin.analysis.api.KaExperimentalApi import org.jetbrains.kotlin.analysis.api.analyze +import org.jetbrains.kotlin.analysis.api.symbols.KaCallableSymbol +import org.jetbrains.kotlin.idea.base.codeInsight.ShortenReferencesFacility +import org.jetbrains.kotlin.idea.base.externalSystem.KotlinBuildSystemFacade +import org.jetbrains.kotlin.idea.base.externalSystem.KotlinBuildSystemSourceSet import org.jetbrains.kotlin.idea.base.facet.implementedModules import org.jetbrains.kotlin.idea.base.facet.implementingModules import org.jetbrains.kotlin.idea.base.facet.isMultiPlatformModule +import org.jetbrains.kotlin.idea.base.facet.isTestModule import org.jetbrains.kotlin.idea.base.resources.KotlinBundle +import org.jetbrains.kotlin.idea.base.util.isAndroidModule import org.jetbrains.kotlin.idea.base.util.module +import org.jetbrains.kotlin.idea.base.util.reformatted import org.jetbrains.kotlin.idea.codeinsight.api.classic.inspections.AbstractKotlinInspection +import org.jetbrains.kotlin.idea.core.overrideImplement.MemberGenerateMode +import org.jetbrains.kotlin.idea.core.overrideImplement.generateMember +import org.jetbrains.kotlin.idea.k2.refactoring.move.processor.createKotlinFile import org.jetbrains.kotlin.idea.searching.kmp.findAllActualForExpect +import org.jetbrains.kotlin.idea.util.sourceRoot import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.name.StandardClassIds -import org.jetbrains.kotlin.psi.KtDeclaration -import org.jetbrains.kotlin.psi.KtModifierList -import org.jetbrains.kotlin.psi.KtVisitorVoid +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.tooling.core.withClosure +import kotlin.io.path.Path +import kotlin.io.path.absolutePathString class KotlinNoActualForExpectInspection : AbstractKotlinInspection() { @@ -59,6 +82,7 @@ class KotlinNoActualForExpectInspection : AbstractKotlinInspection() { } } + @OptIn(KaExperimentalApi::class) override fun buildVisitor( holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession ): PsiElementVisitor { @@ -67,13 +91,13 @@ class KotlinNoActualForExpectInspection : AbstractKotlinInspection() { return object : KtVisitorVoid() { override fun visitModifierList(list: KtModifierList) { - val module = list.module ?: return + val expectModule = list.module ?: return val expectModifier = list.getModifier(KtTokens.EXPECT_KEYWORD) ?: return val parentDeclaration = list.findParentOfType() ?: return if (parentDeclaration.hasOptionalExpectationAnnotation()) return - val leaves = session.getLeaves(module) - val foundActuals = parentDeclaration.findAllActualForExpect(session.getActualsSearchScope(module)) + val leaves = session.getLeaves(expectModule) + val foundActuals = parentDeclaration.findAllActualForExpect(session.getActualsSearchScope(expectModule)) .mapNotNullTo(mutableSetOf()) { it.element?.module }.toSet() val missingActuals = leaves.filter { module -> @@ -81,12 +105,195 @@ class KotlinNoActualForExpectInspection : AbstractKotlinInspection() { } if (missingActuals.isEmpty()) return + /* + Will return all modules that could potentially fix the issue by providing the actual. + In the example above, it will return `nativeMain`, `appleMain`, `iosMain`, `iosX64Main` iosArm64Main` + */ + val allModulesCapableOfProvidingActuals = missingActuals + .withClosure { leafModule -> leafModule.implementedModules - expectModule } + .reversed() /* reversing the list to ensure that modules "closest to the 'expect'" come first. */ + + val actualDeclaration = analyze(parentDeclaration) { + generateMember( + project = expectModule.project, + ktClassMember = null, + symbol = parentDeclaration.symbol as? KaCallableSymbol ?: return@analyze null, + targetClass = null, + copyDoc = false, + mode = MemberGenerateMode.ACTUAL + ) + } + + val fixes = allModulesCapableOfProvidingActuals.mapIndexedNotNull map@{ index, actualModule -> + CreateActualForExpectLocalQuickFix( + priority = index, moduleName = actualModule.name, + sourceSet = KotlinBuildSystemFacade.getInstance().findSourceSet(actualModule) ?: return@map null, + actualDeclaration = actualDeclaration?.createSmartPointer() ?: return@map null, + ) + } + // We only care about the name of the target, not the common submodule of the MPP module val missingModulesWithActuals = missingActuals.joinToString { it.name.substringAfterLast('.') } + holder.registerProblem( - expectModifier, KotlinBundle.message("no.actual.for.expect.declaration", missingModulesWithActuals) + expectModifier, KotlinBundle.message("no.actual.for.expect.declaration", missingModulesWithActuals), + *fixes.toTypedArray() ) } } } +} + +private class CreateActualForExpectLocalQuickFix( + private val priority: Int, + private val moduleName: String, + private val sourceSet: KotlinBuildSystemSourceSet, + private val actualDeclaration: SmartPsiElementPointer, +) : LocalQuickFix { + + override fun startInWriteAction(): Boolean { + return true + } + + override fun getName(): String { + /** + * We're using a 'zero width space' character to influence the sorting of the QuickFix as they + * are traditionally just sorted by their name. + */ + val sortingWhiteSpace = buildString(priority) { repeat(priority) { append('\u200B') } } + return KotlinBundle.message("create.actual.in.0", "$sortingWhiteSpace${sourceSet.name}") + } + + override fun getFamilyName(): String { + return KotlinBundle.message("create.actual") + } + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val module = ModuleManager.getInstance(project).findModuleByName(moduleName) ?: return + val expectDeclaration = descriptor.psiElement.findParentOfType() ?: return + val expectPackageFqn = expectDeclaration.containingKtFile.packageFqName + + /** + * Where is the 'expect' declaration located in regard to its source root? + * For example, src/commonMain/kotlin/my/pkg/foo/Foo.kt + * + * This relative path can then be used to put the actual under the same relative path. + * Note: The package name is used as convention. + */ + val expectRelativePackagePath: String = run { + val expectSourceFileDirectory = expectDeclaration.containingKtFile.containingDirectory?.virtualFile ?: return@run null + val expectSourceRootDirectory = expectDeclaration.containingKtFile.sourceRoot ?: return@run null + VfsUtil.getRelativePath(expectSourceFileDirectory, expectSourceRootDirectory) + } /* Fallback: Construct a path from packageFqName */ ?: expectPackageFqn.pathSegments().joinToString(VfsUtil.VFS_SEPARATOR) + + + /** + * Let's get a directory in the [sourceSet] that would allow us to find or create + * the corresponding KtFile. Create the source root in the IJ model if necessary. + */ + val actualDirectory: PsiDirectory = run { + val actualSourceRoot = sourceSet.findOrCreateMostSuitableKotlinSourceRoot() ?: return + val modifiableModel = module.rootManager.modifiableModel + val contentEntry = modifiableModel.contentEntries.find { contentEntry -> + contentEntry.file?.let { contentEntryFile -> VfsUtil.isAncestor(contentEntryFile, actualSourceRoot, false) } ?: false + } ?: return + + if (contentEntry.sourceFolders.none { it.file == actualSourceRoot }) { + contentEntry.addSourceFolder(actualSourceRoot, module.isTestModule) + } + modifiableModel.commit() + val actualDirectory = expectRelativePackagePath.split(VfsUtil.VFS_SEPARATOR) + .fold(actualSourceRoot) { acc, segment -> acc.findOrCreateDirectory(segment) } + PsiManager.getInstance(project).findDirectory(actualDirectory) ?: return + } + + /** + * Find an existing KtFile that also matches the package name or create a new file: + * Note: We've located the [actualDirectory], and we know what the target file name shall be, + * but this file might already exist, but might have a different package name (which would not match). + * In this case we try to find another file which does not yet exist or has a matching package name. + */ + val actualKtFile: KtFile = run { + val fileName = getActualTargetFileName(module, sourceSet, expectDeclaration.containingKtFile) + /* + We attempt to find or create the file. If there exists af file with the current fileName, which + cannot be re-used (e.g., because of a non-fitting package), then we try to find or create + another file (by counting up foo1.kt, foo2.kt, foo3.kt ...) + */ + val fileNameCandidates = listOf("$fileName.kt") + (1..16).map { "${fileName}$it.kt" } + fileNameCandidates.firstNotNullOfOrNull findOrCreate@{ fileNameCandidate -> + val existing = actualDirectory.findFile(fileNameCandidate) + /* No file with this fileName exists: We can try to create it! */ + ?: return@findOrCreate createKotlinFile( + fileNameCandidate, actualDirectory, expectPackageFqn.asString() + ) + + /* Check: The existing file could still have a different package FQN: In this case, we cannot re-use it! */ + (existing as? KtFile)?.takeIf { file -> file.packageFqName == expectPackageFqn } + } + } ?: return + + /** + * The above call to [findOrCreateKtFileForActualDeclaration] might have created a new source root and therefore triggered + * dumb mode. We execute the action right away because we require smart mode for creating the 'actual' actual (pun intended) + */ + DumbService.getInstance(project).completeJustSubmittedTasks() + + val added = actualKtFile.add(actualDeclaration.element ?: return).reformatted() as? KtNamedDeclaration ?: return + val shortened = ShortenReferencesFacility.getInstance().shorten(added) as? KtDeclaration + + /** + * Find the navigation target: + * + * For example in: + * ``` + * actual fun bar() { + * T0D0("Not yet implemented") + * } + * + * ``` + * + * it would be convenient if we navigate to the 'T0D0' and select the expression, so that + * writing the implementation can be started right away! + */ + val navigationTarget = run { + val target = shortened ?: added + val blockExpression = target.childrenOfType().firstOrNull() ?: return@run target + blockExpression.children.first() as? KtElement ?: return@run target + } + + navigationTarget.navigate(true) + + } +} + +@RequiresWriteLock +private fun KotlinBuildSystemSourceSet.findOrCreateMostSuitableKotlinSourceRoot(): VirtualFile? { + val sourceDirectoryPath = sourceDirectories + /* + Prioritize source directories which contain the name of the source set: + for example, a 'jvmMain' source set should prioritize '.../src/jvmMain/kotlin' + */ + .find { path -> path.contains(Path(name)) } + ?: sourceDirectories.firstOrNull() + ?: return null + + return VfsUtil.createDirectoryIfMissing(sourceDirectoryPath.absolutePathString()) +} + + +/** + * Having a Source File with the same name in the same package in several Source Sets might be a problem. + * e.g. `Foo.kt` in commonMain and `Foo.kt` in jvmMain could produce a broken .jar file. + * Therefore, the file gets mangled by adding a classifier from the module as in `Foo.jvm.kt` + */ +private fun getActualTargetFileName( + module: Module, + sourceSet: KotlinBuildSystemSourceSet, + expect: PsiFile +): String { + /* File name matches: Let's mangle by adding a module string like '.jvm', '.android', '.ios' */ + val moduleClassifier = if (module.isAndroidModule()) "android" + else sourceSet.name.removeSuffix("Main").removeSuffix("Test") + return "${expect.virtualFile.nameWithoutExtension}.$moduleClassifier" } \ No newline at end of file diff --git a/plugins/kotlin/code-insight/override-implement-k2/src/org/jetbrains/kotlin/idea/core/overrideImplement/KtClassMember.kt b/plugins/kotlin/code-insight/override-implement-k2/src/org/jetbrains/kotlin/idea/core/overrideImplement/KtClassMember.kt index f11467110bc1..5e963cfbeb86 100644 --- a/plugins/kotlin/code-insight/override-implement-k2/src/org/jetbrains/kotlin/idea/core/overrideImplement/KtClassMember.kt +++ b/plugins/kotlin/code-insight/override-implement-k2/src/org/jetbrains/kotlin/idea/core/overrideImplement/KtClassMember.kt @@ -99,13 +99,14 @@ context(KaSession) @ApiStatus.Internal fun generateMember( project: Project, - ktClassMember: KtClassMember, + ktClassMember: KtClassMember?, symbol: KaCallableSymbol, targetClass: KtClassOrObject?, copyDoc: Boolean, mode: MemberGenerateMode = MemberGenerateMode.OVERRIDE ): KtCallableDeclaration = with(ktClassMember) { val bodyType = when { + this == null -> BodyType.FromTemplate targetClass?.hasExpectModifier() == true -> BodyType.NoBody symbol.isExtension && mode == MemberGenerateMode.OVERRIDE -> BodyType.FromTemplate else -> bodyType @@ -194,7 +195,7 @@ fun generateMember( } } - if (preferConstructorParameter && ktClassMember.memberInfo.isProperty) { + if (this != null && preferConstructorParameter && memberInfo.isProperty) { return generateConstructorParameter(project, symbol, renderer) } diff --git a/plugins/kotlin/gradle/gradle-java/tests.shared/kotlin.gradle.gradle-java.tests.shared.iml b/plugins/kotlin/gradle/gradle-java/tests.shared/kotlin.gradle.gradle-java.tests.shared.iml index afd7e3b430dd..e8271709d17a 100644 --- a/plugins/kotlin/gradle/gradle-java/tests.shared/kotlin.gradle.gradle-java.tests.shared.iml +++ b/plugins/kotlin/gradle/gradle-java/tests.shared/kotlin.gradle.gradle-java.tests.shared.iml @@ -56,5 +56,6 @@ + \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiModuleTestGenerated.java b/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiModuleTestGenerated.java index deebd8030295..5b5a479d10c5 100644 --- a/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiModuleTestGenerated.java +++ b/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiModuleTestGenerated.java @@ -650,6 +650,11 @@ public abstract class QuickFixMultiModuleTestGenerated extends AbstractQuickFixM runTest("testData/multiModuleQuickFix/createActual/function/"); } + @TestMetadata("functionK2") + public void testFunctionK2() throws Exception { + runTest("testData/multiModuleQuickFix/createActual/functionK2/"); + } + @TestMetadata("functionSameFile") public void testFunctionSameFile() throws Exception { runTest("testData/multiModuleQuickFix/createActual/functionSameFile/"); @@ -700,6 +705,11 @@ public abstract class QuickFixMultiModuleTestGenerated extends AbstractQuickFixM runTest("testData/multiModuleQuickFix/createActual/property/"); } + @TestMetadata("propertyK2") + public void testPropertyK2() throws Exception { + runTest("testData/multiModuleQuickFix/createActual/propertyK2/"); + } + @TestMetadata("sealed") public void testSealed() throws Exception { runTest("testData/multiModuleQuickFix/createActual/sealed/"); diff --git a/plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/functionK2/header/header.kt b/plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/functionK2/header/header.kt new file mode 100644 index 000000000000..cb3e9cebc93b --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/functionK2/header/header.kt @@ -0,0 +1,5 @@ +// "Create actual in 'testModule_JVM'" "true" +// IGNORE_K1 +// TOOL: org.jetbrains.kotlin.idea.k2.codeinsight.inspections.KotlinNoActualForExpectInspection + +expect fun foo(arg: Int): String \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/functionK2/header/header.kt.after b/plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/functionK2/header/header.kt.after new file mode 100644 index 000000000000..17a4af597fd6 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/functionK2/header/header.kt.after @@ -0,0 +1,5 @@ +// "Create actual in 'testModule_JVM'" "true" +// IGNORE_K1 +// TOOL: org.jetbrains.kotlin.idea.k2.codeinsight.inspections.KotlinNoActualForExpectInspection + +expect fun foo(arg: Int): String diff --git a/plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/functionK2/jvm/header.testModule_JVM.kt b/plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/functionK2/jvm/header.testModule_JVM.kt new file mode 100644 index 000000000000..611b3f4a6ebb --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/functionK2/jvm/header.testModule_JVM.kt @@ -0,0 +1 @@ +// foo: to be implemented \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/functionK2/jvm/header.testModule_JVM.kt.after b/plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/functionK2/jvm/header.testModule_JVM.kt.after new file mode 100644 index 000000000000..d4644636ac18 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/functionK2/jvm/header.testModule_JVM.kt.after @@ -0,0 +1,4 @@ +// foo: to be implemented +actual fun foo(arg: Int): String { + TODO("Not yet implemented") +} diff --git a/plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/propertyK2/header/header.kt b/plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/propertyK2/header/header.kt new file mode 100644 index 000000000000..d489488231b6 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/propertyK2/header/header.kt @@ -0,0 +1,5 @@ +// "Create actual in 'testModule_JVM'" "true" +// IGNORE_K1 +// TOOL: org.jetbrains.kotlin.idea.k2.codeinsight.inspections.KotlinNoActualForExpectInspection + +expect var x: Int \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/propertyK2/jvm/header.testModule_JVM.kt b/plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/propertyK2/jvm/header.testModule_JVM.kt new file mode 100644 index 000000000000..04a172b156e5 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/propertyK2/jvm/header.testModule_JVM.kt @@ -0,0 +1 @@ +// x: to be implemented \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/propertyK2/jvm/header.testModule_JVM.kt.after b/plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/propertyK2/jvm/header.testModule_JVM.kt.after new file mode 100644 index 000000000000..c48d477e6b66 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/multiModuleQuickFix/createActual/propertyK2/jvm/header.testModule_JVM.kt.after @@ -0,0 +1,4 @@ +// x: to be implemented +actual var x: Int + get() = TODO("Not yet implemented") + set(value) {} diff --git a/plugins/kotlin/test-framework/kotlin.test-framework.iml b/plugins/kotlin/test-framework/kotlin.test-framework.iml index 9ea395e5e2da..40eb07a56f3c 100644 --- a/plugins/kotlin/test-framework/kotlin.test-framework.iml +++ b/plugins/kotlin/test-framework/kotlin.test-framework.iml @@ -42,5 +42,6 @@ + \ No newline at end of file diff --git a/plugins/kotlin/test-framework/test/org/jetbrains/kotlin/idea/multiplatform/multiPlatformSetup.kt b/plugins/kotlin/test-framework/test/org/jetbrains/kotlin/idea/multiplatform/multiPlatformSetup.kt index 24b4fdb1dbc5..248bd1921bc6 100644 --- a/plugins/kotlin/test-framework/test/org/jetbrains/kotlin/idea/multiplatform/multiPlatformSetup.kt +++ b/plugins/kotlin/test-framework/test/org/jetbrains/kotlin/idea/multiplatform/multiPlatformSetup.kt @@ -5,10 +5,13 @@ package org.jetbrains.kotlin.idea.multiplatform import com.intellij.openapi.application.runReadAction import com.intellij.openapi.module.Module import com.intellij.openapi.module.StdModuleTypes +import com.intellij.openapi.observable.util.whenDisposed import com.intellij.openapi.projectRoots.ProjectJdkTable import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.vfs.LocalFileSystem import org.jetbrains.kotlin.checkers.utils.clearFileFromDiagnosticMarkup +import org.jetbrains.kotlin.idea.base.externalSystem.KotlinBuildSystemFacade +import org.jetbrains.kotlin.idea.base.externalSystem.KotlinBuildSystemSourceSet import org.jetbrains.kotlin.idea.base.platforms.KotlinCommonLibraryKind import org.jetbrains.kotlin.idea.base.platforms.KotlinJavaScriptLibraryKind import org.jetbrains.kotlin.idea.base.platforms.KotlinWasmJsLibraryKind @@ -51,6 +54,7 @@ fun AbstractMultiModuleTest.setupMppProjectFromDirStructure(testRoot: File) { val dirs = testRoot.listFiles().filter { it.isDirectory } val rootInfos = dirs.map { parseDirName(it) } doSetupProject(rootInfos) + setupKotlinBuildSystemFacade() } fun AbstractMultiModuleTest.setupMppProjectFromTextFile(testRoot: File) { @@ -175,15 +179,21 @@ private fun AbstractMultiModuleTest.doSetupProject(rootInfos: List) { platform.isJvm() -> module.addLibrary(TestKotlinArtifacts.kotlinStdlib) platform.isJs() -> module.addLibrary(TestKotlinArtifacts.kotlinStdlibJs, kind = KotlinJavaScriptLibraryKind) platform.isWasmJs() -> module.addLibrary(TestKotlinArtifacts.kotlinStdlibWasmJs, kind = KotlinWasmJsLibraryKind) - platform.isWasmWasi() -> module.addLibrary(TestKotlinArtifacts.kotlinStdlibWasmWasi, kind = KotlinWasmWasiLibraryKind) + platform.isWasmWasi() -> module.addLibrary( + TestKotlinArtifacts.kotlinStdlibWasmWasi, + kind = KotlinWasmWasiLibraryKind + ) + else -> error("Unknown platform $this") } } + is FullJdkDependency -> { ConfigLibraryUtil.configureSdk(module, PluginTestCaseBase.addJdk(testRootDisposable) { PluginTestCaseBase.jdk(TestJdkKind.FULL_JDK) }) } + is CoroutinesDependency -> module.enableCoroutines() is KotlinTestDependency -> when { platform.isJvm() -> module.addLibrary(TestKotlinArtifacts.kotlinTestJunit) @@ -233,6 +243,20 @@ private fun AbstractMultiModuleTest.doSetupProject(rootInfos: List) { } } +/** + * See [KotlinBuildSystemFacade]: + * - Will use the Module's name as Source Set Name + * - Will use the module source roots as source directories + */ +private fun AbstractMultiModuleTest.setupKotlinBuildSystemFacade() { + KotlinBuildSystemFacade.EP_NAME.point.registerExtension(object : KotlinBuildSystemFacade { + override fun findSourceSet(module: Module) = KotlinBuildSystemSourceSet( + name = module.name, + sourceDirectories = module.sourceRoots.map { it.toNioPath() } + ) + }, testRootDisposable) +} + private fun AbstractMultiModuleTest.createModuleWithRoots( moduleId: ModuleId, infos: List