From 02e3156900a304a8b1e33927b34cc1be3436de59 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 18 Oct 2025 00:38:35 +0300 Subject: [PATCH] KT-77979: Ignore uninhabited constructors in exhaustive checking --- .../positive/exhaustiveWhenNothingFields.kt | 21 ++++++++++ .../exhaustiveWhenViolateUpperBounds.kt | 18 ++++++++ .../checkers/FirUpperBoundViolatedHelpers.kt | 34 +++++++++++++++ .../kotlin/fir/types/TypeUnification.kt | 4 +- compiler/fir/resolve/build.gradle.kts | 1 + .../FirWhenExhaustivenessComputer.kt | 42 ++++++++++++++++--- .../transformers/body/resolve/bareTypes.kt | 3 +- .../org/jetbrains/kotlin/fir/FirSession.kt | 5 ++- 8 files changed, 119 insertions(+), 9 deletions(-) create mode 100644 compiler/fir/analysis-tests/testData/resolve/exhaustiveness/positive/exhaustiveWhenNothingFields.kt create mode 100644 compiler/fir/analysis-tests/testData/resolve/exhaustiveness/positive/exhaustiveWhenViolateUpperBounds.kt diff --git a/compiler/fir/analysis-tests/testData/resolve/exhaustiveness/positive/exhaustiveWhenNothingFields.kt b/compiler/fir/analysis-tests/testData/resolve/exhaustiveness/positive/exhaustiveWhenNothingFields.kt new file mode 100644 index 0000000000000..bcdfeed4f295a --- /dev/null +++ b/compiler/fir/analysis-tests/testData/resolve/exhaustiveness/positive/exhaustiveWhenNothingFields.kt @@ -0,0 +1,21 @@ +// FIR_IDENTICAL +// RUN_PIPELINE_TILL: BACKEND +// ISSUE: KT-77979 +// WITH_STDLIB + +sealed interface Adt +data class A(val x: B) : Adt +sealed interface BAdt : Adt +data class B(val x: B) : BAdt +data class C(val x: A) : BAdt + +fun example(a: Adt) { + when (a) { + is A -> {} + is C -> {} + // no need in case B + } +} + +/* GENERATED_FIR_TAGS: classDeclaration, data, functionDeclaration, interfaceDeclaration, isExpression, nullableType, +primaryConstructor, propertyDeclaration, sealed, smartcast, typeParameter, whenExpression, whenWithSubject */ diff --git a/compiler/fir/analysis-tests/testData/resolve/exhaustiveness/positive/exhaustiveWhenViolateUpperBounds.kt b/compiler/fir/analysis-tests/testData/resolve/exhaustiveness/positive/exhaustiveWhenViolateUpperBounds.kt new file mode 100644 index 0000000000000..f974233fbd801 --- /dev/null +++ b/compiler/fir/analysis-tests/testData/resolve/exhaustiveness/positive/exhaustiveWhenViolateUpperBounds.kt @@ -0,0 +1,18 @@ +// FIR_IDENTICAL +// RUN_PIPELINE_TILL: BACKEND +// ISSUE: KT-77979 +// WITH_STDLIB + +sealed interface Adt +data class A(val x: A) : Adt +data class B(val x: Unit) : Adt + +fun example(a: Adt) { + when (a) { + is B -> {} + // no need in case A + } +} + +/* GENERATED_FIR_TAGS: classDeclaration, data, functionDeclaration, interfaceDeclaration, isExpression, nullableType, +out, primaryConstructor, propertyDeclaration, sealed, typeConstraint, typeParameter, whenExpression, whenWithSubject */ diff --git a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/FirUpperBoundViolatedHelpers.kt b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/FirUpperBoundViolatedHelpers.kt index 9fea517fcb54e..a8decb987ec49 100644 --- a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/FirUpperBoundViolatedHelpers.kt +++ b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/FirUpperBoundViolatedHelpers.kt @@ -181,6 +181,40 @@ fun checkUpperBoundViolated( } } +fun checkUpperBoundViolatedNoReport( + typeParameters: List, + typeArguments: List, + session: FirSession, +): Boolean { + val substitution = typeParameters.zip(typeArguments).toMap() + val substitutor = FE10LikeConeSubstitutor(substitution, session) + + val count = minOf(typeParameters.size, typeArguments.size) + val typeSystemContext = session.typeContext + + for (index in 0 until count) { + val argument = typeArguments[index] + val argumentType = argument.type + if (argumentType != null) { + if (argumentType.typeArguments.isEmpty() && argumentType !is ConeTypeParameterType) { + val intersection = + typeSystemContext.intersectTypes(typeParameters[index].resolvedBounds.map { it.coneType }) + val upperBound = substitutor.substituteOrSelf(intersection) + if (!AbstractTypeChecker.isSubtypeOf( + typeSystemContext, + argumentType, + upperBound, + stubTypesEqualToAnything = true + ) + ) { + return true + } + } + } + } + return false +} + context(context: CheckerContext, reporter: DiagnosticReporter) private fun reportUpperBoundViolationWarningIfNecessary( additionalUpperBoundsProvider: FirPlatformUpperBoundsProvider?, diff --git a/compiler/fir/providers/src/org/jetbrains/kotlin/fir/types/TypeUnification.kt b/compiler/fir/providers/src/org/jetbrains/kotlin/fir/types/TypeUnification.kt index cf32d9c0f3842..d77797bf71d88 100644 --- a/compiler/fir/providers/src/org/jetbrains/kotlin/fir/types/TypeUnification.kt +++ b/compiler/fir/providers/src/org/jetbrains/kotlin/fir/types/TypeUnification.kt @@ -23,8 +23,8 @@ fun FirSession.doUnify( targetTypeParameters: Set, result: MutableMap, ): Boolean { - val originalType = originalTypeProjection.type?.lowerBoundIfFlexible()?.fullyExpandedType(this) - val typeWithParameters = typeWithParametersProjection.type?.lowerBoundIfFlexible()?.fullyExpandedType(this) + val originalType = originalTypeProjection.type?.lowerBoundIfFlexible()?.fullyExpandedType() + val typeWithParameters = typeWithParametersProjection.type?.lowerBoundIfFlexible()?.fullyExpandedType() if (typeWithParameters is ConeErrorType) { return true // Return true to avoid loosing `result` substitution diff --git a/compiler/fir/resolve/build.gradle.kts b/compiler/fir/resolve/build.gradle.kts index eff38e289fe3b..c4caaf7e516b7 100644 --- a/compiler/fir/resolve/build.gradle.kts +++ b/compiler/fir/resolve/build.gradle.kts @@ -6,6 +6,7 @@ plugins { dependencies { api(project(":compiler:fir:providers")) api(project(":compiler:fir:semantics")) + api(project(":compiler:fir:checkers")) implementation(project(":core:util.runtime")) compileOnly(libs.guava) diff --git a/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/transformers/FirWhenExhaustivenessComputer.kt b/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/transformers/FirWhenExhaustivenessComputer.kt index a249c225e7b2c..d62d0fc78d912 100644 --- a/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/transformers/FirWhenExhaustivenessComputer.kt +++ b/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/transformers/FirWhenExhaustivenessComputer.kt @@ -11,8 +11,10 @@ import org.jetbrains.kotlin.descriptors.ClassKind import org.jetbrains.kotlin.descriptors.Modality import org.jetbrains.kotlin.diagnostics.WhenMissingCase import org.jetbrains.kotlin.fir.* +import org.jetbrains.kotlin.fir.analysis.checkers.checkUpperBoundViolatedNoReport import org.jetbrains.kotlin.fir.declarations.FirEnumEntry import org.jetbrains.kotlin.fir.declarations.FirFile +import org.jetbrains.kotlin.fir.declarations.declaredProperties import org.jetbrains.kotlin.fir.declarations.getSealedClassInheritors import org.jetbrains.kotlin.fir.declarations.utils.isEnumClass import org.jetbrains.kotlin.fir.declarations.utils.isExpect @@ -21,7 +23,9 @@ import org.jetbrains.kotlin.fir.expressions.* import org.jetbrains.kotlin.fir.expressions.impl.FirElseIfTrueCondition import org.jetbrains.kotlin.fir.resolve.* import org.jetbrains.kotlin.fir.resolve.providers.symbolProvider +import org.jetbrains.kotlin.fir.resolve.substitution.substitutorByMap import org.jetbrains.kotlin.fir.resolve.transformers.WhenOnSealedClassExhaustivenessChecker.ConditionChecker.processBranch +import org.jetbrains.kotlin.fir.resolve.transformers.body.resolve.computeRepresentativeTypeForBareType import org.jetbrains.kotlin.fir.symbols.FirBasedSymbol import org.jetbrains.kotlin.fir.symbols.impl.FirClassSymbol import org.jetbrains.kotlin.fir.symbols.impl.FirRegularClassSymbol @@ -447,12 +451,40 @@ private object WhenOnSealedClassExhaustivenessChecker : WhenExhaustivenessChecke } for (notCheckedRegularClasses in notCheckedRegularClasses) { - destination += WhenMissingCase.IsTypeCheckIsMissing( - notCheckedRegularClasses.classId, - notCheckedRegularClasses.fir.classKind.isSingleton, - notCheckedRegularClasses.ownTypeParameterSymbols.size - ) + if (!isUninhabited(notCheckedRegularClasses, subjectType, session)) { + destination += WhenMissingCase.IsTypeCheckIsMissing( + notCheckedRegularClasses.classId, + notCheckedRegularClasses.fir.classKind.isSingleton, + notCheckedRegularClasses.ownTypeParameterSymbols.size + ) + } + } + } + + private fun isUninhabited( + classSymbol: FirClassSymbol<*>, + subjectType: ConeKotlinType, + session: FirSession, + ): Boolean { + val classType = + session.computeRepresentativeTypeForBareType(classSymbol.defaultType(), subjectType) ?: return false + val boundsViolated = + checkUpperBoundViolatedNoReport(classSymbol.typeParameterSymbols, classType.typeArguments.toList(), session) + val containsNothing: Boolean by lazy { + val typeMapping = + classSymbol.typeParameterSymbols.zip(classType.typeArguments).mapNotNull { (parameter, arg) -> + when (arg) { + is ConeKotlinType -> parameter to arg + is ConeKotlinTypeProjectionOut -> parameter to arg.type + else -> null + } + }.toMap() + val substitutor = substitutorByMap(typeMapping, session) + val typesOfProperties = classSymbol.declaredProperties(session) + .map { substitutor.substituteOrSelf(it.resolvedReturnType) } + typesOfProperties.any { it.isNothing } } + return boundsViolated || containsNothing } private fun inferVariantsFromSubjectSmartCast(subject: FirExpression, data: Info) { diff --git a/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/transformers/body/resolve/bareTypes.kt b/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/transformers/body/resolve/bareTypes.kt index 7a0c1fe07627f..36ac02b20f6bb 100644 --- a/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/transformers/body/resolve/bareTypes.kt +++ b/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/transformers/body/resolve/bareTypes.kt @@ -5,6 +5,7 @@ package org.jetbrains.kotlin.fir.resolve.transformers.body.resolve +import org.jetbrains.kotlin.fir.SessionHolder import org.jetbrains.kotlin.fir.declarations.FirResolvePhase import org.jetbrains.kotlin.fir.declarations.FirTypeAlias import org.jetbrains.kotlin.fir.declarations.FirTypeParameter @@ -16,7 +17,7 @@ import org.jetbrains.kotlin.fir.types.* import org.jetbrains.kotlin.types.AbstractTypeChecker import org.jetbrains.kotlin.types.TypeApproximatorConfiguration -fun BodyResolveComponents.computeRepresentativeTypeForBareType(type: ConeClassLikeType, originalType: ConeKotlinType): ConeKotlinType? { +fun SessionHolder.computeRepresentativeTypeForBareType(type: ConeClassLikeType, originalType: ConeKotlinType): ConeKotlinType? { originalType.lowerBoundIfFlexible().fullyExpandedType().let { if (it !== originalType) return computeRepresentativeTypeForBareType(type, it) } diff --git a/compiler/fir/tree/src/org/jetbrains/kotlin/fir/FirSession.kt b/compiler/fir/tree/src/org/jetbrains/kotlin/fir/FirSession.kt index 10506e4ece5a0..06a4ea5deaafa 100644 --- a/compiler/fir/tree/src/org/jetbrains/kotlin/fir/FirSession.kt +++ b/compiler/fir/tree/src/org/jetbrains/kotlin/fir/FirSession.kt @@ -17,7 +17,7 @@ interface FirSessionComponent abstract class FirSession @PrivateSessionConstructor constructor( val kind: Kind -) : ComponentArrayOwner() { +) : ComponentArrayOwner(), SessionHolder { companion object : ConeTypeRegistry() { inline fun sessionComponentAccessor(): ArrayMapAccessor { return generateAccessor(T::class) @@ -34,6 +34,9 @@ abstract class FirSession @PrivateSessionConstructor constructor( open val builtinTypes: BuiltinTypes = BuiltinTypes() + override val session: FirSession + get() = this + final override val typeRegistry: TypeRegistry = Companion @SessionConfiguration