ExpectActualUtils: add type accessibility check

This commit is contained in:
Dmitry Gridin
2019-08-05 21:08:11 +03:00
parent dffb44c11c
commit 5884c65857
3 changed files with 119 additions and 21 deletions
@@ -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