From 5884c658579f8c262ef154bdc1e9bb73c91aaabf Mon Sep 17 00:00:00 2001 From: Dmitry Gridin Date: Mon, 5 Aug 2019 21:08:11 +0300 Subject: [PATCH] ExpectActualUtils: add type accessibility check --- .../expectactual/CreateExpectedFix.kt | 94 ++++++++++++++++--- .../expectactual/ExpectActualUtils.kt | 44 +++++++-- .../kotlin/idea/util/expressionExt.kt | 2 + 3 files changed, 119 insertions(+), 21 deletions(-) diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/CreateExpectedFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/CreateExpectedFix.kt index 31b01176292..595f318cff7 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/CreateExpectedFix.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/CreateExpectedFix.kt @@ -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( declaration: D, @@ -111,17 +116,36 @@ class CreateExpectedClassFix( outerExpectedClass: KtClassOrObject?, commonModule: Module ) : CreateExpectedFix(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): HashSet { + return elements.filterIsInstance().mapNotNull { it.fqName?.asString() }.toHashSet() +} + +private fun showUnknownTypesDialog(project: Project, declarationsWithNonExistentClasses: List): 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, prefixToRemove: String): List? { +private fun chooseMembers(project: Project, collection: Collection, prefixToRemove: String): List? { val classMembers = collection.mapNotNull { it.resolveToDescriptorIfAny()?.let { descriptor -> Member(prefixToRemove, it, descriptor) } } @@ -158,9 +206,9 @@ private fun chooseMembers(project: Project, collection: Collection { - val thisSequence = if (withSelf) sequenceOf(this) else emptySequence() - val primaryConstructorSequence = primaryConstructorParameters.asSequence() + primaryConstructor.let { +private fun KtClassOrObject.collectDeclarations(withSelf: Boolean = true): Sequence { + val thisSequence: Sequence = if (withSelf) sequenceOf(this) else emptySequence() + val primaryConstructorSequence: Sequence = 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, - selectedElements: Collection + originalElements: Collection, + selectedElements: Collection ) { 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(property, targetExpectedClass, commonModule, { project, element -> +) : CreateExpectedFix(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(function, targetExpectedClass, commonModule, { project, element -> +) : CreateExpectedFix(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) } }) diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/ExpectActualUtils.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/ExpectActualUtils.kt index 523f4c2d057..994846b068b 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/ExpectActualUtils.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/ExpectActualUtils.kt @@ -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().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 = 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() ?: 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): Boolean { + val name = asString() + return name in existedClasses || KotlinFullClassNameIndex.getInstance()[ + name, project, GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module, module.isTestModule) + ].isNotEmpty() +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/util/expressionExt.kt b/idea/src/org/jetbrains/kotlin/idea/util/expressionExt.kt index e8b3195da15..23c4d570715 100644 --- a/idea/src/org/jetbrains/kotlin/idea/util/expressionExt.kt +++ b/idea/src/org/jetbrains/kotlin/idea/util/expressionExt.kt @@ -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