ExpectActualUtils: add type accessibility check
This commit is contained in:
@@ -11,6 +11,7 @@ import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.showOkNoDialog
|
||||
import org.jetbrains.kotlin.descriptors.ClassDescriptor
|
||||
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
|
||||
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
|
||||
@@ -24,7 +25,10 @@ import org.jetbrains.kotlin.idea.core.overrideImplement.makeActual
|
||||
import org.jetbrains.kotlin.idea.core.overrideImplement.makeNotActual
|
||||
import org.jetbrains.kotlin.idea.core.toDescriptor
|
||||
import org.jetbrains.kotlin.idea.core.util.DescriptorMemberChooserObject
|
||||
import org.jetbrains.kotlin.idea.inspections.findExistingEditor
|
||||
import org.jetbrains.kotlin.idea.quickfix.KotlinIntentionActionsFactory
|
||||
import org.jetbrains.kotlin.idea.refactoring.introduce.showErrorHint
|
||||
import org.jetbrains.kotlin.idea.refactoring.renderTrimmed
|
||||
import org.jetbrains.kotlin.idea.util.allowedValOrVar
|
||||
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
|
||||
import org.jetbrains.kotlin.idea.util.liftToExpected
|
||||
@@ -34,6 +38,7 @@ 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 java.util.*
|
||||
|
||||
sealed class CreateExpectedFix<D : KtNamedDeclaration>(
|
||||
declaration: D,
|
||||
@@ -111,17 +116,36 @@ class CreateExpectedClassFix(
|
||||
outerExpectedClass: KtClassOrObject?,
|
||||
commonModule: Module
|
||||
) : CreateExpectedFix<KtClassOrObject>(klass, outerExpectedClass, commonModule, block@{ project, element ->
|
||||
val originalElements = element.collectDeclarations(false).filter(KtDeclaration::canAddActualModifier).toList()
|
||||
val members = originalElements.filterNot(KtDeclaration::isAlwaysActual)
|
||||
val originalElements = element.collectDeclarations(false).toList()
|
||||
val existingClasses = findClasses(originalElements + klass)
|
||||
|
||||
val (members, declarationsWithNonExistentClasses) = originalElements.filterNot(KtNamedDeclaration::isAlwaysActual).partition {
|
||||
it.checkTypeAccessibilityInModule(commonModule, existingClasses)
|
||||
}
|
||||
|
||||
if (!showUnknownTypesDialog(project, declarationsWithNonExistentClasses)) return@block null
|
||||
|
||||
val selectedElements = when {
|
||||
members.all(KtDeclaration::hasActualModifier) -> originalElements
|
||||
members.all(KtDeclaration::hasActualModifier) -> originalElements - declarationsWithNonExistentClasses
|
||||
ApplicationManager.getApplication().isUnitTestMode -> members.filter(KtDeclaration::hasActualModifier)
|
||||
else -> {
|
||||
val prefix = klass.fqName?.asString()?.plus(".") ?: ""
|
||||
chooseMembers(project, members, prefix) ?: return@block null
|
||||
}
|
||||
}.let { selectedElements ->
|
||||
val selectedClasses = findClasses(selectedElements)
|
||||
if (selectedClasses != existingClasses) {
|
||||
val (resultDeclarations, withErrors) = selectedElements.partition {
|
||||
it.checkTypeAccessibilityInModule(commonModule, selectedClasses)
|
||||
}
|
||||
if (!showUnknownTypesDialog(project, withErrors)) return@block null
|
||||
resultDeclarations
|
||||
} else
|
||||
selectedElements
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (originalElements.isNotEmpty()) {
|
||||
project.executeWriteCommand("Repair actual members") {
|
||||
repairActualModifiers(originalElements, selectedElements)
|
||||
@@ -131,6 +155,30 @@ class CreateExpectedClassFix(
|
||||
generateClassOrObject(project, true, element, listOfNotNull(outerExpectedClass))
|
||||
})
|
||||
|
||||
private fun findClasses(elements: List<KtNamedDeclaration>): HashSet<String> {
|
||||
return elements.filterIsInstance<KtClassOrObject>().mapNotNull { it.fqName?.asString() }.toHashSet()
|
||||
}
|
||||
|
||||
private fun showUnknownTypesDialog(project: Project, declarationsWithNonExistentClasses: List<KtNamedDeclaration>): Boolean =
|
||||
declarationsWithNonExistentClasses.isEmpty() || showOkNoDialog(
|
||||
"Unknown types",
|
||||
declarationsWithNonExistentClasses.joinToString(prefix = "These declarations cannot be transformed:\n", separator = "\n") {
|
||||
it.renderTrimmed()
|
||||
},
|
||||
project
|
||||
)
|
||||
|
||||
private fun showUnknownTypesError(element: KtNamedDeclaration) {
|
||||
element.findExistingEditor()?.let { editor ->
|
||||
showErrorHint(
|
||||
element.project,
|
||||
editor,
|
||||
"You cannot create the expect declaration from:\n${element.renderTrimmed()}",
|
||||
"Unknown types"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun KtDeclaration.canAddActualModifier() = when (this) {
|
||||
is KtEnumEntry, is KtClassInitializer -> false
|
||||
is KtParameter -> hasValOrVar()
|
||||
@@ -141,7 +189,7 @@ private fun KtDeclaration.canAddActualModifier() = when (this) {
|
||||
/***
|
||||
* @return null if close without OK
|
||||
*/
|
||||
private fun chooseMembers(project: Project, collection: Collection<KtDeclaration>, prefixToRemove: String): List<KtDeclaration>? {
|
||||
private fun chooseMembers(project: Project, collection: Collection<KtNamedDeclaration>, prefixToRemove: String): List<KtNamedDeclaration>? {
|
||||
val classMembers = collection.mapNotNull {
|
||||
it.resolveToDescriptorIfAny()?.let { descriptor -> Member(prefixToRemove, it, descriptor) }
|
||||
}
|
||||
@@ -158,9 +206,9 @@ private fun chooseMembers(project: Project, collection: Collection<KtDeclaration
|
||||
).run {
|
||||
title = "Choose actual members"
|
||||
setCopyJavadocVisible(false)
|
||||
selectElements(classMembers.filter { filter((it.element as KtDeclaration)) }.toTypedArray())
|
||||
selectElements(classMembers.filter { filter((it.element as KtNamedDeclaration)) }.toTypedArray())
|
||||
show()
|
||||
if (!isOK) null else selectedElements?.map { it.element as KtDeclaration }.orEmpty()
|
||||
if (!isOK) null else selectedElements?.map { it.element as KtNamedDeclaration }.orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,19 +221,27 @@ private class Member(val prefix: String, element: KtElement, descriptor: Declara
|
||||
}
|
||||
}
|
||||
|
||||
private fun KtClassOrObject.collectDeclarations(withSelf: Boolean = true): Sequence<KtDeclaration> {
|
||||
val thisSequence = if (withSelf) sequenceOf(this) else emptySequence()
|
||||
val primaryConstructorSequence = primaryConstructorParameters.asSequence() + primaryConstructor.let {
|
||||
private fun KtClassOrObject.collectDeclarations(withSelf: Boolean = true): Sequence<KtNamedDeclaration> {
|
||||
val thisSequence: Sequence<KtNamedDeclaration> = if (withSelf) sequenceOf(this) else emptySequence()
|
||||
val primaryConstructorSequence: Sequence<KtNamedDeclaration> = primaryConstructorParameters.asSequence() + primaryConstructor.let {
|
||||
if (it != null) sequenceOf(it) else emptySequence()
|
||||
}
|
||||
|
||||
return thisSequence + primaryConstructorSequence + declarations.asSequence().flatMap {
|
||||
if (it is KtClassOrObject) it.collectDeclarations() else sequenceOf(it)
|
||||
if (it.canAddActualModifier())
|
||||
when (it) {
|
||||
is KtClassOrObject -> it.collectDeclarations()
|
||||
is KtNamedDeclaration -> sequenceOf(it)
|
||||
else -> emptySequence()
|
||||
}
|
||||
else
|
||||
emptySequence()
|
||||
}
|
||||
}
|
||||
|
||||
private fun repairActualModifiers(
|
||||
originalElements: Collection<KtDeclaration>,
|
||||
selectedElements: Collection<KtDeclaration>
|
||||
originalElements: Collection<KtNamedDeclaration>,
|
||||
selectedElements: Collection<KtNamedDeclaration>
|
||||
) {
|
||||
if (originalElements.size == selectedElements.size)
|
||||
for (original in originalElements) {
|
||||
@@ -205,7 +261,7 @@ private tailrec fun KtDeclaration.makeActualWithParents() {
|
||||
containingClassOrObject?.takeUnless(KtDeclaration::hasActualModifier)?.makeActualWithParents()
|
||||
}
|
||||
|
||||
private fun KtDeclaration.isAlwaysActual(): Boolean = when (this) {
|
||||
private fun KtNamedDeclaration.isAlwaysActual(): Boolean = when (this) {
|
||||
is KtPrimaryConstructor -> this
|
||||
is KtParameter -> (parent as? KtParameterList)?.parent as? KtPrimaryConstructor
|
||||
else -> null
|
||||
@@ -215,7 +271,11 @@ class CreateExpectedPropertyFix(
|
||||
property: KtNamedDeclaration,
|
||||
targetExpectedClass: KtClassOrObject?,
|
||||
commonModule: Module
|
||||
) : CreateExpectedFix<KtNamedDeclaration>(property, targetExpectedClass, commonModule, { project, element ->
|
||||
) : CreateExpectedFix<KtNamedDeclaration>(property, targetExpectedClass, commonModule, block@{ project, element ->
|
||||
if (!element.checkTypeAccessibilityInModule(commonModule)) {
|
||||
showUnknownTypesError(element)
|
||||
return@block null
|
||||
}
|
||||
val descriptor = element.toDescriptor() as? PropertyDescriptor
|
||||
descriptor?.let { generateProperty(project, true, element, descriptor, targetExpectedClass) }
|
||||
})
|
||||
@@ -224,7 +284,11 @@ class CreateExpectedFunctionFix(
|
||||
function: KtFunction,
|
||||
targetExpectedClass: KtClassOrObject?,
|
||||
commonModule: Module
|
||||
) : CreateExpectedFix<KtFunction>(function, targetExpectedClass, commonModule, { project, element ->
|
||||
) : CreateExpectedFix<KtFunction>(function, targetExpectedClass, commonModule, block@{ project, element ->
|
||||
if (!element.checkTypeAccessibilityInModule(commonModule)) {
|
||||
showUnknownTypesError(element)
|
||||
return@block null
|
||||
}
|
||||
val descriptor = element.toDescriptor() as? FunctionDescriptor
|
||||
descriptor?.let { generateFunction(project, true, element, descriptor, targetExpectedClass) }
|
||||
})
|
||||
|
||||
@@ -8,8 +8,10 @@ package org.jetbrains.kotlin.idea.quickfix.expectactual
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.JavaDirectoryService
|
||||
import com.intellij.psi.search.GlobalSearchScope
|
||||
import org.jetbrains.kotlin.descriptors.*
|
||||
import org.jetbrains.kotlin.idea.caches.project.implementedDescriptors
|
||||
import org.jetbrains.kotlin.idea.caches.project.isTestModule
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.analyzeWithContent
|
||||
import org.jetbrains.kotlin.idea.codeInsight.shorten.addToBeShortenedDescendantsToWaitingSet
|
||||
import org.jetbrains.kotlin.idea.core.KotlinNameSuggester
|
||||
@@ -24,6 +26,8 @@ import org.jetbrains.kotlin.idea.core.overrideImplement.makeNotActual
|
||||
import org.jetbrains.kotlin.idea.core.toDescriptor
|
||||
import org.jetbrains.kotlin.idea.project.platform
|
||||
import org.jetbrains.kotlin.idea.refactoring.createKotlinFile
|
||||
import org.jetbrains.kotlin.idea.search.usagesSearch.descriptor
|
||||
import org.jetbrains.kotlin.idea.stubindex.KotlinFullClassNameIndex
|
||||
import org.jetbrains.kotlin.idea.util.*
|
||||
import org.jetbrains.kotlin.idea.util.application.runWriteAction
|
||||
import org.jetbrains.kotlin.js.descriptorUtils.getJetTypeFqName
|
||||
@@ -36,6 +40,7 @@ import org.jetbrains.kotlin.resolve.BindingContext
|
||||
import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils
|
||||
import org.jetbrains.kotlin.resolve.checkers.ExpectedActualDeclarationChecker
|
||||
import org.jetbrains.kotlin.resolve.checkers.ExperimentalUsageChecker
|
||||
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull
|
||||
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
|
||||
import org.jetbrains.kotlin.resolve.descriptorUtil.module
|
||||
import org.jetbrains.kotlin.types.AbbreviatedType
|
||||
@@ -122,7 +127,7 @@ internal fun KtPsiFactory.generateClassOrObject(
|
||||
val superType = context[BindingContext.TYPE, originalEntry.typeReference]
|
||||
val superClassDescriptor = superType?.constructor?.declarationDescriptor as? ClassDescriptor ?: return@forEach
|
||||
if (superClassDescriptor.kind == ClassKind.CLASS || superClassDescriptor.kind == ClassKind.ENUM_CLASS) {
|
||||
val entryText = IdeDescriptorRenderers.SOURCE_CODE.renderType(superType) ?: return@forEach
|
||||
val entryText = IdeDescriptorRenderers.SOURCE_CODE.renderType(superType)
|
||||
val newGeneratedEntry = if (generateExpectClass) {
|
||||
createSuperTypeEntry(entryText)
|
||||
} else {
|
||||
@@ -152,20 +157,22 @@ internal fun KtPsiFactory.generateClassOrObject(
|
||||
}
|
||||
}
|
||||
|
||||
val existingClasses = originalClass.declarations.asSequence().filterIsInstance<KtClassOrObject>().filter {
|
||||
it.isEffectivelyActual(false)
|
||||
}.toList() + generatedClass + outerClasses
|
||||
|
||||
declLoop@ for (originalDeclaration in originalClass.declarations) {
|
||||
val descriptor = originalDeclaration.toDescriptor() ?: continue
|
||||
if (generateExpectClass && !originalDeclaration.isEffectivelyActual(false)) continue
|
||||
val generatedDeclaration: KtDeclaration = when (originalDeclaration) {
|
||||
is KtClassOrObject -> {
|
||||
generateClassOrObject(project, generateExpectClass, originalDeclaration, outerClasses + generatedClass)
|
||||
}
|
||||
is KtClassOrObject -> generateClassOrObject(project, generateExpectClass, originalDeclaration, existingClasses)
|
||||
is KtCallableDeclaration -> {
|
||||
when (originalDeclaration) {
|
||||
is KtFunction -> generateFunction(
|
||||
project, generateExpectClass, originalDeclaration, descriptor as FunctionDescriptor, generatedClass, outerClasses
|
||||
project, generateExpectClass, originalDeclaration, descriptor as FunctionDescriptor, generatedClass, existingClasses
|
||||
)
|
||||
is KtProperty -> generateProperty(
|
||||
project, generateExpectClass, originalDeclaration, descriptor as PropertyDescriptor, generatedClass, outerClasses
|
||||
project, generateExpectClass, originalDeclaration, descriptor as PropertyDescriptor, generatedClass, existingClasses
|
||||
)
|
||||
else -> continue@declLoop
|
||||
}
|
||||
@@ -310,3 +317,28 @@ class KotlinTypeInaccessibleException(val type: KotlinType) : Exception() {
|
||||
override val message: String
|
||||
get() = "Type ${type.getJetTypeFqName(true)} is not accessible from common code"
|
||||
}
|
||||
|
||||
fun KtNamedDeclaration.checkTypeAccessibilityInModule(module: Module, existedClasses: Set<String> = emptySet()): Boolean {
|
||||
if (hasPrivateModifier()) return false
|
||||
val types = when (this) {
|
||||
is KtClassOrObject -> return fqName?.canFindClassInModule(project, module, existedClasses) == true
|
||||
is KtCallableDeclaration -> {
|
||||
val descriptor = descriptor?.safeAs<CallableDescriptor>() ?: return false
|
||||
val returnType = descriptor.returnType ?: return false
|
||||
(listOfNotNull(descriptor.extensionReceiverParameter) + descriptor.valueParameters).map { it.type } + returnType
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
|
||||
val project = project
|
||||
return types.all { it.fqName?.canFindClassInModule(project, module, existedClasses) ?: false }
|
||||
}
|
||||
|
||||
val KotlinType.fqName: FqName? get() = constructor.declarationDescriptor?.fqNameOrNull()
|
||||
|
||||
private fun FqName.canFindClassInModule(project: Project, module: Module, existedClasses: Set<String>): Boolean {
|
||||
val name = asString()
|
||||
return name in existedClasses || KotlinFullClassNameIndex.getInstance()[
|
||||
name, project, GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module, module.isTestModule)
|
||||
].isNotEmpty()
|
||||
}
|
||||
@@ -29,6 +29,8 @@ fun KtCallExpression.replaceOrCreateTypeArgumentList(newTypeArgumentList: KtType
|
||||
|
||||
fun KtModifierListOwner.hasInlineModifier() = hasModifier(KtTokens.INLINE_KEYWORD)
|
||||
|
||||
fun KtModifierListOwner.hasPrivateModifier() = hasModifier(KtTokens.PRIVATE_KEYWORD)
|
||||
|
||||
fun KtPrimaryConstructor.allowedValOrVar(): Boolean = containingClass()?.let {
|
||||
it.isAnnotation() || it.hasInlineModifier()
|
||||
} ?: false
|
||||
|
||||
Reference in New Issue
Block a user