[FIR] Extension point for function calls

This extension point allows changing the return type of call
 from a declared type to its subtype.
With an idea that such a plugin needs the ability to
control available (extension) properties for this type.
A set of properties should be decided based on the resolved function,
provided arguments, types, and resolved lambdas.
It is not a goal to enable plugins to affect resolution in any way.
Types are not intended to have any structured capabilities

In other words.
The goal was to come up with a way to enable a limited form
of an "extensible records" mechanism through plugins.
- It should give enough value for a practical use.
(more info in the ticket).
- It should have minimal effect on the compilation process
This appears to be an extension point that solves this problem.

^KT-65859 Fixed
This commit is contained in:
Nikita Klimenko
2024-02-09 21:10:56 +02:00
committed by Space Team
parent 4d04546652
commit c49edfef04
14 changed files with 225 additions and 6 deletions
@@ -499,6 +499,15 @@ internal val KT_DIAGNOSTIC_CONVERTER = KtDiagnosticConverterBuilder.buildConvert
token,
)
}
add(FirErrors.PLUGIN_AMBIGUOUS_INTERCEPTED_SYMBOL) { firDiagnostic ->
PluginAmbiguousInterceptedSymbolImpl(
firDiagnostic.a.map { string ->
string
},
firDiagnostic as KtPsiDiagnostic,
token,
)
}
add(FirErrors.RESOLUTION_TO_CLASSIFIER) { firDiagnostic ->
ResolutionToClassifierImpl(
firSymbolBuilder.classifierBuilder.buildClassLikeSymbol(firDiagnostic.a),
@@ -396,6 +396,11 @@ sealed interface KtFirDiagnostic<PSI : PsiElement> : KtDiagnosticWithPsi<PSI> {
val classSymbol: KtClassLikeSymbol
}
interface PluginAmbiguousInterceptedSymbol : KtFirDiagnostic<PsiElement> {
override val diagnosticClass get() = PluginAmbiguousInterceptedSymbol::class
val names: List<String>
}
interface ResolutionToClassifier : KtFirDiagnostic<PsiElement> {
override val diagnosticClass get() = ResolutionToClassifier::class
val classSymbol: KtClassLikeSymbol
@@ -459,6 +459,12 @@ internal class InnerClassConstructorNoReceiverImpl(
token: KtLifetimeToken,
) : KtAbstractFirDiagnostic<PsiElement>(firDiagnostic, token), KtFirDiagnostic.InnerClassConstructorNoReceiver
internal class PluginAmbiguousInterceptedSymbolImpl(
override val names: List<String>,
firDiagnostic: KtPsiDiagnostic,
token: KtLifetimeToken,
) : KtAbstractFirDiagnostic<PsiElement>(firDiagnostic, token), KtFirDiagnostic.PluginAmbiguousInterceptedSymbol
internal class ResolutionToClassifierImpl(
override val classSymbol: KtClassLikeSymbol,
firDiagnostic: KtPsiDiagnostic,
@@ -210,6 +210,9 @@ object DIAGNOSTICS_LIST : DiagnosticList("FirErrors") {
val INNER_CLASS_CONSTRUCTOR_NO_RECEIVER by error<PsiElement> {
parameter<FirRegularClassSymbol>("classSymbol")
}
val PLUGIN_AMBIGUOUS_INTERCEPTED_SYMBOL by error<PsiElement> {
parameter<List<String>>("names")
}
val RESOLUTION_TO_CLASSIFIER by error<PsiElement> {
parameter<FirRegularClassSymbol>("classSymbol")
}
@@ -200,6 +200,7 @@ object FirErrors {
val INTERFACE_AS_FUNCTION: KtDiagnosticFactory1<FirRegularClassSymbol> by error1<PsiElement, FirRegularClassSymbol>()
val EXPECT_CLASS_AS_FUNCTION: KtDiagnosticFactory1<FirRegularClassSymbol> by error1<PsiElement, FirRegularClassSymbol>()
val INNER_CLASS_CONSTRUCTOR_NO_RECEIVER: KtDiagnosticFactory1<FirRegularClassSymbol> by error1<PsiElement, FirRegularClassSymbol>()
val PLUGIN_AMBIGUOUS_INTERCEPTED_SYMBOL: KtDiagnosticFactory1<List<String>> by error1<PsiElement, List<String>>()
val RESOLUTION_TO_CLASSIFIER: KtDiagnosticFactory1<FirRegularClassSymbol> by error1<PsiElement, FirRegularClassSymbol>()
val AMBIGUOUS_ALTERED_ASSIGN: KtDiagnosticFactory1<List<String?>> by error1<PsiElement, List<String?>>()
val FORBIDDEN_BINARY_MOD: KtDiagnosticFactory2<FirBasedSymbol<*>, String> by error2<PsiElement, FirBasedSymbol<*>, String>(SourceElementPositioningStrategies.OPERATOR_MODIFIER)
@@ -64,6 +64,7 @@ val FIR_NON_SUPPRESSIBLE_ERROR_NAMES: Set<String> = setOf(
"INTERFACE_AS_FUNCTION",
"EXPECT_CLASS_AS_FUNCTION",
"INNER_CLASS_CONSTRUCTOR_NO_RECEIVER",
"PLUGIN_AMBIGUOUS_INTERCEPTED_SYMBOL",
"RESOLUTION_TO_CLASSIFIER",
"AMBIGUOUS_ALTERED_ASSIGN",
"FORBIDDEN_BINARY_MOD",
@@ -510,6 +510,7 @@ import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.OVERRIDING_FINAL_
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.PACKAGE_CANNOT_BE_IMPORTED
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.PACKAGE_OR_CLASSIFIER_REDECLARATION
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.PLATFORM_CLASS_MAPPED_TO_KOTLIN
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.PLUGIN_AMBIGUOUS_INTERCEPTED_SYMBOL
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.POTENTIALLY_NON_REPORTED_ANNOTATION
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.PRE_RELEASE_CLASS
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.PRIMARY_CONSTRUCTOR_DELEGATION_CALL_EXPECTED
@@ -882,6 +883,11 @@ object FirErrorsDefaultMessages : BaseDiagnosticRendererFactory() {
"Multiple extensions attempted to alter this assignment at the same time. Extensions: {0}",
COLLECTION(NULLABLE_STRING)
)
map.put(
PLUGIN_AMBIGUOUS_INTERCEPTED_SYMBOL,
"Multiple extensions attempted to modify this function call. Extensions: {0}",
COLLECTION(NULLABLE_STRING)
)
map.put(DEPRECATED_BINARY_MOD, "Convention for ''{0}'' is prohibited. Use ''{1}''.", SYMBOL, STRING)
map.put(FORBIDDEN_BINARY_MOD, "Deprecated convention for ''{0}''. Use ''{1}''.", SYMBOL, STRING)
@@ -397,6 +397,8 @@ private fun mapInapplicableCandidateError(
// Reported later
is TypeParameterAsExpression -> null
is AmbiguousInterceptedSymbol -> FirErrors.PLUGIN_AMBIGUOUS_INTERCEPTED_SYMBOL.createOn(source, rootCause.pluginNames)
else -> genericDiagnostic
}
}.distinct()
@@ -38,6 +38,8 @@ abstract class FirExtensionRegistrar : FirExtensionRegistrarAdapter() {
FirFunctionTypeKindExtension::class,
@OptIn(FirExtensionApiInternals::class)
FirMetadataSerializerPlugin::class,
@OptIn(FirExtensionApiInternals::class)
FirFunctionCallRefinementExtension::class,
)
internal val ALLOWED_EXTENSIONS_FOR_LIBRARY_SESSION = listOf(
@@ -122,6 +124,12 @@ abstract class FirExtensionRegistrar : FirExtensionRegistrarAdapter() {
registerExtension(FirMetadataSerializerPlugin::class, this)
}
@FirExtensionApiInternals
@JvmName("plusFunctionCallRefinementExtension")
operator fun (FirFunctionCallRefinementExtension.Factory).unaryPlus() {
registerExtension(FirFunctionCallRefinementExtension::class, this)
}
// ------------------ reference methods ------------------
@JvmName("plusStatusTransformerExtension")
@@ -195,6 +203,12 @@ abstract class FirExtensionRegistrar : FirExtensionRegistrarAdapter() {
FirMetadataSerializerPlugin.Factory { this.invoke(it) }.unaryPlus()
}
@FirExtensionApiInternals
@JvmName("plusFunctionCallRefinementExtension")
operator fun ((FirSession) -> FirFunctionCallRefinementExtension).unaryPlus() {
FirFunctionCallRefinementExtension.Factory { this.invoke(it) }.unaryPlus()
}
// ------------------ utilities ------------------
@JvmName("bindLeft")
@@ -0,0 +1,90 @@
/*
* Copyright 2010-2023 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.fir.extensions
import org.jetbrains.kotlin.fir.FirSession
import org.jetbrains.kotlin.fir.declarations.FirDeclaration
import org.jetbrains.kotlin.fir.declarations.FirDeclarationDataKey
import org.jetbrains.kotlin.fir.declarations.FirDeclarationDataRegistry
import org.jetbrains.kotlin.fir.expressions.FirFunctionCall
import org.jetbrains.kotlin.fir.resolve.calls.CallInfo
import org.jetbrains.kotlin.fir.symbols.impl.FirNamedFunctionSymbol
import org.jetbrains.kotlin.fir.types.FirResolvedTypeRef
import kotlin.reflect.KClass
/**
* This extension integrates with call resolution mechanism:
* resolution and completion of the receiver
* resolution of arguments
* resolution of the call itself
* - [intercept] is called
* resolution of the outer call
* completion of the call
* - [transform] is called
* completion of the outer call
*
* !!!! This extension is highly unstable and not recommended to use !!!!
*/
@FirExtensionApiInternals
abstract class FirFunctionCallRefinementExtension(session: FirSession) : FirExtension(session) {
companion object {
val NAME = FirExtensionPointName("FunctionCallRefinementExtension")
}
final override val name: FirExtensionPointName
get() = NAME
final override val extensionType: KClass<out FirExtension> = FirFunctionCallRefinementExtension::class
/**
* Allows a call to be completed with a more specific type than the declared return type of function
* ```
* interface Container<out T> { }
* fun Container<T>.add(item: String): Container<Any>
* ```
* at call site `Container<Any>` can be modified to become `Container<NewLocalType>`
* ```
* container.add("A")
* ```
* this `NewLocalType` can be created in [intercept]. It must be later saved into FIR tree in [transform]
* Generated declarations should be local because this [FirExtension] works at body resolve stage and thus cannot create new top level declarations
*
* When [intercept] returns non-null value, a copy will be created from FirFunction that [symbol]
* points to. Copy will be used in call completion instead of original function.
*
* @return null if plugin is not interested in a [symbol]
*/
abstract fun intercept(callInfo: CallInfo, symbol: FirNamedFunctionSymbol): CallReturnType?
/**
*
* Data can be associated with [FirNamedFunctionSymbol] in [callback]
*/
class CallReturnType(val typeRef: FirResolvedTypeRef, val callback: ((FirNamedFunctionSymbol) -> Unit)? = null)
/**
* @param call to a function that was created with modified [FirResolvedTypeRef] as a result of [intercept].
* This function doesn't exist in FIR, it is needed to complete the call.
* @param originalSymbol [intercept] is called with symbol to a declaration that exists somewhere in FIR: library, project code.
* The same symbol is [originalSymbol].
* [transform] needs to generate call to [let] with the same return type as [call]
* and put all generated declarations used in [FirResolvedTypeRef] in statements.
*/
abstract fun transform(call: FirFunctionCall, originalSymbol: FirNamedFunctionSymbol): FirFunctionCall
fun interface Factory : FirExtension.Factory<FirFunctionCallRefinementExtension>
}
@OptIn(FirExtensionApiInternals::class)
val FirExtensionService.callRefinementExtensions: List<FirFunctionCallRefinementExtension> by FirExtensionService.registeredExtensions()
@OptIn(FirExtensionApiInternals::class)
internal class OriginalCallData(val originalSymbol: FirNamedFunctionSymbol, val extension: FirFunctionCallRefinementExtension)
internal object OriginalCallDataKey : FirDeclarationDataKey()
internal var FirDeclaration.originalCallDataForPluginRefinedCall: OriginalCallData? by FirDeclarationDataRegistry.data(OriginalCallDataKey)
@@ -12,9 +12,13 @@ import org.jetbrains.kotlin.fir.declarations.FirDeclarationOrigin
import org.jetbrains.kotlin.fir.declarations.FirResolvePhase
import org.jetbrains.kotlin.fir.declarations.builder.buildErrorFunction
import org.jetbrains.kotlin.fir.declarations.builder.buildErrorProperty
import org.jetbrains.kotlin.fir.declarations.builder.buildSimpleFunctionCopy
import org.jetbrains.kotlin.fir.declarations.fullyExpandedClass
import org.jetbrains.kotlin.fir.diagnostics.ConeDiagnostic
import org.jetbrains.kotlin.fir.expressions.*
import org.jetbrains.kotlin.fir.extensions.*
import org.jetbrains.kotlin.fir.extensions.OriginalCallData
import org.jetbrains.kotlin.fir.extensions.originalCallDataForPluginRefinedCall
import org.jetbrains.kotlin.fir.resolve.isIntegerLiteralOrOperatorCall
import org.jetbrains.kotlin.fir.resolve.toFirRegularClass
import org.jetbrains.kotlin.fir.scopes.FirScope
@@ -31,6 +35,8 @@ class CandidateFactory private constructor(
val context: ResolutionContext,
private val baseSystem: ConstraintStorage
) {
@OptIn(FirExtensionApiInternals::class)
private val callRefinementExtensions = context.session.extensionService.callRefinementExtensions.takeIf { it.isNotEmpty() }
companion object {
private fun buildBaseSystem(context: ResolutionContext, callInfo: CallInfo): ConstraintStorage {
@@ -58,8 +64,21 @@ class CandidateFactory private constructor(
objectsByName: Boolean = false,
isFromOriginalTypeInPresenceOfSmartCast: Boolean = false,
): Candidate {
var pluginAmbiguity: AmbiguousInterceptedSymbol? = null
@Suppress("NAME_SHADOWING")
val symbol = symbol.unwrapIntegerOperatorSymbolIfNeeded(callInfo)
@OptIn(FirExtensionApiInternals::class)
val symbol = if (
callRefinementExtensions != null &&
callInfo.callKind == CallKind.Function &&
symbol is FirNamedFunctionSymbol
) {
val result = symbol.replaceFromPluginsIfNeeded(callRefinementExtensions, callInfo)
pluginAmbiguity = result.second
result.first
} else {
symbol.unwrapIntegerOperatorSymbolIfNeeded(callInfo)
}
val result = Candidate(
symbol,
@@ -81,6 +100,10 @@ class CandidateFactory private constructor(
context.bodyResolveContext.inferenceSession,
)
if (pluginAmbiguity != null) {
result.addDiagnostic(pluginAmbiguity)
}
// The counterpart in FE 1.0 checks if the given descriptor is VariableDescriptor yet not PropertyDescriptor.
// Here, we explicitly check if the referred declaration/symbol is value parameter, local variable, enum entry, or backing field.
val callSite = callInfo.callSite
@@ -122,6 +145,48 @@ class CandidateFactory private constructor(
return result
}
@OptIn(FirExtensionApiInternals::class)
private fun FirNamedFunctionSymbol.replaceFromPluginsIfNeeded(
callRefinementExtensions: List<FirFunctionCallRefinementExtension>,
callInfo: CallInfo,
): Pair<FirBasedSymbol<*>, AmbiguousInterceptedSymbol?> {
var pluginAmbiguity: AmbiguousInterceptedSymbol? = null
fun process(
result: FirFunctionCallRefinementExtension.CallReturnType,
extension: FirFunctionCallRefinementExtension
): FirNamedFunctionSymbol {
val newSymbol = FirNamedFunctionSymbol(callableId)
val function = buildSimpleFunctionCopy(fir) {
body = null
this.symbol = newSymbol
returnTypeRef = result.typeRef
}
function.originalCallDataForPluginRefinedCall = OriginalCallData(this, extension)
result.callback?.invoke(newSymbol)
return newSymbol
}
val variants = callRefinementExtensions.mapNotNull { extension ->
val result = extension.intercept(callInfo, this)
result?.let { it to extension }
}
val firBasedSymbol = when (variants.size) {
0 -> {
unwrapIntegerOperatorSymbolIfNeeded(callInfo)
}
1 -> {
val (result, extension) = variants[0]
process(result, extension)
}
else -> {
pluginAmbiguity =
AmbiguousInterceptedSymbol(variants.map { it.second::class.qualifiedName ?: it.second.javaClass.name })
unwrapIntegerOperatorSymbolIfNeeded(callInfo)
}
}
return Pair(firBasedSymbol, pluginAmbiguity)
}
private fun FirBasedSymbol<*>.isRegularClassWithoutCompanion(session: FirSession): Boolean {
val referencedClass = (this as? FirClassLikeSymbol<*>)?.fullyExpandedClass(session) ?: return false
return referencedClass.classKind != ClassKind.OBJECT && referencedClass.companionObjectSymbol == null
@@ -5,9 +5,11 @@
package org.jetbrains.kotlin.fir.resolve.inference
import org.jetbrains.kotlin.fir.extensions.originalCallDataForPluginRefinedCall
import org.jetbrains.kotlin.fir.resolve.ResolutionMode
import org.jetbrains.kotlin.fir.resolve.calls.Candidate
import org.jetbrains.kotlin.fir.types.ConeKotlinType
import org.jetbrains.kotlin.fir.types.toRegularClassSymbol
import org.jetbrains.kotlin.resolve.calls.inference.components.ConstraintSystemCompletionContext
import org.jetbrains.kotlin.resolve.calls.inference.components.ConstraintSystemCompletionMode
import org.jetbrains.kotlin.resolve.calls.inference.components.TrivialConstraintTypeInferenceOracle
@@ -33,6 +35,9 @@ fun Candidate.computeCompletionMode(
// Full if return type for call has no type variables
csBuilder.isProperType(currentReturnType) -> ConstraintSystemCompletionMode.FULL
// Plugins need fully complete calls. Calls that cannot be completed should not be modified, forcing completion will produce type inference error
currentReturnType.toRegularClassSymbol(components.session)?.fir?.originalCallDataForPluginRefinedCall != null -> ConstraintSystemCompletionMode.FULL
else -> CalculatorForNestedCall(
this, currentReturnType, csBuilder, components.trivialConstraintTypeInferenceOracle
).computeCompletionMode()
@@ -19,9 +19,7 @@ import org.jetbrains.kotlin.fir.expressions.FirOperation.*
import org.jetbrains.kotlin.fir.expressions.builder.*
import org.jetbrains.kotlin.fir.expressions.impl.FirResolvedArgumentList
import org.jetbrains.kotlin.fir.expressions.impl.toAnnotationArgumentMapping
import org.jetbrains.kotlin.fir.extensions.assignAltererExtensions
import org.jetbrains.kotlin.fir.extensions.expressionResolutionExtensions
import org.jetbrains.kotlin.fir.extensions.extensionService
import org.jetbrains.kotlin.fir.extensions.*
import org.jetbrains.kotlin.fir.references.*
import org.jetbrains.kotlin.fir.references.builder.buildErrorNamedReference
import org.jetbrains.kotlin.fir.references.builder.buildExplicitSuperReference
@@ -62,6 +60,8 @@ open class FirExpressionsResolveTransformer(transformer: FirAbstractBodyResolveT
private val expressionResolutionExtensions = session.extensionService.expressionResolutionExtensions.takeIf { it.isNotEmpty() }
private val assignAltererExtensions = session.extensionService.assignAltererExtensions.takeIf { it.isNotEmpty() }
@OptIn(FirExtensionApiInternals::class)
private val callRefinementExtensions = session.extensionService.callRefinementExtensions.takeIf { it.isNotEmpty() }
init {
@Suppress("LeakingThis")
@@ -462,10 +462,20 @@ open class FirExpressionsResolveTransformer(transformer: FirAbstractBodyResolveT
resultExpression, data,
skipEvenPartialCompletion = choosingOptionForAugmentedAssignment,
)
val result = completeInference.transformToIntegerOperatorCallOrApproximateItIfNeeded(data)
var result = completeInference.transformToIntegerOperatorCallOrApproximateItIfNeeded(data)
if (!choosingOptionForAugmentedAssignment) {
dataFlowAnalyzer.exitFunctionCall(result, data.forceFullCompletion)
}
@OptIn(FirExtensionApiInternals::class)
if (callRefinementExtensions != null) {
val reference = result.calleeReference
if (reference is FirResolvedNamedReference) {
val callData = reference.resolvedSymbol.fir.originalCallDataForPluginRefinedCall
if (callData != null) {
result = callData.extension.transform(result, callData.originalSymbol)
}
}
}
addReceiversFromExtensions(result)
@@ -165,4 +165,6 @@ class TypeVariableAsExplicitReceiver(
val typeParameter: FirTypeParameter,
) : ResolutionDiagnostic(RESOLVED_WITH_ERROR)
object CallToDeprecatedOverrideOfHidden : ResolutionDiagnostic(RESOLVED)
object CallToDeprecatedOverrideOfHidden : ResolutionDiagnostic(RESOLVED)
class AmbiguousInterceptedSymbol(val pluginNames: List<String>) : ResolutionDiagnostic(RESOLVED_WITH_ERROR)