From a46ccdbae1dc91876ca75027394d8e07c4a7f3d4 Mon Sep 17 00:00:00 2001 From: Dmitry Savvinov Date: Tue, 3 Oct 2017 15:02:31 +0300 Subject: [PATCH] Effects: add parsing of contracts from PSI - Introduce part of effect system responsible for parsing contracts which were expressed in sources using DSL from stdlib - Add new errors to Errors.java related to contracts and corresponding messages. ========== Effect System introduction: 5/18 --- .../parsing/ContractParsingServices.kt | 80 +++++++++++++ .../contracts/parsing/PsiConditionParser.kt | 111 +++++++++++++++++ .../contracts/parsing/PsiConstantParser.kt | 46 +++++++ .../parsing/PsiContractParserDispatcher.kt | 112 ++++++++++++++++++ .../contracts/parsing/PsiContractsUtils.kt | 92 ++++++++++++++ .../contracts/parsing/PsiEffectParser.kt | 27 +++++ .../parsing/effects/PsiCallsEffectParser.kt | 67 +++++++++++ .../effects/PsiConditionalEffectParser.kt | 47 ++++++++ .../parsing/effects/PsiReturnsEffectParser.kt | 56 +++++++++ .../jetbrains/kotlin/diagnostics/Errors.java | 4 + .../rendering/DefaultErrorMessages.java | 3 + 11 files changed, 645 insertions(+) create mode 100644 compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/ContractParsingServices.kt create mode 100644 compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/PsiConditionParser.kt create mode 100644 compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/PsiConstantParser.kt create mode 100644 compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/PsiContractParserDispatcher.kt create mode 100644 compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/PsiContractsUtils.kt create mode 100644 compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/PsiEffectParser.kt create mode 100644 compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/effects/PsiCallsEffectParser.kt create mode 100644 compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/effects/PsiConditionalEffectParser.kt create mode 100644 compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/effects/PsiReturnsEffectParser.kt diff --git a/compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/ContractParsingServices.kt b/compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/ContractParsingServices.kt new file mode 100644 index 00000000000..f1e5574f4e2 --- /dev/null +++ b/compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/ContractParsingServices.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.contracts.parsing + +import org.jetbrains.kotlin.config.AnalysisFlag +import org.jetbrains.kotlin.config.LanguageFeature +import org.jetbrains.kotlin.config.LanguageVersionSettings +import org.jetbrains.kotlin.contracts.description.ContractDescription +import org.jetbrains.kotlin.contracts.description.ContractProviderKey +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.diagnostics.Errors +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.BindingTrace +import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall +import org.jetbrains.kotlin.resolve.scopes.LexicalScope +import org.jetbrains.kotlin.resolve.scopes.LexicalScopeKind + +class ContractParsingServices(val languageVersionSettings: LanguageVersionSettings) { + fun fastCheckIfContractPresent(element: KtElement): Boolean { + if (!isContractAllowedHere(element)) return false + val firstExpression = ((element as? KtFunction)?.bodyExpression as? KtBlockExpression)?.statements?.firstOrNull() ?: return false + return isContractDescriptionCallFastCheck(firstExpression) + } + + fun checkContractAndRecordIfPresent(expression: KtExpression, trace: BindingTrace, scope: LexicalScope, isFirstStatement: Boolean) { + val ownerDescriptor = scope.ownerDescriptor + if (!isContractDescriptionCallFastCheck(expression) || ownerDescriptor !is FunctionDescriptor) return + val contractProvider = ownerDescriptor.getUserData(ContractProviderKey) ?: return + + val isFeatureTurnedOn = languageVersionSettings.supportsFeature(LanguageFeature.CallsInPlaceEffect) || + languageVersionSettings.supportsFeature(LanguageFeature.ReturnsEffect) + + val contractDescriptor = when { + !isFeatureTurnedOn || !isContractDescriptionCallPreciseCheck(expression, trace.bindingContext) -> null + + !isContractAllowedHere(scope) || !isFirstStatement -> { + trace.report(Errors.CONTRACT_NOT_ALLOWED.on(expression)) + null + } + + else -> parseContract(expression, trace, ownerDescriptor) + } + + contractProvider.setContractDescription(contractDescriptor) + } + + private fun parseContract(expression: KtExpression?, trace: BindingTrace, ownerDescriptor: FunctionDescriptor): ContractDescription? = + PsiContractParserDispatcher(trace, this).parseContract(expression, ownerDescriptor) + + internal fun isContractDescriptionCall(expression: KtExpression, context: BindingContext): Boolean = + isContractDescriptionCallFastCheck(expression) && isContractDescriptionCallPreciseCheck(expression, context) + + private fun isContractAllowedHere(element: KtElement): Boolean = + element is KtNamedFunction && element.isTopLevel && element.hasBlockBody() && !element.hasModifier(KtTokens.OPERATOR_KEYWORD) + + private fun isContractAllowedHere(scope: LexicalScope): Boolean = + scope.kind == LexicalScopeKind.CODE_BLOCK && (scope.parent as? LexicalScope)?.kind == LexicalScopeKind.FUNCTION_INNER_SCOPE + + private fun isContractDescriptionCallFastCheck(expression: KtExpression): Boolean = + expression is KtCallExpression && expression.calleeExpression?.text == "contract" + + private fun isContractDescriptionCallPreciseCheck(expression: KtExpression, context: BindingContext): Boolean = + expression.getResolvedCall(context)?.resultingDescriptor?.isContractCallDescriptor() ?: false +} \ No newline at end of file diff --git a/compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/PsiConditionParser.kt b/compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/PsiConditionParser.kt new file mode 100644 index 00000000000..750f23f57ff --- /dev/null +++ b/compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/PsiConditionParser.kt @@ -0,0 +1,111 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.contracts.parsing + +import org.jetbrains.kotlin.contracts.description.* +import org.jetbrains.kotlin.contracts.description.expressions.* +import org.jetbrains.kotlin.descriptors.ValueDescriptor +import org.jetbrains.kotlin.diagnostics.Errors +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.BindingTrace +import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall +import org.jetbrains.kotlin.resolve.scopes.receivers.ExpressionReceiver + +internal class PsiConditionParser(val trace: BindingTrace, val dispatcher: PsiContractParserDispatcher) : KtVisitor() { + override fun visitIsExpression(expression: KtIsExpression, data: Unit): BooleanExpression? { + val variable = dispatcher.parseVariable(expression.leftHandSide) ?: return null + val typeReference = expression.typeReference ?: return null + val type = trace[BindingContext.TYPE, typeReference] ?: return null + + return IsInstancePredicate(variable, type, expression.isNegated) + } + + override fun visitKtElement(element: KtElement, data: Unit): BooleanExpression? { + val resolvedCall = element.getResolvedCall(trace.bindingContext) + val descriptor = resolvedCall?.resultingDescriptor ?: return null + + // boolean variable + if (descriptor is ValueDescriptor) { + val booleanVariable = dispatcher.parseVariable(element as? KtExpression) ?: return null + // we don't report type mismatch because it will be reported by the typechecker + return booleanVariable as? BooleanVariableReference + } + + // operator + when { + descriptor.isEqualsDescriptor() -> { + val left = dispatcher.parseValue((resolvedCall.dispatchReceiver as? ExpressionReceiver)?.expression) ?: return null + val right = dispatcher.parseValue(resolvedCall.firstArgumentAsExpressionOrNull()) ?: return null + val isNegated = (element as? KtBinaryExpression)?.operationToken == KtTokens.EXCLEQ ?: false + + if (left is ConstantReference && left == ConstantReference.NULL && right is VariableReference) { + return IsNullPredicate(right, isNegated) + } + + if (right is ConstantReference && right == ConstantReference.NULL && left is VariableReference) { + return IsNullPredicate(left, isNegated) + } + + trace.report(Errors.ERROR_IN_CONTRACT_DESCRIPTION.on(element, "only equality comparisons with 'null' allowed")) + return null + } + + else -> { + trace.report(Errors.ERROR_IN_CONTRACT_DESCRIPTION.on(element, "unsupported construction")) + return null + } + } + } + + override fun visitConstantExpression(expression: KtConstantExpression, data: Unit?): BooleanExpression? { + // we don't report type mismatch because it will be reported by the typechecker + return dispatcher.parseConstant(expression) as? BooleanConstantReference + } + + override fun visitCallExpression(expression: KtCallExpression, data: Unit?): BooleanExpression? { + trace.report(Errors.ERROR_IN_CONTRACT_DESCRIPTION.on(expression, "call-expressions are not supported yet")) + return null + } + + override fun visitBinaryExpression(expression: KtBinaryExpression, data: Unit): BooleanExpression? { + val operationConstructor: (BooleanExpression, BooleanExpression) -> BooleanExpression + + when (expression.operationToken) { + KtTokens.ANDAND -> operationConstructor = ::LogicalAnd + KtTokens.OROR -> operationConstructor = ::LogicalOr + else -> return super.visitBinaryExpression(expression, data) // pass binary expression further + } + + val left = expression.left?.accept(this, data) ?: return null + val right = expression.right?.accept(this, data) ?: return null + return operationConstructor(left, right) + } + + override fun visitUnaryExpression(expression: KtUnaryExpression, data: Unit): BooleanExpression? { + if (expression.operationToken != KtTokens.EXCL) return super.visitUnaryExpression(expression, data) + val arg = expression.baseExpression?.accept(this, data) ?: return null + if (arg !is ContractDescriptionValue) { + trace.report(Errors.ERROR_IN_CONTRACT_DESCRIPTION.on(expression.baseExpression!!, "negations in contract description can be applied only to variables/values")) + } + return LogicalNot(arg) + } + + override fun visitParenthesizedExpression(expression: KtParenthesizedExpression, data: Unit): BooleanExpression? = + KtPsiUtil.deparenthesize(expression)?.accept(this, data) +} \ No newline at end of file diff --git a/compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/PsiConstantParser.kt b/compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/PsiConstantParser.kt new file mode 100644 index 00000000000..0b95c480c28 --- /dev/null +++ b/compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/PsiConstantParser.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.contracts.parsing + +import org.jetbrains.kotlin.contracts.description.expressions.BooleanConstantReference +import org.jetbrains.kotlin.contracts.description.expressions.ConstantReference +import org.jetbrains.kotlin.psi.KtConstantExpression +import org.jetbrains.kotlin.psi.KtElement +import org.jetbrains.kotlin.psi.KtVisitor +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.BindingTrace +import org.jetbrains.kotlin.resolve.constants.CompileTimeConstant +import org.jetbrains.kotlin.types.KotlinType + +internal class PsiConstantParser(val trace: BindingTrace) : KtVisitor() { + override fun visitKtElement(element: KtElement, data: Unit?): ConstantReference? = null + + override fun visitConstantExpression(expression: KtConstantExpression, data: Unit?): ConstantReference? { + val type: KotlinType = trace.getType(expression) ?: return null + + val compileTimeConstant: CompileTimeConstant<*> + = trace.get(BindingContext.COMPILE_TIME_VALUE, expression) ?: return null + val value: Any? = compileTimeConstant.getValue(type) + + return when (value) { + true -> BooleanConstantReference.TRUE + false -> BooleanConstantReference.FALSE + null -> ConstantReference.NULL + else -> null + } + } +} \ No newline at end of file diff --git a/compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/PsiContractParserDispatcher.kt b/compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/PsiContractParserDispatcher.kt new file mode 100644 index 00000000000..d979375cedb --- /dev/null +++ b/compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/PsiContractParserDispatcher.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.contracts.parsing + +import org.jetbrains.kotlin.builtins.KotlinBuiltIns +import org.jetbrains.kotlin.contracts.description.BooleanExpression +import org.jetbrains.kotlin.contracts.description.ContractDescription +import org.jetbrains.kotlin.contracts.description.EffectDeclaration +import org.jetbrains.kotlin.contracts.description.expressions.BooleanVariableReference +import org.jetbrains.kotlin.contracts.description.expressions.ConstantReference +import org.jetbrains.kotlin.contracts.description.expressions.ContractDescriptionValue +import org.jetbrains.kotlin.contracts.description.expressions.VariableReference +import org.jetbrains.kotlin.contracts.parsing.ContractsDslNames.CALLS_IN_PLACE_EFFECT +import org.jetbrains.kotlin.contracts.parsing.ContractsDslNames.CONDITIONAL_EFFECT +import org.jetbrains.kotlin.contracts.parsing.ContractsDslNames.RETURNS_EFFECT +import org.jetbrains.kotlin.contracts.parsing.ContractsDslNames.RETURNS_NOT_NULL_EFFECT +import org.jetbrains.kotlin.contracts.parsing.effects.PsiCallsEffectParser +import org.jetbrains.kotlin.contracts.parsing.effects.PsiConditionalEffectParser +import org.jetbrains.kotlin.contracts.parsing.effects.PsiReturnsEffectParser +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.descriptors.ParameterDescriptor +import org.jetbrains.kotlin.descriptors.ReceiverParameterDescriptor +import org.jetbrains.kotlin.diagnostics.Errors +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.KtExpression +import org.jetbrains.kotlin.psi.KtLambdaExpression +import org.jetbrains.kotlin.resolve.BindingTrace +import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall +import org.jetbrains.kotlin.resolve.calls.callUtil.getType + +internal class PsiContractParserDispatcher(val trace: BindingTrace, val contractParsingServices: ContractParsingServices) { + private val conditionParser = PsiConditionParser(trace, this) + private val constantParser = PsiConstantParser(trace) + private val effectsParsers: Map = mapOf( + RETURNS_EFFECT to PsiReturnsEffectParser(trace, this), + RETURNS_NOT_NULL_EFFECT to PsiReturnsEffectParser(trace, this), + CALLS_IN_PLACE_EFFECT to PsiCallsEffectParser(trace, this), + CONDITIONAL_EFFECT to PsiConditionalEffectParser(trace, this) + ) + + fun parseContract(expression: KtExpression?, ownerDescriptor: FunctionDescriptor): ContractDescription? { + if (expression == null) return null + if (!contractParsingServices.isContractDescriptionCall(expression, trace.bindingContext)) return null + + val resolvedCall = expression.getResolvedCall(trace.bindingContext)!! // Must be non-null due to 'isContractDescriptionCall' check + + val lambda = resolvedCall.firstArgumentAsExpressionOrNull() as? KtLambdaExpression ?: return null + + val effects = lambda.bodyExpression?.statements?.mapNotNull { parseEffect(it) } ?: return null + + if (effects.isEmpty()) return null + + return ContractDescription(effects, ownerDescriptor) + } + + fun parseCondition(expression: KtExpression?): BooleanExpression? = expression?.accept(conditionParser, Unit) + + fun parseEffect(expression: KtExpression?): EffectDeclaration? { + if (expression == null) return null + val returnType = expression.getType(trace.bindingContext) ?: return null + val parser = effectsParsers[returnType.constructor.declarationDescriptor?.name] + if (parser == null) { + trace.report(Errors.ERROR_IN_CONTRACT_DESCRIPTION.on(expression, "Unrecognized effect")) + return null + } + return parser.tryParseEffect(expression) + } + + fun parseConstant(expression: KtExpression?): ConstantReference? { + if (expression == null) return null + return expression.accept(constantParser, Unit) + } + + fun parseVariable(expression: KtExpression?): VariableReference? { + if (expression == null) return null + val descriptor = expression.getResolvedCall(trace.bindingContext)?.resultingDescriptor ?: return null + if (descriptor !is ParameterDescriptor) { + trace.report(Errors.ERROR_IN_CONTRACT_DESCRIPTION.on(expression, "only references to parameters are allowed in contract description")) + return null + } + + if (descriptor is ReceiverParameterDescriptor && descriptor.type.constructor.declarationDescriptor?.isFromContractDsl() == true) { + trace.report(Errors.ERROR_IN_CONTRACT_DESCRIPTION.on(expression, "only references to parameters are allowed. Did you miss label on ?")) + } + + return if (KotlinBuiltIns.isBoolean(descriptor.type)) + BooleanVariableReference(descriptor) + else + VariableReference(descriptor) + } + + fun parseValue(expression: KtExpression?): ContractDescriptionValue? { + val variable = parseVariable(expression) + if (variable != null) return variable + + return parseConstant(expression) + } +} \ No newline at end of file diff --git a/compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/PsiContractsUtils.kt b/compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/PsiContractsUtils.kt new file mode 100644 index 00000000000..f15de4823a8 --- /dev/null +++ b/compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/PsiContractsUtils.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.contracts.parsing + +import org.jetbrains.kotlin.descriptors.CallableDescriptor +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.contracts.parsing.ContractsDslNames.CALLS_IN_PLACE +import org.jetbrains.kotlin.contracts.parsing.ContractsDslNames.CONTRACT +import org.jetbrains.kotlin.contracts.parsing.ContractsDslNames.CONTRACTS_DSL_ANNOTATION_FQN +import org.jetbrains.kotlin.contracts.parsing.ContractsDslNames.EFFECT +import org.jetbrains.kotlin.contracts.parsing.ContractsDslNames.IMPLIES +import org.jetbrains.kotlin.contracts.parsing.ContractsDslNames.INVOCATION_KIND_ENUM +import org.jetbrains.kotlin.contracts.parsing.ContractsDslNames.RETURNS +import org.jetbrains.kotlin.contracts.parsing.ContractsDslNames.RETURNS_NOT_NULL +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.KtExpression +import org.jetbrains.kotlin.resolve.calls.model.ExpressionValueArgument +import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall +import org.jetbrains.kotlin.types.typeUtil.isBoolean +import org.jetbrains.kotlin.types.typeUtil.isNullableAny +import org.jetbrains.kotlin.utils.addToStdlib.safeAs + + +object ContractsDslNames { + // Internal marker-annotation for distinguishing our API + val CONTRACTS_DSL_ANNOTATION_FQN = FqName("kotlin.internal.ContractsDsl") + + // Types + val EFFECT = Name.identifier("Effect") + val CONDITIONAL_EFFECT = Name.identifier("ConditionalEffect") + val SIMPLE_EFFECT = Name.identifier("SimpleEffect") + val RETURNS_EFFECT = Name.identifier("Returns") + val RETURNS_NOT_NULL_EFFECT = Name.identifier("ReturnsNotNull") + val CALLS_IN_PLACE_EFFECT = Name.identifier("CallsInPlace") + + // Structure-defining calls + val CONTRACT = Name.identifier("contract") + val IMPLIES = Name.identifier("implies") + + // Effect-declaration calls + val RETURNS = Name.identifier("returns") + val RETURNS_NOT_NULL = Name.identifier("returnsNotNull") + val CALLS_IN_PLACE = Name.identifier("callsInPlace") + + // enum class InvocationKind + val INVOCATION_KIND_ENUM = Name.identifier("InvocationKind") + val EXACTLY_ONCE_KIND = Name.identifier("EXACTLY_ONCE") + val AT_LEAST_ONCE_KIND = Name.identifier("AT_LEAST_ONCE") + val UNKNOWN_KIND = Name.identifier("UNKNOWN") + val AT_MOST_ONCE_KIND = Name.identifier("AT_MOST_ONCE") +} + +fun DeclarationDescriptor.isFromContractDsl(): Boolean = this.annotations.hasAnnotation(CONTRACTS_DSL_ANNOTATION_FQN) + +fun DeclarationDescriptor.isContractCallDescriptor(): Boolean = equalsDslDescriptor(CONTRACT) + +fun DeclarationDescriptor.isImpliesCallDescriptor(): Boolean = equalsDslDescriptor(IMPLIES) + +fun DeclarationDescriptor.isReturnsEffectDescriptor(): Boolean = equalsDslDescriptor(RETURNS) + +fun DeclarationDescriptor.isReturnsNotNullDescriptor(): Boolean = equalsDslDescriptor(RETURNS_NOT_NULL) + +fun DeclarationDescriptor.isEffectDescriptor(): Boolean = equalsDslDescriptor(EFFECT) + +fun DeclarationDescriptor.isCallsInPlaceEffectDescriptor(): Boolean = equalsDslDescriptor(CALLS_IN_PLACE) + +fun DeclarationDescriptor.isInvocationKindEnum(): Boolean = equalsDslDescriptor(INVOCATION_KIND_ENUM) + +fun DeclarationDescriptor.isEqualsDescriptor(): Boolean = + this is FunctionDescriptor && this.name == Name.identifier("equals") && // fast checks + this.returnType?.isBoolean() == true && this.valueParameters.singleOrNull()?.type?.isNullableAny() == true // signature matches + +internal fun ResolvedCall<*>.firstArgumentAsExpressionOrNull(): KtExpression? = + this.valueArgumentsByIndex?.firstOrNull()?.safeAs()?.valueArgument?.getArgumentExpression() + +private fun DeclarationDescriptor.equalsDslDescriptor(dslName: Name): Boolean = this.name == dslName && this.isFromContractDsl() \ No newline at end of file diff --git a/compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/PsiEffectParser.kt b/compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/PsiEffectParser.kt new file mode 100644 index 00000000000..8661521f3ee --- /dev/null +++ b/compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/PsiEffectParser.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.contracts.parsing + +import org.jetbrains.kotlin.contracts.description.EffectDeclaration +import org.jetbrains.kotlin.psi.KtExpression +import org.jetbrains.kotlin.resolve.BindingTrace + +internal interface PsiEffectParser { + fun tryParseEffect(expression: KtExpression): EffectDeclaration? +} + +internal abstract class AbstractPsiEffectParser(val trace: BindingTrace, val contractParserDispatcher: PsiContractParserDispatcher) : PsiEffectParser diff --git a/compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/effects/PsiCallsEffectParser.kt b/compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/effects/PsiCallsEffectParser.kt new file mode 100644 index 00000000000..06813e46737 --- /dev/null +++ b/compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/effects/PsiCallsEffectParser.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.contracts.parsing.effects + +import org.jetbrains.kotlin.contracts.description.CallsEffectDeclaration +import org.jetbrains.kotlin.contracts.description.EffectDeclaration +import org.jetbrains.kotlin.contracts.description.InvocationKind +import org.jetbrains.kotlin.contracts.parsing.* +import org.jetbrains.kotlin.psi.KtExpression +import org.jetbrains.kotlin.resolve.BindingTrace +import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall +import org.jetbrains.kotlin.resolve.calls.model.DefaultValueArgument +import org.jetbrains.kotlin.resolve.calls.model.ExpressionValueArgument +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe +import org.jetbrains.kotlin.resolve.descriptorUtil.parents + +internal class PsiCallsEffectParser( + trace: BindingTrace, + contractParserDispatcher: PsiContractParserDispatcher +) : AbstractPsiEffectParser(trace, contractParserDispatcher) { + + override fun tryParseEffect(expression: KtExpression): EffectDeclaration? { + val resolvedCall = expression.getResolvedCall(trace.bindingContext) ?: return null + val descriptor = resolvedCall.resultingDescriptor + + if (!descriptor.isCallsInPlaceEffectDescriptor()) return null + + val lambda = contractParserDispatcher.parseVariable(resolvedCall.firstArgumentAsExpressionOrNull()) ?: return null + + val kindArgument = resolvedCall.valueArgumentsByIndex?.getOrNull(1) + + val kind = when (kindArgument) { + is DefaultValueArgument -> InvocationKind.UNKNOWN + is ExpressionValueArgument -> kindArgument.valueArgument?.getArgumentExpression()?.toInvocationKind(trace) ?: return null + else -> return null + } + + return CallsEffectDeclaration(lambda, kind) + } + + private fun KtExpression.toInvocationKind(trace: BindingTrace): InvocationKind? { + val descriptor = this.getResolvedCall(trace.bindingContext)?.resultingDescriptor ?: return null + if (!descriptor.parents.first().isInvocationKindEnum()) return null + + return when (descriptor.fqNameSafe.shortName()) { + ContractsDslNames.AT_MOST_ONCE_KIND -> InvocationKind.AT_MOST_ONCE + ContractsDslNames.EXACTLY_ONCE_KIND -> InvocationKind.EXACTLY_ONCE + ContractsDslNames.AT_LEAST_ONCE_KIND -> InvocationKind.AT_LEAST_ONCE + ContractsDslNames.UNKNOWN_KIND -> InvocationKind.UNKNOWN + else -> null + } + } +} \ No newline at end of file diff --git a/compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/effects/PsiConditionalEffectParser.kt b/compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/effects/PsiConditionalEffectParser.kt new file mode 100644 index 00000000000..f0856d95921 --- /dev/null +++ b/compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/effects/PsiConditionalEffectParser.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.contracts.parsing.effects + +import org.jetbrains.kotlin.contracts.description.ConditionalEffectDeclaration +import org.jetbrains.kotlin.contracts.description.EffectDeclaration +import org.jetbrains.kotlin.contracts.parsing.AbstractPsiEffectParser +import org.jetbrains.kotlin.contracts.parsing.PsiContractParserDispatcher +import org.jetbrains.kotlin.contracts.parsing.firstArgumentAsExpressionOrNull +import org.jetbrains.kotlin.contracts.parsing.isImpliesCallDescriptor +import org.jetbrains.kotlin.psi.KtExpression +import org.jetbrains.kotlin.resolve.BindingTrace +import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall +import org.jetbrains.kotlin.resolve.scopes.receivers.ExpressionReceiver +import org.jetbrains.kotlin.utils.addToStdlib.safeAs + +internal class PsiConditionalEffectParser( + trace: BindingTrace, + dispatcher: PsiContractParserDispatcher +) : AbstractPsiEffectParser(trace, dispatcher) { + override fun tryParseEffect(expression: KtExpression): EffectDeclaration? { + val resolvedCall = expression.getResolvedCall(trace.bindingContext) ?: return null + + if (!resolvedCall.resultingDescriptor.isImpliesCallDescriptor()) return null + + val effect = contractParserDispatcher.parseEffect(resolvedCall.dispatchReceiver.safeAs()?.expression) + ?: return null + val condition = contractParserDispatcher.parseCondition(resolvedCall.firstArgumentAsExpressionOrNull()) + ?: return null + + return ConditionalEffectDeclaration(effect, condition) + } +} \ No newline at end of file diff --git a/compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/effects/PsiReturnsEffectParser.kt b/compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/effects/PsiReturnsEffectParser.kt new file mode 100644 index 00000000000..5066acf883a --- /dev/null +++ b/compiler/frontend/src/org/jetbrains/kotlin/contracts/parsing/effects/PsiReturnsEffectParser.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.contracts.parsing.effects + +import org.jetbrains.kotlin.contracts.description.EffectDeclaration +import org.jetbrains.kotlin.contracts.description.ReturnsEffectDeclaration +import org.jetbrains.kotlin.contracts.description.expressions.ConstantReference +import org.jetbrains.kotlin.contracts.parsing.* +import org.jetbrains.kotlin.diagnostics.Errors +import org.jetbrains.kotlin.psi.KtExpression +import org.jetbrains.kotlin.resolve.BindingTrace +import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall + +internal class PsiReturnsEffectParser( + trace: BindingTrace, + contractParserDispatcher: PsiContractParserDispatcher +) : AbstractPsiEffectParser(trace, contractParserDispatcher) { + override fun tryParseEffect(expression: KtExpression): EffectDeclaration? { + val resolvedCall = expression.getResolvedCall(trace.bindingContext) ?: return null + val descriptor = resolvedCall.resultingDescriptor + + if (descriptor.isReturnsNotNullDescriptor()) + return ReturnsEffectDeclaration(ConstantReference.NOT_NULL) + + if (!descriptor.isReturnsEffectDescriptor()) return null + + val argumentExpression = resolvedCall.firstArgumentAsExpressionOrNull() + val constantValue = if (argumentExpression == null) + ConstantReference.WILDCARD + else { + // Note that we distinguish absence of an argument and unparsed argument + val constant = contractParserDispatcher.parseConstant(argumentExpression) + if (constant == null) { + trace.report(Errors.ERROR_IN_CONTRACT_DESCRIPTION.on(argumentExpression, "only true/false/null constants in Returns-effect are currently supported")) + return null + } + constant + } + + return ReturnsEffectDeclaration(constantValue) + } +} \ No newline at end of file diff --git a/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/Errors.java b/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/Errors.java index 04b7846ba98..766ce6556d6 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/Errors.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/Errors.java @@ -983,6 +983,10 @@ public interface Errors { DiagnosticFactory2 PLUGIN_WARNING = DiagnosticFactory2.create(WARNING); DiagnosticFactory2 PLUGIN_INFO = DiagnosticFactory2.create(INFO); + // Function contracts + DiagnosticFactory1 ERROR_IN_CONTRACT_DESCRIPTION = DiagnosticFactory1.create(ERROR); + DiagnosticFactory0 CONTRACT_NOT_ALLOWED = DiagnosticFactory0.create(ERROR); + // Error sets ImmutableSet> UNRESOLVED_REFERENCE_DIAGNOSTICS = ImmutableSet.of( UNRESOLVED_REFERENCE, NAMED_PARAMETER_NOT_FOUND, UNRESOLVED_REFERENCE_WRONG_RECEIVER); 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 75635897722..fced175bf38 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/rendering/DefaultErrorMessages.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/rendering/DefaultErrorMessages.java @@ -889,6 +889,9 @@ public class DefaultErrorMessages { MAP.put(PLUGIN_WARNING, "{0}: {1}", TO_STRING, TO_STRING); MAP.put(PLUGIN_INFO, "{0}: {1}", TO_STRING, TO_STRING); + MAP.put(ERROR_IN_CONTRACT_DESCRIPTION, "Error in contract description: {0}", TO_STRING); + MAP.put(CONTRACT_NOT_ALLOWED, "Contract is not allowed here"); + MAP.setImmutable(); for (Field field : Errors.class.getFields()) {