NI: introduce warning about implicitly inferred Nothing with existing non-Nothing expected type
^KT-35406 Fixed
This commit is contained in:
@@ -780,7 +780,8 @@ public interface Errors {
|
||||
|
||||
DiagnosticFactory0<KtExpression> TYPE_INFERENCE_FAILED_ON_SPECIAL_CONSTRUCT = DiagnosticFactory0.create(ERROR, SPECIAL_CONSTRUCT_TOKEN);
|
||||
|
||||
DiagnosticFactory0<PsiElement> IMPLICIT_NOTHING_AS_TYPE_PARAMETER = DiagnosticFactory0.create(WARNING);
|
||||
DiagnosticFactory0<PsiElement> IMPLICIT_NOTHING_TYPE_ARGUMENT_IN_RETURN_POSITION = DiagnosticFactory0.create(WARNING);
|
||||
DiagnosticFactory0<PsiElement> IMPLICIT_NOTHING_TYPE_ARGUMENT_AGAINST_NOT_NOTHING_EXPECTED_TYPE = DiagnosticFactory0.create(WARNING);
|
||||
|
||||
// Reflection
|
||||
|
||||
|
||||
@@ -117,6 +117,18 @@ fun BindingTrace.reportDiagnosticOnce(diagnostic: Diagnostic) {
|
||||
report(diagnostic)
|
||||
}
|
||||
|
||||
fun BindingTrace.reportDiagnosticOnceWrtDiagnosticFactoryList(
|
||||
diagnosticToReport: Diagnostic,
|
||||
vararg diagnosticFactories: DiagnosticFactory<*>,
|
||||
) {
|
||||
val hasAlreadyReportedDiagnosticFromListOrSameType = bindingContext.diagnostics.forElement(diagnosticToReport.psiElement)
|
||||
.any { diagnostic -> diagnostic.factory == diagnosticToReport.factory || diagnosticFactories.any { it == diagnostic.factory } }
|
||||
|
||||
if (hasAlreadyReportedDiagnosticFromListOrSameType) return
|
||||
|
||||
report(diagnosticToReport)
|
||||
}
|
||||
|
||||
class TypeMismatchDueToTypeProjectionsData(
|
||||
val expectedType: KotlinType,
|
||||
val expressionType: KotlinType,
|
||||
|
||||
+2
-1
@@ -1001,7 +1001,8 @@ public class DefaultErrorMessages {
|
||||
MAP.put(ILLEGAL_SUSPEND_PROPERTY_ACCESS, "Suspend property ''{0}'' should be accessed only from a coroutine or suspend function", NAME);
|
||||
MAP.put(ILLEGAL_RESTRICTED_SUSPENDING_FUNCTION_CALL, "Restricted suspending functions can only invoke member or extension suspending functions on their restricted coroutine scope");
|
||||
MAP.put(NON_MODIFIER_FORM_FOR_BUILT_IN_SUSPEND, "''suspend'' function can only be called in a form of modifier of a lambda: suspend { ... }");
|
||||
MAP.put(IMPLICIT_NOTHING_AS_TYPE_PARAMETER, "One of the type variables was implicitly inferred to Nothing. Please, specify type arguments explicitly to hide this warning.");
|
||||
MAP.put(IMPLICIT_NOTHING_TYPE_ARGUMENT_IN_RETURN_POSITION, "Returning type parameter has been inferred to Nothing implicitly. Please, specify type arguments explicitly to hide this warning. Nothing can produce an exception at runtime.");
|
||||
MAP.put(IMPLICIT_NOTHING_TYPE_ARGUMENT_AGAINST_NOT_NOTHING_EXPECTED_TYPE, "Returning type parameter has been inferred to Nothing implicitly because Nothing is more specific than specified expected type. Please specify type arguments explicitly in accordance with expected type to hide this warning. Nothing can produce an exception at runtime.");
|
||||
MAP.put(RETURN_FOR_BUILT_IN_SUSPEND, "Using implicit label for this lambda is prohibited");
|
||||
MAP.put(MODIFIER_FORM_FOR_NON_BUILT_IN_SUSPEND, "Calls having a form of ''suspend {}'' are deprecated because ''suspend'' in the context will have a meaning of a modifier. Add empty argument list to the call: ''suspend() { ... }''");
|
||||
|
||||
|
||||
+128
-16
@@ -9,12 +9,20 @@ import com.intellij.psi.PsiElement
|
||||
import org.jetbrains.kotlin.builtins.isBuiltinFunctionalType
|
||||
import org.jetbrains.kotlin.builtins.isFunctionOrSuspendFunctionType
|
||||
import org.jetbrains.kotlin.diagnostics.Errors
|
||||
import org.jetbrains.kotlin.diagnostics.reportDiagnosticOnceWrtDiagnosticFactoryList
|
||||
import org.jetbrains.kotlin.resolve.BindingContext
|
||||
import org.jetbrains.kotlin.resolve.calls.SPECIAL_FUNCTION_NAMES
|
||||
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
|
||||
import org.jetbrains.kotlin.resolve.calls.callUtil.getParameterForArgument
|
||||
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
|
||||
import org.jetbrains.kotlin.resolve.calls.model.*
|
||||
import org.jetbrains.kotlin.resolve.calls.tower.NewResolvedCallImpl
|
||||
import org.jetbrains.kotlin.resolve.calls.tower.psiExpression
|
||||
import org.jetbrains.kotlin.resolve.calls.tower.psiKotlinCall
|
||||
import org.jetbrains.kotlin.types.DeferredType
|
||||
import org.jetbrains.kotlin.types.KotlinType
|
||||
import org.jetbrains.kotlin.types.TypeUtils
|
||||
import org.jetbrains.kotlin.types.expressions.ControlStructureTypingUtils
|
||||
import org.jetbrains.kotlin.types.typeUtil.isNothing
|
||||
import org.jetbrains.kotlin.types.typeUtil.isNothingOrNullableNothing
|
||||
import org.jetbrains.kotlin.types.typeUtil.isTypeParameter
|
||||
|
||||
object ImplicitNothingAsTypeParameterCallChecker : CallChecker {
|
||||
@@ -34,27 +42,131 @@ object ImplicitNothingAsTypeParameterCallChecker : CallChecker {
|
||||
* return inv()
|
||||
* }
|
||||
*/
|
||||
override fun check(resolvedCall: ResolvedCall<*>, reportOn: PsiElement, context: CallCheckerContext) {
|
||||
private fun checkByReturnPositionWithoutExpected(
|
||||
resolvedCall: ResolvedCall<*>,
|
||||
reportOn: PsiElement,
|
||||
context: CallCheckerContext,
|
||||
): Boolean {
|
||||
val resultingDescriptor = resolvedCall.resultingDescriptor
|
||||
val expectedType = context.resolutionContext.expectedType
|
||||
val inferredReturnType = resultingDescriptor.returnType
|
||||
val isBuiltinFunctionalType =
|
||||
resolvedCall.resultingDescriptor.dispatchReceiverParameter?.value?.type?.isBuiltinFunctionalType == true
|
||||
|
||||
if (inferredReturnType is DeferredType || isBuiltinFunctionalType)
|
||||
return
|
||||
if (resultingDescriptor.name !in SPECIAL_FUNCTION_NAMES && resolvedCall.call.typeArguments.isEmpty()) {
|
||||
val lambdasFromArgumentsReturnTypes =
|
||||
resolvedCall.candidateDescriptor.valueParameters.filter { it.type.isFunctionOrSuspendFunctionType }
|
||||
.map { it.returnType?.arguments?.last()?.type }.toSet()
|
||||
val unsubstitutedReturnType = resultingDescriptor.original.returnType
|
||||
val expectedType = context.resolutionContext.expectedType
|
||||
val hasImplicitNothing = inferredReturnType?.isNothing() == true &&
|
||||
unsubstitutedReturnType?.isTypeParameter() == true &&
|
||||
(TypeUtils.noExpectedType(expectedType) || !expectedType.isNothing())
|
||||
if (inferredReturnType is DeferredType || isBuiltinFunctionalType) return false
|
||||
if (resultingDescriptor.name in SPECIAL_FUNCTION_NAMES || resolvedCall.call.typeArguments.isNotEmpty()) return false
|
||||
|
||||
if (hasImplicitNothing && unsubstitutedReturnType !in lambdasFromArgumentsReturnTypes) {
|
||||
context.trace.report(Errors.IMPLICIT_NOTHING_AS_TYPE_PARAMETER.on(reportOn))
|
||||
val lambdasFromArgumentsReturnTypes =
|
||||
resolvedCall.candidateDescriptor.valueParameters.filter { it.type.isFunctionOrSuspendFunctionType }
|
||||
.map { it.returnType?.arguments?.last()?.type }.toSet()
|
||||
val unsubstitutedReturnType = resultingDescriptor.original.returnType
|
||||
val hasImplicitNothing = inferredReturnType?.isNothing() == true &&
|
||||
unsubstitutedReturnType?.isTypeParameter() == true &&
|
||||
(TypeUtils.noExpectedType(expectedType) || !expectedType.isNothing())
|
||||
|
||||
if (hasImplicitNothing && unsubstitutedReturnType !in lambdasFromArgumentsReturnTypes) {
|
||||
context.trace.reportDiagnosticOnceWrtDiagnosticFactoryList(
|
||||
Errors.IMPLICIT_NOTHING_TYPE_ARGUMENT_IN_RETURN_POSITION.on(reportOn),
|
||||
Errors.IMPLICIT_NOTHING_TYPE_ARGUMENT_AGAINST_NOT_NOTHING_EXPECTED_TYPE,
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun ResolvedAtom.getResolvedCallAtom(bindingContext: BindingContext): ResolvedCallAtom? {
|
||||
if (this is SingleCallResolutionResult) return resultCallAtom
|
||||
|
||||
val resolutionAtom = atom as? KotlinCallArgument ?: return null
|
||||
val resolvedCall = resolutionAtom.psiExpression.getResolvedCall(bindingContext)
|
||||
|
||||
return if (resolvedCall is NewResolvedCallImpl) resolvedCall.resolvedCallAtom else null
|
||||
}
|
||||
|
||||
private fun findFunctionsWithImplicitNothingAndReport(resolvedAtoms: List<ResolvedAtom>, context: CallCheckerContext): Boolean {
|
||||
var hasAlreadyReportedAtDepth = false
|
||||
|
||||
for (resolvedAtom in resolvedAtoms) {
|
||||
val subResolveAtoms = resolvedAtom.subResolvedAtoms
|
||||
|
||||
if (!subResolveAtoms.isNullOrEmpty() && findFunctionsWithImplicitNothingAndReport(subResolveAtoms, context)) {
|
||||
hasAlreadyReportedAtDepth = true
|
||||
continue
|
||||
}
|
||||
|
||||
val resolvedCallAtom = resolvedAtom.getResolvedCallAtom(context.trace.bindingContext) ?: continue
|
||||
val candidateDescriptor = resolvedCallAtom.candidateDescriptor
|
||||
val isReturnTypeOwnTypeParameter = candidateDescriptor.typeParameters.any { it.defaultType == candidateDescriptor.returnType }
|
||||
val isSpecialCall = candidateDescriptor.name in SPECIAL_FUNCTION_NAMES
|
||||
val hasExplicitTypeArguments = resolvedCallAtom.atom.psiKotlinCall.typeArguments.isNotEmpty() // not required
|
||||
|
||||
if (!isSpecialCall && isReturnTypeOwnTypeParameter && !hasExplicitTypeArguments) {
|
||||
context.trace.reportDiagnosticOnceWrtDiagnosticFactoryList(
|
||||
Errors.IMPLICIT_NOTHING_TYPE_ARGUMENT_AGAINST_NOT_NOTHING_EXPECTED_TYPE.on(
|
||||
resolvedCallAtom.atom.psiKotlinCall.psiCall.run { calleeExpression ?: callElement },
|
||||
),
|
||||
Errors.IMPLICIT_NOTHING_TYPE_ARGUMENT_IN_RETURN_POSITION,
|
||||
)
|
||||
hasAlreadyReportedAtDepth = true
|
||||
}
|
||||
}
|
||||
|
||||
return hasAlreadyReportedAtDepth
|
||||
}
|
||||
|
||||
private fun getSubResolvedAtomsToAnalyze(
|
||||
resolvedCall: ResolvedCall<*>,
|
||||
expectedType: KotlinType,
|
||||
bindingContext: BindingContext,
|
||||
): List<ResolvedAtom>? {
|
||||
if (resolvedCall !is NewResolvedCallImpl) return null
|
||||
|
||||
val hasNotNothingExpectedType = !TypeUtils.noExpectedType(expectedType) && !expectedType.isNothingOrNullableNothing()
|
||||
val hasNothingReturnType = resolvedCall.resultingDescriptor.returnType?.isNothingOrNullableNothing() == true
|
||||
val isSubResolvedAtomsNotEmpty = !resolvedCall.resolvedCallAtom.subResolvedAtoms.isNullOrEmpty()
|
||||
|
||||
if (hasNotNothingExpectedType && hasNothingReturnType && isSubResolvedAtomsNotEmpty) {
|
||||
return resolvedCall.resolvedCallAtom.subResolvedAtoms
|
||||
}
|
||||
|
||||
val resolvedAtomsFromArguments = resolvedCall.valueArguments.values.mapNotNull { argument ->
|
||||
if (argument !is ExpressionValueArgument) return@mapNotNull null
|
||||
|
||||
val resolvedCallForArgument =
|
||||
argument.valueArgument?.getArgumentExpression()?.getResolvedCall(bindingContext) as? NewResolvedCallImpl
|
||||
?: return@mapNotNull null
|
||||
val expectedTypeForArgument = resolvedCall.getParameterForArgument(argument.valueArgument)?.type ?: return@mapNotNull null
|
||||
|
||||
getSubResolvedAtomsToAnalyze(resolvedCallForArgument, expectedTypeForArgument, bindingContext)
|
||||
}.flatten()
|
||||
|
||||
val extensionReceiver = resolvedCall.resolvedCallAtom.extensionReceiverArgument?.psiExpression
|
||||
val resolvedAtomsFromExtensionReceiver = extensionReceiver?.run {
|
||||
val extensionReceiverResolvedCall = getResolvedCall(bindingContext)
|
||||
// It's needed to exclude invoke with extension (when resolved call for extension equals to common resolved call)
|
||||
if (extensionReceiverResolvedCall == resolvedCall) return@run null
|
||||
|
||||
getSubResolvedAtomsToAnalyze(
|
||||
getResolvedCall(bindingContext) ?: return@run null,
|
||||
resolvedCall.resultingDescriptor.extensionReceiverParameter?.type ?: return@run null,
|
||||
bindingContext,
|
||||
)
|
||||
}
|
||||
|
||||
return if (resolvedAtomsFromExtensionReceiver != null) {
|
||||
resolvedAtomsFromArguments + resolvedAtomsFromExtensionReceiver
|
||||
} else resolvedAtomsFromArguments
|
||||
}
|
||||
|
||||
private fun checkAgainstNotNothingExpectedType(resolvedCall: ResolvedCall<*>, context: CallCheckerContext): Boolean {
|
||||
val subResolvedAtoms =
|
||||
getSubResolvedAtomsToAnalyze(resolvedCall, context.resolutionContext.expectedType, context.trace.bindingContext) ?: return false
|
||||
|
||||
return findFunctionsWithImplicitNothingAndReport(subResolvedAtoms, context)
|
||||
}
|
||||
|
||||
override fun check(resolvedCall: ResolvedCall<*>, reportOn: PsiElement, context: CallCheckerContext) {
|
||||
checkByReturnPositionWithoutExpected(resolvedCall, reportOn, context) || checkAgainstNotNothingExpectedType(resolvedCall, context)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user