NI: introduce warning about implicitly inferred Nothing with existing non-Nothing expected type

^KT-35406 Fixed
This commit is contained in:
Victor Petukhov
2020-02-13 16:09:50 +03:00
parent e3184e407d
commit 51edf2b351
18 changed files with 414 additions and 42 deletions
@@ -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,
@@ -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() { ... }''");
@@ -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)
}
}