[kotlin] k2: add a type checker for create expect from actual fix

- disabled case where jvm's HashMap is not mapped to the common HashMap [to be updated]

^KTIJ-33878 fixed

GitOrigin-RevId: 9a4a53b0950937679e87c2fffe716816a667c21f
This commit is contained in:
Anna Kozlova
2025-04-23 19:03:25 +00:00
committed by intellij-monorepo-bot
parent 4ca91861f0
commit f01400e056
18 changed files with 129 additions and 49 deletions
@@ -9,12 +9,14 @@ import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.PsiElement
import com.intellij.psi.createSmartPointer
import com.intellij.util.containers.addIfNotNull
import org.jetbrains.kotlin.analysis.api.KaExperimentalApi
import org.jetbrains.kotlin.analysis.api.KaSession
import org.jetbrains.kotlin.analysis.api.fir.diagnostics.KaFirDiagnostic
import org.jetbrains.kotlin.analysis.api.symbols.KaCallableSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KaClassSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KaValueParameterSymbol
import org.jetbrains.kotlin.analysis.api.types.KaClassType
import org.jetbrains.kotlin.analysis.api.types.KaType
import org.jetbrains.kotlin.idea.base.analysis.api.utils.analyzeInModalWindow
import org.jetbrains.kotlin.idea.base.analysis.api.utils.shortenReferences
@@ -28,6 +30,8 @@ import org.jetbrains.kotlin.idea.core.createFileForDeclaration
import org.jetbrains.kotlin.idea.core.overrideImplement.MemberGenerateMode
import org.jetbrains.kotlin.idea.core.overrideImplement.generateClassWithMembers
import org.jetbrains.kotlin.idea.core.overrideImplement.generateMember
import org.jetbrains.kotlin.idea.k2.refactoring.introduce.extractionEngine.getUnResolvableInScope
import org.jetbrains.kotlin.idea.k2.refactoring.introduce.extractionEngine.isResolvableInScope
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.CreateClassUtil.getTypeDescription
import org.jetbrains.kotlin.idea.refactoring.introduce.showErrorHint
import org.jetbrains.kotlin.idea.refactoring.isInterfaceClass
@@ -39,7 +43,6 @@ import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
import org.jetbrains.kotlin.psi.psiUtil.findDescendantOfType
import org.jetbrains.kotlin.psi.psiUtil.hasActualModifier
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import kotlin.collections.plus
internal object ActualWithoutExpectFactory {
@@ -143,7 +146,7 @@ sealed class CreateExpectedFix<D : KtNamedDeclaration>(
val targetExpectedClass = targetExpectedClassPointer?.element
val expectedFile = targetExpectedClass?.containingKtFile ?: getOrCreateImplementationFile() ?: return
val declaration = element ?: return
val expectPrototype = generate(project, targetExpectedClass, declaration) ?: return
val expectPrototype = generate(project, targetExpectedClass, expectedFile, declaration) ?: return
val target = project.executeWriteCommand(familyName, null) {
val initial = (when {
targetExpectedClass != null && expectPrototype is KtPrimaryConstructor -> targetExpectedClass.add(expectPrototype)
@@ -160,7 +163,7 @@ sealed class CreateExpectedFix<D : KtNamedDeclaration>(
)
}
abstract fun generate(project: Project, targetExpectedClass: KtClassOrObject?, declaration: D): D?
abstract fun generate(project: Project, targetExpectedClass: KtClassOrObject?, expectedFile: KtFile, declaration: D): D?
fun showInaccessibleDeclarationError(
element: PsiElement,
@@ -172,38 +175,80 @@ sealed class CreateExpectedFix<D : KtNamedDeclaration>(
}
}
@OptIn(KaExperimentalApi::class)
protected fun isCorrectAndHaveAccessibleModifiers(
declaration: KtNamedDeclaration,
showErrorHint: Boolean = false,
expectedFile: KtFile,
): Boolean {
val inaccessibleModifier = INACCESSIBLE_MODIFIERS.find { declaration.hasModifier(it) }
if (inaccessibleModifier != null) {
if (showErrorHint) showInaccessibleDeclarationError(
showInaccessibleDeclarationError(
declaration,
KotlinBundle.message("the.declaration.has.0.modifier", inaccessibleModifier)
)
return false
}
if (declaration is KtFunction && declaration.hasBody() && declaration.containingClassOrObject?.isInterfaceClass() == true) {
if (showErrorHint) showInaccessibleDeclarationError(
showInaccessibleDeclarationError(
declaration,
KotlinBundle.message("the.function.declaration.shouldn.t.have.a.default.implementation")
)
return false
}
//todo check accessibility of types in the common module
//if (!showErrorHint) return checkAccessibility(declaration)
//
//val types = incorrectTypes(declaration).ifEmpty { return true }
//showInaccessibleDeclarationError(
// declaration,
// KotlinBundle.message(
// "some.types.are.not.accessible.from.0.1",
// targetModule.name,
// TypeAccessibilityChecker.typesToString(types)
// )
//)
val unresolvedTypes = analyzeInModalWindow(declaration, KotlinBundle.message("fix.change.signature.prepare")) {
fun MutableList<KaType>.processTypeParametersOwner(owner: KtTypeParameterListOwner) {
owner.typeParameters.forEach { addIfNotNull(it.extendsBound?.type) }
owner.typeConstraints.forEach { addIfNotNull(it.boundTypeReference?.type) }
owner.annotationEntries.forEach { addIfNotNull(it.typeReference?.type) }
}
val usedTypes = when (declaration) {
is KtConstructor<*> -> declaration.valueParameters.mapNotNull { it.typeReference?.type }
is KtFunction -> buildList {
addIfNotNull(declaration.receiverTypeReference?.type)
addIfNotNull(declaration.returnType)
for (parameter in declaration.valueParameters) {
addIfNotNull(parameter.returnType)
}
processTypeParametersOwner(declaration)
}
is KtProperty -> buildList {
addIfNotNull(declaration.receiverTypeReference?.type)
addIfNotNull(declaration.returnType)
processTypeParametersOwner(declaration)
}
is KtClass -> buildList {
if (declaration.isInline()) {
declaration.primaryConstructor?.valueParameters?.forEach { addIfNotNull(it.returnType) }
}
processTypeParametersOwner(declaration)
}
else -> emptyList()
}.map { it.abbreviation ?: it }.distinct()
val expectedVirtualFile = expectedFile.virtualFile
usedTypes.mapNotNull {
getUnResolvableInScope(it, expectedFile, mutableSetOf()) { classSymbol ->
val psi = (classSymbol.getExpectsForActual().firstOrNull() ?: classSymbol).psi
val useScope = psi?.useScope
psi == declaration || useScope != null && expectedVirtualFile != null && useScope.contains(expectedVirtualFile)
}
}.mapNotNull { (it as? KaClassType)?.classId?.shortClassName?.asString() }
}
if (unresolvedTypes.isNotEmpty()) {
showInaccessibleDeclarationError(
declaration,
KotlinBundle.message(
"some.types.are.not.accessible.from.0.1",
module.name,
unresolvedTypes.joinToString()
)
)
}
return true
}
@@ -224,9 +269,10 @@ internal class CreateExpectedClassFix(
override fun generate(
project: Project,
targetExpectedClass: KtClassOrObject?,
expectedFile: KtFile,
declaration: KtNamedDeclaration,
): KtNamedDeclaration? {
if (!isCorrectAndHaveAccessibleModifiers(declaration, true)) return null
if (!isCorrectAndHaveAccessibleModifiers(declaration, expectedFile)) return null
return analyzeInModalWindow(declaration, KotlinBundle.message("fix.change.signature.prepare")) {
val classSymbol = declaration.symbol as? KaClassSymbol ?: return@analyzeInModalWindow null
@@ -252,9 +298,10 @@ internal class CreateExpectedCallableMemberFix(
override fun generate(
project: Project,
targetExpectedClass: KtClassOrObject?,
expectedFile: KtFile,
declaration: KtNamedDeclaration,
): KtNamedDeclaration? {
if (!isCorrectAndHaveAccessibleModifiers(declaration, true)) return null
if (!isCorrectAndHaveAccessibleModifiers(declaration, expectedFile)) return null
return analyzeInModalWindow(declaration, KotlinBundle.message("fix.change.signature.prepare")) {
val callableSymbol = declaration.symbol as? KaCallableSymbol ?: return@analyzeInModalWindow null
@@ -1,7 +1,7 @@
// "Create expected class in common module testModule_Common" "true"
// SHOULD_FAIL_WITH: Some types are not accessible from testModule_Common:
// SHOULD_FAIL_WITH: JvmAnnotationClass
// IGNORE_K2
package one.two
@@ -1,7 +1,7 @@
// "Create expected class in common module testModule_Common" "true"
// SHOULD_FAIL_WITH: Some types are not accessible from testModule_Common:,Some
// DISABLE_ERRORS
// IGNORE_K2
interface Some
@@ -1,4 +1,6 @@
// "Add missing actual declarations" "true"
// K2_ACTION: "Create actual in 'testModule_JVM'" "true"
// K2_TOOL: org.jetbrains.kotlin.idea.k2.codeinsight.inspections.KotlinNoActualForExpectInspection
// SHOULD_FAIL_WITH: Cannot generate class: Type &lt;Unknown&gt; is not accessible from target module
// DISABLE_ERRORS
// IGNORE_K2
@@ -1,7 +1,7 @@
// "Create expected function in common module testModule_Common" "true"
// SHOULD_FAIL_WITH: Some types are not accessible from testModule_Common:,Some
// DISABLE_ERRORS
// IGNORE_K2
class Some
actual fun <caret>foo(some: Some) {}
@@ -1,7 +1,7 @@
// "Create expected function in common module testModule_Common" "true"
// SHOULD_FAIL_WITH: Some types are not accessible from testModule_Common:,Some
// DISABLE_ERRORS
// IGNORE_K2
interface Some
@@ -1,7 +1,7 @@
// "Create expected function in common module testModule_Common" "true"
// SHOULD_FAIL_WITH: Some types are not accessible from testModule_Common:,Some
// DISABLE_ERRORS
// IGNORE_K2
class Some
actual fun <caret>foo(some: List<Some>) {}
@@ -1,7 +1,7 @@
// "Create expected function in common module testModule_Common" "true"
// SHOULD_FAIL_WITH: Some types are not accessible from testModule_Common:,Some
// DISABLE_ERRORS
// IGNORE_K2
interface Some
actual fun <T : Some> <caret>foo(some: List<T>) {}
@@ -1,7 +1,7 @@
// "Create expected function in common module testModule_Common" "true"
// SHOULD_FAIL_WITH: Some types are not accessible from testModule_Common:,Some
// DISABLE_ERRORS
// IGNORE_K2
interface Some
actual fun <T : CommonClass, F : Some> <caret>foo(some: List<T>) {}
@@ -1,7 +1,7 @@
// "Create expected property in common module testModule_Common" "true"
// SHOULD_FAIL_WITH: Some types are not accessible from testModule_Common:,Some
// DISABLE_ERRORS
// IGNORE_K2
interface Some
actual val foo<caret>: Some = TODO()
@@ -1,7 +1,7 @@
// "Create expected property in common module testModule_Common" "true"
// SHOULD_FAIL_WITH: Some types are not accessible from testModule_Common:,Some
// DISABLE_ERRORS
// IGNORE_K2
class Some<T>
actual val <T>Some<T>.<caret>foo: Some<T> get() = TODO()
@@ -1,7 +1,7 @@
// "Create expected property in common module testModule_Common" "true"
// SHOULD_FAIL_WITH: Some types are not accessible from testModule_Common:,A
// DISABLE_ERRORS
// IGNORE_K2
interface A
actual val <T: A> Some<T>.<caret>foo: Some<T> get() = TODO()
@@ -1,7 +1,6 @@
// "Create expected function in common module testModule_Common" "true"
// SHOULD_FAIL_WITH: Some types are not accessible from testModule_Common:,SomeString
// DISABLE_ERRORS
// IGNORE_K2
typealias SomeString = String
@@ -1,7 +1,6 @@
// "Create expected function in common module proj_Common" "true"
// SHOULD_FAIL_WITH: Some types are not accessible from proj_Common:,ArrayList
// DISABLE_ERRORS
// IGNORE_K2
import java.util.ArrayList
@@ -1,7 +1,6 @@
// "Create expected function in common module proj_Common" "true"
// SHOULD_FAIL_WITH: Some types are not accessible from proj_Common:,ArrayList
// DISABLE_ERRORS
// IGNORE_K2
import java.util.ArrayList
@@ -1,5 +1,5 @@
// "Create expected property in common module proj_Common" "true"
// DISABLE_ERRORS
// IGNORE_K2
actual val some<caret>: HashMap<Int, Int> = TODO()
@@ -1,5 +1,5 @@
// "Create expected property in common module proj_Common" "true"
// DISABLE_ERRORS
// IGNORE_K2
actual val some: HashMap<Int, Int> = TODO()
@@ -3,13 +3,14 @@ package org.jetbrains.kotlin.idea.k2.refactoring.introduce.extractionEngine
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.PsiUtil
import com.intellij.psi.util.parentOfType
import org.jetbrains.kotlin.analysis.api.KaExperimentalApi
import org.jetbrains.kotlin.analysis.api.KaSession
import org.jetbrains.kotlin.analysis.api.analyze
import org.jetbrains.kotlin.analysis.api.renderer.declarations.impl.KaDeclarationRendererForSource
import org.jetbrains.kotlin.analysis.api.symbols.KaAnonymousObjectSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KaClassSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KaClassLikeSymbol
import org.jetbrains.kotlin.analysis.api.symbols.typeParameters
import org.jetbrains.kotlin.analysis.api.types.*
import org.jetbrains.kotlin.idea.k2.refactoring.extractFunction.Parameter
@@ -121,47 +122,80 @@ class KotlinTypeDescriptor(private val data: IExtractionData) : TypeDescriptor<K
*/
context(KaSession)
@OptIn(KaExperimentalApi::class)
fun isResolvableInScope(typeToCheck: KaType, scope: PsiElement, typeParameters: MutableSet<TypeParameter>): Boolean {
fun isResolvableInScope(
typeToCheck: KaType,
scope: PsiElement,
typeParameters: MutableSet<TypeParameter>,
): Boolean {
return getUnResolvableInScope(typeToCheck, scope, typeParameters) == null
}
context(KaSession)
@OptIn(KaExperimentalApi::class)
fun getUnResolvableInScope(
typeToCheck: KaType,
scope: PsiElement,
typeParameters: MutableSet<TypeParameter>,
classAccessibilityChecker: (KaClassLikeSymbol) -> Boolean = { true }
): KaType? {
require(scope.containingFile is KtFile)
((typeToCheck as? KaTypeParameterType)?.symbol?.psi as? KtTypeParameter)?.let { typeParameter ->
val typeParameterListOwner = typeParameter.parentOfType<KtTypeParameterListOwner>()
if (typeParameterListOwner == null || !PsiTreeUtil.isAncestor(typeParameterListOwner, scope, true)) {
typeParameters.add(TypeParameter(typeParameter, typeParameter.collectRelevantConstraints()))
}
return true
return null
}
if (typeToCheck is KaClassType) {
val classSymbol = typeToCheck.symbol
if ((classSymbol as? KaAnonymousObjectSymbol)?.superTypes?.all { isResolvableInScope(it, scope, typeParameters) } == true) {
return true
val unresolvedInSuperType = (classSymbol as? KaAnonymousObjectSymbol)?.superTypes?.firstNotNullOfOrNull {
getUnResolvableInScope(
it,
scope,
typeParameters,
classAccessibilityChecker
)
}
if (unresolvedInSuperType != null) {
return unresolvedInSuperType
}
if ((classSymbol as? KaClassSymbol)?.classId == null) {
if (classSymbol.classId == null) {
//because org.jetbrains.kotlin.fir.FirVisibilityChecker.Default always return true for local classes,
//let's be pessimistic here and prohibit local classes completely
return false
return typeToCheck
}
if (!classAccessibilityChecker(classSymbol)) {
return typeToCheck
}
val fileSymbol = (scope.containingFile as KtFile).symbol
if (!createUseSiteVisibilityChecker(fileSymbol, receiverExpression = null, scope).isVisible(classSymbol)) {
return false
return typeToCheck
}
typeToCheck.typeArguments.mapNotNull { it.type }.forEach {
if (!isResolvableInScope(it, scope, typeParameters)) return false
val unresolvedInTypeArguments = typeToCheck.typeArguments.mapNotNull { it.type }.firstNotNullOfOrNull {
getUnResolvableInScope(it, scope, typeParameters, classAccessibilityChecker)
}
if (unresolvedInTypeArguments != null) {
return unresolvedInTypeArguments
}
}
if (typeToCheck is KaErrorType) {
return false
return typeToCheck
}
if (typeToCheck is KaIntersectionType) {
return false
return typeToCheck
}
if (typeToCheck is KaDefinitelyNotNullType) {
if (!isResolvableInScope(typeToCheck.original, scope, typeParameters)) return false
val unresolvedOriginal = getUnResolvableInScope(typeToCheck.original, scope, typeParameters, classAccessibilityChecker)
if (unresolvedOriginal != null) {
return unresolvedOriginal
}
}
return true
return null
}