diff --git a/compiler/fir/analysis-tests/legacy-fir-tests/tests-gen/org/jetbrains/kotlin/fir/LazyBodyIsNotTouchedTilContractsPhaseTestGenerated.java b/compiler/fir/analysis-tests/legacy-fir-tests/tests-gen/org/jetbrains/kotlin/fir/LazyBodyIsNotTouchedTilContractsPhaseTestGenerated.java index 124824f3f63..aa2497319b0 100644 --- a/compiler/fir/analysis-tests/legacy-fir-tests/tests-gen/org/jetbrains/kotlin/fir/LazyBodyIsNotTouchedTilContractsPhaseTestGenerated.java +++ b/compiler/fir/analysis-tests/legacy-fir-tests/tests-gen/org/jetbrains/kotlin/fir/LazyBodyIsNotTouchedTilContractsPhaseTestGenerated.java @@ -980,6 +980,24 @@ public class LazyBodyIsNotTouchedTilContractsPhaseTestGenerated extends Abstract } } + @TestMetadata("compiler/fir/analysis-tests/testData/resolve/contracts") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class Contracts extends AbstractLazyBodyIsNotTouchedTilContractsPhaseTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTest, this, testDataFilePath); + } + + public void testAllFilesPresentInContracts() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("compiler/fir/analysis-tests/testData/resolve/contracts"), Pattern.compile("^([^.]+)\\.kt$"), null, true); + } + + @TestMetadata("genericContract.kt") + public void testGenericContract() throws Exception { + runTest("compiler/fir/analysis-tests/testData/resolve/contracts/genericContract.kt"); + } + } + @TestMetadata("compiler/fir/analysis-tests/testData/resolve/delegates") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class) diff --git a/compiler/fir/analysis-tests/testData/resolve/contracts/genericContract.fir.txt b/compiler/fir/analysis-tests/testData/resolve/contracts/genericContract.fir.txt new file mode 100644 index 00000000000..8aa1583b5b0 --- /dev/null +++ b/compiler/fir/analysis-tests/testData/resolve/contracts/genericContract.fir.txt @@ -0,0 +1,35 @@ +FILE: genericContract.kt + public final inline fun requreIsInstance(value: R|kotlin/Any|): R|kotlin/Unit| + [R|Contract description] + < + Returns(WILDCARD) -> value is T + > + { + when () { + (R|/value| !is R|T|) -> { + throw R|java/lang/IllegalArgumentException.IllegalArgumentException|() + } + } + + } + public final fun test_1(s: R|kotlin/Any|): R|kotlin/Unit| { + R|/requreIsInstance|(R|/s|) + R|/s|.R|kotlin/String.length| + } + public final inline fun requreIsInstanceOf(value: R|kotlin/Any|, requiredValue: R|T|): R|kotlin/Unit| + [R|Contract description] + < + Returns(WILDCARD) -> value is T + > + { + when () { + (R|/value| !is R|T|) -> { + throw R|java/lang/IllegalArgumentException.IllegalArgumentException|() + } + } + + } + public final fun test_2(x: R|kotlin/Any|, s: R|kotlin/String|): R|kotlin/Unit| { + R|/requreIsInstanceOf|(R|/x|, R|/s|) + R|/x|.R|kotlin/String.length| + } diff --git a/compiler/fir/analysis-tests/testData/resolve/contracts/genericContract.kt b/compiler/fir/analysis-tests/testData/resolve/contracts/genericContract.kt new file mode 100644 index 00000000000..ec5eb213873 --- /dev/null +++ b/compiler/fir/analysis-tests/testData/resolve/contracts/genericContract.kt @@ -0,0 +1,25 @@ +// WITH_STDLIB + +import kotlin.contracts.* + +inline fun requreIsInstance(value: Any) contract [ + returns() implies (value is T) +] { + if (value !is T) throw IllegalArgumentException() +} + +fun test_1(s: Any) { + requreIsInstance(s) + s.length +} + +inline fun requreIsInstanceOf(value: Any, requiredValue: T) contract [ + returns() implies (value is T) +] { + if (value !is T) throw IllegalArgumentException() +} + +fun test_2(x: Any, s: String) { + requreIsInstanceOf(x, s) + x.length +} diff --git a/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirDiagnosticTestGenerated.java b/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirDiagnosticTestGenerated.java index b2dd83f9227..fd77d8d27a9 100644 --- a/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirDiagnosticTestGenerated.java +++ b/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirDiagnosticTestGenerated.java @@ -1128,6 +1128,22 @@ public class FirDiagnosticTestGenerated extends AbstractFirDiagnosticTest { } } + @Nested + @TestMetadata("compiler/fir/analysis-tests/testData/resolve/contracts") + @TestDataPath("$PROJECT_ROOT") + public class Contracts { + @Test + public void testAllFilesPresentInContracts() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("compiler/fir/analysis-tests/testData/resolve/contracts"), Pattern.compile("^([^.]+)\\.kt$"), null, true); + } + + @Test + @TestMetadata("genericContract.kt") + public void testGenericContract() throws Exception { + runTest("compiler/fir/analysis-tests/testData/resolve/contracts/genericContract.kt"); + } + } + @Nested @TestMetadata("compiler/fir/analysis-tests/testData/resolve/delegates") @TestDataPath("$PROJECT_ROOT") diff --git a/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirDiagnosticsWithLightTreeTestGenerated.java b/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirDiagnosticsWithLightTreeTestGenerated.java index 810faaf301e..1072ba6c78a 100644 --- a/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirDiagnosticsWithLightTreeTestGenerated.java +++ b/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirDiagnosticsWithLightTreeTestGenerated.java @@ -1138,6 +1138,23 @@ public class FirDiagnosticsWithLightTreeTestGenerated extends AbstractFirDiagnos } } + @Nested + @TestMetadata("compiler/fir/analysis-tests/testData/resolve/contracts") + @TestDataPath("$PROJECT_ROOT") + @Execution(ExecutionMode.SAME_THREAD) + public class Contracts { + @Test + public void testAllFilesPresentInContracts() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("compiler/fir/analysis-tests/testData/resolve/contracts"), Pattern.compile("^([^.]+)\\.kt$"), null, true); + } + + @Test + @TestMetadata("genericContract.kt") + public void testGenericContract() throws Exception { + runTest("compiler/fir/analysis-tests/testData/resolve/contracts/genericContract.kt"); + } + } + @Nested @TestMetadata("compiler/fir/analysis-tests/testData/resolve/delegates") @TestDataPath("$PROJECT_ROOT") diff --git a/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/dfa/FirDataFlowAnalyzer.kt b/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/dfa/FirDataFlowAnalyzer.kt index 4f9bbf1790a..a2f7da22584 100644 --- a/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/dfa/FirDataFlowAnalyzer.kt +++ b/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/dfa/FirDataFlowAnalyzer.kt @@ -23,9 +23,12 @@ import org.jetbrains.kotlin.fir.resolve.dfa.cfg.* import org.jetbrains.kotlin.fir.resolve.dfa.contracts.buildContractFir import org.jetbrains.kotlin.fir.resolve.dfa.contracts.createArgumentsMapping import org.jetbrains.kotlin.fir.resolve.inference.inferenceComponents +import org.jetbrains.kotlin.fir.resolve.substitution.ConeSubstitutor +import org.jetbrains.kotlin.fir.resolve.substitution.ConeSubstitutorByMap import org.jetbrains.kotlin.fir.resolve.transformers.body.resolve.FirAbstractBodyResolveTransformer import org.jetbrains.kotlin.fir.resolve.transformers.body.resolve.resultType import org.jetbrains.kotlin.fir.symbols.AbstractFirBasedSymbol +import org.jetbrains.kotlin.fir.symbols.impl.FirTypeParameterSymbol import org.jetbrains.kotlin.name.CallableId import org.jetbrains.kotlin.name.StandardClassIds import org.jetbrains.kotlin.fir.symbols.impl.FirVariableSymbol @@ -865,12 +868,24 @@ abstract class FirDataFlowAnalyzer( val conditionalEffects = contractDescription.effects.map { it.effect }.filterIsInstance() if (conditionalEffects.isEmpty()) return val argumentsMapping = createArgumentsMapping(qualifiedAccess) ?: return + + val typeParameters = (owner as? FirTypeParameterRefsOwner)?.typeParameters + val substitutor = if (!typeParameters.isNullOrEmpty()) { + @Suppress("UNCHECKED_CAST") + val substitutionFromArguments = typeParameters.zip(qualifiedAccess.typeArguments).map { (typeParameterRef, typeArgument) -> + typeParameterRef.symbol to typeArgument.toConeTypeProjection().type + }.filter { it.second != null }.toMap() as Map + ConeSubstitutorByMap(substitutionFromArguments) + } else { + ConeSubstitutor.Empty + } + contractDescriptionVisitingMode = true graphBuilder.enterContract(qualifiedAccess).mergeIncomingFlow() val lastFlow = graphBuilder.lastNode.flow val functionCallVariable = variableStorage.getOrCreateVariable(lastFlow, qualifiedAccess) for (conditionalEffect in conditionalEffects) { - val fir = conditionalEffect.buildContractFir(argumentsMapping) ?: continue + val fir = conditionalEffect.buildContractFir(argumentsMapping, substitutor) ?: continue val effect = conditionalEffect.effect as? ConeReturnsEffectDeclaration ?: continue fir.transformSingle(components.transformer, ResolutionMode.ContextDependent) val argumentVariable = variableStorage.getOrCreateVariable(lastFlow, fir) diff --git a/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/dfa/contracts/ConeConditionalEffectToFirVisitor.kt b/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/dfa/contracts/ConeConditionalEffectToFirVisitor.kt index 57441d2661f..876abb7a493 100644 --- a/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/dfa/contracts/ConeConditionalEffectToFirVisitor.kt +++ b/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/dfa/contracts/ConeConditionalEffectToFirVisitor.kt @@ -11,16 +11,20 @@ import org.jetbrains.kotlin.fir.expressions.* import org.jetbrains.kotlin.fir.expressions.builder.* import org.jetbrains.kotlin.fir.expressions.impl.FirNoReceiverExpression import org.jetbrains.kotlin.fir.references.builder.buildSimpleNamedReference +import org.jetbrains.kotlin.fir.resolve.substitution.ConeSubstitutor import org.jetbrains.kotlin.fir.types.builder.buildResolvedTypeRef import org.jetbrains.kotlin.types.ConstantValueKind import org.jetbrains.kotlin.util.OperatorNameConventions -private object ConeConditionalEffectToFirVisitor : ConeContractDescriptionVisitor>() { - override fun visitConditionalEffectDeclaration(conditionalEffect: ConeConditionalEffectDeclaration, data: Map): FirExpression? { +private class ConeConditionalEffectToFirVisitor( + val valueParametersMapping: Map, + val substitutor: ConeSubstitutor +) : ConeContractDescriptionVisitor() { + override fun visitConditionalEffectDeclaration(conditionalEffect: ConeConditionalEffectDeclaration, data: Nothing?): FirExpression? { return conditionalEffect.condition.accept(this, data) } - override fun visitConstantDescriptor(constantReference: ConeConstantReference, data: Map): FirExpression? { + override fun visitConstantDescriptor(constantReference: ConeConstantReference, data: Nothing?): FirExpression? { return when (constantReference) { ConeBooleanConstantReference.TRUE -> buildConstExpression(null, ConstantValueKind.Boolean, true) ConeBooleanConstantReference.FALSE -> buildConstExpression(null, ConstantValueKind.Boolean, false) @@ -31,7 +35,7 @@ private object ConeConditionalEffectToFirVisitor : ConeContractDescriptionVisito override fun visitLogicalBinaryOperationContractExpression( binaryLogicExpression: ConeBinaryLogicExpression, - data: Map + data: Nothing? ): FirExpression? { val leftExpression = binaryLogicExpression.left.accept(this, data) ?: return null val rightExpression = binaryLogicExpression.right.accept(this, data) ?: return null @@ -42,7 +46,7 @@ private object ConeConditionalEffectToFirVisitor : ConeContractDescriptionVisito } } - override fun visitLogicalNot(logicalNot: ConeLogicalNot, data: Map): FirExpression? { + override fun visitLogicalNot(logicalNot: ConeLogicalNot, data: Nothing?): FirExpression? { val explicitReceiver = logicalNot.arg.accept(this, data) ?: return null return buildFunctionCall { calleeReference = buildSimpleNamedReference { name = OperatorNameConventions.NOT } @@ -50,7 +54,7 @@ private object ConeConditionalEffectToFirVisitor : ConeContractDescriptionVisito } } - override fun visitIsInstancePredicate(isInstancePredicate: ConeIsInstancePredicate, data: Map): FirExpression? { + override fun visitIsInstancePredicate(isInstancePredicate: ConeIsInstancePredicate, data: Nothing?): FirExpression? { val argument = isInstancePredicate.arg.accept(this@ConeConditionalEffectToFirVisitor, data) ?: return null return buildTypeOperatorCall { argumentList = buildUnaryArgumentList(argument) @@ -59,11 +63,11 @@ private object ConeConditionalEffectToFirVisitor : ConeContractDescriptionVisito } else { FirOperation.IS } - conversionTypeRef = buildResolvedTypeRef { type = isInstancePredicate.type } + conversionTypeRef = buildResolvedTypeRef { type = substitutor.substituteOrSelf(isInstancePredicate.type) } } } - override fun visitIsNullPredicate(isNullPredicate: ConeIsNullPredicate, data: Map): FirExpression? { + override fun visitIsNullPredicate(isNullPredicate: ConeIsNullPredicate, data: Nothing?): FirExpression? { val argument = isNullPredicate.arg.accept(this, data) ?: return null return buildEqualityOperatorCall { operation = if (isNullPredicate.isNegated) { @@ -75,15 +79,18 @@ private object ConeConditionalEffectToFirVisitor : ConeContractDescriptionVisito } } - override fun visitValueParameterReference(valueParameterReference: ConeValueParameterReference, data: Map): FirExpression? { - return data[valueParameterReference.parameterIndex] + override fun visitValueParameterReference(valueParameterReference: ConeValueParameterReference, data: Nothing?): FirExpression? { + return valueParametersMapping[valueParameterReference.parameterIndex] } private fun createConstNull(): FirConstExpression<*> = buildConstExpression(null, ConstantValueKind.Null, null) } -fun ConeConditionalEffectDeclaration.buildContractFir(argumentMapping: Map): FirExpression? { - return condition.accept(ConeConditionalEffectToFirVisitor, argumentMapping) +fun ConeConditionalEffectDeclaration.buildContractFir( + argumentMapping: Map, + substitutor: ConeSubstitutor +): FirExpression? { + return condition.accept(ConeConditionalEffectToFirVisitor(argumentMapping, substitutor), null) } fun createArgumentsMapping(qualifiedAccess: FirQualifiedAccess): Map? { @@ -106,4 +113,4 @@ fun createArgumentsMapping(qualifiedAccess: FirQualifiedAccess): Map cast(value: Any?): T { fun test_1(x: Any) { requireIsInstance(x) - x.length + x.length } fun test_2(x: Any) { val s: String = cast(x) - x.length + x.length }