diff --git a/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/Errors.java b/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/Errors.java index bb63c050a46..4bc61728159 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/Errors.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/Errors.java @@ -337,6 +337,7 @@ public interface Errors { // Result class DiagnosticFactory0 RESULT_CLASS_IN_RETURN_TYPE = DiagnosticFactory0.create(ERROR); + DiagnosticFactory1 RESULT_CLASS_WITH_NULLABLE_OPERATOR = DiagnosticFactory1.create(ERROR); // Secondary constructors diff --git a/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/rendering/DefaultErrorMessages.java b/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/rendering/DefaultErrorMessages.java index a6ae400a284..83e4d2eae01 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/rendering/DefaultErrorMessages.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/rendering/DefaultErrorMessages.java @@ -666,6 +666,7 @@ public class DefaultErrorMessages { MAP.put(SECONDARY_CONSTRUCTOR_WITH_BODY_INSIDE_INLINE_CLASS, "Secondary constructors with bodies are reserved for for future releases"); MAP.put(RESULT_CLASS_IN_RETURN_TYPE, "'kotlin.Result' cannot be used as a return type"); + MAP.put(RESULT_CLASS_WITH_NULLABLE_OPERATOR, "Expression of type 'kotlin.Result' cannot be used as a left operand of ''{0}''", STRING); MAP.put(VARIANCE_ON_TYPE_PARAMETER_NOT_ALLOWED, "Variance annotations are only allowed for type parameters of classes and interfaces"); MAP.put(BOUND_ON_TYPE_ALIAS_PARAMETER_NOT_ALLOWED, "Bounds are not allowed on type alias parameters"); diff --git a/compiler/frontend/src/org/jetbrains/kotlin/resolve/TargetPlatform.kt b/compiler/frontend/src/org/jetbrains/kotlin/resolve/TargetPlatform.kt index d40a9babade..be7f77e120d 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/resolve/TargetPlatform.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/resolve/TargetPlatform.kt @@ -132,7 +132,7 @@ private val DEFAULT_CALL_CHECKERS = listOf( CallableReferenceCompatibilityChecker(), LateinitIntrinsicApplicabilityChecker, UnderscoreUsageChecker, AssigningNamedArgumentToVarargChecker(), PrimitiveNumericComparisonCallChecker, LambdaWithSuspendModifierCallChecker, - UselessElvisCallChecker() + UselessElvisCallChecker(), ResultTypeWithNullableOperatorsChecker() ) private val DEFAULT_TYPE_CHECKERS = emptyList() private val DEFAULT_CLASSIFIER_USAGE_CHECKERS = listOf( diff --git a/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/checkers/ResultTypeWithNullableOperatorsChecker.kt b/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/checkers/ResultTypeWithNullableOperatorsChecker.kt new file mode 100644 index 00000000000..3fac3b10d5c --- /dev/null +++ b/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/checkers/ResultTypeWithNullableOperatorsChecker.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.resolve.calls.checkers + +import com.intellij.psi.PsiElement +import org.jetbrains.kotlin.diagnostics.Errors +import org.jetbrains.kotlin.diagnostics.reportDiagnosticOnce +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.KtBinaryExpression +import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall +import org.jetbrains.kotlin.resolve.checkers.isResultType +import org.jetbrains.kotlin.types.expressions.ControlStructureTypingUtils + +class ResultTypeWithNullableOperatorsChecker : CallChecker { + override fun check(resolvedCall: ResolvedCall<*>, reportOn: PsiElement, context: CallCheckerContext) { + val name = resolvedCall.resultingDescriptor.name + val operationNode = resolvedCall.call.callOperationNode + + when { + operationNode?.elementType == KtTokens.SAFE_ACCESS -> { + val receiver = resolvedCall.resultingDescriptor.dispatchReceiverParameter ?: return + if (receiver.type.isResultType()) { + context.trace.report(Errors.RESULT_CLASS_WITH_NULLABLE_OPERATOR.on(operationNode!!.psi, "?.")) + } + } + + name == ControlStructureTypingUtils.ResolveConstruct.EXCL_EXCL.specialFunctionName -> { + if (resolvedCall.resultingDescriptor.returnType?.isResultType() == true) { + context.trace.report(Errors.RESULT_CLASS_WITH_NULLABLE_OPERATOR.on(reportOn, "!!")) + } + } + + name == ControlStructureTypingUtils.ResolveConstruct.ELVIS.specialFunctionName -> { + val elvisBinaryExpression = resolvedCall.call.callElement as? KtBinaryExpression ?: return + val left = elvisBinaryExpression.left ?: return + val leftType = context.trace.getType(left) ?: return + + if (leftType.isResultType()) { + context.trace.reportDiagnosticOnce(Errors.RESULT_CLASS_WITH_NULLABLE_OPERATOR.on(reportOn, "?:")) + } + + // Additional check for case `a ?: b ?: c`, where `b` is Result + // This is needed because inference will give common supertype for `a ?: b` which might not be Result + if (left is KtBinaryExpression) { + val lastExpression = left.right ?: return + val lastExpressionType = context.trace.getType(lastExpression) ?: return + + if (lastExpressionType.isResultType()) { + context.trace.reportDiagnosticOnce(Errors.RESULT_CLASS_WITH_NULLABLE_OPERATOR.on(reportOn, "?:")) + } + } + } + } + } +} \ No newline at end of file diff --git a/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/ResultClassInReturnTypeChecker.kt b/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/ResultClassInReturnTypeChecker.kt index df0779c4655..9c909b43bd6 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/ResultClassInReturnTypeChecker.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/ResultClassInReturnTypeChecker.kt @@ -15,7 +15,7 @@ import org.jetbrains.kotlin.types.KotlinType class ResultClassInReturnTypeChecker : DeclarationChecker { companion object { - private const val RESULT_NAME = "Result" + internal const val RESULT_NAME = "Result" } override fun check(declaration: KtDeclaration, descriptor: DeclarationDescriptor, context: DeclarationCheckerContext) { @@ -29,8 +29,7 @@ class ResultClassInReturnTypeChecker : DeclarationChecker { } private fun isForbiddenReturnType(returnType: KotlinType, declarationDescriptor: DeclarationDescriptor): Boolean { - val descriptor = returnType.constructor.declarationDescriptor ?: return false - if (!descriptor.isResultClass()) return false + if (!returnType.isResultType()) return false if (declarationDescriptor is PropertyDescriptor || declarationDescriptor is PropertyGetterDescriptor) { val visibility = (declarationDescriptor as DeclarationDescriptorWithVisibility).effectiveVisibility() @@ -46,11 +45,15 @@ class ResultClassInReturnTypeChecker : DeclarationChecker { return true } +} - private fun DeclarationDescriptor.isResultClass(): Boolean { - val container = containingDeclaration ?: return false - return container is PackageFragmentDescriptor && - container.fqName == KotlinBuiltIns.BUILT_INS_PACKAGE_FQ_NAME && - name.asString() == RESULT_NAME - } -} \ No newline at end of file +internal fun KotlinType.isResultType(): Boolean { + return this.constructor.declarationDescriptor?.isResultClass() == true +} + +private fun DeclarationDescriptor.isResultClass(): Boolean { + val container = containingDeclaration ?: return false + return container is PackageFragmentDescriptor && + container.fqName == KotlinBuiltIns.BUILT_INS_PACKAGE_FQ_NAME && + name.asString() == ResultClassInReturnTypeChecker.RESULT_NAME +} diff --git a/compiler/testData/diagnostics/testsWithStdLib/coroutines/usageOfResultTypeWithNullableOperators.kt b/compiler/testData/diagnostics/testsWithStdLib/coroutines/usageOfResultTypeWithNullableOperators.kt new file mode 100644 index 00000000000..703385d091b --- /dev/null +++ b/compiler/testData/diagnostics/testsWithStdLib/coroutines/usageOfResultTypeWithNullableOperators.kt @@ -0,0 +1,38 @@ +// !DIAGNOSTICS: -UNUSED_EXPRESSION + +fun id(x: T): T = x + +private val asFun: () -> Result? = TODO() +private val Int.intResult: Result? + get() = null + +fun returnInt(): Int? = 0 + + +fun nullableOperators(r1: Result?, b: Boolean) { + if (b) { + r1!! + asFun()!! + returnInt()?.intResult!!.toString() + } + + if (b) { + id(r1)!! + } + + if (b) { + r1?.toString() + returnInt()?.intResult?.toString() + asFun()?.toString() + id(r1)?.toString() + } + + if (b) { + r1 ?: 0 + r1 ?: r1 + asFun() ?: r1 ?: 0 + id(asFun()) ?: 0 + + returnInt() ?: returnInt() ?: asFun() ?: 0 + } +} diff --git a/compiler/testData/diagnostics/testsWithStdLib/coroutines/usageOfResultTypeWithNullableOperators.txt b/compiler/testData/diagnostics/testsWithStdLib/coroutines/usageOfResultTypeWithNullableOperators.txt new file mode 100644 index 00000000000..d6238bf130b --- /dev/null +++ b/compiler/testData/diagnostics/testsWithStdLib/coroutines/usageOfResultTypeWithNullableOperators.txt @@ -0,0 +1,7 @@ +package + +private val asFun: () -> kotlin.Result? +private val kotlin.Int.intResult: kotlin.Result? +public fun id(/*0*/ x: T): T +public fun nullableOperators(/*0*/ r1: kotlin.Result?, /*1*/ b: kotlin.Boolean): kotlin.Unit +public fun returnInt(): kotlin.Int? diff --git a/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestWithStdLibGenerated.java b/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestWithStdLibGenerated.java index 5f52ec6d1c3..819a12bb378 100644 --- a/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestWithStdLibGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestWithStdLibGenerated.java @@ -1663,6 +1663,11 @@ public class DiagnosticsTestWithStdLibGenerated extends AbstractDiagnosticsTestW runTest("compiler/testData/diagnostics/testsWithStdLib/coroutines/usageOfResultTypeInReturnType.kt"); } + @TestMetadata("usageOfResultTypeWithNullableOperators.kt") + public void testUsageOfResultTypeWithNullableOperators() throws Exception { + runTest("compiler/testData/diagnostics/testsWithStdLib/coroutines/usageOfResultTypeWithNullableOperators.kt"); + } + @TestMetadata("compiler/testData/diagnostics/testsWithStdLib/coroutines/callableReference") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class) diff --git a/compiler/tests/org/jetbrains/kotlin/checkers/javac/DiagnosticsTestWithStdLibUsingJavacGenerated.java b/compiler/tests/org/jetbrains/kotlin/checkers/javac/DiagnosticsTestWithStdLibUsingJavacGenerated.java index f3eabf132e6..337c3cc98ce 100644 --- a/compiler/tests/org/jetbrains/kotlin/checkers/javac/DiagnosticsTestWithStdLibUsingJavacGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/checkers/javac/DiagnosticsTestWithStdLibUsingJavacGenerated.java @@ -1663,6 +1663,11 @@ public class DiagnosticsTestWithStdLibUsingJavacGenerated extends AbstractDiagno runTest("compiler/testData/diagnostics/testsWithStdLib/coroutines/usageOfResultTypeInReturnType.kt"); } + @TestMetadata("usageOfResultTypeWithNullableOperators.kt") + public void testUsageOfResultTypeWithNullableOperators() throws Exception { + runTest("compiler/testData/diagnostics/testsWithStdLib/coroutines/usageOfResultTypeWithNullableOperators.kt"); + } + @TestMetadata("compiler/testData/diagnostics/testsWithStdLib/coroutines/callableReference") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class)