[Assign plugin] Add a compiler plugin for overloading assign ('=') operator

This commit is contained in:
Anže Sodja
2022-09-01 07:51:38 +02:00
committed by Dmitriy Novozhilov
parent 6052962f50
commit 09d6dfc8bf
62 changed files with 2822 additions and 6 deletions
@@ -0,0 +1,23 @@
description = "Kotlin Assignment Compiler Plugin (K1)"
plugins {
kotlin("jvm")
id("jps-compatible")
}
dependencies {
implementation(project(":kotlin-assignment-compiler-plugin.common"))
compileOnly(project(":compiler:frontend"))
compileOnly(project(":compiler:frontend.java"))
compileOnly(intellijCore())
}
sourceSets {
"main" { projectDefault() }
"test" { projectDefault() }
}
runtimeJar()
sourcesJar()
javadocJar()
@@ -0,0 +1,101 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.assignment.plugin
import org.jetbrains.kotlin.cfg.getElementParentDeclaration
import org.jetbrains.kotlin.assignment.plugin.AssignmentPluginNames.ASSIGN_METHOD
import org.jetbrains.kotlin.assignment.plugin.diagnostics.ErrorsAssignmentPlugin.CALL_ERROR_ASSIGN_METHOD_SHOULD_RETURN_UNIT
import org.jetbrains.kotlin.assignment.plugin.diagnostics.ErrorsAssignmentPlugin.NO_APPLICABLE_ASSIGN_METHOD
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
import org.jetbrains.kotlin.descriptors.VariableDescriptor
import org.jetbrains.kotlin.extensions.internal.InternalNonStableExtensionPoints
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.BindingContextUtils
import org.jetbrains.kotlin.resolve.calls.CallResolver
import org.jetbrains.kotlin.resolve.calls.context.TemporaryTraceAndCache
import org.jetbrains.kotlin.resolve.calls.results.OverloadResolutionResults
import org.jetbrains.kotlin.resolve.calls.results.OverloadResolutionResultsUtil
import org.jetbrains.kotlin.resolve.calls.util.CallMaker.makeCallWithExpressions
import org.jetbrains.kotlin.resolve.extensions.AssignResolutionAltererExtension
import org.jetbrains.kotlin.resolve.scopes.LexicalWritableScope
import org.jetbrains.kotlin.resolve.scopes.receivers.ExpressionReceiver
import org.jetbrains.kotlin.resolve.scopes.receivers.ExpressionReceiver.Companion.create
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.expressions.ExpressionTypingComponents
import org.jetbrains.kotlin.types.expressions.ExpressionTypingContext
import org.jetbrains.kotlin.types.expressions.KotlinTypeInfo
import org.jetbrains.kotlin.types.typeUtil.isUnit
import org.jetbrains.kotlin.types.typeUtil.makeNotNullable
import java.util.*
class CliAssignPluginResolutionAltererExtension(
private val annotations: List<String>
) : AbstractAssignPluginResolutionAltererExtension() {
override fun getAnnotationFqNames(modifierListOwner: KtModifierListOwner?): List<String> = annotations
}
@OptIn(InternalNonStableExtensionPoints::class)
abstract class AbstractAssignPluginResolutionAltererExtension : AssignResolutionAltererExtension {
override fun needOverloadAssign(expression: KtBinaryExpression, leftType: KotlinType?, bindingContext: BindingContext): Boolean {
return expression.isValPropertyAssignment(bindingContext) && leftType.hasSpecialAnnotation(expression)
}
private fun KtBinaryExpression.isValPropertyAssignment(bindingContext: BindingContext): Boolean {
val descriptor: VariableDescriptor? = BindingContextUtils.extractVariableFromResolvedCall(bindingContext, this.left)
return descriptor is PropertyDescriptor && !descriptor.isVar
}
private fun KotlinType?.hasSpecialAnnotation(expression: KtBinaryExpression): Boolean =
this?.constructor?.declarationDescriptor?.hasSpecialAnnotation(expression.getElementParentDeclaration()) ?: false
override fun resolveAssign(
bindingContext: BindingContext,
expression: KtBinaryExpression,
leftOperand: KtExpression,
left: KtExpression,
leftInfo: KotlinTypeInfo,
context: ExpressionTypingContext,
components: ExpressionTypingComponents,
scope: LexicalWritableScope
): KotlinTypeInfo? {
var leftType: KotlinType = leftInfo.type!!
if (leftOperand is KtSafeQualifiedExpression) {
leftType = leftType.makeNotNullable()
}
val receiver = create(left, leftType, context.trace.bindingContext)
val operationSign: KtSimpleNameExpression = expression.operationReference
val temporaryForAssignmentOperation: TemporaryTraceAndCache =
TemporaryTraceAndCache.create(context, "trace to check assignment operation like '=' for", expression)
val temporaryContext = context.replaceTraceAndCache(temporaryForAssignmentOperation).replaceScope(scope)
val methodDescriptors: OverloadResolutionResults<FunctionDescriptor> =
components.callResolver.resolveMethodCall(temporaryContext, receiver, expression)
val methodReturnType: KotlinType? = OverloadResolutionResultsUtil.getResultingType(methodDescriptors, context)
if (methodDescriptors.isSuccess && methodReturnType != null) {
temporaryForAssignmentOperation.commit()
return if (methodReturnType.isUnit()) {
leftInfo.replaceType(methodReturnType)
} else {
context.trace.report(CALL_ERROR_ASSIGN_METHOD_SHOULD_RETURN_UNIT.on(operationSign))
null
}
}
context.trace.report(NO_APPLICABLE_ASSIGN_METHOD.on(operationSign))
return null
}
private fun CallResolver.resolveMethodCall(
context: ExpressionTypingContext, receiver: ExpressionReceiver, binaryExpression: KtBinaryExpression
): OverloadResolutionResults<FunctionDescriptor> {
val call = makeCallWithExpressions(
binaryExpression, receiver, null, binaryExpression.operationReference, Collections.singletonList(binaryExpression.right)
)
return resolveCallWithGivenName(context, call, binaryExpression.operationReference, ASSIGN_METHOD)
}
}
@@ -0,0 +1,66 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.assignment.plugin.diagnostics
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.assignment.plugin.AssignmentPluginNames.ASSIGN_METHOD
import org.jetbrains.kotlin.assignment.plugin.diagnostics.ErrorsAssignmentPlugin.DECLARATION_ERROR_ASSIGN_METHOD_SHOULD_RETURN_UNIT
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.descriptors.SimpleFunctionDescriptor
import org.jetbrains.kotlin.diagnostics.DiagnosticSink
import org.jetbrains.kotlin.extensions.AnnotationBasedExtension
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtFunction
import org.jetbrains.kotlin.psi.KtModifierListOwner
import org.jetbrains.kotlin.resolve.checkers.DeclarationChecker
import org.jetbrains.kotlin.resolve.checkers.DeclarationCheckerContext
import org.jetbrains.kotlin.resolve.descriptorUtil.isExtension
class AssignmentPluginDeclarationChecker(private val annotations: List<String>) : DeclarationChecker {
private val annotationMatchingService = AnnotationMatchingService(annotations)
override fun check(declaration: KtDeclaration, descriptor: DeclarationDescriptor, context: DeclarationCheckerContext) {
if (descriptor is SimpleFunctionDescriptor) {
if (!descriptor.isAssignMethod()) return
val receiverClass = if (descriptor.isExtension) {
descriptor.extensionReceiverParameter?.type?.constructor?.declarationDescriptor as? ClassDescriptor
} else {
descriptor.containingDeclaration as? ClassDescriptor
}
val ktFunction = declaration as? KtFunction
if (receiverClass != null && ktFunction != null) {
checkAssignMethod(descriptor, receiverClass, ktFunction, context.trace)
}
}
}
private fun checkAssignMethod(
method: SimpleFunctionDescriptor,
receiverClass: ClassDescriptor,
declaration: KtFunction,
diagnosticHolder: DiagnosticSink
) {
if (!annotationMatchingService.isAnnotated(receiverClass)) {
return
}
if (method.returnType?.let { KotlinBuiltIns.isUnit(it) } != true) {
diagnosticHolder.report(DECLARATION_ERROR_ASSIGN_METHOD_SHOULD_RETURN_UNIT.on(declaration))
}
}
private fun SimpleFunctionDescriptor.isAssignMethod(): Boolean {
return valueParameters.size == 1 && name == ASSIGN_METHOD
}
private class AnnotationMatchingService(val annotations: List<String>) : AnnotationBasedExtension {
override fun getAnnotationFqNames(modifierListOwner: KtModifierListOwner?): List<String> = annotations
fun isAnnotated(descriptor: ClassDescriptor): Boolean = descriptor.hasSpecialAnnotation(null)
}
}
@@ -0,0 +1,34 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.assignment.plugin.diagnostics
import org.jetbrains.kotlin.assignment.plugin.diagnostics.ErrorsAssignmentPlugin.*
import org.jetbrains.kotlin.diagnostics.rendering.DefaultErrorMessages
import org.jetbrains.kotlin.diagnostics.rendering.DiagnosticFactoryToRendererMap
object DefaultErrorMessagesAssignmentPlugin : DefaultErrorMessages.Extension {
private val MAP = DiagnosticFactoryToRendererMap("ValueContainerAssignment")
override fun getMap() = MAP
init {
MAP.put(
DECLARATION_ERROR_ASSIGN_METHOD_SHOULD_RETURN_UNIT,
"Function 'assign' used for '=' overload should return 'Unit'"
)
MAP.put(
CALL_ERROR_ASSIGN_METHOD_SHOULD_RETURN_UNIT,
"Function 'assign' used for '=' overload should return 'Unit'"
)
MAP.put(
NO_APPLICABLE_ASSIGN_METHOD,
"No applicable 'assign' function found for '=' overload"
)
}
}
@@ -0,0 +1,36 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.assignment.plugin.diagnostics;
import org.jetbrains.kotlin.diagnostics.DiagnosticFactory0;
import org.jetbrains.kotlin.diagnostics.Errors;
import org.jetbrains.kotlin.diagnostics.PositioningStrategies;
import org.jetbrains.kotlin.diagnostics.Severity;
import org.jetbrains.kotlin.psi.KtDeclaration;
import org.jetbrains.kotlin.psi.KtSimpleNameExpression;
import static org.jetbrains.kotlin.diagnostics.PositioningStrategies.DECLARATION_RETURN_TYPE;
import static org.jetbrains.kotlin.diagnostics.Severity.ERROR;
public interface ErrorsAssignmentPlugin {
DiagnosticFactory0<KtDeclaration> DECLARATION_ERROR_ASSIGN_METHOD_SHOULD_RETURN_UNIT =
DiagnosticFactory0.create(Severity.ERROR, DECLARATION_RETURN_TYPE);
DiagnosticFactory0<KtSimpleNameExpression> CALL_ERROR_ASSIGN_METHOD_SHOULD_RETURN_UNIT =
DiagnosticFactory0.create(ERROR, PositioningStrategies.CALL_EXPRESSION);
DiagnosticFactory0<KtSimpleNameExpression> NO_APPLICABLE_ASSIGN_METHOD =
DiagnosticFactory0.create(ERROR, PositioningStrategies.CALL_EXPRESSION);
@SuppressWarnings("unused")
Object _initializer = new Object() {
{
Errors.Initializer.initializeFactoryNamesAndDefaultErrorMessages(
ErrorsAssignmentPlugin.class,
DefaultErrorMessagesAssignmentPlugin.INSTANCE
);
}
};
}