KTIJ-16167: Implemented new CreateMissedActualsFix with UI dialog for selecting source sets

GitOrigin-RevId: 63c909bbacf2383228e7d01b3cbe99ebe4d44c42
This commit is contained in:
Konstantin Tskhovrebov
2023-04-20 11:19:01 +00:00
committed by intellij-monorepo-bot
parent 86fc992330
commit e7f3fd5d25
9 changed files with 559 additions and 272 deletions
@@ -168,6 +168,8 @@ fix.add.argument.name.step.choose.parameter.title=Choose Parameter Name
fix.create.expect.actual=Create expect / actual declaration
fix.create.missing.actual.members=Add missing actual members
fix.create.missing.actual.declarations=Add missing actual declarations
fix.create.missing.actual.declarations.title=Add Missing Actual Declarations
fix.create.declaration.error=Cannot generate {0}: {1}
fix.create.declaration.error.inaccessible.type=Inaccessible Type
@@ -766,6 +768,7 @@ label.text.file.name=File &name:
label.text.introduce.as=&Introduce as:\u0020
label.text.name=&Name:
label.text.package.name=Packa&ge name:
label.text.source.sets=Source sets:
label.text.receiver.type=Receiver &type:
label.text.target.file.name=Target file name:
label.text.to.file=To &file:
@@ -111,7 +111,7 @@ private fun findLongestExistingPackage(
private val kotlinSourceRootTypes: Set<JpsModuleSourceRootType<JavaSourceRootProperties>> =
setOf(SourceKotlinRootType, TestSourceKotlinRootType) + JavaModuleSourceRootTypes.SOURCES
private class PureKotlinSourceFoldersHolder {
class PureKotlinSourceFoldersHolder {
private val moduleMap = mutableMapOf<Module, Collection<String>?>()
/***
@@ -143,7 +143,7 @@ private fun Module.findNonGeneratedKotlinSourceFolders(): Sequence<SourceFolder>
it.jpsElement.getProperties(kotlinSourceRootTypes)?.isForGeneratedSources != true
}
private fun Module.findExistingNonGeneratedKotlinSourceRootFiles(
fun Module.findExistingNonGeneratedKotlinSourceRootFiles(
pureKotlinSourceFoldersHolder: PureKotlinSourceFoldersHolder
): List<VirtualFile> = findNonGeneratedKotlinSourceFolders().toExistingFiles(project, pureKotlinSourceFoldersHolder)
@@ -157,7 +157,7 @@ private fun Sequence<SourceFolder>.toExistingFiles(
sourceFolder.file?.takeIf { pureKotlinSourceFoldersHolder.hasPurePrefixInVirtualFile(project, it) }
}.toList()
private fun Module.findOrConfigureKotlinSourceRoots(pureKotlinSourceFoldersHolder: PureKotlinSourceFoldersHolder): List<VirtualFile> {
fun Module.findOrConfigureKotlinSourceRoots(pureKotlinSourceFoldersHolder: PureKotlinSourceFoldersHolder): List<VirtualFile> {
val nonGeneratedSourceFolders = findNonGeneratedKotlinSourceFolders().toList()
nonGeneratedSourceFolders.asSequence().toExistingFiles(project, pureKotlinSourceFoldersHolder).ifNotEmpty { return this }
return listOfNotNull(createSourceRootDirectory(nonGeneratedSourceFolders))
@@ -7,14 +7,21 @@ import com.intellij.codeInspection.LocalInspectionToolSession
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.psi.PsiElementVisitor
import org.jetbrains.kotlin.analyzer.ModuleInfo
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
import org.jetbrains.kotlin.analyzer.moduleInfo
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.idea.base.facet.implementingModules
import org.jetbrains.kotlin.idea.base.projectStructure.moduleInfo.ModuleSourceInfo
import org.jetbrains.kotlin.idea.base.projectStructure.moduleInfo.PlatformModuleInfo
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
import org.jetbrains.kotlin.idea.caches.project.implementingDescriptors
import org.jetbrains.kotlin.idea.caches.resolve.findModuleDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
import org.jetbrains.kotlin.idea.quickfix.expectactual.CreateActualClassFix
import org.jetbrains.kotlin.platform.isCommon
import org.jetbrains.kotlin.platform.oldFashionedDescription
import org.jetbrains.kotlin.idea.codeinsight.api.classic.inspections.AbstractKotlinInspection
import org.jetbrains.kotlin.idea.quickfix.expectactual.CreateMissedActualsFix
import org.jetbrains.kotlin.idea.util.actualsForExpected
import org.jetbrains.kotlin.idea.util.projectStructure.module
import org.jetbrains.kotlin.platform.isMultiPlatform
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.classOrObjectVisitor
@@ -26,8 +33,6 @@ import org.jetbrains.kotlin.resolve.multiplatform.ExpectedActualResolver
import org.jetbrains.kotlin.resolve.multiplatform.OptionalAnnotationUtil
import org.jetbrains.kotlin.resolve.multiplatform.onlyFromThisModule
import org.jetbrains.kotlin.idea.codeinsight.api.classic.inspections.AbstractKotlinInspection
class OptionalExpectationInspection : AbstractKotlinInspection() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor {
return classOrObjectVisitor(fun(classOrObject: KtClassOrObject) {
@@ -41,37 +46,20 @@ class OptionalExpectationInspection : AbstractKotlinInspection() {
val implementingModules = classOrObject.findModuleDescriptor().implementingDescriptors
if (implementingModules.isEmpty()) return
for (actualModuleDescriptor in implementingModules) {
val compatibility = ExpectedActualResolver.findActualForExpected(
descriptor, actualModuleDescriptor, onlyFromThisModule(actualModuleDescriptor)
) ?: continue
if (!compatibility.allStrongIncompatibilities() &&
(ExpectActualCompatibility.Compatible in compatibility ||
!compatibility.values.flatMapTo(
hashSetOf()
) { it }.all { actual ->
val expectedOnes = ExpectedActualResolver.findExpectedForActual(
actual, onlyFromThisModule(descriptor.module)
)
expectedOnes != null && ExpectActualCompatibility.Compatible in expectedOnes.keys
})
) continue
val platform = actualModuleDescriptor.platform ?: continue
if (platform.isCommon()) continue
val displayedName = actualModuleDescriptor.getCapability(ModuleInfo.Capability)?.displayedName ?: ""
val actualModule = (actualModuleDescriptor.getCapability(ModuleInfo.Capability) as? ModuleSourceInfo)?.module ?: continue
holder.registerProblem(
classOrObject.nameIdentifier ?: classOrObject,
KotlinBundle.message(
"optionally.expected.annotation.has.no.actual.annotation.in.module.0.for.platform.1",
displayedName,
platform.oldFashionedDescription
),
IntentionWrapper(CreateActualClassFix(classOrObject, actualModule, platform))
)
}
val actualizedLeafModules = descriptor.actualsForExpected()
.flatMap { it.module.implementingDescriptors.plus(it.module) } //all actualized modules
.filter { it.implementingDescriptors.isEmpty() } //only leafs
.toSet()
val notActualizedLeafModules = implementingModules
.filter { it.implementingDescriptors.isEmpty() && it !in actualizedLeafModules }
.mapNotNull { (it.moduleInfo as? ModuleSourceInfo)?.module }
.toSet()
if (notActualizedLeafModules.isEmpty()) return
holder.registerProblem(
classOrObject.nameIdentifier ?: classOrObject,
KotlinBundle.message("fix.create.missing.actual.declarations"),
IntentionWrapper(CreateMissedActualsFix(classOrObject, notActualizedLeafModules))
)
})
}
}
@@ -23,8 +23,8 @@ import org.jetbrains.kotlin.idea.quickfix.createFromUsage.createVariable.CreateL
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.createVariable.CreateParameterByNamedArgumentActionFactory
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.createVariable.CreateParameterByRefActionFactory
import org.jetbrains.kotlin.idea.quickfix.expectactual.AddActualFix
import org.jetbrains.kotlin.idea.quickfix.expectactual.CreateActualFix
import org.jetbrains.kotlin.idea.quickfix.expectactual.CreateExpectedFix
import org.jetbrains.kotlin.idea.quickfix.expectactual.CreateMissedActualsFix
import org.jetbrains.kotlin.idea.quickfix.migration.MigrateExperimentalToRequiresOptInFix
import org.jetbrains.kotlin.idea.quickfix.migration.MigrateExternalExtensionFix
import org.jetbrains.kotlin.idea.quickfix.migration.MigrateTypeParameterListFix
@@ -600,7 +600,7 @@ class QuickFixRegistrar : QuickFixContributor {
ACTUAL_WITHOUT_EXPECT.registerFactory(RemoveModifierFixBase.createRemoveModifierFromListOwnerPsiBasedFactory(ACTUAL_KEYWORD))
ACTUAL_WITHOUT_EXPECT.registerFactory(CreateExpectedFix)
NO_ACTUAL_FOR_EXPECT.registerFactory(CreateActualFix)
NO_ACTUAL_FOR_EXPECT.registerFactory(CreateMissedActualsFix)
NO_ACTUAL_CLASS_MEMBER_FOR_EXPECTED_CLASS.registerFactory(AddActualFix)
ACTUAL_MISSING.registerFactory(AddModifierFixFE10.createFactory(ACTUAL_KEYWORD))
@@ -1,119 +0,0 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.kotlin.idea.quickfix.expectactual
import com.intellij.ide.util.EditorHelper
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.ThrowableComputable
import com.intellij.util.SlowOperations
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
import org.jetbrains.kotlin.idea.base.util.reformatted
import org.jetbrains.kotlin.idea.core.ShortenReferences
import org.jetbrains.kotlin.idea.codeinsight.api.classic.quickfixes.KotlinQuickFixAction
import org.jetbrains.kotlin.idea.quickfix.TypeAccessibilityChecker
import org.jetbrains.kotlin.idea.refactoring.introduce.showErrorHint
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.createSmartPointer
import org.jetbrains.kotlin.psi.psiUtil.startOffset
abstract class AbstractCreateDeclarationFix<D : KtNamedDeclaration>(
declaration: D,
protected val module: Module,
protected val generateIt: KtPsiFactory.(Project, TypeAccessibilityChecker, D) -> D?
) : KotlinQuickFixAction<D>(declaration) {
override fun getFamilyName(): String = KotlinBundle.message("fix.create.expect.actual")
protected val elementType: String = element.getTypeDescription()
override fun startInWriteAction() = false
protected abstract fun findExistingFileToCreateDeclaration(
originalFile: KtFile,
originalDeclaration: KtNamedDeclaration
): KtFile?
protected fun getOrCreateImplementationFile(): KtFile? {
val declaration = element as? KtNamedDeclaration ?: return null
val parent = declaration.parent
return (parent as? KtFile)?.let { findExistingFileToCreateDeclaration(it, declaration) }
?: createFileForDeclaration(module, declaration)
}
protected fun doGenerate(
project: Project,
editor: Editor?,
originalFile: KtFile,
targetFile: KtFile,
targetClass: KtClassOrObject?
) {
val factory = KtPsiFactory(project)
val targetClassPointer = targetClass?.createSmartPointer()
val targetFilePointer = targetFile.createSmartPointer()
DumbService.getInstance(project).runWhenSmart(fun() {
val generated = try {
element?.let {
SlowOperations.allowSlowOperations(ThrowableComputable {
factory.generateIt(project, TypeAccessibilityChecker.create(project, module), it)
})
}
} catch (e: KotlinTypeInaccessibleException) {
if (editor != null) {
showErrorHint(
project, editor,
escapeXml(KotlinBundle.message("fix.create.declaration.error", elementType, e.message)),
KotlinBundle.message("fix.create.declaration.error.inaccessible.type")
)
}
null
} ?: return
val shortened = project.executeWriteCommand(KotlinBundle.message("fix.create.expect.actual"), null) {
val resultTargetFile = targetFilePointer.element ?: return@executeWriteCommand null
if (resultTargetFile.packageDirective?.fqName != originalFile.packageDirective?.fqName &&
resultTargetFile.declarations.isEmpty()
) {
val packageDirective = originalFile.packageDirective
if (packageDirective != null) {
val oldPackageDirective = resultTargetFile.packageDirective
val newPackageDirective = packageDirective.copy() as KtPackageDirective
if (oldPackageDirective != null) {
if (oldPackageDirective.text.isEmpty()) {
resultTargetFile.addAfter(factory.createNewLine(2), resultTargetFile.importList ?: oldPackageDirective)
}
oldPackageDirective.replace(newPackageDirective)
} else {
resultTargetFile.add(newPackageDirective)
}
}
}
val resultTargetClass = targetClassPointer?.element
val generatedDeclaration = when {
resultTargetClass != null -> {
if (generated is KtPrimaryConstructor && resultTargetClass is KtClass)
resultTargetClass.createPrimaryConstructorIfAbsent().replace(generated)
else
resultTargetClass.addDeclaration(generated as KtNamedDeclaration)
}
else -> {
resultTargetFile.add(factory.createNewLine(1))
resultTargetFile.add(generated) as KtElement
}
}
ShortenReferences.DEFAULT.process(generatedDeclaration.reformatted() as KtElement)
} ?: return
EditorHelper.openInEditor(shortened)?.caretModel?.moveToOffset(
(shortened as? KtNamedDeclaration)?.nameIdentifier?.startOffset ?: shortened.startOffset,
true,
)
})
}
}
@@ -1,100 +0,0 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.kotlin.idea.quickfix.expectactual
import com.intellij.codeInsight.intention.IntentionAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.analyzer.ModuleInfo
import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor
import org.jetbrains.kotlin.diagnostics.Diagnostic
import org.jetbrains.kotlin.diagnostics.DiagnosticFactory
import org.jetbrains.kotlin.diagnostics.Errors
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
import org.jetbrains.kotlin.idea.base.projectStructure.moduleInfo.ModuleSourceInfo
import org.jetbrains.kotlin.idea.core.toDescriptor
import org.jetbrains.kotlin.idea.quickfix.KotlinSingleIntentionActionFactory
import org.jetbrains.kotlin.idea.quickfix.TypeAccessibilityChecker
import org.jetbrains.kotlin.idea.util.actualsForExpected
import org.jetbrains.kotlin.platform.TargetPlatform
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.hasExpectModifier
sealed class CreateActualFix<D : KtNamedDeclaration>(
declaration: D,
actualModule: Module,
private val actualPlatform: TargetPlatform,
generateIt: KtPsiFactory.(Project, TypeAccessibilityChecker, D) -> D?
) : AbstractCreateDeclarationFix<D>(declaration, actualModule, generateIt) {
override fun getText() = KotlinBundle.message(
"create.actual.0.for.module.1.2",
elementType,
module.name,
actualPlatform.singleOrNull()?.platformName ?: actualPlatform
)
final override fun invoke(project: Project, editor: Editor?, file: KtFile) {
val actualFile = getOrCreateImplementationFile() ?: return
doGenerate(project, editor, originalFile = file, targetFile = actualFile, targetClass = null)
}
override fun findExistingFileToCreateDeclaration(
originalFile: KtFile,
originalDeclaration: KtNamedDeclaration
): KtFile? {
for (otherDeclaration in originalFile.declarations) {
if (otherDeclaration === originalDeclaration) continue
if (!otherDeclaration.hasExpectModifier()) continue
val actualDeclaration = otherDeclaration.actualsForExpected(module).singleOrNull() ?: continue
return actualDeclaration.containingKtFile
}
return null
}
companion object : KotlinSingleIntentionActionFactory() {
override fun createAction(diagnostic: Diagnostic): IntentionAction? {
val d = DiagnosticFactory.cast(diagnostic, Errors.NO_ACTUAL_FOR_EXPECT)
val declaration = d.psiElement as? KtNamedDeclaration ?: return null
val compatibility = d.c
// For function we allow it, because overloads are possible
if (compatibility.isNotEmpty() && declaration !is KtFunction) return null
val actualModuleDescriptor = d.b
val actualModule = (actualModuleDescriptor.getCapability(ModuleInfo.Capability) as? ModuleSourceInfo)?.module ?: return null
val actualPlatform = actualModuleDescriptor.platform ?: return null
return when (declaration) {
is KtClassOrObject -> CreateActualClassFix(declaration, actualModule, actualPlatform)
is KtFunction, is KtProperty -> CreateActualCallableMemberFix(
declaration as KtCallableDeclaration,
actualModule,
actualPlatform
)
else -> null
}
}
}
}
class CreateActualClassFix(
klass: KtClassOrObject,
actualModule: Module,
actualPlatform: TargetPlatform
) : CreateActualFix<KtClassOrObject>(klass, actualModule, actualPlatform, block@{ project, checker, element ->
checker.findAndApplyExistingClasses(element.collectDeclarationsForAddActualModifier().toList())
if (!checker.isCorrectAndHaveAccessibleModifiers(element, true)) return@block null
generateClassOrObject(project, false, element, checker = checker)
})
class CreateActualCallableMemberFix(
declaration: KtCallableDeclaration,
actualModule: Module,
actualPlatform: TargetPlatform
) : CreateActualFix<KtCallableDeclaration>(declaration, actualModule, actualPlatform, block@{ project, checker, element ->
if (!checker.isCorrectAndHaveAccessibleModifiers(element, true)) return@block null
val descriptor = element.toDescriptor() as? CallableMemberDescriptor
descriptor?.let { generateCallable(project, false, element, descriptor, checker = checker) }
})
@@ -29,6 +29,7 @@ import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
import org.jetbrains.kotlin.idea.util.application.isUnitTestMode
import org.jetbrains.kotlin.idea.util.liftToExpected
import org.jetbrains.kotlin.idea.base.util.module
import org.jetbrains.kotlin.idea.codeinsight.api.classic.quickfixes.KotlinQuickFixAction
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
import org.jetbrains.kotlin.psi.psiUtil.createSmartPointer
@@ -39,9 +40,15 @@ import org.jetbrains.kotlin.utils.addToStdlib.safeAs
sealed class CreateExpectedFix<D : KtNamedDeclaration>(
declaration: D,
targetExpectedClass: KtClassOrObject?,
commonModule: Module,
generateIt: KtPsiFactory.(Project, TypeAccessibilityChecker, D) -> D?
) : AbstractCreateDeclarationFix<D>(declaration, commonModule, generateIt) {
val module: Module,
val generateIt: KtPsiFactory.(Project, TypeAccessibilityChecker, D) -> D?
) : KotlinQuickFixAction<D>(declaration) {
override fun getFamilyName(): String = KotlinBundle.message("fix.create.expect.actual")
protected val elementType: String = element.getTypeDescription()
override fun startInWriteAction() = false
private val targetExpectedClassPointer = targetExpectedClass?.createSmartPointer()
@@ -50,10 +57,11 @@ sealed class CreateExpectedFix<D : KtNamedDeclaration>(
final override fun invoke(project: Project, editor: Editor?, file: KtFile) {
val targetExpectedClass = targetExpectedClassPointer?.element
val expectedFile = targetExpectedClass?.containingKtFile ?: getOrCreateImplementationFile() ?: return
doGenerate(project, editor, originalFile = file, targetFile = expectedFile, targetClass = targetExpectedClass)
val declaration = element ?: return
generateExpectOrActualInFile(project, editor, originalFile = file, targetFile = expectedFile, targetClass = targetExpectedClass, declaration, module, generateIt)
}
override fun findExistingFileToCreateDeclaration(
private fun findExistingFileToCreateDeclaration(
originalFile: KtFile,
originalDeclaration: KtNamedDeclaration
): KtFile? {
@@ -67,6 +75,13 @@ sealed class CreateExpectedFix<D : KtNamedDeclaration>(
return null
}
private fun getOrCreateImplementationFile(): KtFile? {
val declaration = element as? KtNamedDeclaration ?: return null
val parent = declaration.parent
return (parent as? KtFile)?.let { findExistingFileToCreateDeclaration(it, declaration) }
?: createFileForDeclaration(module, declaration)
}
companion object : KotlinIntentionActionsFactory() {
override fun doCreateActions(diagnostic: Diagnostic): List<IntentionAction> {
val d = DiagnosticFactory.cast(diagnostic, Errors.ACTUAL_WITHOUT_EXPECT)
@@ -84,13 +99,11 @@ sealed class CreateExpectedFix<D : KtNamedDeclaration>(
?: actualDeclaration.module?.implementedModules
?: return emptyList()
return when (actualDeclaration) {
is KtClassOrObject -> expectedModules.map { CreateExpectedClassFix(actualDeclaration, expectedContainingClass, it) }
is KtClassOrObject -> expectedModules.map {
CreateExpectedClassFix(actualDeclaration, expectedContainingClass, it)
}
is KtProperty, is KtParameter, is KtFunction -> expectedModules.map {
CreateExpectedCallableMemberFix(
actualDeclaration as KtCallableDeclaration,
expectedContainingClass,
it
)
CreateExpectedCallableMemberFix(actualDeclaration as KtCallableDeclaration, expectedContainingClass, it)
}
else -> emptyList()
}
@@ -0,0 +1,413 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.quickfix.expectactual
import com.intellij.codeInsight.intention.IntentionAction
import com.intellij.icons.AllIcons
import com.intellij.ide.wizard.setMinimumWidthForAllRowLabels
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.externalSystem.service.project.manage.SourceFolderManager
import com.intellij.openapi.module.Module
import com.intellij.openapi.observable.properties.AtomicProperty
import com.intellij.openapi.observable.util.transform
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.findOrCreateDirectory
import com.intellij.psi.PsiDirectory
import com.intellij.ui.components.JBTextField
import com.intellij.ui.dsl.builder.*
import com.intellij.ui.layout.ComponentPredicate
import org.jetbrains.kotlin.analyzer.ModuleInfo
import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor
import org.jetbrains.kotlin.diagnostics.Diagnostic
import org.jetbrains.kotlin.diagnostics.DiagnosticFactory
import org.jetbrains.kotlin.diagnostics.Errors
import org.jetbrains.kotlin.idea.base.facet.implementedModules
import org.jetbrains.kotlin.idea.base.facet.implementingModules
import org.jetbrains.kotlin.idea.base.facet.kotlinSourceRootType
import org.jetbrains.kotlin.idea.base.facet.platform.platform
import org.jetbrains.kotlin.idea.base.projectStructure.moduleInfo.ModuleSourceInfo
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
import org.jetbrains.kotlin.idea.codeinsight.api.classic.quickfixes.KotlinQuickFixAction
import org.jetbrains.kotlin.idea.core.PureKotlinSourceFoldersHolder
import org.jetbrains.kotlin.idea.core.findExistingNonGeneratedKotlinSourceRootFiles
import org.jetbrains.kotlin.idea.core.findOrConfigureKotlinSourceRoots
import org.jetbrains.kotlin.idea.core.toDescriptor
import org.jetbrains.kotlin.idea.quickfix.KotlinSingleIntentionActionFactory
import org.jetbrains.kotlin.idea.refactoring.getOrCreateKotlinFile
import org.jetbrains.kotlin.idea.util.application.isHeadlessEnvironment
import org.jetbrains.kotlin.idea.util.application.isUnitTestMode
import org.jetbrains.kotlin.idea.util.projectStructure.module
import org.jetbrains.kotlin.idea.util.sourceRoot
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.platform.isMultiPlatform
import org.jetbrains.kotlin.psi.*
import java.io.File
import java.nio.file.Path
import javax.swing.JComponent
import kotlin.io.path.Path
import kotlin.io.path.name
import kotlin.io.path.nameWithoutExtension
import kotlin.io.path.pathString
class CreateMissedActualsFix(
val declaration: KtNamedDeclaration,
val notActualizedLeafModules: Collection<Module>
) : KotlinQuickFixAction<KtNamedDeclaration>(declaration) {
override fun startInWriteAction(): Boolean = false
override fun getFamilyName(): String = KotlinBundle.message("fix.create.expect.actual")
override fun getText(): String = KotlinBundle.message("fix.create.missing.actual.declarations")
override fun invoke(project: Project, editor: Editor?, file: KtFile) {
val moduleWithExpect = declaration.module ?: return
val simpleModuleNames = moduleWithExpect.implementingModules.plus(moduleWithExpect).getSimpleSourceSetNames()
val testMode = isUnitTestMode() || isHeadlessEnvironment()
if (!testMode) {
CreateMissedActualsDialog(
project,
editor,
declaration,
moduleWithExpect,
notActualizedLeafModules,
simpleModuleNames
).show()
} else {
val defaultPath = declaration.containingKtFile.let { f ->
f.virtualFilePath.removePrefix(f.sourceRoot?.path.orEmpty())
}
generateActualsForSelectedModules(
project,
editor,
declaration,
defaultPath,
notActualizedLeafModules,
simpleModuleNames
)
}
}
//returns modules with associated names which are cleared from common prefix
private fun Collection<Module>.getSimpleSourceSetNames(): Map<Module, String> {
if (this.size < 2) return this.associateWith { it.name }
val commonPrefix = this.map { it.name }.zipWithNext().map { (a, b) -> a.commonPrefixWith(b) }.minBy { it.length }
val prefixToRemove = commonPrefix.substringBeforeLast(".", "").let { if (it.isEmpty()) it else "$it." }
return this.associateWith { it.name.removePrefix(prefixToRemove) }
}
companion object : KotlinSingleIntentionActionFactory() {
override fun createAction(diagnostic: Diagnostic): IntentionAction? =
createIntentionAction(listOf(diagnostic))
override fun doCreateActionsForAllProblems(sameTypeDiagnostics: Collection<Diagnostic>): List<IntentionAction> =
listOfNotNull(createIntentionAction(sameTypeDiagnostics))
private fun createIntentionAction(sameTypeDiagnostics: Collection<Diagnostic>): IntentionAction? {
val first = sameTypeDiagnostics.firstOrNull() ?: return null
val declaration = first.psiElement as? KtNamedDeclaration ?: return null
val diagnostics = sameTypeDiagnostics
.filter { it.psiElement == declaration }
.map { DiagnosticFactory.cast(it, Errors.NO_ACTUAL_FOR_EXPECT) }
.filter {
val compatibility = it.c
// For function declarations we allow it, because overloads are possible
compatibility.isEmpty() || declaration is KtFunction
}
val notActualizedLeafModules = diagnostics.mapNotNull {
val actualModuleDescriptor = it.b
(actualModuleDescriptor.getCapability(ModuleInfo.Capability) as? ModuleSourceInfo)?.module
}
if (notActualizedLeafModules.isEmpty()) return null
return CreateMissedActualsFix(declaration, notActualizedLeafModules)
}
}
}
private val LOG = Logger.getInstance(CreateMissedActualsDialog::class.java)
private class CreateMissedActualsDialog(
val project: Project,
val editor: Editor?,
val declaration: KtNamedDeclaration,
val moduleWithExpect: Module,
val notActualizedLeafModules: Collection<Module>,
val simpleModuleNames: Map<Module, String>
) : DialogWrapper(project, true) {
private val notActualizedModules = getNotActualizedModules()
private var filePathTextField: JBTextField? = null
private val filePathProperty = AtomicProperty(
declaration.containingKtFile.let { file ->
file.virtualFilePath.removePrefix(file.sourceRoot?.path.orEmpty())
}
)
private val selectedModules = mutableListOf<Module>()
private val selectedModulesListeners = mutableListOf<() -> Unit>()
init {
title = KotlinBundle.message("fix.create.missing.actual.declarations.title")
init()
}
override fun doValidate(): ValidationInfo? {
val filePath = getNewFilePathForModule(moduleWithExpect).nameWithoutExtension
if (filePath.isBlank()) {
return ValidationInfo(KotlinBundle.message("text.file.name.cannot.be.empty"), filePathTextField)
}
return null
}
override fun doOKAction() {
super.doOKAction()
generateActualsForSelectedModules(
project,
editor,
declaration,
filePathProperty.get(),
selectedModules,
simpleModuleNames
)
}
override fun createCenterPanel(): JComponent = panel {
val sortedModules = moduleWithExpect.sortChildrenByHMPP()
row(KotlinBundle.message("label.text.package.name")) {
textField()
.text(declaration.containingKtFile.packageFqName.asString())
.align(AlignX.FILL)
.enabled(false)
}
row(KotlinBundle.message("label.text.file.name")) {
textField()
.align(AlignX.FILL)
.bindText(filePathProperty)
.also { filePathTextField = it.component }
}
row {
panel {
row { label(KotlinBundle.message("label.text.source.sets")) }
separator()
sortedModules.forEach { item ->
val selectionPredicate = ModuleCheckBoxPredicate(item.module)
row {
val checkbox = checkBox(" ".repeat(item.level))
.onChanged { cb ->
if (cb.isSelected) {
selectedModules.add(item.module)
} else {
selectedModules.remove(item.module)
}
selectedModulesListeners.forEach { listener -> listener() }
}
icon(AllIcons.Actions.ModuleDirectory)
label(simpleModuleNames.getOrDefault(item.module, item.module.name))
label("")
.bindText(filePathProperty.transform { File.separator + getNewFilePathForModule(item.module) })
.enabled(false)
.visibleIf(checkbox.selected)
}.enabledIf(selectionPredicate)
}
}
}
}.apply {
withMinimumWidth(500)
setMinimumWidthForAllRowLabels(90)
}
private data class ModuleWithLevel(val module: Module, val level: Int)
private fun Module.sortChildrenByHMPP(): List<ModuleWithLevel> {
fun Module.getChildren(): List<Module> {
val implementing = implementingModules.toSet()
return implementing.filter { module ->
module.implementedModules.all { it !in implementing }
}
}
fun sort(parent: Module, level: Int): List<ModuleWithLevel> {
val children = parent.getChildren().sortedBy { it.name }
if (children.isEmpty()) return emptyList()
return children.flatMap { listOf(ModuleWithLevel(it, level)).plus(sort(it, level + 1)) }
}
//'distinct' filters duplicates in a ruby-like dependencies case
return sort(this, 0).distinctBy { it.module.name }
}
private fun getNotActualizedModules(): List<Module> {
val allModules = moduleWithExpect.implementingModules
val allLeafModules = allModules.filter { module ->
//`isMultiPlatform` is just for optimization
!module.platform.isMultiPlatform() && module.implementingModules.isEmpty()
}
val actualizedLeafModules = allLeafModules.filter { module ->
module !in notActualizedLeafModules
}
val allActualizedModules = actualizedLeafModules
.flatMap { module -> module.implementedModules }
.plus(actualizedLeafModules)
return allModules.filter { module -> module !in allActualizedModules }
}
private fun getNewFilePathForModule(module: Module): Path {
val simpleName = simpleModuleNames[module].orEmpty()
val suffixToRemove = listOf("Main", "Test").firstOrNull { simpleName.endsWith(it) }.orEmpty()
val modulePlatformName = simpleName.removeSuffix(suffixToRemove).takeIf { it.isNotBlank() }
return Path(
filePathProperty.get().removePrefix(File.separator).removeSuffix(".kt") +
modulePlatformName?.let { ".$it" }.orEmpty() + ".kt"
)
}
//Observer for UI changes
private inner class ModuleCheckBoxPredicate(val module: Module) : ComponentPredicate() {
override fun addListener(listener: (Boolean) -> Unit) {
val onChange: () -> Unit = { listener(invoke()) }
selectedModulesListeners.add(onChange)
}
override fun invoke(): Boolean {
val shouldBeActualized = module in notActualizedModules
val anyParentOrChildIsSelected = module.implementedModules.plus(module.implementingModules).any { it in selectedModules }
return shouldBeActualized && !anyParentOrChildIsSelected
}
}
}
private fun getNewFilePathForModule(commonFilePath: String, module: Module, simpleModuleNames: Map<Module, String>): Path {
val simpleName = simpleModuleNames[module].orEmpty()
val suffixToRemove = listOf("Main", "Test").firstOrNull { simpleName.endsWith(it) }.orEmpty()
val modulePlatformName = simpleName.removeSuffix(suffixToRemove).takeIf { it.isNotBlank() }
return Path(
commonFilePath.removePrefix(File.separator).removeSuffix(".kt") +
modulePlatformName?.let { ".$it" }.orEmpty() + ".kt"
)
}
private fun getOrCreateKotlinFileForSpecificPackage(
fileName: String,
targetDir: PsiDirectory,
declarationPackage: FqName
): KtFile {
var file = getOrCreateKotlinFile(fileName, targetDir, declarationPackage.asString())
var fileIndex = 0
//if file exists and has other declarations with a different package, we have to generate a new file
fun KtFile.needOtherFile() = declarations.isNotEmpty() && packageDirective?.fqName != declarationPackage
while (file.needOtherFile()) {
fileIndex++
val newName = with(fileName) {
val (name, suffixAndExtension) = split(".", limit = 2)
"$name$fileIndex.$suffixAndExtension"
}
file = getOrCreateKotlinFile(newName, targetDir, declarationPackage.asString())
}
return file
}
private fun Module.selectExistingSourceRoot(
pureKotlinSourceFoldersHolder: PureKotlinSourceFoldersHolder
): VirtualFile? {
val roots = findExistingNonGeneratedKotlinSourceRootFiles(pureKotlinSourceFoldersHolder).sortedBy { it.path }
if (roots.size < 2) return roots.firstOrNull()
val root = roots.firstOrNull {
it.name.equals("kotlin", true)
} ?: roots.first()
LOG.warn("${this.name} contains more then one source roots. ${root.name} was selected.")
return root
}
private fun generateActualsForSelectedModules(
project: Project,
editor: Editor?,
declaration: KtNamedDeclaration,
commonFilePath: String,
selectedModules: Collection<Module>,
simpleModuleNames: Map<Module, String>
) {
val sourceFolderManager = SourceFolderManager.getInstance(project)
val pureKotlinSourceFoldersHolder = PureKotlinSourceFoldersHolder()
val moduleSourceRoots = selectedModules.associateWith { module ->
module.selectExistingSourceRoot(pureKotlinSourceFoldersHolder) ?: run {
val newRoot = module.findOrConfigureKotlinSourceRoots(pureKotlinSourceFoldersHolder)
.also { if (it.size > 1) LOG.warn("In ${module.name} were configured more then one source roots. ${it.first().name} was selected.") }
.firstOrNull()
if (newRoot == null) {
LOG.warn("Can't configure new source root for ${module.name}")
return@associateWith null
}
val sourceRootType = module.kotlinSourceRootType
if (sourceRootType == null) {
LOG.warn("Can't configure new source root for ${module.name} because `kotlinSourceRootType == null`")
return@associateWith null
}
sourceFolderManager.addSourceFolder(module, newRoot.url, sourceRootType)
newRoot
}
}
sourceFolderManager.rescanAndUpdateSourceFolders()
runWriteAction {
selectedModules.forEach { module ->
val root = moduleSourceRoots[module] ?: return@forEach
val moduleFile = getNewFilePathForModule(commonFilePath, module, simpleModuleNames)
val dir: PsiDirectory = declaration.manager.findDirectory(
root.findOrCreateDirectory(moduleFile.parent?.pathString.orEmpty())
) ?: return@forEach
val file = getOrCreateKotlinFileForSpecificPackage(moduleFile.name, dir, declaration.containingKtFile.packageFqName)
if (declaration is KtCallableDeclaration) {
generateExpectOrActualInFile(
project,
editor,
declaration.containingKtFile,
file,
null,
declaration,
module
) block@{ project, checker, element ->
if (!checker.isCorrectAndHaveAccessibleModifiers(element, true)) return@block null
val descriptor = element.toDescriptor() as? CallableMemberDescriptor
descriptor?.let { generateCallable(project, false, element, descriptor, checker = checker) }
}
} else if (declaration is KtClassOrObject) {
generateExpectOrActualInFile(
project,
editor,
declaration.containingKtFile,
file,
null,
declaration,
module
) block@{ project, checker, element ->
checker.findAndApplyExistingClasses(element.collectDeclarationsForAddActualModifier().toList())
if (!checker.isCorrectAndHaveAccessibleModifiers(element, true)) return@block null
generateClassOrObject(project, false, element, checker = checker)
}
}
}
}
}
@@ -2,12 +2,16 @@
package org.jetbrains.kotlin.idea.quickfix.expectactual
import com.intellij.ide.util.EditorHelper
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.ThrowableComputable
import com.intellij.psi.JavaDirectoryService
import com.intellij.psi.PsiElement
import com.intellij.util.SlowOperations
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor
import org.jetbrains.kotlin.descriptors.annotations.Annotations
@@ -16,9 +20,11 @@ import org.jetbrains.kotlin.idea.base.psi.isInlineOrValue
import org.jetbrains.kotlin.idea.base.psi.mustHaveValOrVar
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
import org.jetbrains.kotlin.idea.base.util.names.FqNames
import org.jetbrains.kotlin.idea.base.util.reformatted
import org.jetbrains.kotlin.idea.caches.resolve.analyzeWithContent
import org.jetbrains.kotlin.idea.codeInsight.shorten.addToBeShortenedDescendantsToWaitingSet
import org.jetbrains.kotlin.idea.codeinsight.utils.findExistingEditor
import org.jetbrains.kotlin.idea.core.ShortenReferences
import org.jetbrains.kotlin.idea.core.findOrCreateDirectoryForPackage
import org.jetbrains.kotlin.idea.core.getFqNameWithImplicitPrefix
import org.jetbrains.kotlin.idea.core.overrideImplement.BodyType.EmptyOrTemplate
@@ -34,13 +40,16 @@ import org.jetbrains.kotlin.idea.refactoring.fqName.fqName
import org.jetbrains.kotlin.idea.refactoring.introduce.showErrorHint
import org.jetbrains.kotlin.idea.refactoring.isInterfaceClass
import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
import org.jetbrains.kotlin.idea.util.isEffectivelyActual
import org.jetbrains.kotlin.lexer.KtModifierKeywordToken
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
import org.jetbrains.kotlin.psi.psiUtil.createSmartPointer
import org.jetbrains.kotlin.psi.psiUtil.hasActualModifier
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.checkers.OptInNames
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull
@@ -49,6 +58,80 @@ import org.jetbrains.kotlin.resolve.source.KotlinSourceElement
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
fun <D : KtNamedDeclaration> generateExpectOrActualInFile(
project: Project,
editor: Editor?,
originalFile: KtFile,
targetFile: KtFile,
targetClass: KtClassOrObject?,
element: D,
module: Module,
generateIt: KtPsiFactory.(Project, TypeAccessibilityChecker, D) -> D?
) {
val factory = KtPsiFactory(project)
val targetClassPointer = targetClass?.createSmartPointer()
val targetFilePointer = targetFile.createSmartPointer()
DumbService.getInstance(project).runWhenSmart(fun() {
val generated = try {
SlowOperations.allowSlowOperations(ThrowableComputable {
factory.generateIt(project, TypeAccessibilityChecker.create(project, module), element)
})
} catch (e: KotlinTypeInaccessibleException) {
if (editor != null) {
showErrorHint(
project, editor,
escapeXml(KotlinBundle.message("fix.create.declaration.error", element.getTypeDescription(), e.message)),
KotlinBundle.message("fix.create.declaration.error.inaccessible.type")
)
}
null
} ?: return
val shortened = project.executeWriteCommand(KotlinBundle.message("fix.create.expect.actual"), null) {
val resultTargetFile = targetFilePointer.element ?: return@executeWriteCommand null
if (resultTargetFile.packageDirective?.fqName != originalFile.packageDirective?.fqName &&
resultTargetFile.declarations.isEmpty()
) {
val packageDirective = originalFile.packageDirective
if (packageDirective != null) {
val oldPackageDirective = resultTargetFile.packageDirective
val newPackageDirective = packageDirective.copy() as KtPackageDirective
if (oldPackageDirective != null) {
if (oldPackageDirective.text.isEmpty()) {
resultTargetFile.addAfter(factory.createNewLine(2), resultTargetFile.importList ?: oldPackageDirective) //
}
oldPackageDirective.replace(newPackageDirective)
} else {
resultTargetFile.add(newPackageDirective)
}
}
}
val resultTargetClass = targetClassPointer?.element
val generatedDeclaration = when {
resultTargetClass != null -> {
if (generated is KtPrimaryConstructor && resultTargetClass is KtClass)
resultTargetClass.createPrimaryConstructorIfAbsent().replace(generated)
else
resultTargetClass.addDeclaration(generated as KtNamedDeclaration)
}
else -> {
resultTargetFile.add(factory.createNewLine(1))
resultTargetFile.add(generated) as KtElement
}
}
ShortenReferences.DEFAULT.process(generatedDeclaration.reformatted() as KtElement)
} ?: return
EditorHelper.openInEditor(shortened)?.caretModel?.moveToOffset(
(shortened as? KtNamedDeclaration)?.nameIdentifier?.startOffset ?: shortened.startOffset,
true,
)
})
}
fun createFileForDeclaration(module: Module, declaration: KtNamedDeclaration): KtFile? {
val fileName = declaration.name ?: return null
@@ -90,6 +173,7 @@ fun KtPsiFactory.createClassHeaderCopyByText(originalClass: KtClassOrObject): Kt
} else {
createObject(text)
}
is KtEnumEntry -> createEnumEntry(text)
else -> createClass(text)
}.apply {
@@ -106,6 +190,7 @@ fun KtNamedDeclaration?.getTypeDescription(): String = when (this) {
isAnnotation() -> KotlinBundle.message("text.annotation.class")
else -> KotlinBundle.message("text.class")
}
is KtProperty, is KtParameter -> KotlinBundle.message("text.property")
is KtFunction -> KotlinBundle.message("text.function")
else -> KotlinBundle.message("text.declaration")
@@ -158,6 +243,7 @@ internal fun KtPsiFactory.generateClassOrObject(
originalDeclaration,
checker
)
is KtFunction, is KtProperty -> checker.runInContext(existingFqNamesWithSuperTypes) {
generateCallable(
project,
@@ -168,6 +254,7 @@ internal fun KtPsiFactory.generateClassOrObject(
this
)
}
else -> continue@declLoop
}
generatedClass.addDeclaration(generatedDeclaration)
@@ -322,6 +409,7 @@ private fun repairAnnotationEntries(
val typeReference = target.typeReference ?: return
repairAnnotationEntries(typeReference, descriptor.type, checker)
}
is TypeParameterDescriptor -> {
if (target !is KtTypeParameter) return
val extendsBound = target.extendsBound ?: return
@@ -329,6 +417,7 @@ private fun repairAnnotationEntries(
repairAnnotationEntries(extendsBound, upperBound, checker)
}
}
is CallableDescriptor -> {
val extension = descriptor.extensionReceiverParameter
val receiver = target.safeAs<KtCallableDeclaration>()?.receiverTypeReference