https://kotlinlang.org logo
Title
m

MerlinTHS

03/06/2023, 3:03 PM
Hi. I experimented a bit with K2s
FirExpressionResolutionExtension
. I mainly "misused" it for some code modifications becuase implicit bodies are resolved at this phase. When I try to build code for throwing an ( Java ) exception ( let's say java.lang.Exception ), it works as long as I only use the noarg constructor.
context (FirSession)
fun buildExceptionThrow(): FirThrowExpression = buildThrowExpression {
    val exceptionId = ClassId.fromString("java/lang/Exception")
    val constructor = exceptionId.noArgConstructor

    exception = buildFunctionCall {
        typeRef = buildResolvedTypeRef {
            type = exceptionId.createConeType(this@FirSession)
        }
        calleeReference = buildResolvedNamedReference {
            name = constructor.name
            resolvedSymbol = constructor
        }
    }
}

context (FirSession)
val ClassId.noArgConstructor: FirConstructorSymbol
    get() = symbolProvider
        .getClassDeclaredConstructors(this)
        .firstOrNull {
            it.valueParameterSymbols.isEmpty()
        } ?: throw IllegalStateException("Noarg constructor not found!")
Now I want to use the message constructor ( org.jetbrains.kotlin.fir.java.declarations.FirJavaConstructor@68cb8e52: public constructor(p0: java.lang.String): R|java/lang/Exception| ).
@OptIn(SymbolInternals::class)
val FirConstructorSymbol.takesSingleString: Boolean
    get() = with(valueParameterSymbols) {
        (size == 1) && first().fir.returnTypeRef.coneType.isString
    }
Filtering the parameters is kind of tricky because simply calling the
first().resolvedReturnType.isString
on an
FirJavaTypeRef
would immediately result in an
ClassCastException
(
java.lang.ClassCastException: class org.jetbrains.kotlin.fir.types.jvm.FirJavaTypeRef cannot be cast to class org.jetbrains.kotlin.fir.types.FirResolvedTypeRef (org.jetbrains.kotlin.fir.types.jvm.FirJavaTypeRef and org.jetbrains.kotlin.fir.types.FirResolvedTypeRef are in unnamed module of loader 'app'
). Maybe someone knows a better way to check the types than having to access symbol internals.
context (FirSession)
fun buildExceptionThrow(
    message: String
): FirThrowExpression = buildThrowExpression {
    val exceptionId = ClassId.fromString("java/lang/Exception")
    val constructor = exceptionId.singleStringConstructor

    exception = buildFunctionCall {
        typeRef = buildResolvedTypeRef {
            type = exceptionId.createConeType(this@FirSession)
        }
        calleeReference = buildResolvedNamedReference {
            name = constructor.name
            resolvedSymbol = constructor
        }
        argumentList = buildArgumentList {
            arguments.add(message asResolved ConstantValueKind.String)
        }
    }
}

context (FirSession)
infix fun <Type> Type.asResolved(
    kind: ConstantValueKind<Type>
): FirConstExpression<Type> =
    buildConstExpression(null, kind, value = this).apply {
        replaceTypeRef(
            buildResolvedTypeRef {
                type = kind.expectedConeType(this@FirSession)
            }
        )
    }

context (FirSession)
val ClassId.singleStringConstructor: FirConstructorSymbol
    get() = symbolProvider
        .getClassDeclaredConstructors(this)
        .firstOrNull(FirConstructorSymbol::takesSingleString)
        ?: throw IllegalStateException("No constructor with single String argument found!")
When I pass the argument in the
FirArgumentListBuilder
as an
FirConstExpression
, I get
java.lang.IllegalStateException: Expected FirResolvedTypeRef with ConeKotlinType but was FirJavaTypeRef java.lang.String
. I already tried to replace the typeRef with a
FirJavaTypeRef
, but to build one using
buildJavaTypeRef
I need an instance of
JavaType
- and I'm not sure how to get that. Does someone have experience with calling Java functions ( in this case a constructor ) in FIR?
d

dmitriy.novozhilov

03/06/2023, 3:15 PM
It's illegal to extract callable symbols via symbol itself (
symbolProvider.getClassDeclaredConstructors(this)
) Instead of it you should use scopes (
symbol.unsubstitutedScope(session)
m

MerlinTHS

03/06/2023, 10:55 PM
It works! I replaced it with
context (FirSession)
val ClassId.singleStringConstructor: FirConstructorSymbol get() =
    when(val symbol = symbolProvider.getClassLikeSymbolByClassId(this)) {
        is FirClassSymbol -> symbol.findConstructor { takesSingleString }
        else -> null
    } ?: throw IllegalStateException("No constructor with single String argument found!")

context (FirSession)
fun FirClassSymbol<*>.findConstructor(
    predicate: FirConstructorSymbol.() -> Boolean
): FirConstructorSymbol? = 
    unsubstitutedScope(this@FirSession, ScopeSession(), true)
        .getDeclaredConstructors()
        .firstOrNull(predicate)
Now the types changed. Instead of
org.jetbrains.kotlin.fir.java.declarations.FirJavaConstructor@4a36a35d: public constructor(p0: java.lang.String): R|java/lang/Exception|
it's now
org.jetbrains.kotlin.fir.declarations.impl.FirConstructorImpl@4f1f2f84: public constructor(p0: R|kotlin/String!|): R|java/lang/Exception|
. Since it's a Java method, the constructor takes a platform-type String. To make it work for this simple scenario I just checked the rendered text representation.
val FirConstructorSymbol.takesSingleString: Boolean
    get() = with(valueParameterSymbols) {
        (size == 1) && first().resolvedReturnType.renderReadable() == "String!"
    }
Are there any utility methods to check platform types? ( Something like FirTypeRef.isBuiltinType(classId: ClassId, isNullable: Boolean) from FirTypeUtils.kt )
d

dmitriy.novozhilov

03/07/2023, 9:56 AM
unsubstitutedScope(this@FirSession, ScopeSession(), true).getDeclaredConstructors()
Actually, for constructors it's enough to use declared scope, it does not requre scope session
Something like
FirTypeRef.isBuiltinType(classId: ClassId, isNullable: Boolean)
FirTypeRef
represents some place in tree which have type, not type itself If you want to use types you need to extract
ConeKotlinType
from it (
typeRef.coneType
or
typeRef.coneTypeSafe<ConeKotlinType>()
if this particular type ref may be not resolved at moment you ask for it) Then you can use various number of utilities which exist for cone types If you want to check if this type with type constuctor with specific id then it's enough to check
coneType.classId == classId
Also if you work with potentially platform (flexible) types, this may be useful: •
coneType.lowerIfFlexible()
unwraps flexible type to lower bound (
String! = (String..String?)
to
String
) •
coneType is ConeFlexibleType
to check if type is really flexible
m

MerlinTHS

03/07/2023, 12:18 PM
The
lowerIfFlexible()
I found in org/jetbrains/kotlin/types/flexibleTypes.kt extends only
KotlinType
( which isn't a supertype of
ConeKotlinType
). I ended up with using
lowerBoundIfFlexible()
from org/jetbrains/kotlin/fir/types/ConeTypeUtils.kt which extends
ConeKotlinType
.
d

dmitriy.novozhilov

03/07/2023, 12:48 PM
Yeah, I meant exactly that method I wrote those names by memory without referencing to actual codebase
m

MerlinTHS

03/07/2023, 1:20 PM
But it was very helpful at all! The only thing I can't figure out is where to get that declared scope from (?) There's the
declaredMemberScope()
extension for
FirClassSymbols
but it seems to be far away from what I'm looking for ( And would again cause a
ClassCastException
because the class symbol for
java.lang.Exception
originally contains a Java parameter reference of type
FirJavaTypeRef
).
d

dmitriy.novozhilov

03/07/2023, 1:23 PM
Oh, for Java classes it's required to use use-site scopes I forgot that declared scope for then does not contain enhancement (it's a process which converts Java types to Kotlin types, with nullability, class mapping and other stuff)
m

MerlinTHS

03/07/2023, 1:33 PM
Ok, sounds like using a
ScopeSession
like
unsubstitutedScope(this@FirSession, ScopeSession(), true)
        .getDeclaredConstructors()
is currently the best solution for working with Java classes.