Skip to content
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
1a14877
conditionally sign
sugarmanz May 4, 2022
4a04b8f
wip
sugarmanz May 5, 2022
69fc4f5
wip on version catalog
sugarmanz May 5, 2022
a9b3423
wip ksp
sugarmanz May 12, 2022
b2f2d7c
finish compiler plugin re-org
sugarmanz May 12, 2022
149aab4
fix all tests for compiler plugin and enhance validations
sugarmanz May 12, 2022
0115548
re-enable docs + example-application
sugarmanz May 13, 2022
eb94c1f
gradle plugin
sugarmanz May 14, 2022
42fc7be
maven plugin
sugarmanz May 17, 2022
0d8a02f
fix some links
sugarmanz Jun 14, 2022
06ef099
fix gradle and maven readmes and lint
sugarmanz Jun 14, 2022
dd055b1
try to fix signing
sugarmanz Jun 14, 2022
e840096
fix dependency
sugarmanz Jun 14, 2022
a093a2a
hacky way to fix test dependency
sugarmanz Jun 15, 2022
c909271
api dump
sugarmanz Jun 15, 2022
e22a41b
remove unnecessary option
sugarmanz Jun 16, 2022
51fa2eb
add missing deps to version catalog
sugarmanz Jun 16, 2022
28a752a
Get SyncHook working with Kotlin Poet
stabbylambda Jun 18, 2022
5d2e121
Implement AsyncSeriesHook
stabbylambda Jun 21, 2022
bf0aff7
SyncBailHook and AsyncSeriesBailHook
stabbylambda Jun 21, 2022
e516b7a
SyncWaterfallHook
stabbylambda Jun 22, 2022
4b3242a
SyncLoopHook
stabbylambda Jun 22, 2022
0ddca19
AsyncParallelBailHook
stabbylambda Jun 22, 2022
06c0a23
Make all the tests pass! 🎉
stabbylambda Jun 22, 2022
8f140f7
Cleanup
stabbylambda Jun 22, 2022
826dc8d
compiler-plugin -> processor
sugarmanz Jun 22, 2022
768a268
lint,dump,knit
sugarmanz Jun 22, 2022
c789985
make knit happy (and me sad :()
sugarmanz Jun 22, 2022
9895f28
upgrade knit
sugarmanz Jun 22, 2022
30311d5
Fix the generics test
stabbylambda Jun 22, 2022
3036757
upgrade arrow meta and handle no requested version a bit more gracefully
sugarmanz Jun 22, 2022
bdae47e
Merge remote-tracking branch 'origin/ksp' into poet
stabbylambda Jun 22, 2022
102e812
remove example-library api folder
sugarmanz Jun 22, 2022
d29a221
Remove all the string-based codegen
stabbylambda Jun 22, 2022
7ff5810
Merge branch 'ksp' into poet
stabbylambda Jun 22, 2022
5492d62
Fix compiler issues
stabbylambda Jun 22, 2022
e595089
Merge remote-tracking branch 'origin/main' into poet
stabbylambda Jun 22, 2022
ef7fbc2
Make the apiCheck happy
stabbylambda Jun 22, 2022
f3d7ae1
Make the linter happy
stabbylambda Jun 22, 2022
ed8b732
Remove all the excess Poet descriptors
stabbylambda Jun 22, 2022
9561222
Merge remote-tracking branch 'origin/main' into poet
stabbylambda Jun 24, 2022
4132d5d
Pass TypeResolvers through to the signature
stabbylambda Jun 24, 2022
892252b
Move all KSP -> Poet stuff into the validators
stabbylambda Jun 24, 2022
0837c34
Split all the ksp/poet stuff into different parts
stabbylambda Jun 24, 2022
36f213c
Clean up duplicate cases
stabbylambda Jun 24, 2022
cadcac6
Fix external api
stabbylambda Jun 24, 2022
6073b81
More cleanup
stabbylambda Jun 24, 2022
de2b1fa
Switch from withEither to andThen
stabbylambda Jun 27, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ class SyncWaterfallHookTests {
@Test
fun `waterfall taps work with arity 2`() {
val h = Hook2<String, Int>()
h.tap("continue") { _, x, y -> "$x David" }
h.tap("continue again") { _, x, y -> "$x Jeremiah" }
h.tap("continue") { _, x, _ -> "$x David" }
h.tap("continue again") { _, x, _ -> "$x Jeremiah" }

val result = h.call("Kian", 3)
Assertions.assertEquals("Kian David Jeremiah", result)
Expand Down
2 changes: 0 additions & 2 deletions processor/api/processor.api
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
public final class com/intuit/hooks/plugin/ksp/HooksProcessor : com/google/devtools/ksp/processing/SymbolProcessor {
public fun <init> (Lcom/google/devtools/ksp/processing/CodeGenerator;Lcom/google/devtools/ksp/processing/KSPLogger;)V
public fun finish ()V
public fun onError ()V
public fun process (Lcom/google/devtools/ksp/processing/Resolver;)Ljava/util/List;
}

Expand Down
2 changes: 1 addition & 1 deletion processor/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
dependencies {
implementation(libs.kotlin.stdlib)
implementation(libs.ksp.spa)
// implementation(libs.ksp.poet)
implementation(libs.ksp.poet)
implementation(libs.arrow.core)

testImplementation(project(":hooks"))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,54 +1,62 @@
package com.intuit.hooks.plugin.codegen

import com.google.devtools.ksp.getVisibility
import com.google.devtools.ksp.symbol.*
import com.intuit.hooks.plugin.ksp.text
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I was migrating from meta -> KSP in the first place, I tried to pull out all the intermediate data into pure codegen constructs, rather than a dependence on the modeling framework. This would ensure the codegen code is truly agnostic and you could potentially use the codegen module without needing the KSP part. i.e. if we want to use another processing framework (live javax.processing), or change the actual way we represent the DSL, or generate hooks via CLI.

I would like to try to continue following the strict separation of concerns. They can certainly be updated from simple String representations to whatever poet representation works best (TypeName, etc.).

Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed this earlier, but just putting this here for posterity. Today, we have multiple areas where we reach into the type resolver to get the information necessary to codegen the new class. When @sugarmanz looked at this, some of that was happening in the codegen, some in the validator, some in the processor. Instead what we discussed was making the whole pipeline a bit cleaner by changing it to be:

  1. process KSP -> Poet
  2. validate Poet constructs
  3. code generate from Poet constructs

That way any potential frontend (we're not switching away from KSP anytime soon) just has to convert to KotlinPoet classes.

import com.intuit.hooks.plugin.ksp.validation.HookAnnotation
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ksp.TypeParameterResolver
import com.squareup.kotlinpoet.ksp.toKModifier
import com.squareup.kotlinpoet.ksp.toTypeName
import com.squareup.kotlinpoet.ksp.toTypeParameterResolver

internal data class HookMember(
val name: String,
val visibility: String,
)

internal data class HookSignature(
val text: String,
val isSuspend: Boolean,
val returnType: String,
/** For hooks that return a wrapped result, like [BailResult], this is the inner type */
val returnTypeType: String?,
val hookFunctionSignatureType: KSTypeReference,
val hookFunctionSignatureReference: KSCallableReference
) {
val nullableReturnTypeType = "${returnTypeType}${if (returnTypeType?.last() == '?') "" else "?"}"

override fun toString() = text
val isSuspend get() = hookFunctionSignatureType.modifiers.contains(Modifier.SUSPEND)
val returnTypeText get() = hookFunctionSignatureReference.returnType.text
val returnType get() = hookFunctionSignatureReference.returnType.toTypeName()
val returnTypeType
get() = hookFunctionSignatureReference.returnType.element?.typeArguments?.firstOrNull()?.toTypeName()!!
val nullableReturnTypeType get() = returnTypeType.copy(nullable = true)
val parameters get() = hookFunctionSignatureReference.functionParameters

override fun toString() = hookFunctionSignatureType.text
}

internal class HookParameter(
val name: String?,
val type: String,
val parameter: KSValueParameter,
val position: Int,
)

internal val HookParameter.withType get() = "$withoutType: $type"
internal val HookParameter.withoutType get() = name ?: "p$position"
) {
val name: String? get() = parameter.name?.asString()
val type: String get() = parameter.type.text
val withType get() = "$withoutType: $type"
val withoutType get() = name ?: "p$position"
}

internal data class HookInfo(
val property: HookMember,
val hookType: HookType,
val hookSignature: HookSignature,
val params: List<HookParameter>,

val propertyDeclaration: KSPropertyDeclaration,
val annotation: HookAnnotation
) {
// TODO: Should this actually default to public?
val propertyVisibility get() = propertyDeclaration.getVisibility().toKModifier() ?: KModifier.PUBLIC
val zeroArity = params.isEmpty()
val isAsync = hookType.properties.contains(HookProperty.Async)
val parentResolver get() = (this.propertyDeclaration.parent as? KSClassDeclaration)?.typeParameters?.toTypeParameterResolver() ?: TypeParameterResolver.EMPTY
}

internal val HookInfo.tapMethod get() = if (!zeroArity) """
public fun tap(name: String, f: $hookSignature): String? = tap(name, generateRandomId(), f)
public fun tap(name: String, id: String, f: $hookSignature): String? = super.tap(name, id) { _: HookContext, $paramsWithTypes -> f($paramsWithoutTypes) }
""".trimIndent() else ""
internal val HookInfo.paramsWithTypes get() = params.joinToString(transform = HookParameter::withType)
internal val HookInfo.paramsWithoutTypes get() = params.joinToString(transform = HookParameter::withoutType)
internal fun HookInfo.generateClass() = this.hookType.generateClass(this)
internal fun HookInfo.generateProperty() = (if (hookType == HookType.AsyncParallelBailHook) "@kotlinx.coroutines.ExperimentalCoroutinesApi\n" else "") +
"override val ${property.name}: $className = $className()"
internal fun HookInfo.generateImports(): List<String> = emptyList()

internal val HookInfo.superType get() = this.hookType.toString()

internal val HookInfo.className get() = "${property.name.replaceFirstChar(Char::titlecase)}$superType"
internal val HookInfo.typeParameter get() = "(${if (isAsync) "suspend " else ""}(HookContext, $paramsWithTypes) -> ${hookSignature.returnType})"
internal val HookInfo.interceptParameter get() = "${if (isAsync) "suspend " else ""}(HookContext, $paramsWithTypes) -> Unit"
116 changes: 10 additions & 106 deletions processor/src/main/kotlin/com/intuit/hooks/plugin/codegen/HookType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,112 +8,16 @@ internal sealed class HookProperty {
}

internal enum class HookType(vararg val properties: HookProperty) {
SyncHook {
override fun generateClass(info: HookInfo): String {
// todo: potentially protected
return """|${info.property.visibility} inner class ${info.className} : ${info.superType}<${info.typeParameter}>() {
| public fun call(${info.paramsWithTypes}): Unit = super.call { f, context -> f(context, ${info.paramsWithoutTypes}) }
| ${info.tapMethod}
|}"""
}
},
SyncBailHook(HookProperty.Bail) {
override fun generateClass(info: HookInfo): String {
// todo: Potentially protected
return """|${info.property.visibility} inner class ${info.className} : ${info.superType}<${info.typeParameter}, ${info.hookSignature.returnTypeType}>() {
| public fun call(${info.paramsWithTypes}): ${info.hookSignature.nullableReturnTypeType} = super.call { f, context -> f(context, ${info.paramsWithoutTypes}) }
| ${info.tapMethod}
|}"""
}
},
SyncWaterfallHook(HookProperty.Waterfall) {
override fun generateClass(info: HookInfo): String {
val accumulatorName = info.params.first().withoutType
return """|${info.property.visibility} inner class ${info.className} : ${info.superType}<${info.typeParameter}, ${info.params.first().type}>() {
| public fun call(${info.paramsWithTypes}): ${info.hookSignature.returnType} = super.call($accumulatorName,
| invokeTap = { f, $accumulatorName, context -> f(context, ${info.paramsWithoutTypes}) },
| invokeInterceptor = { f, context -> f(context, ${info.paramsWithoutTypes})}
| )
| ${info.tapMethod}
|}"""
}
},

SyncLoopHook(HookProperty.Loop) {
override fun generateClass(info: HookInfo): String {
return """|${info.property.visibility} inner class ${info.className}: ${info.superType}<${info.typeParameter}, ${info.interceptParameter}>() {
| public fun call(${info.paramsWithTypes}): Unit = super.call(
| invokeTap = { f, context -> f(context, ${info.paramsWithoutTypes}) },
| invokeInterceptor = { f, context -> f(context, ${info.paramsWithoutTypes}) }
| )
| ${info.tapMethod}
|}"""
}
},

AsyncParallelHook(HookProperty.Async) {
override fun generateClass(info: HookInfo): String {
return """|${info.property.visibility} inner class ${info.className}: ${info.superType}<${info.typeParameter}>() {
| public suspend fun call(${info.paramsWithTypes}): Unit = super.call { f, context -> f(context, ${info.paramsWithoutTypes}) }
| ${info.tapMethod}
|}"""
}
},

AsyncParallelBailHook(HookProperty.Async, HookProperty.Bail) {
override fun generateClass(info: HookInfo): String {
return """|@kotlinx.coroutines.ExperimentalCoroutinesApi
|${info.property.visibility} inner class ${info.className}: ${info.superType}<${info.typeParameter}, ${info.hookSignature.returnTypeType}>() {
| public suspend fun call(concurrency: Int, ${info.paramsWithTypes}): ${info.hookSignature.nullableReturnTypeType} = super.call(concurrency) { f, context -> f(context, ${info.paramsWithoutTypes}) }
| ${info.tapMethod}
|}"""
}
},

AsyncSeriesHook(HookProperty.Async) {
override fun generateClass(info: HookInfo): String {
return """|${info.property.visibility} inner class ${info.className}: ${info.superType}<${info.typeParameter}>() {
| public suspend fun call(${info.paramsWithTypes}): Unit = super.call { f, context -> f(context, ${info.paramsWithoutTypes}) }
| ${info.tapMethod}
|}"""
}
},

AsyncSeriesBailHook(HookProperty.Async, HookProperty.Bail) {
override fun generateClass(info: HookInfo): String {
return """|${info.property.visibility} inner class ${info.className}: ${info.superType}<${info.typeParameter}, ${info.hookSignature.returnTypeType}>() {
| public suspend fun call(${info.paramsWithTypes}): ${info.hookSignature.nullableReturnTypeType} = super.call { f, context -> f(context, ${info.paramsWithoutTypes}) }
| ${info.tapMethod}
|}"""
}
},

AsyncSeriesWaterfallHook(HookProperty.Async, HookProperty.Waterfall) {
override fun generateClass(info: HookInfo): String {
val accumulatorName = info.params.first().withoutType
return """|${info.property.visibility} inner class ${info.className} : ${info.superType}<${info.typeParameter}, ${info.params.first().type}>() {
| public suspend fun call(${info.paramsWithTypes}): ${info.hookSignature.returnType} = super.call($accumulatorName,
| invokeTap = { f, $accumulatorName, context -> f(context, ${info.paramsWithoutTypes}) },
| invokeInterceptor = { f, context -> f(context, ${info.paramsWithoutTypes})}
| )
| ${info.tapMethod}
|}"""
}
},

AsyncSeriesLoopHook(HookProperty.Async, HookProperty.Loop) {
override fun generateClass(info: HookInfo): String {
return """|${info.property.visibility} inner class ${info.className}: ${info.superType}<${info.typeParameter}, ${info.interceptParameter}>() {
| public suspend fun call(${info.paramsWithTypes}): Unit = super.call(
| invokeTap = { f, context -> f(context, ${info.paramsWithoutTypes}) },
| invokeInterceptor = { f, context -> f(context, ${info.paramsWithoutTypes}) }
| )
| ${info.tapMethod}
|}"""
}
};

abstract fun generateClass(info: HookInfo): String
SyncHook,
SyncBailHook(HookProperty.Bail),
SyncWaterfallHook(HookProperty.Waterfall),
SyncLoopHook(HookProperty.Loop),
AsyncParallelHook(HookProperty.Async),
AsyncParallelBailHook(HookProperty.Async, HookProperty.Bail),
AsyncSeriesHook(HookProperty.Async),
AsyncSeriesBailHook(HookProperty.Async, HookProperty.Bail),
AsyncSeriesWaterfallHook(HookProperty.Async, HookProperty.Waterfall),
AsyncSeriesLoopHook(HookProperty.Async, HookProperty.Loop);

companion object {
val annotationDslMarkers = values().map {
Expand Down
Loading