I'm a complete codegen noob, but can someone expla...
# ksp
u
I'm a complete codegen noob, but can someone explain to me - if I have codegen that generates new types & I reference the generated types - how can that build at all after say a fresh git clone? Isn't that a chicken & egg problem?
r
In general yes
j
Yes, but it depends on the codegen approach for how you can solve it. For a proper Kotlin compiler plugin, the code generation is taking place at the same time as compilation. So you can fill in anything missing on-the-fly. For KSP, you generally only refer to new types within method bodies (which KSP doesn't care about) or as supertypes, in which case an error type is usually substituted.
u
So..if I reference the generated code from a function body, then it will build fine but will be red in the IDE until the first build?
j
I believe the import into the IDE should trigger the codegen
u
yes? I thought ksp was a build time thing
r
I'm using KSP but never seen it being triggered by the import ...
j
Ah. I'm not actually a KSP user. I just keep forgetting to leave.
r
From my experience with KSP it works exactly like this - errors in IDE until first gradle build.
u
😄 yea sorry I thought ksp but didnt type it out my use case is generating
metro
DI code for it to pick up with ksp, to make certain cases a bit nicer would you not do this in
ksp
?
j
I'm not really sure what you're asking. Metro is a compiler plugin. In Kotlin 2.3.0 you can explicitly order compiler plugins to run something you want Metro to see before it. Additionally, compiler plugins can generate FIR which is indexed in the IDE and avoids the red code problem.
u
I have this plugin architecture I want to achieve and the code I need is basically this
Copy code
@Inject
@SingleIn(UserScope::class)
@ContributesIntoSet(UserScope::class)
@QualifierFor(UserScope::class)
class SecondPlugin(
    private val logger: LOG
) : Plugin {
    override fun initialize() {
        logger.d(message = "Hello I'm SecondPlugin")
    }
}
in pure metro, but as you can see the
UserScope
parameter duplicated, so I was thinking of having something like
Copy code
@Inject
@PluginIn(UserScope::class)
class SecondPlugin(
    private val logger: LOG
) : Plugin {
    override fun initialize() {
        logger.d(message = "Hello I'm SecondPlugin")
    }
}
which would then be picked up by my processor & would emit the "what used to be @Module in dagger" syntax to add the rest from the first sample to make it functionally equivalent -- and Metro doesnt allow to plug custom processing in, so I was thinking
ksp
completely on top of metro (before metro) and im thinking
ksp
only because I was told compiler plugins are hardcore 😄
j
They're not that bad, but it can be intimidating for sure. There's some possible plans in place to make it easier to build them. Doing a compiler plugin would allow you to "just" replace the
@PluginId
with the 3 other annotations directly. And then you'd emit a plugin ordering compiler argument to ensure you run before Metro (again, on Kotlin 2.3+ only).
But perhaps KSP is a good starter. I don't see what the concern about red code would be, though.
u
I see, I'll keep that in mind -- because yes, I just want sort of typealias that expands into the other 3 annotations, sort of like swift macros do .. the concern is mostly irrational, I'm okay with it since you explained it but now I'm kind of curious how did
DaggerXComponent
literal even work, I don't remember it being red after a fresh clone - but I might be mistaken thanks!
r
@ursus I faced a similar issue in the past. Of having to reference types created during the code generation. Note KSP can have multiple processing phases, so here was my workaround: _ For phase 1 where I generate minimal "stub" files I can reference later (using Kotlin Poet for code generation). It returns a ListKSClassDeclaration. For phase 2, it checks if phase 1 returned a non-empty list (has data to work with). I didn't use raw KSClassDeclaration for phase 2, but instead mapped it into a custom class, which is serialized into a JSON file and passed into Kotlin Poet again. The problem is you cannot overwrite a file created during KSP phase, so I did:
Copy code
} catch (e: FileAlreadyExistsException) {
        e.file.writeBytes(content)
    }
Which is a bit hackish I admit.
_ The reason for the custom KSP mapping class and the models.json file is because originally the KSP modules and Kotlin Poet modules were launched on separate JVMs — because I had no clue what I was doing. Theoretically, it does have the benefit that it could pick up annotations declarations with
private
visibility across multiple modules, but I do not find a pressing need to support it for my personal use case.
e
note that by default, the IDE only runs certain bundled compiler plugins (serialization, lombok, etc.)
👀 1
declarations generated by third-party compiler plugins are not visible in the IDE without some configuration to allow them, https://zacsweers.github.io/metro/latest/installation/#ide-support
(and this doesn't apply to KSP since it doesn't work like a compiler plugin that way, but since you mentioned Metro earlier)
💯 2