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