google / ksp

Kotlin Symbol Processing API
https://github.com/google/ksp
Apache License 2.0
2.84k stars 266 forks source link

KSPropertyDeclaration#getter / #setter always null #17

Closed SergejIsbrecht closed 4 years ago

SergejIsbrecht commented 4 years ago

Version:

plugins {
    kotlin("jvm") version "1.4.0-rc" apply false
}

implementation("org.jetbrains.kotlin:kotlin-symbol-processing-api:1.4.0-rc-dev-experimental-20200731")
implementation("org.jetbrains.kotlin:kotlin-ksp:1.4.0-rc-dev-experimental-20200731")

Processor


class PropertiesProcessor : SymbolProcessor {
    override fun finish() {
    }

    override fun init(
        options: Map<String, String>,
        kotlinVersion: KotlinVersion,
        codeGenerator: CodeGenerator,
        logger: KSPLogger
    ) {
    }

    override fun process(resolver: Resolver) {
        val funReturnTypes = resolver.getSymbolsWithAnnotation("Immutable")

        funReturnTypes.filterIsInstance<KSClassDeclaration>()
            .flatMap { d -> d.getDeclaredProperties().map { d to it } }
            .forEach {
                println("clazz ${it.first.qualifiedName?.asString()} | propName ${it.second.qualifiedName?.asString()} |  get ${it.second.getter?.returnType?.resolve()?.declaration?.qualifiedName?.asString()} | set get ${it.second.setter}")
            }
    }
}

Test-Case

                @PublicApi
                interface MyApi {
                    fun flowableDataClass(): Immutable
                    fun flowableMutableDataClass(): Mutable
                }

                data class Immutable(val s: String)
                data class Mutable(var s: String)

Output

clazz Mutable | propName Mutable.s |  get null | set get null
clazz Mutable | propName Mutable.q |  get null | set get null
clazz Address | propName Address.zip |  get null | set get null
clazz Address | propName Address.what |  get null | set get null

Expected behavior

Actual behavior

neetopia commented 4 years ago

We modeled KS symbols to represent what is shown in code, so implicit elements like these accessors not declared in code is not created. You will be able to get these accessors if you declared them explicitly like

val a:String
get() = ...

However we've later decided to include such implicit elements as well for KSP, I have a PR address implicit accessors, see android/kotlin-for-ksp#92

SergejIsbrecht commented 4 years ago

@neetopia , thank you for your quick response.

Regaring given issue: In my opinion the Api woul be inconsistent, when getter/ setter were null. Aren't properties ctor(val i : Int) a sythetic construct? It woul then make sense, that the Property woul not be returne with #getDeclaredProperties, when not explicitly declared.

Regarding what I want to achieve: I am currently trying to recursively parse a data class for deep-immutability. This is done by checking the data-class for properties. If all properties are setters and no other fields are exposed recursively, then it is deep-immutable (further checks for Int/String/... List are included).

Do you have a better idea to achieve given requirement?

Any thoughts, when the PR will be merged?

Thank you for your help

neetopia commented 4 years ago

With that PR merged, you will be able to get synthetic accessors for properties. I don't get what you mean by "ctor(val i:Int) is a synthetic constructor". In the case of a data class, you declare all properties in primary constructor and therefore they are all not synthesized, and available from KSClassDeclaration.declarations.filterIsInstance<KSPropertyDeclaration>

SergejIsbrecht commented 4 years ago

I don't get what you mean by ...

Well, I have very limited knowledge about the Kotlin compiler, but I would guess, that all properties are synthetic, when defined via a constructor, because the compiler generates it at the convenience of the developer, when using "var/ val".

In the case of a data class, you declare all properties in primary constructor and therefore they are all not synthesized

Does that mean, that I would get get/set != null for a declared property?

Test-Case

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
annotation class Immutable                

@Immutable
data class Mutable(var s: String, val q: Int)

@Immutable
class Address {
    var zip: String = "123456"
    val what: Int = 42
}

        val funReturnTypes = resolver.getSymbolsWithAnnotation("Immutable")

        funReturnTypes.filterIsInstance<KSClassDeclaration>()
                .flatMap { d -> d.declarations }
                .filterIsInstance<KSPropertyDeclaration>()
                .forEach {
                    println("clazz ${it.qualifiedName?.asString()} | propName ${it.qualifiedName?.asString()} |  get ${it.getter?.returnType?.resolve()?.declaration?.qualifiedName?.asString()} | set get ${it.setter}")
                }

Output:

clazz Mutable.s | propName Mutable.s |  get null | set get null
clazz Mutable.q | propName Mutable.q |  get null | set get null

Result get/set null for declared property, even though accessed through #declarations

neetopia commented 4 years ago

KSP models source code therefore for properties declared in primary constructor they are modeled as source code based symbols.

For your test case, what was failing is the accessors, they are not explicit in code. By explicit I mean declared by get()= {} and set(...) = {}. In current KSP release implicit accessors are not supported, you can expect them to be supported in next release.

neetopia commented 4 years ago

And for properties declared in primary constructors, they must have val or var keyword therefore they are explicit.

SergejIsbrecht commented 4 years ago

@neetopia,

I think there was a misconception from my side. In my view property also included the implicit getter and setter, but know I see, that the accessors are a separate from properties.

Thank you for the explanation.

neetopia commented 4 years ago

Glad to hear your confusion is resolved :) Feel free to reopen if more questions on this.