FIR/Analysis API: Get parameter name from function type notation or

`@ParameterName` annotation, which is also now added during type
resolution.
This commit is contained in:
Mark Punzalan
2021-10-12 07:51:30 +00:00
committed by Dmitriy Novozhilov
parent afb68d15d6
commit 167dc81d3b
27 changed files with 282 additions and 86 deletions
@@ -6,29 +6,42 @@
package org.jetbrains.kotlin.analysis.api.fir.symbols
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.fir.declarations.FirResolvePhase
import org.jetbrains.kotlin.fir.declarations.FirValueParameter
import org.jetbrains.kotlin.fir.renderWithType
import org.jetbrains.kotlin.fir.types.arrayElementType
import org.jetbrains.kotlin.fir.types.coneType
import org.jetbrains.kotlin.analysis.api.fir.findPsi
import org.jetbrains.kotlin.analysis.low.level.api.fir.api.FirModuleResolveState
import org.jetbrains.kotlin.analysis.api.fir.KtSymbolByFirBuilder
import org.jetbrains.kotlin.analysis.api.fir.findPsi
import org.jetbrains.kotlin.analysis.api.fir.symbols.annotations.KtFirAnnotationCall
import org.jetbrains.kotlin.analysis.api.fir.symbols.annotations.containsAnnotation
import org.jetbrains.kotlin.analysis.api.fir.symbols.annotations.getAnnotationClassIds
import org.jetbrains.kotlin.analysis.api.fir.symbols.annotations.toAnnotationsList
import org.jetbrains.kotlin.analysis.api.fir.utils.cached
import org.jetbrains.kotlin.analysis.api.fir.utils.firRef
import org.jetbrains.kotlin.analysis.api.fir.utils.weakRef
import org.jetbrains.kotlin.analysis.api.symbols.KtValueParameterSymbol
import org.jetbrains.kotlin.analysis.api.symbols.markers.KtAnnotationCall
import org.jetbrains.kotlin.analysis.api.symbols.markers.KtTypeAndAnnotations
import org.jetbrains.kotlin.analysis.api.symbols.pointers.KtPsiBasedSymbolPointer
import org.jetbrains.kotlin.analysis.api.symbols.pointers.KtSymbolPointer
import org.jetbrains.kotlin.analysis.api.tokens.ValidityToken
import org.jetbrains.kotlin.analysis.low.level.api.fir.api.FirModuleResolveState
import org.jetbrains.kotlin.analysis.low.level.api.fir.api.getOrBuildFirOfType
import org.jetbrains.kotlin.analysis.low.level.api.fir.api.withFirDeclaration
import org.jetbrains.kotlin.builtins.StandardNames
import org.jetbrains.kotlin.fir.declarations.FirDeclaration
import org.jetbrains.kotlin.fir.declarations.FirResolvePhase
import org.jetbrains.kotlin.fir.declarations.FirValueParameter
import org.jetbrains.kotlin.fir.declarations.getAnnotationsByClassId
import org.jetbrains.kotlin.fir.expressions.FirAnnotation
import org.jetbrains.kotlin.fir.expressions.FirConstExpression
import org.jetbrains.kotlin.fir.expressions.unwrapArgument
import org.jetbrains.kotlin.fir.psi
import org.jetbrains.kotlin.fir.renderWithType
import org.jetbrains.kotlin.fir.types.arrayElementType
import org.jetbrains.kotlin.fir.types.coneType
import org.jetbrains.kotlin.fir.types.customAnnotations
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.name.StandardClassIds
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
internal class KtFirValueParameterSymbol(
fir: FirValueParameter,
@@ -39,7 +52,45 @@ internal class KtFirValueParameterSymbol(
override val firRef = firRef(fir, resolveState)
override val psi: PsiElement? by firRef.withFirAndCache { fir -> fir.findPsi(fir.moduleData.session) }
override val name: Name get() = firRef.withFir { it.name }
override val name: Name by cached {
firRef.withFir { fir ->
val defaultName = fir.name
if (fir.psi != null) return@withFir defaultName
// The case where PSI is null is when calling `invoke()` on a variable with functional type, e.g. `x(1)` below:
//
// fun foo(x: (item: Int) -> Unit) { x(1) }
// fun bar(x: Function1<@ParameterName("item") Int, Unit>) { x(1) }
//
// The function being called is `invoke(p1: Int)` on `Function1<Int, Unit>` which is from the stdlib, and therefore no source
// PSI for the function or its parameters. In that case, we use the `@ParameterName` annotation on the parameter type if present
// and fall back to the parameter names from the `invoke()` function (`p1`, `p2`, etc.).
//
// Note: During type resolution, `@ParameterName` type annotations are added based on the names (which are optional) in the
// function type notation. Therefore the `x` parameter in both example functions above have the same type and type annotations.
fir.withFirDeclaration(resolveState, FirResolvePhase.TYPES) {
val parameterNameAnnotation =
fir.returnTypeRef.coneType.attributes.customAnnotations
.getAnnotationsByClassId(StandardNames.FqNames.parameterNameClassId)
.singleOrNull()?.safeAs<FirAnnotation>() ?: return@withFirDeclaration defaultName
// The parent KtDeclaration is where the variable with functional type and `@ParameterName` annotation is declared.
val parentKtDeclaration =
parameterNameAnnotation.psi?.getNonStrictParentOfType<KtDeclaration>() ?: return@withFirDeclaration defaultName
val parentFirDeclaration = parentKtDeclaration.getOrBuildFirOfType<FirDeclaration>(resolveState)
// Resolve to ARGUMENTS_OF_ANNOTATIONS phase to get `name` argument from mapping.
val parameterNameFromAnnotation =
parentFirDeclaration.withFirDeclaration(resolveState, FirResolvePhase.ARGUMENTS_OF_ANNOTATIONS) {
val nameArgument =
parameterNameAnnotation.argumentMapping.mapping[StandardClassIds.Annotations.ParameterNames.parameterNameName]
nameArgument?.unwrapArgument()?.safeAs<FirConstExpression<*>>()?.value as? String
}
parameterNameFromAnnotation?.let { Name.identifier(it) } ?: defaultName
}
}
}
override val isVararg: Boolean get() = firRef.withFir { it.isVararg }
override val annotatedType: KtTypeAndAnnotations by firRef.withFirAndCache(FirResolvePhase.TYPES) { fir ->
if (fir.isVararg) {
@@ -91,6 +91,7 @@ private fun KtCall.stringRepresentation(): String {
append(": ${annotatedType.type.render()}")
}
is KtValueParameterSymbol -> "${if (isVararg) "vararg " else ""}$name: ${annotatedType.type.render()}"
is KtVariableSymbol -> "${if (isVal) "val" else "var"} $name: ${annotatedType.type.render()}"
is KtSuccessCallTarget -> symbol.stringValue()
is KtErrorCallTarget -> "ERR<${this.diagnostic.defaultMessage}, [${candidates.joinToString { it.stringValue() }}]>"
is Boolean -> toString()
@@ -402,6 +402,42 @@ public class ResolveCallTestGenerated extends AbstractResolveCallTest {
runTest("analysis/analysis-api/testData/analysisSession/resolveCall/variableAsFunctionLikeCall.kt");
}
@Test
@TestMetadata("variableAsFunctionWithParameterName.kt")
public void testVariableAsFunctionWithParameterName() throws Exception {
runTest("analysis/analysis-api/testData/analysisSession/resolveCall/variableAsFunctionWithParameterName.kt");
}
@Test
@TestMetadata("variableAsFunctionWithParameterNameAnnotation.kt")
public void testVariableAsFunctionWithParameterNameAnnotation() throws Exception {
runTest("analysis/analysis-api/testData/analysisSession/resolveCall/variableAsFunctionWithParameterNameAnnotation.kt");
}
@Test
@TestMetadata("variableAsFunctionWithParameterNameAnnotationConflict.kt")
public void testVariableAsFunctionWithParameterNameAnnotationConflict() throws Exception {
runTest("analysis/analysis-api/testData/analysisSession/resolveCall/variableAsFunctionWithParameterNameAnnotationConflict.kt");
}
@Test
@TestMetadata("variableAsFunctionWithParameterNameGeneric.kt")
public void testVariableAsFunctionWithParameterNameGeneric() throws Exception {
runTest("analysis/analysis-api/testData/analysisSession/resolveCall/variableAsFunctionWithParameterNameGeneric.kt");
}
@Test
@TestMetadata("variableAsFunctionWithParameterNameInNonFunctionType.kt")
public void testVariableAsFunctionWithParameterNameInNonFunctionType() throws Exception {
runTest("analysis/analysis-api/testData/analysisSession/resolveCall/variableAsFunctionWithParameterNameInNonFunctionType.kt");
}
@Test
@TestMetadata("variableAsFunctionWithParameterNameMixed.kt")
public void testVariableAsFunctionWithParameterNameMixed() throws Exception {
runTest("analysis/analysis-api/testData/analysisSession/resolveCall/variableAsFunctionWithParameterNameMixed.kt");
}
@Test
@TestMetadata("variableWithExtensionInvoke.kt")
public void testVariableWithExtensionInvoke() throws Exception {
@@ -67,12 +67,13 @@ public sealed class KtDeclaredFunctionCall : KtCall() {
* fun Int.invoke() {}
*/
public class KtVariableWithInvokeFunctionCall(
public val target: KtVariableLikeSymbol,
private val _target: KtVariableLikeSymbol,
private val _argumentMapping: LinkedHashMap<KtExpression, KtValueParameterSymbol>,
private val _targetFunction: KtCallTarget,
private val _substitutor: KtSubstitutor,
override val token: ValidityToken
) : KtDeclaredFunctionCall() {
public val target: KtVariableLikeSymbol get() = withValidityAssertion { _target }
override val argumentMapping: LinkedHashMap<KtExpression, KtValueParameterSymbol>
get() = withValidityAssertion { _argumentMapping }
override val targetFunction: KtCallTarget
@@ -138,4 +138,15 @@ public abstract class KtValueParameterSymbol : KtVariableLikeSymbol(), KtSymbolW
public abstract val isVararg: Boolean
abstract override fun createPointer(): KtSymbolPointer<KtValueParameterSymbol>
/**
* The name of the value parameter. For a parameter of `FunctionN.invoke()` functions, the name is taken from the function type
* notation, if a name is present. For example:
* ```
* fun foo(x: (item: Int, String) -> Unit) =
* x(1, "") // or `x.invoke(1, "")`
* ```
* The names of the value parameters for `invoke()` are "item" and "p2" (its default parameter name).
*/
abstract override val name: Name
}
@@ -0,0 +1,3 @@
fun call(x: (a: Int, b: String) -> Unit) {
<expr>x(1, "")</expr>
}
@@ -0,0 +1,5 @@
KtFunctionalTypeVariableCall:
target = x: kotlin.Function2<@R|kotlin.ParameterName|(name = String(a)) kotlin.Int, @R|kotlin.ParameterName|(name = String(b)) kotlin.String, kotlin.Unit>
argumentMapping = { 1 -> (a: @R|kotlin.ParameterName|(name = String(a)) kotlin.Int), "" -> (b: @R|kotlin.ParameterName|(name = String(b)) kotlin.String) }
targetFunction = kotlin/Function2.invoke(<dispatch receiver>: kotlin.Function2<@R|kotlin.ParameterName|(name = String(a)) kotlin.Int, @R|kotlin.ParameterName|(name = String(b)) kotlin.String, kotlin.Unit>, a: @R|kotlin.ParameterName|(name = String(a)) kotlin.Int, b: @R|kotlin.ParameterName|(name = String(b)) kotlin.String): kotlin.Unit
substitutor = <empty substitutor>
@@ -0,0 +1,3 @@
fun call(x: Function2<@ParameterName("a") Int, @ParameterName("b") String, Unit>) {
<expr>x(1, "")</expr>
}
@@ -0,0 +1,5 @@
KtFunctionalTypeVariableCall:
target = x: kotlin.Function2<@R|kotlin.ParameterName|(name = String(a)) kotlin.Int, @R|kotlin.ParameterName|(name = String(b)) kotlin.String, kotlin.Unit>
argumentMapping = { 1 -> (a: @R|kotlin.ParameterName|(name = String(a)) kotlin.Int), "" -> (b: @R|kotlin.ParameterName|(name = String(b)) kotlin.String) }
targetFunction = kotlin/Function2.invoke(<dispatch receiver>: kotlin.Function2<@R|kotlin.ParameterName|(name = String(a)) kotlin.Int, @R|kotlin.ParameterName|(name = String(b)) kotlin.String, kotlin.Unit>, a: @R|kotlin.ParameterName|(name = String(a)) kotlin.Int, b: @R|kotlin.ParameterName|(name = String(b)) kotlin.String): kotlin.Unit
substitutor = <empty substitutor>
@@ -0,0 +1,4 @@
// @ParameterName annotation takes precedence over name in function type parameter
fun call(x: (notMe: @ParameterName("a") Int, meNeither: @ParameterName("b") String) -> Unit) {
<expr>x(1, "")</expr>
}
@@ -0,0 +1,5 @@
KtFunctionalTypeVariableCall:
target = x: kotlin.Function2<@R|kotlin.ParameterName|(name = String(a)) kotlin.Int, @R|kotlin.ParameterName|(name = String(b)) kotlin.String, kotlin.Unit>
argumentMapping = { 1 -> (a: @R|kotlin.ParameterName|(name = String(a)) kotlin.Int), "" -> (b: @R|kotlin.ParameterName|(name = String(b)) kotlin.String) }
targetFunction = kotlin/Function2.invoke(<dispatch receiver>: kotlin.Function2<@R|kotlin.ParameterName|(name = String(a)) kotlin.Int, @R|kotlin.ParameterName|(name = String(b)) kotlin.String, kotlin.Unit>, a: @R|kotlin.ParameterName|(name = String(a)) kotlin.Int, b: @R|kotlin.ParameterName|(name = String(b)) kotlin.String): kotlin.Unit
substitutor = <empty substitutor>
@@ -0,0 +1,6 @@
fun <T> T.foo(): (a: T) -> Unit = TODO()
fun call() {
val x = 123.foo()
<expr>x(1)</expr>
}
@@ -0,0 +1,5 @@
KtFunctionalTypeVariableCall:
target = val x: kotlin.Function1<@R|kotlin.ParameterName|(name = String(a)) kotlin.Int, kotlin.Unit>
argumentMapping = { 1 -> (a: @R|kotlin.ParameterName|(name = String(a)) kotlin.Int) }
targetFunction = kotlin/Function1.invoke(<dispatch receiver>: kotlin.Function1<@R|kotlin.ParameterName|(name = String(a)) kotlin.Int, kotlin.Unit>, a: @R|kotlin.ParameterName|(name = String(a)) kotlin.Int): kotlin.Unit
substitutor = <empty substitutor>
@@ -0,0 +1,4 @@
// @ParameterName annotation is ignored when not used in function type
fun call(a: @ParameterName("notMe") Int, b: @ParameterName("meNeither") String) {
<expr>call(1, "")</expr>
}
@@ -0,0 +1,4 @@
KtFunctionCall:
argumentMapping = { 1 -> (a: @R|kotlin.ParameterName|(name = String(notMe)) kotlin.Int), "" -> (b: @R|kotlin.ParameterName|(name = String(meNeither)) kotlin.String) }
targetFunction = /call(a: @R|kotlin.ParameterName|(name = String(notMe)) kotlin.Int, b: @R|kotlin.ParameterName|(name = String(meNeither)) kotlin.String): kotlin.Unit
substitutor = <empty substitutor>
@@ -0,0 +1,3 @@
fun call(x: (a: Int, String) -> Unit) {
<expr>x(1, "")</expr>
}
@@ -0,0 +1,5 @@
KtFunctionalTypeVariableCall:
target = x: kotlin.Function2<@R|kotlin.ParameterName|(name = String(a)) kotlin.Int, kotlin.String, kotlin.Unit>
argumentMapping = { 1 -> (a: @R|kotlin.ParameterName|(name = String(a)) kotlin.Int), "" -> (p2: kotlin.String) }
targetFunction = kotlin/Function2.invoke(<dispatch receiver>: kotlin.Function2<@R|kotlin.ParameterName|(name = String(a)) kotlin.Int, kotlin.String, kotlin.Unit>, a: @R|kotlin.ParameterName|(name = String(a)) kotlin.Int, p2: kotlin.String): kotlin.Unit
substitutor = <empty substitutor>