From d30461afc608a57793bf94e2ea5150886fe1f7da Mon Sep 17 00:00:00 2001 From: Alexey Sedunov Date: Thu, 20 Jul 2017 20:27:07 +0300 Subject: [PATCH] Create Class from Usage: Support multiple containers --- .../callableBuilder/CallableBuilder.kt | 3 +- ...mCallWithConstructorCalleeActionFactory.kt | 5 +- ...teClassFromConstructorCallActionFactory.kt | 17 +++--- ...assFromReferenceExpressionActionFactory.kt | 52 +++++++++++-------- ...eateClassFromTypeReferenceActionFactory.kt | 5 +- .../createClass/CreateClassFromUsageFix.kt | 51 ++++++++++++------ .../createClass/createClassUtils.kt | 49 ++++++++--------- ...TypeAliasFromTypeReferenceActionFactory.kt | 7 ++- 8 files changed, 112 insertions(+), 77 deletions(-) diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/callableBuilder/CallableBuilder.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/callableBuilder/CallableBuilder.kt index 6a957b5be7f..359d57fd70b 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/callableBuilder/CallableBuilder.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/callableBuilder/CallableBuilder.kt @@ -490,7 +490,8 @@ class CallableBuilder(val config: CallableBuilderConfiguration) { val safeName = name.quoteIfNeeded() when (kind) { ClassKind.ENUM_ENTRY -> { - if (!(targetParent is KtClass && targetParent.isEnum())) throw AssertionError("Enum class expected: ${targetParent.text}") + val targetParent = targetParents.singleOrNull() + if (!(targetParent is KtClass && targetParent.isEnum())) throw AssertionError("Enum class expected: ${targetParent?.text}") val hasParameters = targetParent.primaryConstructorParameters.isNotEmpty() psiFactory.createEnumEntry("$safeName${if (hasParameters) "()" else " "}") } diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createClass/CreateClassFromCallWithConstructorCalleeActionFactory.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createClass/CreateClassFromCallWithConstructorCalleeActionFactory.kt index 0b8a795342f..fa1aadfe0bb 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createClass/CreateClassFromCallWithConstructorCalleeActionFactory.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createClass/CreateClassFromCallWithConstructorCalleeActionFactory.kt @@ -25,6 +25,7 @@ import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.psiUtil.isAncestor import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.types.Variance +import org.jetbrains.kotlin.utils.ifEmpty import java.util.* object CreateClassFromCallWithConstructorCalleeActionFactory : CreateClassFromUsageFactory() { @@ -61,7 +62,7 @@ object CreateClassFromCallWithConstructorCalleeActionFactory : CreateClassFromUs val qualifier = userType.qualifier?.referenceExpression val qualifierDescriptor = qualifier?.let { context[BindingContext.REFERENCE_TARGET, it] } - val targetParent = getTargetParentByQualifier(file, qualifier != null, qualifierDescriptor) ?: return null + val targetParents = getTargetParentsByQualifier(file, qualifier != null, qualifierDescriptor).ifEmpty { return null } val anyType = module.builtIns.nullableAnyType val valueArguments = element.valueArguments @@ -80,7 +81,7 @@ object CreateClassFromCallWithConstructorCalleeActionFactory : CreateClassFromUs return ClassInfo( name = calleeRef.getReferencedName(), - targetParent = targetParent, + targetParents = targetParents, expectedTypeInfo = TypeInfo.Empty, parameterInfos = parameterInfos, open = !isAnnotation, diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createClass/CreateClassFromConstructorCallActionFactory.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createClass/CreateClassFromConstructorCallActionFactory.kt index 95cded102b3..d27597eedff 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createClass/CreateClassFromConstructorCallActionFactory.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createClass/CreateClassFromConstructorCallActionFactory.kt @@ -26,6 +26,7 @@ import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType import org.jetbrains.kotlin.psi.psiUtil.getQualifiedExpressionForSelectorOrThis import org.jetbrains.kotlin.resolve.calls.callUtil.getCall import org.jetbrains.kotlin.types.Variance +import org.jetbrains.kotlin.utils.ifEmpty import java.util.* object CreateClassFromConstructorCallActionFactory: CreateClassFromUsageFactory() { @@ -43,11 +44,12 @@ object CreateClassFromConstructorCallActionFactory: CreateClassFromUsageFactory< val (context, moduleDescriptor) = element.analyzeFullyAndGetResult() val file = element.containingFile as? KtFile ?: return emptyList() val call = element.getCall(context) ?: return emptyList() - val targetParent = getTargetParentByCall(call, file, context) ?: return emptyList() + val targetParents = getTargetParentsByCall(call, file, context).ifEmpty { return emptyList() } val classKind = if (inAnnotationEntry) ClassKind.ANNOTATION_CLASS else ClassKind.PLAIN_CLASS val fullCallExpr = element.getQualifiedExpressionForSelectorOrThis() - if (!fullCallExpr.getInheritableTypeInfo(context, moduleDescriptor, targetParent).second(classKind)) return emptyList() + val expectedType = fullCallExpr.guessTypeForClass(context, moduleDescriptor) + if (expectedType != null && !targetParents.any { getClassKindFilter(expectedType, it)(classKind) }) return emptyList() return listOf(classKind) } @@ -75,7 +77,7 @@ object CreateClassFromConstructorCallActionFactory: CreateClassFromUsageFactory< val (context, moduleDescriptor) = callExpr.analyzeFullyAndGetResult() val call = callExpr.getCall(context) ?: return null - val targetParent = getTargetParentByCall(call, file, context) ?: return null + val targetParents = getTargetParentsByCall(call, file, context).ifEmpty { return null } val inner = isInnerClassExpected(call) val valueArguments = callExpr.valueArguments @@ -90,14 +92,17 @@ object CreateClassFromConstructorCallActionFactory: CreateClassFromUsageFactory< val classKind = if (inAnnotationEntry) ClassKind.ANNOTATION_CLASS else ClassKind.PLAIN_CLASS - val (expectedTypeInfo, filter) = fullCallExpr.getInheritableTypeInfo(context, moduleDescriptor, targetParent) - if (!filter(classKind)) return null + val expectedType = fullCallExpr.guessTypeForClass(context, moduleDescriptor) + val expectedTypeInfo = expectedType?.toClassTypeInfo() ?: TypeInfo.Empty + val filteredParents = if (expectedType != null) { + targetParents.filter { getClassKindFilter(expectedType, it)(classKind) }.ifEmpty { return null } + } else targetParents val typeArgumentInfos = if (inAnnotationEntry) Collections.emptyList() else callExpr.getTypeInfoForTypeArguments() return ClassInfo( name = name, - targetParent = targetParent, + targetParents = filteredParents, expectedTypeInfo = expectedTypeInfo, inner = inner, typeArguments = typeArgumentInfos, diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createClass/CreateClassFromReferenceExpressionActionFactory.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createClass/CreateClassFromReferenceExpressionActionFactory.kt index 0160ec5ef50..9f6dddf1f43 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createClass/CreateClassFromReferenceExpressionActionFactory.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createClass/CreateClassFromReferenceExpressionActionFactory.kt @@ -25,6 +25,7 @@ import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.psiUtil.* import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.calls.callUtil.getCall +import org.jetbrains.kotlin.utils.ifEmpty import java.util.Arrays import java.util.Collections @@ -69,11 +70,15 @@ object CreateClassFromReferenceExpressionActionFactory : CreateClassFromUsageFac val receiverSelector = (fullCallExpr as? KtQualifiedExpression)?.receiverExpression?.getQualifiedElementSelector() as? KtReferenceExpression val qualifierDescriptor = receiverSelector?.let { context[BindingContext.REFERENCE_TARGET, it] } - val targetParent = - getTargetParentByQualifier(element.containingKtFile, receiverSelector != null, qualifierDescriptor) - ?: return Collections.emptyList() + val targetParents = getTargetParentsByQualifier( + element.containingKtFile, + receiverSelector != null, + qualifierDescriptor + ).ifEmpty { return emptyList() } - element.getCreatePackageFixIfApplicable(targetParent)?.let { return emptyList() } + targetParents.forEach { + if (element.getCreatePackageFixIfApplicable(it) != null) return emptyList() + } if (!name.checkClassName()) return emptyList() @@ -82,7 +87,7 @@ object CreateClassFromReferenceExpressionActionFactory : CreateClassFromUsageFac .filter { when (it) { ClassKind.ANNOTATION_CLASS -> inImport - ClassKind.ENUM_ENTRY -> inImport && isEnum(targetParent) + ClassKind.ENUM_ENTRY -> inImport && targetParents.any { isEnum(it) } else -> true } } @@ -95,19 +100,22 @@ object CreateClassFromReferenceExpressionActionFactory : CreateClassFromUsageFac if (fullCallExpr.getAssignmentByLHS() != null) return Collections.emptyList() val call = element.getCall(context) ?: return Collections.emptyList() - val targetParent = getTargetParentByCall(call, file, context) ?: return Collections.emptyList() + val targetParents = getTargetParentsByCall(call, file, context).ifEmpty { return emptyList() } if (isInnerClassExpected(call)) return Collections.emptyList() - val filter = fullCallExpr.getInheritableTypeInfo(context, moduleDescriptor, targetParent).second + val allKinds = Arrays.asList(ClassKind.OBJECT, ClassKind.ENUM_ENTRY) - return Arrays.asList(ClassKind.OBJECT, ClassKind.ENUM_ENTRY) - .filter { - filter(it) && when (it) { - ClassKind.OBJECT -> true - ClassKind.ENUM_ENTRY -> isEnum(targetParent) - else -> false - } + val expectedType = fullCallExpr.guessTypeForClass(context, moduleDescriptor) + + return allKinds.filter { classKind -> + targetParents.any { targetParent -> + (expectedType == null || getClassKindFilter(expectedType, targetParent)(classKind)) && when (classKind) { + ClassKind.OBJECT -> true + ClassKind.ENUM_ENTRY -> isEnum(targetParent) + else -> false } + } + } } override fun extractFixData(element: KtSimpleNameExpression, diagnostic: Diagnostic): ClassInfo? { @@ -123,25 +131,27 @@ object CreateClassFromReferenceExpressionActionFactory : CreateClassFromUsageFac val receiverSelector = (fullCallExpr as? KtQualifiedExpression)?.receiverExpression?.getQualifiedElementSelector() as? KtReferenceExpression val qualifierDescriptor = receiverSelector?.let { context[BindingContext.REFERENCE_TARGET, it] } - val targetParent = - getTargetParentByQualifier(element.containingKtFile, receiverSelector != null, qualifierDescriptor) - ?: return null + val targetParents = getTargetParentsByQualifier( + element.containingKtFile, + receiverSelector != null, + qualifierDescriptor + ).ifEmpty { return null } return ClassInfo( name = name, - targetParent = targetParent, + targetParents = targetParents, expectedTypeInfo = TypeInfo.Empty ) } val call = element.getCall(context) ?: return null - val targetParent = getTargetParentByCall(call, file, context) ?: return null + val targetParents = getTargetParentsByCall(call, file, context).ifEmpty { return null } - val expectedTypeInfo = fullCallExpr.getInheritableTypeInfo(context, moduleDescriptor, targetParent).first + val expectedTypeInfo = fullCallExpr.guessTypeForClass(context, moduleDescriptor)?.toClassTypeInfo() ?: TypeInfo.Empty return ClassInfo( name = name, - targetParent = targetParent, + targetParents = targetParents, expectedTypeInfo = expectedTypeInfo ) } diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createClass/CreateClassFromTypeReferenceActionFactory.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createClass/CreateClassFromTypeReferenceActionFactory.kt index bd6631e29b5..e58de1c9c0e 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createClass/CreateClassFromTypeReferenceActionFactory.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createClass/CreateClassFromTypeReferenceActionFactory.kt @@ -27,6 +27,7 @@ import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall import org.jetbrains.kotlin.types.KotlinType import org.jetbrains.kotlin.types.TypeIntersector import org.jetbrains.kotlin.types.Variance +import org.jetbrains.kotlin.utils.ifEmpty import java.util.* object CreateClassFromTypeReferenceActionFactory : CreateClassFromUsageFactory() { @@ -82,14 +83,14 @@ object CreateClassFromTypeReferenceActionFactory : CreateClassFromUsageFactory, val expectedTypeInfo: TypeInfo, val inner: Boolean = false, val open: Boolean = false, @@ -74,14 +76,19 @@ open class CreateClassFromUsageFix protected constructor ( if (!super.isAvailable(project, editor, file)) return false with(classInfo) { if (kind == DEFAULT) return false - if (targetParent is PsiClass) { - if (kind == OBJECT || kind == ENUM_ENTRY) return false - if (targetParent.isInterface && inner) return false + targetParents.forEach { + if (it is PsiClass) { + if (kind == OBJECT || kind == ENUM_ENTRY) return false + if (it.isInterface && inner) return false + } } + } return true } + override fun startInWriteAction() = false + override fun invoke(project: Project, editor: Editor?, file: KtFile) { fun createFileByPackage(psiPackage: PsiPackage): KtFile? { val directories = psiPackage.directories.filter { it.canRefactor() } @@ -114,20 +121,30 @@ open class CreateClassFromUsageFix protected constructor ( return targetFile } - with (classInfo) { - val targetParent = - when (targetParent) { - is KtElement, is PsiClass -> targetParent - is PsiPackage -> createFileByPackage(targetParent) - else -> throw AssertionError("Unexpected element: " + targetParent.text) - } ?: return + if (editor == null) return - val constructorInfo = PrimaryConstructorInfo(classInfo, expectedTypeInfo) - val builder = CallableBuilderConfiguration( - Collections.singletonList(constructorInfo), element as KtElement, file, editor, false, kind == PLAIN_CLASS || kind == INTERFACE - ).createBuilder() - builder.placement = CallablePlacement.NoReceiver(targetParent) - project.executeCommand(text) { builder.build() } + with (classInfo) { + chooseContainerElementIfNecessary(targetParents, editor, "Choose class container", true, { it }) { + runWriteAction { + val targetParent = + when (it) { + is KtElement, is PsiClass -> it + is PsiPackage -> createFileByPackage(it) + else -> throw AssertionError("Unexpected element: " + it.text) + } ?: return@runWriteAction + val constructorInfo = PrimaryConstructorInfo(classInfo, expectedTypeInfo) + val builder = CallableBuilderConfiguration( + Collections.singletonList(constructorInfo), + element as KtElement, + file, + editor, + false, + kind == PLAIN_CLASS || kind == INTERFACE + ).createBuilder() + builder.placement = CallablePlacement.NoReceiver(targetParent) + project.executeCommand(text) { builder.build() } + } + } } } diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createClass/createClassUtils.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createClass/createClassUtils.kt index 21628af3c6a..f7aeea266f3 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createClass/createClassUtils.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createClass/createClassUtils.kt @@ -50,56 +50,57 @@ internal fun String.checkClassName(): Boolean = isNotEmpty() && Character.isUppe private fun String.checkPackageName(): Boolean = isNotEmpty() && Character.isLowerCase(first()) -internal fun getTargetParentByQualifier( +internal fun getTargetParentsByQualifier( file: KtFile, isQualified: Boolean, - qualifierDescriptor: DeclarationDescriptor?): PsiElement? { + qualifierDescriptor: DeclarationDescriptor? +): List { val project = file.project - val targetParent: PsiElement? = when { + val targetParents: List = when { !isQualified -> - file + listOf(file) qualifierDescriptor is ClassDescriptor -> - DescriptorToSourceUtilsIde.getAnyDeclaration(project, qualifierDescriptor) + listOfNotNull(DescriptorToSourceUtilsIde.getAnyDeclaration(project, qualifierDescriptor)) qualifierDescriptor is PackageViewDescriptor -> if (qualifierDescriptor.fqName != file.packageFqName) { - JavaPsiFacade.getInstance(project).findPackage(qualifierDescriptor.fqName.asString()) + listOfNotNull(JavaPsiFacade.getInstance(project).findPackage(qualifierDescriptor.fqName.asString())) } - else file + else listOf(file) else -> - null + emptyList() } - return if (targetParent?.canRefactor() ?: false) return targetParent else null + return targetParents.filter { it.canRefactor() } } -internal fun getTargetParentByCall(call: Call, file: KtFile, context: BindingContext): PsiElement? { +internal fun getTargetParentsByCall(call: Call, file: KtFile, context: BindingContext): List { val receiver = call.explicitReceiver return when (receiver) { - null -> getTargetParentByQualifier(file, false, null) - is Qualifier -> getTargetParentByQualifier(file, true, context[BindingContext.REFERENCE_TARGET, receiver.referenceExpression]) - is ReceiverValue -> getTargetParentByQualifier(file, true, receiver.type.constructor.declarationDescriptor) + null -> getTargetParentsByQualifier(file, false, null) + is Qualifier -> getTargetParentsByQualifier(file, true, context[BindingContext.REFERENCE_TARGET, receiver.referenceExpression]) + is ReceiverValue -> getTargetParentsByQualifier(file, true, receiver.type.constructor.declarationDescriptor) else -> throw AssertionError("Unexpected receiver: $receiver") } } internal fun isInnerClassExpected(call: Call) = call.explicitReceiver is ReceiverValue -internal fun KtExpression.getInheritableTypeInfo( - context: BindingContext, - moduleDescriptor: ModuleDescriptor, - containingDeclaration: PsiElement): Pair Boolean> { - val types = guessTypes(context, moduleDescriptor, coerceUnusedToUnit = false) - if (types.size != 1) return TypeInfo.Empty to { _ -> true } +internal fun KtExpression.guessTypeForClass(context: BindingContext, moduleDescriptor: ModuleDescriptor) = + guessTypes(context, moduleDescriptor, coerceUnusedToUnit = false).singleOrNull() - val type = types.first() - val descriptor = type.constructor.declarationDescriptor ?: return TypeInfo.Empty to { _ -> false } +internal fun KotlinType.toClassTypeInfo(): TypeInfo { + return TypeInfo.ByType(this, Variance.OUT_VARIANCE).noSubstitutions() +} - val canHaveSubtypes = !(type.constructor.isFinal || type.containsStarProjections()) +internal fun getClassKindFilter(expectedType: KotlinType, containingDeclaration: PsiElement): (ClassKind) -> Boolean { + val descriptor = expectedType.constructor.declarationDescriptor ?: return { _ -> false } + + val canHaveSubtypes = !(expectedType.constructor.isFinal || expectedType.containsStarProjections()) val isEnum = DescriptorUtils.isEnumClass(descriptor) if (!(canHaveSubtypes || isEnum) - || descriptor is TypeParameterDescriptor) return TypeInfo.Empty to { _ -> false } + || descriptor is TypeParameterDescriptor) return { _ -> false } - return TypeInfo.ByType(type, Variance.OUT_VARIANCE).noSubstitutions() to { classKind -> + return { classKind -> when (classKind) { ClassKind.ENUM_ENTRY -> isEnum && containingDeclaration == DescriptorToSourceUtils.descriptorToDeclaration(descriptor) ClassKind.INTERFACE -> containingDeclaration !is PsiClass diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createTypeAlias/CreateTypeAliasFromTypeReferenceActionFactory.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createTypeAlias/CreateTypeAliasFromTypeReferenceActionFactory.kt index 53df814a3a6..c48625e2aac 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createTypeAlias/CreateTypeAliasFromTypeReferenceActionFactory.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createTypeAlias/CreateTypeAliasFromTypeReferenceActionFactory.kt @@ -40,17 +40,16 @@ object CreateTypeAliasFromTypeReferenceActionFactory : KotlinSingleIntentionActi if (!element.languageVersionSettings.supportsFeature(LanguageFeature.TypeAliases)) return null val classInfo = CreateClassFromTypeReferenceActionFactory.extractFixData(element, diagnostic) ?: return null - if (classInfo.targetParent is KtDeclaration) return null - if (classInfo.targetParent is PsiPackage) return null + val targetParent = classInfo.targetParents.singleOrNull { it !is KtDeclaration && it !is PsiPackage } ?: return null val expectedType = getTypeConstraintInfo(element)?.upperBound if (expectedType != null && expectedType.containsError()) return null val validator = CollectingNameValidator( - filter = NewDeclarationNameValidator(classInfo.targetParent, null, NewDeclarationNameValidator.Target.FUNCTIONS_AND_CLASSES) + filter = NewDeclarationNameValidator(targetParent, null, NewDeclarationNameValidator.Target.FUNCTIONS_AND_CLASSES) ) val typeParameterNames = KotlinNameSuggester.suggestNamesForTypeParameters(classInfo.typeArguments.size, validator) - return TypeAliasInfo(classInfo.name, classInfo.targetParent, typeParameterNames, expectedType) + return TypeAliasInfo(classInfo.name, targetParent, typeParameterNames, expectedType) } override fun createFix(originalElement: KtUserType, data: TypeAliasInfo) = CreateTypeAliasFromUsageFix(originalElement, data)