Retrieve Kotlin Property type from Kotlin psi API

The original stackoverflow question is here.

I’m trying to create new rule for detekt project. To do this I have to know exact type of Kotlin property. For example, val x: Int has type Int.

Unfortunately, for property of type private val a = 3 I receive the following:

  1. property.typeReference is null
  2. property.typeParameters is empty
  3. property.typeConstraints is empty
  4. property.typeParameterList is empty
  5. property.text is private val a = 3
  6. property.node.children().joinToString() has object notation of previous item
  7. property.delegate is null
  8. property.getType(bindingContext) is null (the property bindingContext is part of KtTreeVisitorVoid used

Question: how can I get name of type (or, what is better, object KClass) to compare the actual property type with Boolean class type? (e.g. I just need to get if property boolean of not)

Code:

    override fun visitProperty(property: org.jetbrains.kotlin.psi.KtProperty) {
        val type: String = ??? //property.typeReference?.text - doesn't work

        if(property.identifierName().startsWith("is") && type != "Boolean") {
            report(CodeSmell(
                issue,
                Entity.from(property),
                message = "Non-boolean properties shouldn't start with 'is' prefix. Actual type: $type")
            )
        }
    }

My guess is you have to do some basic type inference. I’m not sure, but looks like PSI is just an AST, the first stage of program analysis, nothing more. Types, symbol resolution, etc. comes after this in the form of some kotlin specific IR.

Agree, probably it is. However, do you know, how it is better to get resolved type during the compilation? Does psi have rights visiting functions for this (I didn’t find).

Of course, I can validate the classes compiled, however for now detekt works with psi directly…

For such a simple diagnostic i would probably write relatively simple type inference code just to match for Boolean type.

However, do you know, how it is better to get resolved type during the compilation?

I am not familiar with detekt, if it bootstraps the whole kotlin compiler then i would use provided IR, otherwise, the only better way i now is to write a compiler plugin using IR, which contains comprehensive information about all types.

Plugin code would look like this:

val KEY_ENABLED = CompilerConfigurationKey<Boolean>("whether the plugin is enabled")

class MyComponentRegistrar : ComponentRegistrar {
    override fun registerProjectComponents(
        project: MockProject,
        configuration: CompilerConfiguration
    ) {
        if (configuration[KEY_ENABLED] == false) return
        
        IrGenerationExtension.registerExtension(project, MyExtension())
    }
}

class MyExtension : IrGenerationExtension {
    override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
        val classes = moduleFragment.files
            .flatMap { it.declarations }
            .filterIsInstance<IrClass>()

        for (clazz in classes) {
            for (property in clazz.properties) {
                val type = property.getter?.returnType ?: continue
                val namedAsBoolProperty = property.name.asString().let {
                    it.startsWith("is") && (it.getOrNull(3)?.isUpperCase() == true)
                }
                if (namedAsBoolProperty && !type.isBoolean()) {
                    reportCodeSmell()
                }
            }
        }
    }
}

Dependency used: org.jetbrains.kotlin:kotlin-compiler-embeddable.

If the plugin is not an option, but you need accurate information about the types, then perhaps it makes sense to bootstrap the compiler by yourself.
Here is an example code, however I don’t think it will work, since the API is very unstable and changes even in minor versions of kotlinc:

val psi = ktParser.parse(code)

class FirJvmModuleInfo : ModuleInfo {
    override val name = Name.special("<dependencies>")
    val dependencies: MutableList<ModuleInfo> = mutableListOf()
    override val platform get() = JvmPlatforms.jvm18
    override val analyzerServices get() = JvmPlatformAnalyzerServices
    override fun dependencies(): List<ModuleInfo> = dependencies
    
}

val moduleInfo = FirJvmModuleInfo()
val scope = GlobalSearchScope.filesScope(project, listOf(psi.virtualFile))
        .uniteWith(TopDownAnalyzerFacadeForJVM.AllJavaSourcesInProjectScope(project))

val firSession: FirSession = FirJavaModuleBasedSession(moduleInfo, provider, scope).also {
    val dependenciesInfo = FirJvmModuleInfo()
    moduleInfo.dependencies.add(dependenciesInfo)
    val librariesScope = ProjectScope.getLibrariesScope(project)
    FirLibrarySession.create(
        dependenciesInfo, provider, librariesScope,
        project, env.createPackagePartProvider(librariesScope)
    )
}

val firFile = RawFirBuilder(firSession, stubMode = false).buildFirFile(psi)

firFile.runResolve(toPhase = FirResolvePhase.TYPES)
1 Like

Wow, that’s cool, thank you!

Also am I right, that probably exact type is impossible get at this level of compilation? As I understand from your answer and from psi logic, the pre-parsed code will be used for different targets then, so the real types can be different for different platforms.

Boolean Java type exists on JVM platform only (obviously). It means, that theoretically we need some kind of switch for all possible targets.

Question: am I right about the possibility above?

yes, you are right

isBoolean() will only detect kotlin.Boolean (the underlying plaform type does not matter), but not explicit java.lang.Boolean.

You may write your own, for example:

backend IR:

val IrType.isKtOrJavaBoolean get() = 
    isBoolean() || (this is IrSimpleType && classifier.isClassWithFqName((FqName("java.lang.Boolean").toUnsafe()))

FIR:

val FirTypeRef.isKtOrJavaBoolean: Boolean get() = 
    isBoolean() || (this is ConeClassLikeType && classId == ClassId(FqName("java.lang.Boolean")))
2 Likes