Actually, it works! However, IntelliJ doesn't want...
# compiler
k
Actually, it works! However, IntelliJ doesn't want to compile when we try to use the synthetic property:
Copy code
// Dummy extension needed for compilation. Without this, IntelliJ will not compile our program
val Any.test: String
    get() = "Will never Run"

fun main() {
    println(4.test) // => "Test String" (our injected property return value)
}
How can I get around this / get IntelliJ to recognize the synthetic property?
v
Have you tried adding the extension on the classpath? For instance, create a small module, and add it to compileOnly like in https://github.com/JakeWharton/cite
k
Thanks for the example, I will take a look!
Correct me if I'm wrong, but his show up because they are global variables in generated code. I want to instead generate extension properties for existing classes, similar to something like JOOQ. Do I have to generate Kotlin code that matches my wanted extension property? If so, I'm maybe misunderstanding but I don't know why I should make this a compiler plugin when things like KotlinPoet can do that already.
v
Can you create
val Any.test: String
once and add it as a dependency? If so, that might be the best option as you won’t need to generate and compile it every time
So IDEA recognizes them without extra configuration
If you need just generate an extension property, then you probably need KSP plugin rather that a compiler plugin
k
I believe so, but my future goal is to create extension properties based on SQL column names, which means that the extension property names could be anything. It sounds like I will have to generate code, which means that KSP might be better in the long run, maybe using KotlinPoet for easier data generation.
v
If your input data is SQL or database, then you don’t need KSP. Any generator would do. KSP is designed to process Kotlin code (e.g. find and do something with annotated classes)
k
Gotcha. I was hoping that I could accomplish this without any code generation and have the compiler plugin / IDEA plugin resolve the extension property names for me by querying the database and caching the results, but it sounds like I'm trying to do something really difficult when codegen is much easier 🙂 I will look into that, I appreciate your help!
v
Exactly. As you generate code you get IDE support “for free”
k
Actually, it seems like codegen can't do what I'm trying to do. I want to generate different properties for the same class depending on context and where it's being used. E.g. a Query object that has different properties depending on whether it's been joined with another Query already or not This contextual generation would then be dependent on the AST and therefore be known at compile time. That's why I'm looking for a method that doesn't involve leaning on codegen, I don't need to generate code as much as I need to: 1) emit valid IR for objects based on what SQL table they're referencing (right now it does technically emit the new property) 2) allow the code to compile after emitting it, which seems to be the part I can't figure out, but it seems I can't use codegen to do it because of the nature of the dynamic properties
Ideally, I'd like to be able to do something like:
Copy code
val a = Query(table_a) // dynamically has all the columns of table_a as props
val b = Query(table_b) // same for table_b
val newQuery = a.naturalJoin(b) // newQuery should now dynamically have all the properties of A and B
y
This can be achieved with some clever trickery and code generation. Let's have a code generator that generates an object
TableA
for every table. Have it also generate extension vals
Query<TableA>.propA
for every column of TableA Now, by suppressing a compiler error (I know I know, it's unsupported, but it sure seems more reliable than the compiler plugin API right now) we can get an intersection type and thus have our
newQuery
be considered both a query on
TableA
and a query on
TableB
, thus the columns of both tables are valid on it. (Playground):
Copy code
class Query<Table>(val tables: List<Table>) {
    constructor(table: Table): this(listOf(table))
}

// Misusing type bounds to get an intersection type
@Suppress("BOUNDS_NOT_ALLOWED_IF_BOUNDED_BY_TYPE_PARAMETER")
fun <Q1: Query<*>, Q2: Query<*>, QR> Q1.naturalJoin(other: Q2): QR
    where QR: Q1, QR : Q2 
{
    val result = Query<Any>(tables + other.tables)
    return result as QR
}

fun main() {
    val a = Query(TableA) // has all the columns of TableB as props
	val b = Query(TableB) // same for TableB
	val newQuery = a.naturalJoin(b) // newQuery should now have all the properties of A and B
    println(newQuery.strA + newQuery.strB)
    println(newQuery.intA + newQuery.intB)
}

// Generated by plugin
object TableA
object TableB

val Query<TableA>.strA: String get() = "foo"

val Query<TableA>.intA: Int get() = 7

val Query<TableB>.strB: String get() = "bar"

val Query<TableB>.intB: Int get() = 42