[FIR] Prefer the non-smartcasted receiver in resolution if it's visible

Previously, we would ignore the candidate with the non-smartcasted
receiver if they have the same symbol.
Now we prefer them when they're visible or when the smart cast changes
the nullability.
This commit is contained in:
Kirill Rakhman
2023-12-18 17:32:28 +01:00
committed by Space Team
parent 5186ba80e2
commit ad2689ecbf
15 changed files with 105 additions and 251 deletions
@@ -13,6 +13,7 @@ import org.jetbrains.kotlin.fir.resolve.calls.CandidateFactory
import org.jetbrains.kotlin.fir.scopes.FirScope
import org.jetbrains.kotlin.fir.symbols.FirBasedSymbol
import org.jetbrains.kotlin.resolve.calls.tasks.ExplicitReceiverKind
import org.jetbrains.kotlin.resolve.calls.tower.CandidateApplicability
internal class CandidateFactoriesAndCollectors(
// Common calls
@@ -85,8 +86,8 @@ private class TowerScopeLevelProcessor(
scope: FirScope,
objectsByName: Boolean,
isFromOriginalTypeInPresenceOfSmartCast: Boolean,
) {
resultCollector.consumeCandidate(
): CandidateApplicability {
return resultCollector.consumeCandidate(
group, candidateFactory.createCandidate(
callInfo,
symbol,
@@ -31,6 +31,7 @@ import org.jetbrains.kotlin.fir.types.*
import org.jetbrains.kotlin.fir.types.impl.FirImplicitAnyTypeRef
import org.jetbrains.kotlin.fir.utils.exceptions.withConeTypeEntry
import org.jetbrains.kotlin.name.StandardClassIds.Annotations.HidesMembers
import org.jetbrains.kotlin.resolve.calls.tower.CandidateApplicability
import org.jetbrains.kotlin.types.AbstractTypeChecker
import org.jetbrains.kotlin.utils.SmartList
import org.jetbrains.kotlin.utils.exceptions.errorWithAttachment
@@ -59,7 +60,7 @@ abstract class TowerScopeLevel {
scope: FirScope,
objectsByName: Boolean = false,
isFromOriginalTypeInPresenceOfSmartCast: Boolean = false,
)
): CandidateApplicability
}
}
@@ -86,14 +87,13 @@ class MemberScopeTowerLevel(
val scope = dispatchReceiverValue.scope(session, scopeSession) ?: return ProcessResult.SCOPE_EMPTY
var (empty, candidates) = scope.collectCandidates(processScopeMembers)
val scopeWithoutSmartcast = getOriginalReceiverExpressionIfStableSmartCast()
?.resolvedType
?.scope(
session,
scopeSession,
bodyResolveComponents.returnTypeCalculator.callableCopyTypeCalculator,
requiredMembersPhase = FirResolvePhase.STATUS,
)
val receiverTypeWithoutSmartCast = getOriginalReceiverExpressionIfStableSmartCast()?.resolvedType
val scopeWithoutSmartcast = receiverTypeWithoutSmartCast?.scope(
session,
scopeSession,
bodyResolveComponents.returnTypeCalculator.callableCopyTypeCalculator,
requiredMembersPhase = FirResolvePhase.STATUS,
)
if (scopeWithoutSmartcast == null) {
consumeCandidates(
@@ -107,12 +107,22 @@ class MemberScopeTowerLevel(
scopeWithoutSmartcast.collectCandidates(processScopeMembers).let { (isEmpty, originalCandidates) ->
empty = empty && isEmpty
for (originalCandidate in originalCandidates) {
map[originalCandidate.member] = MemberFromSmartcastScope(originalCandidate, cameFromSmartcast = false)
map[originalCandidate.member] = MemberFromSmartcastScope(originalCandidate, DispatchReceiverToUse.UnwrapSmartcast)
}
}
for (candidateFromSmartCast in candidates) {
map[candidateFromSmartCast.member] = MemberFromSmartcastScope(candidateFromSmartCast, cameFromSmartcast = true)
val existing = map[candidateFromSmartCast.member]
// If both scopes return the same symbol, we want to prefer the candidate from the original scope without smartcast
// with two exceptions:
// - When the smart-casted type is always null, we want to return it and report UNSAFE_CALL.
// - When the original type can be null, in this case the smart-case either makes it not-null or the call is red anyway.
if (existing == null || dispatchReceiverValue.type.isNullableNothing || receiverTypeWithoutSmartCast.canBeNull) {
map[candidateFromSmartCast.member] = MemberFromSmartcastScope(candidateFromSmartCast, DispatchReceiverToUse.SmartcastWithoutUnwrapping)
} else {
existing.dispatchReceiverToUse = DispatchReceiverToUse.SmartcastIfUnwrappedInvisible
}
}
consumeCandidates(
@@ -170,9 +180,15 @@ class MemberScopeTowerLevel(
return if (empty) ProcessResult.SCOPE_EMPTY else ProcessResult.FOUND
}
private data class MemberFromSmartcastScope<T : FirCallableSymbol<*>>(
private enum class DispatchReceiverToUse(val unwrapSmartcast: Boolean) {
UnwrapSmartcast(true),
SmartcastWithoutUnwrapping(false),
SmartcastIfUnwrappedInvisible(true),
}
private class MemberFromSmartcastScope<T : FirCallableSymbol<*>>(
val memberWithBaseScope: MemberWithBaseScope<T>,
val cameFromSmartcast: Boolean
var dispatchReceiverToUse: DispatchReceiverToUse,
)
private fun <T : FirCallableSymbol<*>> FirTypeScope.collectCandidates(
@@ -208,13 +224,11 @@ class MemberScopeTowerLevel(
?: candidatesWithSmartcast?.values?.map { it.memberWithBaseScope }
?: error("candidatesWithoutSmartcast or candidatesWithSmartcast should be not null")
for (candidateWithScope in candidates) {
val (candidate, scope) = candidateWithScope
for ((candidate, scope) in candidates) {
if (candidate.hasConsistentExtensionReceiver(givenExtensionReceiverOptions)) {
val isFromOriginalTypeInPresenceOfSmartCast = candidatesWithSmartcast != null &&
!candidatesWithSmartcast.getValue(candidateWithScope.member).cameFromSmartcast
val dispatchReceiverToUse = when {
val dispatchReceiverToUse = candidatesWithSmartcast?.getValue(candidate)?.dispatchReceiverToUse
val isFromOriginalTypeInPresenceOfSmartCast = dispatchReceiverToUse?.unwrapSmartcast == true
val dispatchReceiver = when {
isFromOriginalTypeInPresenceOfSmartCast ->
getOriginalReceiverExpressionIfStableSmartCast()
// For a chain inference stub in dispatch receiver, we have to provide an explicit cast to Any
@@ -229,13 +243,23 @@ class MemberScopeTowerLevel(
else -> dispatchReceiverValue.receiverExpression
}
output.consumeCandidate(
val applicability = output.consumeCandidate(
candidate,
dispatchReceiverToUse,
dispatchReceiver,
givenExtensionReceiverOptions,
scope,
isFromOriginalTypeInPresenceOfSmartCast = isFromOriginalTypeInPresenceOfSmartCast
)
if (applicability == CandidateApplicability.K2_VISIBILITY_ERROR && dispatchReceiverToUse == DispatchReceiverToUse.SmartcastIfUnwrappedInvisible) {
output.consumeCandidate(
candidate,
dispatchReceiverValue.receiverExpression,
givenExtensionReceiverOptions,
scope,
isFromOriginalTypeInPresenceOfSmartCast = false
)
}
}
}
}
@@ -42,6 +42,7 @@ import org.jetbrains.kotlin.fir.visitors.FirDefaultTransformer
import org.jetbrains.kotlin.fir.visitors.FirTransformer
import org.jetbrains.kotlin.fir.visitors.transformSingle
import org.jetbrains.kotlin.resolve.calls.inference.model.InferredEmptyIntersection
import org.jetbrains.kotlin.resolve.calls.tasks.ExplicitReceiverKind
import org.jetbrains.kotlin.resolve.calls.tower.isSuccess
import org.jetbrains.kotlin.types.TypeApproximatorConfiguration
import org.jetbrains.kotlin.types.Variance
@@ -140,6 +141,18 @@ class FirCallCompletionResultsWriterTransformer(
replaceCalleeReference(calleeReference.toResolvedReference())
replaceDispatchReceiver(dispatchReceiver)
replaceExtensionReceiver(extensionReceiver)
// If the explicit receiver is a smartcast expression, we can choose to use the unwrapped expression as dispatch receiver.
// To maintain the invariant
// explicitReceiver != null => explicitReceiver == dispatchReceiver || explicitReceiver == extensionReceiver
// we update the explicit receiver here.
// We only do this if the candidate is successful, otherwise, we can lose the explicit receiver node in red code like
// fun f(s: String, action: (String.() -> Unit)?) {
// s.action?.let { it() }
//}
if (subCandidate.explicitReceiverKind == ExplicitReceiverKind.DISPATCH_RECEIVER && subCandidate.applicability.isSuccess) {
replaceExplicitReceiver(dispatchReceiver)
}
}
qualifiedAccessExpression.replaceContextReceiverArguments(subCandidate.contextReceiverArguments())