From e46f59f435c52ff0ea96a8b0f810e271b8a55876 Mon Sep 17 00:00:00 2001 From: Jeremiah Zucker Date: Tue, 16 May 2023 09:26:55 -0400 Subject: [PATCH 1/5] WIP: Validated -> Raise --- build.gradle.kts | 1 + .../intuit/hooks/plugin/codegen/HookType.kt | 6 +- .../intuit/hooks/plugin/ksp/HooksProcessor.kt | 85 +++++++------ .../ksp/validation/AnnotationValidations.kt | 76 +++++++++++- .../ksp/validation/HookPropertyValidations.kt | 68 +++++++++-- .../plugin/ksp/validation/HookValidations.kt | 114 ++++++++++++++++-- settings.gradle.kts | 2 +- 7 files changed, 290 insertions(+), 62 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 37c159b..f3e502d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -176,6 +176,7 @@ subprojects { val configure: KotlinCompile.() -> Unit = { kotlinOptions { freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" + freeCompilerArgs += "-Xcontext-receivers" } } diff --git a/processor/src/main/kotlin/com/intuit/hooks/plugin/codegen/HookType.kt b/processor/src/main/kotlin/com/intuit/hooks/plugin/codegen/HookType.kt index aa28d30..dda4204 100644 --- a/processor/src/main/kotlin/com/intuit/hooks/plugin/codegen/HookType.kt +++ b/processor/src/main/kotlin/com/intuit/hooks/plugin/codegen/HookType.kt @@ -20,8 +20,10 @@ internal enum class HookType(vararg val properties: HookProperty) { AsyncSeriesLoopHook(HookProperty.Async, HookProperty.Loop); companion object { - val annotationDslMarkers = values().map { - it.name.dropLast(4) + val supportedHookTypes = values().map(HookType::name) + + val annotationDslMarkers = supportedHookTypes.map { + it.dropLast(4) } } } diff --git a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/HooksProcessor.kt b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/HooksProcessor.kt index f8c5497..d018b01 100644 --- a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/HooksProcessor.kt +++ b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/HooksProcessor.kt @@ -1,14 +1,18 @@ package com.intuit.hooks.plugin.ksp import arrow.core.* -import arrow.typeclasses.Semigroup +import arrow.core.raise.Raise +import arrow.core.raise.recover import com.google.devtools.ksp.getVisibility import com.google.devtools.ksp.processing.* import com.google.devtools.ksp.symbol.* import com.google.devtools.ksp.validate import com.google.devtools.ksp.visitor.KSDefaultVisitor import com.intuit.hooks.plugin.codegen.* +import com.intuit.hooks.plugin.ksp.validation.* +import com.intuit.hooks.plugin.ksp.validation.EdgeCase import com.intuit.hooks.plugin.ksp.validation.HookValidationError +import com.intuit.hooks.plugin.ksp.validation.error import com.intuit.hooks.plugin.ksp.validation.validateProperty import com.squareup.kotlinpoet.* import com.squareup.kotlinpoet.ksp.* @@ -26,62 +30,66 @@ public class HooksProcessor( return emptyList() } - private inner class HookPropertyVisitor : KSDefaultVisitor>() { - override fun visitPropertyDeclaration(property: KSPropertyDeclaration, parentResolver: TypeParameterResolver): ValidatedNel { - return if (property.modifiers.contains(Modifier.ABSTRACT)) - validateProperty(property, parentResolver) - else - HookValidationError.NotAnAbstractProperty(property).invalidNel() + private inner class HookPropertyVisitor : KSDefaultVisitor() { + + context(Raise>) + override fun visitPropertyDeclaration(property: KSPropertyDeclaration, parentResolver: TypeParameterResolver): HookInfo { + ensure(property.modifiers.contains(Modifier.ABSTRACT)) { + HookValidationError.NotAnAbstractProperty(property) + } + + return property.validateProperty(parentResolver) } - override fun defaultHandler(node: KSNode, data: TypeParameterResolver): ValidatedNel = - TODO("Not yet implemented") + override fun defaultHandler(node: KSNode, data: TypeParameterResolver) = error("Should not happen.") } private inner class HookFileVisitor : KSVisitorVoid() { override fun visitFile(file: KSFile, data: Unit) { - val hookContainers = file.declarations.filter { - it is KSClassDeclaration - }.flatMap { - it.accept(HookContainerVisitor(), Unit) - }.mapNotNull { v -> - v.valueOr { errors -> - errors.forEach { error -> logger.error(error.message, error.symbol) } - null - } - }.toList() - - if (hookContainers.isEmpty()) return - - val packageName = file.packageName.asString() - val name = file.fileName.split(".").first() - - generateFile(packageName, "${name}Hooks", hookContainers).writeTo(codeGenerator, aggregating = false, originatingKSFiles = listOf(file)) + recover({ + val containers = file.declarations + .filterIsInstance() + .flatMap { it.accept(HookContainerVisitor(), this) } + .ifEmpty { raise(EdgeCase.NoHooksDefined(file)) } + + val packageName = file.packageName.asString() + val name = file.fileName.split(".").first() + + // May raise some additional errors + generateFile(packageName, "${name}Hooks", containers.toList()) + .writeTo(codeGenerator, aggregating = false, originatingKSFiles = listOf(file)) + }, { errors: Nel -> + errors.filterIsInstance().forEach(logger::error) + }, { throwable: Throwable -> + logger.error("Uncaught exception while processing file: ${throwable.localizedMessage}", file) + logger.exception(throwable) + }) } } - private inner class HookContainerVisitor : KSDefaultVisitor>>() { - override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit): List> { + private inner class HookContainerVisitor : KSDefaultVisitor>, Sequence>() { + // TODO: Try with context receiver + override fun visitClassDeclaration( + classDeclaration: KSClassDeclaration, + raise: Raise> + ): Sequence = with(raise) { val superTypeNames = classDeclaration.superTypes .filter { it.toString().contains("Hooks") } .toList() return if (superTypeNames.isEmpty()) { classDeclaration.declarations - .filter { it is KSClassDeclaration && it.validate() } - .flatMap { it.accept(this, Unit) } - .toList() + .filter { it is KSClassDeclaration && it.validate() /* TODO: Tie in validations to KSP */ } + .flatMap { it.accept(this@HookContainerVisitor, raise) } } else if (superTypeNames.any { it.resolve().declaration.qualifiedName?.getQualifier() == "com.intuit.hooks.dsl" }) { val parentResolver = classDeclaration.typeParameters.toTypeParameterResolver() classDeclaration.getAllProperties() .map { it.accept(HookPropertyVisitor(), parentResolver) } - .sequence(Semigroup.nonEmptyList()) - .map { hooks -> createHooksContainer(classDeclaration, hooks) } - .let(::listOf) - } else { - emptyList() - } + // TODO: Maybe curry class declaration + .run { createHooksContainer(classDeclaration, toList()) } + .let { sequenceOf(it) } + } else emptySequence() } fun ClassKind.toTypeSpecKind(): TypeSpec.Kind = when (this) { @@ -109,8 +117,7 @@ public class HooksProcessor( ) } - override fun defaultHandler(node: KSNode, data: Unit): List> = - TODO("Not yet implemented") + override fun defaultHandler(node: KSNode, data: Raise>) = TODO("Not yet implemented") } public class Provider : SymbolProcessorProvider { diff --git a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/AnnotationValidations.kt b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/AnnotationValidations.kt index 70dc0cb..53d9009 100644 --- a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/AnnotationValidations.kt +++ b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/AnnotationValidations.kt @@ -1,6 +1,10 @@ package com.intuit.hooks.plugin.ksp.validation import arrow.core.* +import arrow.core.raise.Raise +import arrow.core.raise.ensure +import arrow.core.raise.recover +import arrow.core.raise.zipOrAccumulate import com.google.devtools.ksp.getVisibility import com.google.devtools.ksp.symbol.* import com.intuit.hooks.plugin.codegen.HookInfo @@ -23,11 +27,36 @@ import com.squareup.kotlinpoet.ksp.toTypeName val hookFunctionSignatureReference get() = hookFunctionSignatureType.element as? KSCallableReference ?: throw HooksProcessor.Exception("Hook type argument must be a function for $symbol") - val type get() = toString().let(HookType::valueOf) + // NOTE: THIS IS AMAZING - can provide typical nullable APIs for consumers who don't care about working with the explicit typed errors + val type get() = recover({ type }, { null }) + + // TODO: Maybe put in smart constructor, but this is so cool to be able to provide + // an alternative API for those who would prefer raise over exceptions + context(Raise) val type: HookType get() { + ensure(toString() in HookType.supportedHookTypes) { + HookValidationError.NoCodeGenerator(this) + } + + return HookType.valueOf(toString()) + } override fun toString() = "${symbol.shortName.asString()}Hook" } +context(Raise>) +internal fun KSPropertyDeclaration.validateHookAnnotation(parentResolver: TypeParameterResolver): HookInfo { + val annotation = ensure { onlyHasASingleDslAnnotation() } + + return zipOrAccumulate( + { simpleName.asString() }, + { annotation.hasCodeGenerator() }, + { annotation.mustBeHookType(parentResolver) }, + { annotation.validateParameters(parentResolver) }, + { getVisibility().toKModifier() ?: KModifier.PUBLIC }, + ::HookInfo + ) +} + /** Build [HookInfo] from the validated [HookAnnotation] found on the [property] */ internal fun KSPropertyDeclaration.validateHookAnnotation(parentResolver: TypeParameterResolver): ValidatedNel = onlyHasASingleDslAnnotation().andThen { annotation -> @@ -46,6 +75,16 @@ internal fun KSPropertyDeclaration.validateHookAnnotation(parentResolver: TypePa } } +// TODO: This'd be a good smart constructor use case +context(Raise) private fun KSPropertyDeclaration.onlyHasASingleDslAnnotation(): HookAnnotation { + val annotations = annotations.filter { it.shortName.asString() in annotationDslMarkers }.toList() + return when (annotations.size) { + 0 -> raise(HookValidationError.NoHookDslAnnotations(this)) + 1 -> annotations.single() + else -> raise(HookValidationError.TooManyHookDslAnnotations(annotations, this)) + }.let(::HookAnnotation) +} + private fun KSPropertyDeclaration.onlyHasASingleDslAnnotation(): ValidatedNel { val annotations = annotations.filter { it.shortName.asString() in annotationDslMarkers }.toList() return when (annotations.size) { @@ -55,6 +94,16 @@ private fun KSPropertyDeclaration.onlyHasASingleDslAnnotation(): ValidatedNel) private fun HookAnnotation.validateParameters(parentResolver: TypeParameterResolver): List = try { + hookFunctionSignatureReference.functionParameters.mapIndexed { index: Int, parameter: KSValueParameter -> + val name = parameter.name?.asString() + val type = parameter.type.toTypeName(parentResolver) + HookParameter(name, type, index) + } +} catch (exception: Exception) { + raise(HookValidationError.MustBeHookTypeSignature(this)) +} + private fun validateParameters(annotation: HookAnnotation, parentResolver: TypeParameterResolver): ValidatedNel> = try { annotation.hookFunctionSignatureReference.functionParameters.mapIndexed { index: Int, parameter: KSValueParameter -> val name = parameter.name?.asString() @@ -65,12 +114,35 @@ private fun validateParameters(annotation: HookAnnotation, parentResolver: TypeP HookValidationError.MustBeHookTypeSignature(annotation).invalidNel() } +// TODO: This would be obsolete with smart constructor +context(Raise) private fun HookAnnotation.hasCodeGenerator(): HookType = type + private fun hasCodeGenerator(annotation: HookAnnotation): ValidatedNel = try { - annotation.type.valid() + annotation.type!!.valid() } catch (e: Exception) { HookValidationError.NoCodeGenerator(annotation).invalidNel() } +/** TODO: Another good smart constructor example */ +context(Raise) +private fun HookAnnotation.mustBeHookType(parentResolver: TypeParameterResolver): HookSignature = try { + val isSuspend: Boolean = hookFunctionSignatureType.modifiers.contains(Modifier.SUSPEND) + // I'm leaving this here because KSP knows that it's (String) -> Int, whereas once it gets to Poet, it's just kotlin.Function1 + val text = hookFunctionSignatureType.text + val hookFunctionSignatureType = hookFunctionSignatureType.toTypeName(parentResolver) + val returnType = hookFunctionSignatureReference.returnType.toTypeName(parentResolver) + val returnTypeType = hookFunctionSignatureReference.returnType.element?.typeArguments?.firstOrNull()?.toTypeName(parentResolver) + + HookSignature( + text, + isSuspend, + returnType, + returnTypeType, + hookFunctionSignatureType + ) +} catch (exception: Exception) { + raise(HookValidationError.MustBeHookTypeSignature(this)) +} private fun mustBeHookType(annotation: HookAnnotation, parentResolver: TypeParameterResolver): ValidatedNel = try { val isSuspend: Boolean = annotation.hookFunctionSignatureType.modifiers.contains(Modifier.SUSPEND) // I'm leaving this here because KSP knows that it's (String) -> Int, whereas once it gets to Poet, it's just kotlin.Function1 diff --git a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookPropertyValidations.kt b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookPropertyValidations.kt index 77b07b1..ea583e1 100644 --- a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookPropertyValidations.kt +++ b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookPropertyValidations.kt @@ -1,12 +1,15 @@ package com.intuit.hooks.plugin.ksp.validation -import arrow.core.ValidatedNel -import arrow.core.invalidNel -import arrow.core.valid -import arrow.core.zip +import arrow.core.* +import arrow.core.raise.* import com.google.devtools.ksp.symbol.KSPropertyDeclaration import com.intuit.hooks.plugin.codegen.HookInfo import com.intuit.hooks.plugin.codegen.HookProperty +import kotlin.contracts.CallsInPlace +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind.AT_MOST_ONCE +import kotlin.contracts.contract +import kotlin.experimental.ExperimentalTypeInference internal fun HookProperty.validate( info: HookInfo, @@ -18,6 +21,26 @@ internal fun HookProperty.validate( is HookProperty.Waterfall -> validate(info, property) } +context(Raise>) +internal fun HookProperty.validate( + info: HookInfo, + property: KSPropertyDeclaration, +) { + when (this) { + is HookProperty.Bail -> Unit + is HookProperty.Loop -> Unit + is HookProperty.Async -> raiseSingle { + info.validateAsync(property) + } + is HookProperty.Waterfall -> validate(info, property) + } +} + +context(Raise) +private fun HookInfo.validateAsync(property: KSPropertyDeclaration) { + ensure(hookSignature.isSuspend) { HookValidationError.AsyncHookWithoutSuspend(property) } +} + private fun HookProperty.Async.validate( info: HookInfo, property: KSPropertyDeclaration, @@ -29,9 +52,21 @@ private fun HookProperty.Waterfall.validate( info: HookInfo, property: KSPropertyDeclaration, ): ValidatedNel = - arity(info, property).zip( - parameters(info, property), - ) { _, _ -> this } + Either.zipOrAccumulate( + arity(info, property).toEither(), + parameters(info, property).toEither() + ) { _, _ -> this }.toValidated() + +context(Raise>) +private fun HookProperty.Waterfall.validate( + info: HookInfo, + property: KSPropertyDeclaration, +) { + zipOrAccumulate( + { arity(info, property) }, + { parameters(info, property) }, + ) { _, _ -> } +} private fun HookProperty.Waterfall.arity( info: HookInfo, @@ -41,6 +76,15 @@ private fun HookProperty.Waterfall.arity( else HookValidationError.WaterfallMustHaveParameters(property).invalidNel() } +context(Raise) +private fun HookProperty.Waterfall.arity( + info: HookInfo, + property: KSPropertyDeclaration, +) { + ensure(!info.zeroArity) { HookValidationError.WaterfallMustHaveParameters(property) } +} + + private fun HookProperty.Waterfall.parameters( info: HookInfo, property: KSPropertyDeclaration, @@ -48,3 +92,13 @@ private fun HookProperty.Waterfall.parameters( return if (info.hookSignature.returnType == info.params.firstOrNull()?.type) valid() else HookValidationError.WaterfallParameterTypeMustMatch(property).invalidNel() } + +context(Raise) +private fun HookProperty.Waterfall.parameters( + info: HookInfo, + property: KSPropertyDeclaration, +) { + ensure(info.hookSignature.returnType == info.params.firstOrNull()?.type) { + HookValidationError.WaterfallParameterTypeMustMatch(property) + } +} diff --git a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookValidations.kt b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookValidations.kt index a2c39f0..012d276 100644 --- a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookValidations.kt +++ b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookValidations.kt @@ -1,13 +1,37 @@ package com.intuit.hooks.plugin.ksp.validation import arrow.core.* +import arrow.core.raise.* +import arrow.core.raise.ensure +import com.google.devtools.ksp.processing.KSPLogger import com.google.devtools.ksp.symbol.* import com.intuit.hooks.plugin.codegen.HookInfo import com.intuit.hooks.plugin.ksp.text +import com.intuit.hooks.plugin.ksp.validation.ensure import com.squareup.kotlinpoet.ksp.TypeParameterResolver +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract +import kotlin.experimental.ExperimentalTypeInference + +//context(HookValidationError) +internal fun KSPLogger.error(validationError: HookValidationError) { + error(validationError.message, validationError.symbol) +} + +internal sealed interface LogicalFailure + +/** Logical failure that can be ignored */ +internal sealed interface EdgeCase : LogicalFailure { + class NoHooksDefined(val file: KSFile) : EdgeCase +} + +/** Logical failure that should probably be reported */ +internal sealed interface ErrorCase : LogicalFailure { + val message: String +} // TODO: It'd be nice if the validations were codegen framework agnostic -internal sealed class HookValidationError(val message: String, val symbol: KSNode) { +internal sealed class HookValidationError(override val message: String, val symbol: KSNode) : ErrorCase { class AsyncHookWithoutSuspend(symbol: KSNode) : HookValidationError("Async hooks must be defined with a suspend function signature", symbol) class WaterfallMustHaveParameters(symbol: KSNode) : HookValidationError("Waterfall hooks must take at least one parameter", symbol) class WaterfallParameterTypeMustMatch(symbol: KSNode) : HookValidationError("Waterfall hooks must specify the same types for the first parameter and the return type", symbol) @@ -17,22 +41,90 @@ internal sealed class HookValidationError(val message: String, val symbol: KSNod class TooManyHookDslAnnotations(annotations: List, property: KSPropertyDeclaration) : HookValidationError("This hook has more than a single hook DSL annotation: $annotations", property) class UnsupportedAbstractPropertyType(property: KSPropertyDeclaration) : HookValidationError("Abstract property type (${property.type.text}) not supported. Hook properties must be of type com.intuit.hooks.Hook", property) class NotAnAbstractProperty(property: KSPropertyDeclaration) : HookValidationError("Hooks can only be abstract properties", property) + + operator fun component1(): String = message + + operator fun component2(): KSNode = symbol +} + +context(Raise>) +internal fun KSPropertyDeclaration.validateProperty(parentResolver: TypeParameterResolver): HookInfo { + // 1. validate types + // 2. validation annotation and + // 3. validate properties against type + + // why is validateHookType wrapped in ensure while nothing else is? + // great question! this is because validateHookType is a singularly + // concerned validation function that has an explicitly matching + // raise context. validateHookType will _only_ ever raise a singular + // error, and therefore, shouldn't be treated as if it might have + // many to raise. We use ensure to narrow down the raise type param + // to what we expect, and then unwrap to explicitly re-raise within + // a non-empty-list context. + + ensure { + validateHookType() + } + + return validateHookAnnotation(parentResolver).also { + validateHookProperties(it) + } } /** main entrypoint for validating [KSPropertyDeclaration]s as valid annotated hook members */ -internal fun validateProperty(property: KSPropertyDeclaration, parentResolver: TypeParameterResolver): ValidatedNel = with(property) { - // validate property has the correct type - validateHookType() - .andThen { validateHookAnnotation(parentResolver) } - // validate property against hook info with specific hook type validations - .andThen { info -> validateHookProperties(info) } +//internal fun validateProperty(property: KSPropertyDeclaration, parentResolver: TypeParameterResolver): ValidatedNel = with(property) { +// // validate property has the correct type +// validateHookType() +// .andThen { validateHookAnnotation(parentResolver) } +// // validate property against hook info with specific hook type validations +// .andThen { info -> validateHookProperties(info) } +// +// recover { validateHookType() } +// .map { validateHookProperties(parentResolver) } +// .map { info -> validateHookProperties(info) } +// +// fold( +// { validateHookType() }, +// ) +//} + +context(Raise) +private fun KSPropertyDeclaration.validateHookType() { + ensure(type.text == "Hook") { + HookValidationError.UnsupportedAbstractPropertyType(this) + } } private fun KSPropertyDeclaration.validateHookType(): ValidatedNel = if (type.text == "Hook") type.valid() else HookValidationError.UnsupportedAbstractPropertyType(this).invalidNel() -private fun KSPropertyDeclaration.validateHookProperties(hookInfo: HookInfo) = - hookInfo.hookType.properties.map { it.validate(hookInfo, this) } - .sequence() - .map { hookInfo } + +context(Raise>) private fun KSPropertyDeclaration.validateHookProperties(info: HookInfo) { + info.hookType.properties.map { + it.validate(info, this) + } +} + +//private fun KSPropertyDeclaration.validateHookProperties(hookInfo: HookInfo): Validated, HookInfo> = +// hookInfo.hookType.properties.map { it.validate(hookInfo, this) } +// .sequence() +// .map { hookInfo } + +/** Helper for accumulating errors from single-error validators */ +@RaiseDSL +@OptIn(ExperimentalTypeInference::class) +internal fun Raise>.ensure(@BuilderInference block: Raise.() -> A): A = + recover(block) { e: Error -> raise(e.nel()) } + +/** Helper for accumulating errors from single-error validators */ +@RaiseDSL +public inline fun Raise>.ensure(condition: Boolean, raise: () -> Error) { + recover({ ensure(condition, raise) }) { e: Error -> raise(e.nel()) } +} + +/** Raise a _logical failure_ of type [Error] */ +@RaiseDSL +public inline fun Raise>.raise(r: Error): Nothing { + raise(r.nel()) +} diff --git a/settings.gradle.kts b/settings.gradle.kts index f38845c..b197cfd 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,7 +15,7 @@ dependencyResolutionManagement { create("libs") { version("kotlin", "1.6.21") version("ktlint", "0.45.2") - version("arrow", "1.1.2") + version("arrow", "1.2.0-RC") version("ksp", "1.6.21-1.0.6") version("poet", "1.12.0") version("junit", "5.7.0") From d0ce3b10872546f06d74fa205020dd2e8be9130e Mon Sep 17 00:00:00 2001 From: Jeremiah Zucker Date: Tue, 16 May 2023 09:59:01 -0400 Subject: [PATCH 2/5] WIP: Validated -> Raise 2 --- .../kotlin/com/intuit/hooks/plugin/Raise.kt | 29 ++++++++ .../intuit/hooks/plugin/ksp/HooksProcessor.kt | 2 + .../ksp/validation/AnnotationValidations.kt | 63 +---------------- .../ksp/validation/HookPropertyValidations.kt | 45 +----------- .../plugin/ksp/validation/HookValidations.kt | 69 ++----------------- .../plugin/ksp/validation/LogicalFailure.kt | 16 +++++ 6 files changed, 56 insertions(+), 168 deletions(-) create mode 100644 processor/src/main/kotlin/com/intuit/hooks/plugin/Raise.kt create mode 100644 processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/LogicalFailure.kt diff --git a/processor/src/main/kotlin/com/intuit/hooks/plugin/Raise.kt b/processor/src/main/kotlin/com/intuit/hooks/plugin/Raise.kt new file mode 100644 index 0000000..e0aa4b5 --- /dev/null +++ b/processor/src/main/kotlin/com/intuit/hooks/plugin/Raise.kt @@ -0,0 +1,29 @@ +package com.intuit.hooks.plugin + +import arrow.core.Nel +import arrow.core.nel +import arrow.core.raise.Raise +import arrow.core.raise.RaiseDSL +import arrow.core.raise.ensure +import arrow.core.raise.recover +import kotlin.experimental.ExperimentalTypeInference + +// Collection of [Raise] helpers for accumulating errors from a single error context + +/** Helper for accumulating errors from single-error validators */ +@RaiseDSL +@OptIn(ExperimentalTypeInference::class) +internal fun Raise>.ensure(@BuilderInference block: Raise.() -> A): A = + recover(block) { e: Error -> raise(e.nel()) } + +/** Helper for accumulating errors from single-error validators */ +@RaiseDSL +public inline fun Raise>.ensure(condition: Boolean, raise: () -> Error) { + recover({ ensure(condition, raise) }) { e: Error -> raise(e.nel()) } +} + +/** Raise a _logical failure_ of type [Error] */ +@RaiseDSL +public inline fun Raise>.raise(r: Error): Nothing { + raise(r.nel()) +} \ No newline at end of file diff --git a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/HooksProcessor.kt b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/HooksProcessor.kt index d018b01..03c962c 100644 --- a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/HooksProcessor.kt +++ b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/HooksProcessor.kt @@ -9,11 +9,13 @@ import com.google.devtools.ksp.symbol.* import com.google.devtools.ksp.validate import com.google.devtools.ksp.visitor.KSDefaultVisitor import com.intuit.hooks.plugin.codegen.* +import com.intuit.hooks.plugin.ensure import com.intuit.hooks.plugin.ksp.validation.* import com.intuit.hooks.plugin.ksp.validation.EdgeCase import com.intuit.hooks.plugin.ksp.validation.HookValidationError import com.intuit.hooks.plugin.ksp.validation.error import com.intuit.hooks.plugin.ksp.validation.validateProperty +import com.intuit.hooks.plugin.raise import com.squareup.kotlinpoet.* import com.squareup.kotlinpoet.ksp.* diff --git a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/AnnotationValidations.kt b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/AnnotationValidations.kt index 53d9009..44f8087 100644 --- a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/AnnotationValidations.kt +++ b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/AnnotationValidations.kt @@ -12,6 +12,7 @@ import com.intuit.hooks.plugin.codegen.HookParameter import com.intuit.hooks.plugin.codegen.HookSignature import com.intuit.hooks.plugin.codegen.HookType import com.intuit.hooks.plugin.codegen.HookType.Companion.annotationDslMarkers +import com.intuit.hooks.plugin.ensure import com.intuit.hooks.plugin.ksp.HooksProcessor import com.intuit.hooks.plugin.ksp.text import com.squareup.kotlinpoet.KModifier @@ -43,6 +44,7 @@ import com.squareup.kotlinpoet.ksp.toTypeName override fun toString() = "${symbol.shortName.asString()}Hook" } +/** Build [HookInfo] from the validated [HookAnnotation] found on the [property] */ context(Raise>) internal fun KSPropertyDeclaration.validateHookAnnotation(parentResolver: TypeParameterResolver): HookInfo { val annotation = ensure { onlyHasASingleDslAnnotation() } @@ -57,24 +59,6 @@ internal fun KSPropertyDeclaration.validateHookAnnotation(parentResolver: TypePa ) } -/** Build [HookInfo] from the validated [HookAnnotation] found on the [property] */ -internal fun KSPropertyDeclaration.validateHookAnnotation(parentResolver: TypeParameterResolver): ValidatedNel = - onlyHasASingleDslAnnotation().andThen { annotation -> - - val hasCodeGenerator = hasCodeGenerator(annotation) - val mustBeHookType = mustBeHookType(annotation, parentResolver) - val validateParameters = validateParameters(annotation, parentResolver) - val hookMember = simpleName.asString() - val propertyVisibility = this.getVisibility().toKModifier() ?: KModifier.PUBLIC - - hasCodeGenerator.zip( - mustBeHookType, - validateParameters - ) { hookType: HookType, hookSignature: HookSignature, hookParameters: List -> - HookInfo(hookMember, hookType, hookSignature, hookParameters, propertyVisibility) - } - } - // TODO: This'd be a good smart constructor use case context(Raise) private fun KSPropertyDeclaration.onlyHasASingleDslAnnotation(): HookAnnotation { val annotations = annotations.filter { it.shortName.asString() in annotationDslMarkers }.toList() @@ -85,15 +69,6 @@ context(Raise) private fun KSPropertyDeclaration.onlyHasASi }.let(::HookAnnotation) } -private fun KSPropertyDeclaration.onlyHasASingleDslAnnotation(): ValidatedNel { - val annotations = annotations.filter { it.shortName.asString() in annotationDslMarkers }.toList() - return when (annotations.size) { - 0 -> HookValidationError.NoHookDslAnnotations(this).invalidNel() - 1 -> annotations.single().let(::HookAnnotation).valid() - else -> HookValidationError.TooManyHookDslAnnotations(annotations, this).invalidNel() - } -} - context(Raise) private fun HookAnnotation.validateParameters(parentResolver: TypeParameterResolver): List = try { hookFunctionSignatureReference.functionParameters.mapIndexed { index: Int, parameter: KSValueParameter -> val name = parameter.name?.asString() @@ -104,25 +79,9 @@ context(Raise) private fun HookAnnotation.validateParameter raise(HookValidationError.MustBeHookTypeSignature(this)) } -private fun validateParameters(annotation: HookAnnotation, parentResolver: TypeParameterResolver): ValidatedNel> = try { - annotation.hookFunctionSignatureReference.functionParameters.mapIndexed { index: Int, parameter: KSValueParameter -> - val name = parameter.name?.asString() - val type = parameter.type.toTypeName(parentResolver) - HookParameter(name, type, index) - }.valid() -} catch (exception: Exception) { - HookValidationError.MustBeHookTypeSignature(annotation).invalidNel() -} - // TODO: This would be obsolete with smart constructor context(Raise) private fun HookAnnotation.hasCodeGenerator(): HookType = type -private fun hasCodeGenerator(annotation: HookAnnotation): ValidatedNel = try { - annotation.type!!.valid() -} catch (e: Exception) { - HookValidationError.NoCodeGenerator(annotation).invalidNel() -} - /** TODO: Another good smart constructor example */ context(Raise) private fun HookAnnotation.mustBeHookType(parentResolver: TypeParameterResolver): HookSignature = try { @@ -143,21 +102,3 @@ private fun HookAnnotation.mustBeHookType(parentResolver: TypeParameterResolver) } catch (exception: Exception) { raise(HookValidationError.MustBeHookTypeSignature(this)) } -private fun mustBeHookType(annotation: HookAnnotation, parentResolver: TypeParameterResolver): ValidatedNel = try { - val isSuspend: Boolean = annotation.hookFunctionSignatureType.modifiers.contains(Modifier.SUSPEND) - // I'm leaving this here because KSP knows that it's (String) -> Int, whereas once it gets to Poet, it's just kotlin.Function1 - val text = annotation.hookFunctionSignatureType.text - val hookFunctionSignatureType = annotation.hookFunctionSignatureType.toTypeName(parentResolver) - val returnType = annotation.hookFunctionSignatureReference.returnType.toTypeName(parentResolver) - val returnTypeType = annotation.hookFunctionSignatureReference.returnType.element?.typeArguments?.firstOrNull()?.toTypeName(parentResolver) - - HookSignature( - text, - isSuspend, - returnType, - returnTypeType, - hookFunctionSignatureType - ).valid() -} catch (exception: Exception) { - HookValidationError.MustBeHookTypeSignature(annotation).invalidNel() -} diff --git a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookPropertyValidations.kt b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookPropertyValidations.kt index ea583e1..c35dbd8 100644 --- a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookPropertyValidations.kt +++ b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookPropertyValidations.kt @@ -5,21 +5,13 @@ import arrow.core.raise.* import com.google.devtools.ksp.symbol.KSPropertyDeclaration import com.intuit.hooks.plugin.codegen.HookInfo import com.intuit.hooks.plugin.codegen.HookProperty +import com.intuit.hooks.plugin.ensure import kotlin.contracts.CallsInPlace import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind.AT_MOST_ONCE import kotlin.contracts.contract import kotlin.experimental.ExperimentalTypeInference -internal fun HookProperty.validate( - info: HookInfo, - property: KSPropertyDeclaration, -): ValidatedNel = when (this) { - is HookProperty.Bail -> valid() - is HookProperty.Loop -> valid() - is HookProperty.Async -> validate(info, property) - is HookProperty.Waterfall -> validate(info, property) -} context(Raise>) internal fun HookProperty.validate( @@ -29,7 +21,7 @@ internal fun HookProperty.validate( when (this) { is HookProperty.Bail -> Unit is HookProperty.Loop -> Unit - is HookProperty.Async -> raiseSingle { + is HookProperty.Async -> ensure { info.validateAsync(property) } is HookProperty.Waterfall -> validate(info, property) @@ -41,22 +33,6 @@ private fun HookInfo.validateAsync(property: KSPropertyDeclaration) { ensure(hookSignature.isSuspend) { HookValidationError.AsyncHookWithoutSuspend(property) } } -private fun HookProperty.Async.validate( - info: HookInfo, - property: KSPropertyDeclaration, -): ValidatedNel = - if (info.hookSignature.isSuspend) valid() - else HookValidationError.AsyncHookWithoutSuspend(property).invalidNel() - -private fun HookProperty.Waterfall.validate( - info: HookInfo, - property: KSPropertyDeclaration, -): ValidatedNel = - Either.zipOrAccumulate( - arity(info, property).toEither(), - parameters(info, property).toEither() - ) { _, _ -> this }.toValidated() - context(Raise>) private fun HookProperty.Waterfall.validate( info: HookInfo, @@ -68,14 +44,6 @@ private fun HookProperty.Waterfall.validate( ) { _, _ -> } } -private fun HookProperty.Waterfall.arity( - info: HookInfo, - property: KSPropertyDeclaration, -): ValidatedNel { - return if (!info.zeroArity) valid() - else HookValidationError.WaterfallMustHaveParameters(property).invalidNel() -} - context(Raise) private fun HookProperty.Waterfall.arity( info: HookInfo, @@ -84,15 +52,6 @@ private fun HookProperty.Waterfall.arity( ensure(!info.zeroArity) { HookValidationError.WaterfallMustHaveParameters(property) } } - -private fun HookProperty.Waterfall.parameters( - info: HookInfo, - property: KSPropertyDeclaration, -): ValidatedNel { - return if (info.hookSignature.returnType == info.params.firstOrNull()?.type) valid() - else HookValidationError.WaterfallParameterTypeMustMatch(property).invalidNel() -} - context(Raise) private fun HookProperty.Waterfall.parameters( info: HookInfo, diff --git a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookValidations.kt b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookValidations.kt index 012d276..cae6a07 100644 --- a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookValidations.kt +++ b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookValidations.kt @@ -6,28 +6,13 @@ import arrow.core.raise.ensure import com.google.devtools.ksp.processing.KSPLogger import com.google.devtools.ksp.symbol.* import com.intuit.hooks.plugin.codegen.HookInfo +import com.intuit.hooks.plugin.ensure import com.intuit.hooks.plugin.ksp.text -import com.intuit.hooks.plugin.ksp.validation.ensure import com.squareup.kotlinpoet.ksp.TypeParameterResolver -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract -import kotlin.experimental.ExperimentalTypeInference -//context(HookValidationError) -internal fun KSPLogger.error(validationError: HookValidationError) { - error(validationError.message, validationError.symbol) -} - -internal sealed interface LogicalFailure - -/** Logical failure that can be ignored */ -internal sealed interface EdgeCase : LogicalFailure { - class NoHooksDefined(val file: KSFile) : EdgeCase -} - -/** Logical failure that should probably be reported */ -internal sealed interface ErrorCase : LogicalFailure { - val message: String +context(HookValidationError) +internal fun KSPLogger.error() { + error(message, symbol) } // TODO: It'd be nice if the validations were codegen framework agnostic @@ -47,6 +32,7 @@ internal sealed class HookValidationError(override val message: String, val symb operator fun component2(): KSNode = symbol } +/** main entrypoint for validating [KSPropertyDeclaration]s as valid annotated hook members */ context(Raise>) internal fun KSPropertyDeclaration.validateProperty(parentResolver: TypeParameterResolver): HookInfo { // 1. validate types @@ -71,23 +57,6 @@ internal fun KSPropertyDeclaration.validateProperty(parentResolver: TypeParamete } } -/** main entrypoint for validating [KSPropertyDeclaration]s as valid annotated hook members */ -//internal fun validateProperty(property: KSPropertyDeclaration, parentResolver: TypeParameterResolver): ValidatedNel = with(property) { -// // validate property has the correct type -// validateHookType() -// .andThen { validateHookAnnotation(parentResolver) } -// // validate property against hook info with specific hook type validations -// .andThen { info -> validateHookProperties(info) } -// -// recover { validateHookType() } -// .map { validateHookProperties(parentResolver) } -// .map { info -> validateHookProperties(info) } -// -// fold( -// { validateHookType() }, -// ) -//} - context(Raise) private fun KSPropertyDeclaration.validateHookType() { ensure(type.text == "Hook") { @@ -95,36 +64,8 @@ private fun KSPropertyDeclaration.validateHookType() { } } -private fun KSPropertyDeclaration.validateHookType(): ValidatedNel = - if (type.text == "Hook") type.valid() - else HookValidationError.UnsupportedAbstractPropertyType(this).invalidNel() - - context(Raise>) private fun KSPropertyDeclaration.validateHookProperties(info: HookInfo) { info.hookType.properties.map { it.validate(info, this) } } - -//private fun KSPropertyDeclaration.validateHookProperties(hookInfo: HookInfo): Validated, HookInfo> = -// hookInfo.hookType.properties.map { it.validate(hookInfo, this) } -// .sequence() -// .map { hookInfo } - -/** Helper for accumulating errors from single-error validators */ -@RaiseDSL -@OptIn(ExperimentalTypeInference::class) -internal fun Raise>.ensure(@BuilderInference block: Raise.() -> A): A = - recover(block) { e: Error -> raise(e.nel()) } - -/** Helper for accumulating errors from single-error validators */ -@RaiseDSL -public inline fun Raise>.ensure(condition: Boolean, raise: () -> Error) { - recover({ ensure(condition, raise) }) { e: Error -> raise(e.nel()) } -} - -/** Raise a _logical failure_ of type [Error] */ -@RaiseDSL -public inline fun Raise>.raise(r: Error): Nothing { - raise(r.nel()) -} diff --git a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/LogicalFailure.kt b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/LogicalFailure.kt new file mode 100644 index 0000000..d07274c --- /dev/null +++ b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/LogicalFailure.kt @@ -0,0 +1,16 @@ +package com.intuit.hooks.plugin.ksp.validation + +import com.google.devtools.ksp.symbol.KSFile + +/** Base construct to represent a reason to not execute happy-path logic */ +internal sealed interface LogicalFailure + +/** Logical failure that can be ignored, valid edge case */ +internal sealed interface EdgeCase : LogicalFailure { + class NoHooksDefined(val file: KSFile) : EdgeCase +} + +/** Logical failure that should probably be reported, something bad happened */ +internal sealed interface ErrorCase : LogicalFailure { + val message: String +} \ No newline at end of file From 75b46cd101deb5293c4356006e87f604fa1782b7 Mon Sep 17 00:00:00 2001 From: Jeremiah Zucker Date: Wed, 17 May 2023 16:16:55 -0400 Subject: [PATCH 3/5] working raise implementation --- .../com/intuit/hooks/AsyncSeriesBailHook.kt | 1 + .../kotlin/com/intuit/hooks/SyncBailHook.kt | 1 + processor/api/processor.api | 11 + .../kotlin/com/intuit/hooks/plugin/Raise.kt | 72 ++++- .../intuit/hooks/plugin/codegen/HookType.kt | 4 +- .../intuit/hooks/plugin/ksp/HooksProcessor.kt | 52 ++-- .../intuit/hooks/plugin/ksp/KSRaiseVisitor.kt | 257 ++++++++++++++++++ .../ksp/validation/AnnotationValidations.kt | 9 + .../ksp/validation/HookPropertyValidations.kt | 6 - .../plugin/ksp/validation/HookValidations.kt | 45 ++- .../plugin/ksp/validation/LogicalFailure.kt | 2 +- .../hooks/plugin/HookValidationErrors.kt | 21 +- settings.gradle.kts | 12 +- 13 files changed, 404 insertions(+), 89 deletions(-) create mode 100644 processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/KSRaiseVisitor.kt diff --git a/hooks/src/main/kotlin/com/intuit/hooks/AsyncSeriesBailHook.kt b/hooks/src/main/kotlin/com/intuit/hooks/AsyncSeriesBailHook.kt index 658a915..845d1f1 100644 --- a/hooks/src/main/kotlin/com/intuit/hooks/AsyncSeriesBailHook.kt +++ b/hooks/src/main/kotlin/com/intuit/hooks/AsyncSeriesBailHook.kt @@ -7,6 +7,7 @@ public abstract class AsyncSeriesBailHook>, R> : Asyn taps.forEach { tapInfo -> when (val result = invokeWithContext(tapInfo.f, context)) { is BailResult.Bail -> return@call result.value + is BailResult.Continue<*> -> Unit } } diff --git a/hooks/src/main/kotlin/com/intuit/hooks/SyncBailHook.kt b/hooks/src/main/kotlin/com/intuit/hooks/SyncBailHook.kt index a8921d1..b029e97 100644 --- a/hooks/src/main/kotlin/com/intuit/hooks/SyncBailHook.kt +++ b/hooks/src/main/kotlin/com/intuit/hooks/SyncBailHook.kt @@ -13,6 +13,7 @@ public abstract class SyncBailHook>, R> : SyncBaseHoo taps.forEach { tapInfo -> when (val result = invokeWithContext(tapInfo.f, context)) { is BailResult.Bail -> return@call result.value + is BailResult.Continue<*> -> Unit } } diff --git a/processor/api/processor.api b/processor/api/processor.api index 74f2001..ffc7588 100644 --- a/processor/api/processor.api +++ b/processor/api/processor.api @@ -1,3 +1,14 @@ +public final class com/intuit/hooks/plugin/RaiseKt { + public static final fun accumulate (Larrow/core/raise/Raise;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function2;)V + public static final fun ensure (Larrow/core/raise/Raise;ZLkotlin/jvm/functions/Function0;)V + public static final fun mapOrAccumulate (Larrow/core/raise/Raise;Larrow/core/NonEmptyList;Lkotlin/jvm/functions/Function2;)Larrow/core/NonEmptyList; + public static final fun mapOrAccumulate (Larrow/core/raise/Raise;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function2;)Ljava/util/List; + public static final fun mapOrAccumulate (Larrow/core/raise/Raise;Lkotlin/sequences/Sequence;Lkotlin/jvm/functions/Function2;)Ljava/util/List; + public static final fun mapOrAccumulate-jkbboic (Larrow/core/raise/Raise;Ljava/util/Set;Lkotlin/jvm/functions/Function2;)Ljava/util/Set; + public static final fun raise (Larrow/core/raise/Raise;Ljava/lang/Object;)Ljava/lang/Void; + public static final fun raiseAll (Larrow/core/raise/Raise;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function2;)Ljava/util/List; +} + public final class com/intuit/hooks/plugin/ksp/HooksProcessor : com/google/devtools/ksp/processing/SymbolProcessor { public fun (Lcom/google/devtools/ksp/processing/CodeGenerator;Lcom/google/devtools/ksp/processing/KSPLogger;)V public fun process (Lcom/google/devtools/ksp/processing/Resolver;)Ljava/util/List; diff --git a/processor/src/main/kotlin/com/intuit/hooks/plugin/Raise.kt b/processor/src/main/kotlin/com/intuit/hooks/plugin/Raise.kt index e0aa4b5..58e5740 100644 --- a/processor/src/main/kotlin/com/intuit/hooks/plugin/Raise.kt +++ b/processor/src/main/kotlin/com/intuit/hooks/plugin/Raise.kt @@ -1,18 +1,15 @@ +@file:OptIn(ExperimentalTypeInference::class) + package com.intuit.hooks.plugin -import arrow.core.Nel -import arrow.core.nel -import arrow.core.raise.Raise -import arrow.core.raise.RaiseDSL -import arrow.core.raise.ensure -import arrow.core.raise.recover +import arrow.core.* +import arrow.core.raise.* import kotlin.experimental.ExperimentalTypeInference // Collection of [Raise] helpers for accumulating errors from a single error context /** Helper for accumulating errors from single-error validators */ @RaiseDSL -@OptIn(ExperimentalTypeInference::class) internal fun Raise>.ensure(@BuilderInference block: Raise.() -> A): A = recover(block) { e: Error -> raise(e.nel()) } @@ -22,8 +19,65 @@ public inline fun Raise>.ensure(condition: Boolean, raise: () recover({ ensure(condition, raise) }) { e: Error -> raise(e.nel()) } } -/** Raise a _logical failure_ of type [Error] */ +/** Raise a _logical failure_ of type [Error] in a multi-[Error] accumulator */ @RaiseDSL public inline fun Raise>.raise(r: Error): Nothing { raise(r.nel()) -} \ No newline at end of file +} + +@RaiseDSL +public inline fun Raise>.raiseAll( + iterable: Iterable, + @BuilderInference transform: Raise>.(A) -> Unit +): List = mapOrAccumulate(iterable) { arg -> + recover, Unit>({ transform(arg) }) { errors -> + this@raiseAll.raise(errors) + } +} + +/** Explicitly accumulate errors that may have been raised while processing each element */ +context(Raise>) +@RaiseDSL +public inline fun Iterable.accumulate( + @BuilderInference operation: Raise>.(A) -> Unit +) { + flatMap { + recover({ + operation(it); emptyList() + }) { it } + }.toNonEmptyListOrNull()?.let { raise(it) } +} + +/** [mapOrAccumulate] variant that accumulates errors from a validator that may raise multiple errors */ +context(Raise>) +@RaiseDSL +public inline fun Sequence.mapOrAccumulate( // TODO: Consider renaming + @BuilderInference operation: Raise>.(A) -> B +): List = toList().mapOrAccumulate(operation) + +/** [mapOrAccumulate] variant that accumulates errors from a validator that may raise multiple errors */ +context(Raise>) +@RaiseDSL +public inline fun Iterable.mapOrAccumulate( // TODO: Consider renaming + @BuilderInference operation: Raise>.(A) -> B +): List = recover({ + mapOrAccumulate(this@mapOrAccumulate) { operation(it) } +}) { errors -> raise(errors.flatMap { it }) } + +/** [mapOrAccumulate] variant that accumulates errors from a validator that may raise multiple errors */ +context(Raise>) +@RaiseDSL +public inline fun NonEmptyList.mapOrAccumulate( // TODO: Consider renaming + @BuilderInference operation: Raise>.(A) -> B +): NonEmptyList = recover({ + mapOrAccumulate(this@mapOrAccumulate) { operation(it) } +}) { errors -> raise(errors.flatMap { it }) } + +/** [mapOrAccumulate] variant that accumulates errors from a validator that may raise multiple errors */ +context(Raise>) +@RaiseDSL +public inline fun NonEmptySet.mapOrAccumulate( // TODO: Consider renaming + @BuilderInference operation: Raise>.(A) -> B +): NonEmptySet = recover({ + mapOrAccumulate(this@mapOrAccumulate) { operation(it) } +}) { errors -> raise(errors.flatMap { it }) } diff --git a/processor/src/main/kotlin/com/intuit/hooks/plugin/codegen/HookType.kt b/processor/src/main/kotlin/com/intuit/hooks/plugin/codegen/HookType.kt index dda4204..78175af 100644 --- a/processor/src/main/kotlin/com/intuit/hooks/plugin/codegen/HookType.kt +++ b/processor/src/main/kotlin/com/intuit/hooks/plugin/codegen/HookType.kt @@ -7,7 +7,7 @@ internal sealed class HookProperty { object Waterfall : HookProperty() } -internal enum class HookType(vararg val properties: HookProperty) { +internal enum class HookType(val properties: Set) { SyncHook, SyncBailHook(HookProperty.Bail), SyncWaterfallHook(HookProperty.Waterfall), @@ -19,6 +19,8 @@ internal enum class HookType(vararg val properties: HookProperty) { AsyncSeriesWaterfallHook(HookProperty.Async, HookProperty.Waterfall), AsyncSeriesLoopHook(HookProperty.Async, HookProperty.Loop); + constructor(vararg properties: HookProperty) : this(properties.toSet()) + companion object { val supportedHookTypes = values().map(HookType::name) diff --git a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/HooksProcessor.kt b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/HooksProcessor.kt index 03c962c..70a499f 100644 --- a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/HooksProcessor.kt +++ b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/HooksProcessor.kt @@ -7,21 +7,20 @@ import com.google.devtools.ksp.getVisibility import com.google.devtools.ksp.processing.* import com.google.devtools.ksp.symbol.* import com.google.devtools.ksp.validate -import com.google.devtools.ksp.visitor.KSDefaultVisitor import com.intuit.hooks.plugin.codegen.* -import com.intuit.hooks.plugin.ensure import com.intuit.hooks.plugin.ksp.validation.* import com.intuit.hooks.plugin.ksp.validation.EdgeCase import com.intuit.hooks.plugin.ksp.validation.HookValidationError import com.intuit.hooks.plugin.ksp.validation.error import com.intuit.hooks.plugin.ksp.validation.validateProperty +import com.intuit.hooks.plugin.mapOrAccumulate import com.intuit.hooks.plugin.raise import com.squareup.kotlinpoet.* import com.squareup.kotlinpoet.ksp.* public class HooksProcessor( private val codeGenerator: CodeGenerator, - private val logger: KSPLogger, + private val logger: KSPLogger ) : SymbolProcessor { override fun process(resolver: Resolver): List { @@ -32,18 +31,10 @@ public class HooksProcessor( return emptyList() } - private inner class HookPropertyVisitor : KSDefaultVisitor() { - + private inner class HookPropertyVisitor : KSRaiseVisitor() { context(Raise>) - override fun visitPropertyDeclaration(property: KSPropertyDeclaration, parentResolver: TypeParameterResolver): HookInfo { - ensure(property.modifiers.contains(Modifier.ABSTRACT)) { - HookValidationError.NotAnAbstractProperty(property) - } - - return property.validateProperty(parentResolver) - } - - override fun defaultHandler(node: KSNode, data: TypeParameterResolver) = error("Should not happen.") + override fun visitPropertyDeclaration(property: KSPropertyDeclaration, data: TypeParameterResolver): HookInfo = + property.validateProperty(data) } private inner class HookFileVisitor : KSVisitorVoid() { @@ -51,7 +42,7 @@ public class HooksProcessor( recover({ val containers = file.declarations .filterIsInstance() - .flatMap { it.accept(HookContainerVisitor(), this) } + .flatMap { it.accept(HookContainerVisitor(), Unit) } .ifEmpty { raise(EdgeCase.NoHooksDefined(file)) } val packageName = file.packageName.asString() @@ -69,12 +60,10 @@ public class HooksProcessor( } } - private inner class HookContainerVisitor : KSDefaultVisitor>, Sequence>() { - // TODO: Try with context receiver - override fun visitClassDeclaration( - classDeclaration: KSClassDeclaration, - raise: Raise> - ): Sequence = with(raise) { + private inner class HookContainerVisitor : KSRaiseVisitor, HookValidationError>() { + + context(Raise>) + override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit): Sequence { val superTypeNames = classDeclaration.superTypes .filter { it.toString().contains("Hooks") } .toList() @@ -82,32 +71,35 @@ public class HooksProcessor( return if (superTypeNames.isEmpty()) { classDeclaration.declarations .filter { it is KSClassDeclaration && it.validate() /* TODO: Tie in validations to KSP */ } - .flatMap { it.accept(this@HookContainerVisitor, raise) } + .flatMap { it.accept(this@HookContainerVisitor, Unit) } } else if (superTypeNames.any { it.resolve().declaration.qualifiedName?.getQualifier() == "com.intuit.hooks.dsl" }) { val parentResolver = classDeclaration.typeParameters.toTypeParameterResolver() classDeclaration.getAllProperties() - .map { it.accept(HookPropertyVisitor(), parentResolver) } - // TODO: Maybe curry class declaration - .run { createHooksContainer(classDeclaration, toList()) } + .mapOrAccumulate { it.accept(HookPropertyVisitor(), parentResolver) } + .let { createHooksContainer(classDeclaration, it) } .let { sequenceOf(it) } - } else emptySequence() + } else { + emptySequence() + } } - fun ClassKind.toTypeSpecKind(): TypeSpec.Kind = when (this) { + context(Raise>) + fun KSClassDeclaration.toTypeSpecKind(): TypeSpec.Kind = when (classKind) { ClassKind.CLASS -> TypeSpec.Kind.CLASS ClassKind.INTERFACE -> TypeSpec.Kind.INTERFACE ClassKind.OBJECT -> TypeSpec.Kind.OBJECT - else -> throw NotImplementedError("Hooks in constructs other than class, interface, and object aren't supported") + else -> raise(HookValidationError.UnsupportedContainer(this)) } + context(Raise>) fun createHooksContainer(classDeclaration: KSClassDeclaration, hooks: List): HooksContainer { val name = "${classDeclaration.parentDeclaration?.simpleName?.asString() ?: ""}${classDeclaration.simpleName.asString()}Impl" val visibilityModifier = classDeclaration.getVisibility().toKModifier() ?: KModifier.PUBLIC val typeArguments = classDeclaration.typeParameters.map { it.toTypeVariableName() } val className = classDeclaration.toClassName() - val typeSpecKind = classDeclaration.classKind.toTypeSpecKind() + val typeSpecKind = classDeclaration.toTypeSpecKind() return HooksContainer( name, @@ -118,8 +110,6 @@ public class HooksProcessor( hooks ) } - - override fun defaultHandler(node: KSNode, data: Raise>) = TODO("Not yet implemented") } public class Provider : SymbolProcessorProvider { diff --git a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/KSRaiseVisitor.kt b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/KSRaiseVisitor.kt new file mode 100644 index 0000000..f463e50 --- /dev/null +++ b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/KSRaiseVisitor.kt @@ -0,0 +1,257 @@ +package com.intuit.hooks.plugin.ksp + +import arrow.core.Nel +import arrow.core.raise.Raise +import com.google.devtools.ksp.symbol.* +import com.google.devtools.ksp.visitor.KSDefaultVisitor +import com.intuit.hooks.plugin.ksp.RaiseContext.Companion.RaiseContext + +internal data class RaiseContext( + val raise: Raise>, + val data: D +) { + companion object { + fun Raise>.RaiseContext(data: D): RaiseContext = RaiseContext(this, data) + } +} + +/** Visitor extension to execute all visitations within the context of a [Raise] */ +internal abstract class KSRaiseVisitor : KSDefaultVisitor, R>() { + + context(Raise>) + open fun defaultHandler(node: KSNode, data: D): R = error("KSRaiseVisitor default implementation. This shouldn't happen unless the visitor doesn't provide the right overrides.") + + final override fun defaultHandler(node: KSNode, data: RaiseContext): R = with(data.raise) { + defaultHandler(node, data.data) + } + + context(Raise>) + open fun visitNode(node: KSNode, data: D): R { + return defaultHandler(node, data) + } + + final override fun visitNode(node: KSNode, data: RaiseContext): R = with(data.raise) { + visitNode(node, data.data) + } + + context(Raise>) + open fun visitAnnotated(annotated: KSAnnotated, data: D): R { + return defaultHandler(annotated, data) + } + + final override fun visitAnnotated(annotated: KSAnnotated, data: RaiseContext): R = with(data.raise) { + visitAnnotated(annotated, data.data) + } + + context(Raise>) + open fun visitAnnotation(annotation: KSAnnotation, data: D): R { + return defaultHandler(annotation, data) + } + + final override fun visitAnnotation(annotation: KSAnnotation, data: RaiseContext): R = with(data.raise) { + visitAnnotation(annotation, data.data) + } + + context(Raise>) + open fun visitModifierListOwner(modifierListOwner: KSModifierListOwner, data: D): R { + return defaultHandler(modifierListOwner, data) + } + + final override fun visitModifierListOwner(modifierListOwner: KSModifierListOwner, data: RaiseContext): R = with(data.raise) { + visitModifierListOwner(modifierListOwner, data.data) + } + + context(Raise>) + open fun visitDeclaration(declaration: KSDeclaration, data: D): R { + return defaultHandler(declaration, data) + } + + final override fun visitDeclaration(declaration: KSDeclaration, data: RaiseContext): R = with(data.raise) { + visitDeclaration(declaration, data.data) + } + + context(Raise>) + open fun visitDeclarationContainer(declarationContainer: KSDeclarationContainer, data: D): R { + return defaultHandler(declarationContainer, data) + } + + final override fun visitDeclarationContainer(declarationContainer: KSDeclarationContainer, data: RaiseContext): R = with(data.raise) { + visitDeclarationContainer(declarationContainer, data.data) + } + + context(Raise>) + open fun visitDynamicReference(reference: KSDynamicReference, data: D): R { + return defaultHandler(reference, data) + } + + final override fun visitDynamicReference(reference: KSDynamicReference, data: RaiseContext): R = with(data.raise) { + visitDynamicReference(reference, data.data) + } + + context(Raise>) + open fun visitFile(file: KSFile, data: D): R { + return defaultHandler(file, data) + } + + final override fun visitFile(file: KSFile, data: RaiseContext): R = with(data.raise) { + visitFile(file, data.data) + } + + context(Raise>) + open fun visitFunctionDeclaration(function: KSFunctionDeclaration, data: D): R { + return defaultHandler(function, data) + } + + final override fun visitFunctionDeclaration(function: KSFunctionDeclaration, data: RaiseContext): R = with(data.raise) { + visitFunctionDeclaration(function, data.data) + } + + context(Raise>) + open fun visitCallableReference(reference: KSCallableReference, data: D): R { + return defaultHandler(reference, data) + } + + final override fun visitCallableReference(reference: KSCallableReference, data: RaiseContext): R = with(data.raise) { + visitCallableReference(reference, data.data) + } + + context(Raise>) + open fun visitParenthesizedReference(reference: KSParenthesizedReference, data: D): R { + return defaultHandler(reference, data) + } + + final override fun visitParenthesizedReference(reference: KSParenthesizedReference, data: RaiseContext): R = with(data.raise) { + visitParenthesizedReference(reference, data.data) + } + + context(Raise>) + open fun visitPropertyDeclaration(property: KSPropertyDeclaration, data: D): R { + return defaultHandler(property, data) + } + + final override fun visitPropertyDeclaration(property: KSPropertyDeclaration, data: RaiseContext): R = with(data.raise) { + visitPropertyDeclaration(property, data.data) + } + + context(Raise>) + open fun visitPropertyAccessor(accessor: KSPropertyAccessor, data: D): R { + return defaultHandler(accessor, data) + } + + final override fun visitPropertyAccessor(accessor: KSPropertyAccessor, data: RaiseContext): R = with(data.raise) { + visitPropertyAccessor(accessor, data.data) + } + + context(Raise>) + open fun visitPropertyGetter(getter: KSPropertyGetter, data: D): R { + return defaultHandler(getter, data) + } + + final override fun visitPropertyGetter(getter: KSPropertyGetter, data: RaiseContext): R = with(data.raise) { + visitPropertyGetter(getter, data.data) + } + + context(Raise>) + open fun visitPropertySetter(setter: KSPropertySetter, data: D): R { + return defaultHandler(setter, data) + } + + final override fun visitPropertySetter(setter: KSPropertySetter, data: RaiseContext): R = with(data.raise) { + visitPropertySetter(setter, data.data) + } + + context(Raise>) + open fun visitClassifierReference(reference: KSClassifierReference, data: D): R { + return defaultHandler(reference, data) + } + + final override fun visitClassifierReference(reference: KSClassifierReference, data: RaiseContext): R = with(data.raise) { + visitClassifierReference(reference, data.data) + } + + context(Raise>) + open fun visitDefNonNullReference(reference: KSDefNonNullReference, data: D): R { + return defaultHandler(reference, data) + } + + final override fun visitDefNonNullReference(reference: KSDefNonNullReference, data: RaiseContext): R = with(data.raise) { + visitDefNonNullReference(reference, data.data) + } + + context(Raise>) + open fun visitReferenceElement(element: KSReferenceElement, data: D): R { + return defaultHandler(element, data) + } + + final override fun visitReferenceElement(element: KSReferenceElement, data: RaiseContext): R = with(data.raise) { + visitReferenceElement(element, data.data) + } + + context(Raise>) + open fun visitTypeAlias(typeAlias: KSTypeAlias, data: D): R { + return defaultHandler(typeAlias, data) + } + + final override fun visitTypeAlias(typeAlias: KSTypeAlias, data: RaiseContext): R = with(data.raise) { + visitTypeAlias(typeAlias, data.data) + } + + context(Raise>) + open fun visitTypeArgument(typeArgument: KSTypeArgument, data: D): R { + return defaultHandler(typeArgument, data) + } + + final override fun visitTypeArgument(typeArgument: KSTypeArgument, data: RaiseContext): R = with(data.raise) { + visitTypeArgument(typeArgument, data.data) + } + + context(Raise>) + open fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: D): R { + return defaultHandler(classDeclaration, data) + } + + final override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: RaiseContext): R = with(data.raise) { + visitClassDeclaration(classDeclaration, data.data) + } + + context(Raise>) + open fun visitTypeParameter(typeParameter: KSTypeParameter, data: D): R { + return defaultHandler(typeParameter, data) + } + + final override fun visitTypeParameter(typeParameter: KSTypeParameter, data: RaiseContext): R = with(data.raise) { + visitTypeParameter(typeParameter, data.data) + } + + context(Raise>) + open fun visitTypeReference(typeReference: KSTypeReference, data: D): R { + return defaultHandler(typeReference, data) + } + + final override fun visitTypeReference(typeReference: KSTypeReference, data: RaiseContext): R = with(data.raise) { + visitTypeReference(typeReference, data.data) + } + + context(Raise>) + open fun visitValueParameter(valueParameter: KSValueParameter, data: D): R { + return defaultHandler(valueParameter, data) + } + + final override fun visitValueParameter(valueParameter: KSValueParameter, data: RaiseContext): R = with(data.raise) { + visitValueParameter(valueParameter, data.data) + } + + context(Raise>) + open fun visitValueArgument(valueArgument: KSValueArgument, data: D): R { + return defaultHandler(valueArgument, data) + } + + final override fun visitValueArgument(valueArgument: KSValueArgument, data: RaiseContext): R = with(data.raise) { + visitValueArgument(valueArgument, data.data) + } +} + +context(Raise>) +internal fun KSNode.accept(visitor: KSVisitor, R>, data: D): R { + return accept(visitor, RaiseContext(data)) +} diff --git a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/AnnotationValidations.kt b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/AnnotationValidations.kt index 44f8087..52a4aeb 100644 --- a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/AnnotationValidations.kt +++ b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/AnnotationValidations.kt @@ -47,6 +47,15 @@ import com.squareup.kotlinpoet.ksp.toTypeName /** Build [HookInfo] from the validated [HookAnnotation] found on the [property] */ context(Raise>) internal fun KSPropertyDeclaration.validateHookAnnotation(parentResolver: TypeParameterResolver): HookInfo { + // why is onlyHasASingleDslAnnotation wrapped in ensure while nothing else + // is? great question! this is because onlyHasASingleDslAnnotation is a + // singularly concerned validation function that has an explicitly matching + // raise context. onlyHasASingleDslAnnotation will _only_ ever raise a singular + // error, and therefore, shouldn't be treated as if it might have + // many to raise. We use ensure to narrow down the raise type param + // to what we expect, and then unwrap to explicitly re-raise within + // a non-empty-list context. + val annotation = ensure { onlyHasASingleDslAnnotation() } return zipOrAccumulate( diff --git a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookPropertyValidations.kt b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookPropertyValidations.kt index c35dbd8..b6aeb50 100644 --- a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookPropertyValidations.kt +++ b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookPropertyValidations.kt @@ -6,12 +6,6 @@ import com.google.devtools.ksp.symbol.KSPropertyDeclaration import com.intuit.hooks.plugin.codegen.HookInfo import com.intuit.hooks.plugin.codegen.HookProperty import com.intuit.hooks.plugin.ensure -import kotlin.contracts.CallsInPlace -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.InvocationKind.AT_MOST_ONCE -import kotlin.contracts.contract -import kotlin.experimental.ExperimentalTypeInference - context(Raise>) internal fun HookProperty.validate( diff --git a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookValidations.kt b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookValidations.kt index cae6a07..231e5e3 100644 --- a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookValidations.kt +++ b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookValidations.kt @@ -1,13 +1,16 @@ package com.intuit.hooks.plugin.ksp.validation -import arrow.core.* -import arrow.core.raise.* +import arrow.core.Nel +import arrow.core.mapOrAccumulate +import arrow.core.raise.Raise import arrow.core.raise.ensure +import arrow.core.raise.zipOrAccumulate import com.google.devtools.ksp.processing.KSPLogger import com.google.devtools.ksp.symbol.* import com.intuit.hooks.plugin.codegen.HookInfo import com.intuit.hooks.plugin.ensure import com.intuit.hooks.plugin.ksp.text +import com.intuit.hooks.plugin.mapOrAccumulate import com.squareup.kotlinpoet.ksp.TypeParameterResolver context(HookValidationError) @@ -24,8 +27,9 @@ internal sealed class HookValidationError(override val message: String, val symb class NoCodeGenerator(annotation: HookAnnotation) : HookValidationError("This hook plugin has no code generator for $annotation", annotation.symbol) class NoHookDslAnnotations(property: KSPropertyDeclaration) : HookValidationError("Hook property must be annotated with a DSL annotation", property) class TooManyHookDslAnnotations(annotations: List, property: KSPropertyDeclaration) : HookValidationError("This hook has more than a single hook DSL annotation: $annotations", property) - class UnsupportedAbstractPropertyType(property: KSPropertyDeclaration) : HookValidationError("Abstract property type (${property.type.text}) not supported. Hook properties must be of type com.intuit.hooks.Hook", property) + class UnsupportedPropertyType(property: KSPropertyDeclaration) : HookValidationError("Property type (${property.type.text}) not supported. Hook properties must be of type com.intuit.hooks.Hook", property) class NotAnAbstractProperty(property: KSPropertyDeclaration) : HookValidationError("Hooks can only be abstract properties", property) + class UnsupportedContainer(declaration: KSClassDeclaration) : HookValidationError("Hooks in constructs other than class, interface, and object aren't supported", declaration) operator fun component1(): String = message @@ -36,36 +40,27 @@ internal sealed class HookValidationError(override val message: String, val symb context(Raise>) internal fun KSPropertyDeclaration.validateProperty(parentResolver: TypeParameterResolver): HookInfo { // 1. validate types - // 2. validation annotation and - // 3. validate properties against type + validateHookType() - // why is validateHookType wrapped in ensure while nothing else is? - // great question! this is because validateHookType is a singularly - // concerned validation function that has an explicitly matching - // raise context. validateHookType will _only_ ever raise a singular - // error, and therefore, shouldn't be treated as if it might have - // many to raise. We use ensure to narrow down the raise type param - // to what we expect, and then unwrap to explicitly re-raise within - // a non-empty-list context. + // 2. validation annotation and + val info = validateHookAnnotation(parentResolver) - ensure { - validateHookType() - } + // 3. validate properties against type + validateHookProperties(info) - return validateHookAnnotation(parentResolver).also { - validateHookProperties(it) - } + return info } -context(Raise) +context(Raise>) private fun KSPropertyDeclaration.validateHookType() { - ensure(type.text == "Hook") { - HookValidationError.UnsupportedAbstractPropertyType(this) - } + zipOrAccumulate( + { ensure(type.text == "Hook") { HookValidationError.UnsupportedPropertyType(this@validateHookType) } }, + { ensure(modifiers.contains(Modifier.ABSTRACT)) { HookValidationError.NotAnAbstractProperty(this@validateHookType) } }, + ) { _, _ -> } } context(Raise>) private fun KSPropertyDeclaration.validateHookProperties(info: HookInfo) { - info.hookType.properties.map { - it.validate(info, this) + info.hookType.properties.mapOrAccumulate { + it.validate(info, this@validateHookProperties) } } diff --git a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/LogicalFailure.kt b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/LogicalFailure.kt index d07274c..28afe66 100644 --- a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/LogicalFailure.kt +++ b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/LogicalFailure.kt @@ -13,4 +13,4 @@ internal sealed interface EdgeCase : LogicalFailure { /** Logical failure that should probably be reported, something bad happened */ internal sealed interface ErrorCase : LogicalFailure { val message: String -} \ No newline at end of file +} diff --git a/processor/src/test/kotlin/com/intuit/hooks/plugin/HookValidationErrors.kt b/processor/src/test/kotlin/com/intuit/hooks/plugin/HookValidationErrors.kt index 5c332fc..14f4588 100644 --- a/processor/src/test/kotlin/com/intuit/hooks/plugin/HookValidationErrors.kt +++ b/processor/src/test/kotlin/com/intuit/hooks/plugin/HookValidationErrors.kt @@ -20,8 +20,8 @@ class HookValidationErrors { ) val (_, result) = compile(testHooks) - result.assertOk() - result.assertContainsMessages("Abstract property type (Int) not supported") + result.assertCompilationError() + result.assertContainsMessages("Property type (Int) not supported") } @Test fun `hook property does not have any hook annotation`() { @@ -38,7 +38,7 @@ class HookValidationErrors { ) val (_, result) = compile(testHooks) - result.assertOk() + result.assertCompilationError() result.assertContainsMessages("Hook property must be annotated with a DSL annotation") } @@ -59,7 +59,7 @@ class HookValidationErrors { ) val (_, result) = compile(testHooks) - result.assertOk() + result.assertCompilationError() result.assertContainsMessages("This hook has more than a single hook DSL annotation: [@Sync, @SyncBail]") } @@ -78,7 +78,7 @@ class HookValidationErrors { ) val (_, result) = compile(testHooks) - result.assertOk() + result.assertCompilationError() result.assertContainsMessages("Async hooks must be defined with a suspend function signature") } @@ -97,7 +97,7 @@ class HookValidationErrors { ) val (_, result) = compile(testHooks) - result.assertOk() + result.assertCompilationError() result.assertContainsMessages("Waterfall hooks must take at least one parameter") } @@ -116,7 +116,7 @@ class HookValidationErrors { ) val (_, result) = compile(testHooks) - result.assertOk() + result.assertCompilationError() result.assertContainsMessages("Waterfall hooks must specify the same types for the first parameter and the return type") } @@ -130,18 +130,19 @@ class HookValidationErrors { internal abstract class TestHooks : Hooks() { @AsyncSeriesWaterfall<() -> String> abstract val realBad: Hook - abstract val state: Int + val state: Int } """ ) val (_, result) = compile(testHooks) - result.assertOk() + result.assertCompilationError() result.assertContainsMessages( "Async hooks must be defined with a suspend function signature", "Waterfall hooks must take at least one parameter", "Waterfall hooks must specify the same types for the first parameter and the return type", - "Abstract property type (Int) not supported", + "Property type (Int) not supported", + "Hooks can only be abstract properties" ) } } diff --git a/settings.gradle.kts b/settings.gradle.kts index b197cfd..d7c4b56 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,10 +13,10 @@ enableFeaturePreview("ONE_LOCKFILE_PER_PROJECT") dependencyResolutionManagement { versionCatalogs { create("libs") { - version("kotlin", "1.6.21") - version("ktlint", "0.45.2") + version("kotlin", "1.8.10") + version("ktlint", "0.49.1") version("arrow", "1.2.0-RC") - version("ksp", "1.6.21-1.0.6") + version("ksp", "1.8.10-1.0.9") version("poet", "1.12.0") version("junit", "5.7.0") version("knit", "0.4.0") @@ -29,8 +29,8 @@ dependencyResolutionManagement { plugin("nexus", "io.github.gradle-nexus.publish-plugin").version("1.0.0") plugin("gradle.publish", "com.gradle.plugin-publish").version("0.13.0") - plugin("ktlint", "org.jlleitschuh.gradle.ktlint").version("10.3.0") - plugin("api", "org.jetbrains.kotlinx.binary-compatibility-validator").version("0.9.0") + plugin("ktlint", "org.jlleitschuh.gradle.ktlint").version("11.3.2") + plugin("api", "org.jetbrains.kotlinx.binary-compatibility-validator").version("0.13.1") plugin("knit", "kotlinx-knit").versionRef("knit") plugin("dokka", "org.jetbrains.dokka").versionRef("kotlin") @@ -86,7 +86,7 @@ dependencyResolutionManagement { library("junit.bom", "org.junit", "junit-bom").version("5.7.0") library("junit.jupiter", "org.junit.jupiter", "junit-jupiter").withoutVersion() library("mockk", "io.mockk", "mockk").version("1.10.2") - library("ksp.testing", "com.github.tschuchortdev", "kotlin-compile-testing-ksp").version("1.4.8") + library("ksp.testing", "com.github.tschuchortdev", "kotlin-compile-testing-ksp").version("1.5.0") library("knit.testing", "org.jetbrains.kotlinx", "kotlinx-knit-test").versionRef("knit") bundle("testing", listOf("junit.jupiter", "mockk")) From 62d297f398ac8450adeefd5103b18af7600a2fb7 Mon Sep 17 00:00:00 2001 From: Jeremiah Zucker Date: Wed, 17 May 2023 16:43:54 -0400 Subject: [PATCH 4/5] lint --- .editorconfig | 4 ++- build.gradle.kts | 3 +- .../com/intuit/hooks/docs/HooksTheme.kt | 2 +- .../src/test/kotlin/CarHooksTest.kt | 2 +- .../src/test/kotlin/GenericHookTests.kt | 4 ++- .../hooks/plugin/gradle/HooksGradlePlugin.kt | 7 +++-- .../src/test/kotlin/HooksGradlePluginTest.kt | 6 ++-- .../main/kotlin/com/intuit/hooks/BaseHook.kt | 2 +- .../main/kotlin/com/intuit/hooks/dsl/Hooks.kt | 3 +- .../intuit/hooks/AsyncSeriesLoopHookTests.kt | 6 ++-- .../hooks/AsyncSeriesWaterfallHookTests.kt | 2 +- .../com/intuit/hooks/SyncLoopHookTests.kt | 2 +- .../intuit/hooks/SyncWaterfallHookTests.kt | 4 +-- maven-plugin/build.gradle.kts | 2 +- .../kotlin/com/intuit/hooks/plugin/Raise.kt | 8 ++--- .../intuit/hooks/plugin/codegen/HookInfo.kt | 7 +++-- .../com/intuit/hooks/plugin/codegen/Poet.kt | 9 +++--- .../intuit/hooks/plugin/ksp/HooksProcessor.kt | 4 +-- .../com/intuit/hooks/plugin/ksp/Text.kt | 5 ++- .../ksp/validation/AnnotationValidations.kt | 4 +-- .../ksp/validation/HookPropertyValidations.kt | 8 ++--- .../hooks/plugin/HookValidationErrors.kt | 16 +++++----- .../intuit/hooks/plugin/HooksProcessorTest.kt | 31 ++++++++++--------- .../intuit/hooks/plugin/KotlinCompilation.kt | 2 +- settings.gradle.kts | 2 +- 25 files changed, 80 insertions(+), 65 deletions(-) diff --git a/.editorconfig b/.editorconfig index 494a4e2..3d70a00 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,2 +1,4 @@ [*.{kt,kts}] -disabled_rules=no-wildcard-imports \ No newline at end of file +ktlint_code_style = ktlint_official +ktlint_disabled_rules = no-wildcard-imports +ij_kotlin_allow_trailing_comma_on_call_site = true diff --git a/build.gradle.kts b/build.gradle.kts index f3e502d..f86cf33 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -60,8 +60,9 @@ tasks { val publish by creating { group = "publishing" - if (!isSnapshot) + if (!isSnapshot) { finalizedBy("closeAndReleaseSonatypeStagingRepository", ":docs:orchidDeploy", ":gradle-plugin:publishPlugins") + } } val version by creating { diff --git a/docs/src/orchid/kotlin/com/intuit/hooks/docs/HooksTheme.kt b/docs/src/orchid/kotlin/com/intuit/hooks/docs/HooksTheme.kt index 107adf1..09a1a20 100644 --- a/docs/src/orchid/kotlin/com/intuit/hooks/docs/HooksTheme.kt +++ b/docs/src/orchid/kotlin/com/intuit/hooks/docs/HooksTheme.kt @@ -35,6 +35,6 @@ class HooksTheme @Inject constructor(context: OrchidContext) : Theme(context, "H listOfNotNull(super.getResourceSource(), delegateTheme.resourceSource), emptyList(), priority, - ThemeResourceSource + ThemeResourceSource, ) } diff --git a/example-library/src/test/kotlin/CarHooksTest.kt b/example-library/src/test/kotlin/CarHooksTest.kt index 92882a0..cf81a25 100644 --- a/example-library/src/test/kotlin/CarHooksTest.kt +++ b/example-library/src/test/kotlin/CarHooksTest.kt @@ -1,7 +1,7 @@ package com.intuit.hooks.example.library import com.intuit.hooks.example.library.car.Car -import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test internal class CarHooksTest { diff --git a/example-library/src/test/kotlin/GenericHookTests.kt b/example-library/src/test/kotlin/GenericHookTests.kt index 3389518..a66d848 100644 --- a/example-library/src/test/kotlin/GenericHookTests.kt +++ b/example-library/src/test/kotlin/GenericHookTests.kt @@ -1,6 +1,7 @@ package com.intuit.hooks.example.library -import com.intuit.hooks.BailResult.* +import com.intuit.hooks.BailResult.Bail +import com.intuit.hooks.BailResult.Continue import com.intuit.hooks.HookContext import com.intuit.hooks.LoopResult import com.intuit.hooks.example.library.generic.GenericHooksImpl @@ -140,6 +141,7 @@ class GenericHookTests { val result = h.call("Kian") Assertions.assertEquals("bail now", result) } + @Test fun `async series loop`() = runBlocking { var incrementedA = 0 diff --git a/gradle-plugin/src/main/kotlin/com/intuit/hooks/plugin/gradle/HooksGradlePlugin.kt b/gradle-plugin/src/main/kotlin/com/intuit/hooks/plugin/gradle/HooksGradlePlugin.kt index a70420e..1b2601f 100644 --- a/gradle-plugin/src/main/kotlin/com/intuit/hooks/plugin/gradle/HooksGradlePlugin.kt +++ b/gradle-plugin/src/main/kotlin/com/intuit/hooks/plugin/gradle/HooksGradlePlugin.kt @@ -21,17 +21,18 @@ public class HooksGradlePlugin : Plugin { private fun Project.addDependency(configuration: String, dependencyNotation: String) = configurations .getByName(configuration).dependencies.add( - dependencies.create(dependencyNotation) + dependencies.create(dependencyNotation), ) override fun apply(project: Project): Unit = with(project) { extensions.create( "hooks", - HooksGradleExtension::class.java + HooksGradleExtension::class.java, ) - if (!pluginManager.hasPlugin("com.google.devtools.ksp")) + if (!pluginManager.hasPlugin("com.google.devtools.ksp")) { pluginManager.apply("com.google.devtools.ksp") + } addDependency("api", "com.intuit.hooks:hooks:$hooksVersion") addDependency("ksp", "com.intuit.hooks:processor:$hooksVersion") diff --git a/gradle-plugin/src/test/kotlin/HooksGradlePluginTest.kt b/gradle-plugin/src/test/kotlin/HooksGradlePluginTest.kt index ee46eaa..344c9bb 100644 --- a/gradle-plugin/src/test/kotlin/HooksGradlePluginTest.kt +++ b/gradle-plugin/src/test/kotlin/HooksGradlePluginTest.kt @@ -33,7 +33,7 @@ class HooksGradlePluginTest { kotlin("jvm") id("com.intuit.hooks") } - """ + """, ) } @@ -41,7 +41,7 @@ class HooksGradlePluginTest { buildFile.appendKotlin( """ hooks {} - """ + """, ) assertDoesNotThrow { @@ -66,7 +66,7 @@ class HooksGradlePluginTest { @Sync<(String) -> Unit> abstract val testSyncHook: Hook } - """ + """, ) val runner = GradleRunner.create() diff --git a/hooks/src/main/kotlin/com/intuit/hooks/BaseHook.kt b/hooks/src/main/kotlin/com/intuit/hooks/BaseHook.kt index d858a2f..963d5f3 100644 --- a/hooks/src/main/kotlin/com/intuit/hooks/BaseHook.kt +++ b/hooks/src/main/kotlin/com/intuit/hooks/BaseHook.kt @@ -37,7 +37,7 @@ public data class TapInfo> internal constructor( public val name: String, public val id: String, public val type: String, - public val f: FWithContext, + public val f: FWithContext // val stage: Int, // todo: maybe this should be forEachIndexed? // before?: string | Array // todo: do we even really need this? ) diff --git a/hooks/src/main/kotlin/com/intuit/hooks/dsl/Hooks.kt b/hooks/src/main/kotlin/com/intuit/hooks/dsl/Hooks.kt index 9981c92..65e7b4d 100644 --- a/hooks/src/main/kotlin/com/intuit/hooks/dsl/Hooks.kt +++ b/hooks/src/main/kotlin/com/intuit/hooks/dsl/Hooks.kt @@ -60,7 +60,8 @@ public abstract class Hooks { ReplaceWith("@Hooks.AsyncParallelBail"), DeprecationLevel.ERROR, ) - @ExperimentalCoroutinesApi protected fun >> asyncParallelBailHook(): AsyncParallelBailHook<*, *> = stub() + @ExperimentalCoroutinesApi + protected fun >> asyncParallelBailHook(): AsyncParallelBailHook<*, *> = stub() protected annotation class AsyncSeries> diff --git a/hooks/src/test/kotlin/com/intuit/hooks/AsyncSeriesLoopHookTests.kt b/hooks/src/test/kotlin/com/intuit/hooks/AsyncSeriesLoopHookTests.kt index 3c91114..9241b0f 100644 --- a/hooks/src/test/kotlin/com/intuit/hooks/AsyncSeriesLoopHookTests.kt +++ b/hooks/src/test/kotlin/com/intuit/hooks/AsyncSeriesLoopHookTests.kt @@ -1,6 +1,8 @@ package com.intuit.hooks -import io.mockk.* +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Assertions @@ -10,7 +12,7 @@ class AsyncSeriesLoopHookTests { class LoopHook1 : AsyncSeriesLoopHook LoopResult, suspend (HookContext, T1) -> Unit>() { suspend fun call(p1: T1) = super.call( invokeTap = { f, context -> f(context, p1) }, - invokeInterceptor = { f, context -> f(context, p1) } + invokeInterceptor = { f, context -> f(context, p1) }, ) } diff --git a/hooks/src/test/kotlin/com/intuit/hooks/AsyncSeriesWaterfallHookTests.kt b/hooks/src/test/kotlin/com/intuit/hooks/AsyncSeriesWaterfallHookTests.kt index 129ce6d..daa5cc0 100644 --- a/hooks/src/test/kotlin/com/intuit/hooks/AsyncSeriesWaterfallHookTests.kt +++ b/hooks/src/test/kotlin/com/intuit/hooks/AsyncSeriesWaterfallHookTests.kt @@ -10,7 +10,7 @@ class AsyncSeriesWaterfallHookTests { suspend fun call(p1: R) = super.call( p1, invokeTap = { f, r, context -> f(context, r) }, - invokeInterceptor = { f, context -> f(context, p1) } + invokeInterceptor = { f, context -> f(context, p1) }, ) } diff --git a/hooks/src/test/kotlin/com/intuit/hooks/SyncLoopHookTests.kt b/hooks/src/test/kotlin/com/intuit/hooks/SyncLoopHookTests.kt index a3c1a02..e6984df 100644 --- a/hooks/src/test/kotlin/com/intuit/hooks/SyncLoopHookTests.kt +++ b/hooks/src/test/kotlin/com/intuit/hooks/SyncLoopHookTests.kt @@ -13,7 +13,7 @@ class SyncLoopHookTests { class LoopHook1 : SyncLoopHook<(HookContext, T1) -> LoopResult, (HookContext, T1) -> Unit>() { fun call(p1: T1) = super.call( invokeTap = { f, context -> f(context, p1) }, - invokeInterceptor = { f, context -> f(context, p1) } + invokeInterceptor = { f, context -> f(context, p1) }, ) } diff --git a/hooks/src/test/kotlin/com/intuit/hooks/SyncWaterfallHookTests.kt b/hooks/src/test/kotlin/com/intuit/hooks/SyncWaterfallHookTests.kt index c79ca61..119e957 100644 --- a/hooks/src/test/kotlin/com/intuit/hooks/SyncWaterfallHookTests.kt +++ b/hooks/src/test/kotlin/com/intuit/hooks/SyncWaterfallHookTests.kt @@ -8,7 +8,7 @@ class SyncWaterfallHookTests { fun call(p1: T1) = super.call( p1, invokeTap = { f, acc, context -> f(context, acc) }, - invokeInterceptor = { f, context -> f(context, p1) } + invokeInterceptor = { f, context -> f(context, p1) }, ) } @@ -16,7 +16,7 @@ class SyncWaterfallHookTests { fun call(p1: T1, p2: T2) = super.call( p1, invokeTap = { f, acc, context -> f(context, acc, p2) }, - invokeInterceptor = { f, context -> f(context, p1, p2) } + invokeInterceptor = { f, context -> f(context, p1, p2) }, ) } diff --git a/maven-plugin/build.gradle.kts b/maven-plugin/build.gradle.kts index 8d9ad26..30bdda6 100644 --- a/maven-plugin/build.gradle.kts +++ b/maven-plugin/build.gradle.kts @@ -16,7 +16,7 @@ tasks { from( configurations.compileClasspath.get().filter { dependency -> dependency.absolutePath.contains("kotlin-maven-symbol-processing") - }.map(::zipTree) + }.map(::zipTree), ) { this.duplicatesStrategy = DuplicatesStrategy.EXCLUDE } diff --git a/processor/src/main/kotlin/com/intuit/hooks/plugin/Raise.kt b/processor/src/main/kotlin/com/intuit/hooks/plugin/Raise.kt index 58e5740..24e068a 100644 --- a/processor/src/main/kotlin/com/intuit/hooks/plugin/Raise.kt +++ b/processor/src/main/kotlin/com/intuit/hooks/plugin/Raise.kt @@ -44,7 +44,7 @@ public inline fun Iterable.accumulate( flatMap { recover({ operation(it); emptyList() - }) { it } + },) { it } }.toNonEmptyListOrNull()?.let { raise(it) } } @@ -62,7 +62,7 @@ public inline fun Iterable.mapOrAccumulate( // TODO: Consider r @BuilderInference operation: Raise>.(A) -> B ): List = recover({ mapOrAccumulate(this@mapOrAccumulate) { operation(it) } -}) { errors -> raise(errors.flatMap { it }) } +},) { errors -> raise(errors.flatMap { it }) } /** [mapOrAccumulate] variant that accumulates errors from a validator that may raise multiple errors */ context(Raise>) @@ -71,7 +71,7 @@ public inline fun NonEmptyList.mapOrAccumulate( // TODO: Consid @BuilderInference operation: Raise>.(A) -> B ): NonEmptyList = recover({ mapOrAccumulate(this@mapOrAccumulate) { operation(it) } -}) { errors -> raise(errors.flatMap { it }) } +},) { errors -> raise(errors.flatMap { it }) } /** [mapOrAccumulate] variant that accumulates errors from a validator that may raise multiple errors */ context(Raise>) @@ -80,4 +80,4 @@ public inline fun NonEmptySet.mapOrAccumulate( // TODO: Conside @BuilderInference operation: Raise>.(A) -> B ): NonEmptySet = recover({ mapOrAccumulate(this@mapOrAccumulate) { operation(it) } -}) { errors -> raise(errors.flatMap { it }) } +},) { errors -> raise(errors.flatMap { it }) } diff --git a/processor/src/main/kotlin/com/intuit/hooks/plugin/codegen/HookInfo.kt b/processor/src/main/kotlin/com/intuit/hooks/plugin/codegen/HookInfo.kt index c8ba7de..3c80ed7 100644 --- a/processor/src/main/kotlin/com/intuit/hooks/plugin/codegen/HookInfo.kt +++ b/processor/src/main/kotlin/com/intuit/hooks/plugin/codegen/HookInfo.kt @@ -14,8 +14,9 @@ internal data class HooksContainer( val superclass get() = originalClassName.let { if (typeArguments.isNotEmpty()) { it.parameterizedBy(typeArguments) - } else + } else { it + } } } @@ -24,7 +25,7 @@ internal data class HookSignature( val isSuspend: Boolean, val returnType: TypeName, val returnTypeType: TypeName?, - val hookFunctionSignatureType: TypeName, + val hookFunctionSignatureType: TypeName ) { val nullableReturnTypeType: TypeName get() { requireNotNull(returnTypeType) @@ -36,7 +37,7 @@ internal data class HookSignature( internal class HookParameter( val name: String?, val type: TypeName, - val position: Int, + val position: Int ) { val withType get() = "$withoutType: $type" val withoutType get() = name ?: "p$position" diff --git a/processor/src/main/kotlin/com/intuit/hooks/plugin/codegen/Poet.kt b/processor/src/main/kotlin/com/intuit/hooks/plugin/codegen/Poet.kt index 0115eb1..359f910 100644 --- a/processor/src/main/kotlin/com/intuit/hooks/plugin/codegen/Poet.kt +++ b/processor/src/main/kotlin/com/intuit/hooks/plugin/codegen/Poet.kt @@ -44,8 +44,9 @@ internal fun HookInfo.generateClass(): TypeSpec { val callBuilder = FunSpec.builder("call") .addParameters(parameterSpecs) .apply { - if (this@generateClass.isAsync) + if (this@generateClass.isAsync) { addModifiers(KModifier.SUSPEND) + } } val (superclass, call) = when (hookType) { @@ -66,7 +67,7 @@ internal fun HookInfo.generateClass(): TypeSpec { .addCode( "return super.call(invokeTap = %L, invokeInterceptor = %L)", CodeBlock.of("{ f, context -> f(context, $paramsWithoutTypes) }"), - CodeBlock.of("{ f, context -> f(context, $paramsWithoutTypes) }") + CodeBlock.of("{ f, context -> f(context, $paramsWithoutTypes) }"), ) Pair(superclass, call) @@ -81,7 +82,7 @@ internal fun HookInfo.generateClass(): TypeSpec { "return super.call(%N, invokeTap = %L, invokeInterceptor = %L)", accumulatorName, CodeBlock.of("{ f, %N, context -> f(context, $paramsWithoutTypes) }", accumulatorName), - CodeBlock.of("{ f, context -> f(context, $paramsWithoutTypes) }") + CodeBlock.of("{ f, context -> f(context, $paramsWithoutTypes) }"), ) Pair(superclass, call) @@ -127,7 +128,7 @@ private val HookInfo.lambdaTypeName get() = createHookContextLambda(hookSignatur private fun HookInfo.createHookContextLambda(returnType: TypeName): LambdaTypeName { val get = LambdaTypeName.get( parameters = listOf(ParameterSpec.unnamed(hookContext)) + parameterSpecs, - returnType = returnType + returnType = returnType, ) return if (this.isAsync) get.copy(suspending = true) else get diff --git a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/HooksProcessor.kt b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/HooksProcessor.kt index 70a499f..69c1ced 100644 --- a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/HooksProcessor.kt +++ b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/HooksProcessor.kt @@ -56,7 +56,7 @@ public class HooksProcessor( }, { throwable: Throwable -> logger.error("Uncaught exception while processing file: ${throwable.localizedMessage}", file) logger.exception(throwable) - }) + },) } } @@ -107,7 +107,7 @@ public class HooksProcessor( typeSpecKind, visibilityModifier, typeArguments, - hooks + hooks, ) } } diff --git a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/Text.kt b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/Text.kt index eef71f6..cd730bd 100644 --- a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/Text.kt +++ b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/Text.kt @@ -9,8 +9,11 @@ internal val KSTypeArgument.text: String get() = when (variance) { else -> "${variance.label} ${type!!.text}" } -internal val List.text: String get() = if (isEmpty()) "" else +internal val List.text: String get() = if (isEmpty()) { + "" +} else { "<${joinToString(transform = KSTypeArgument::text)}>" +} internal val KSTypeReference.text: String get() = element?.let { when (it) { diff --git a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/AnnotationValidations.kt b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/AnnotationValidations.kt index 52a4aeb..acbcecc 100644 --- a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/AnnotationValidations.kt +++ b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/AnnotationValidations.kt @@ -64,7 +64,7 @@ internal fun KSPropertyDeclaration.validateHookAnnotation(parentResolver: TypePa { annotation.mustBeHookType(parentResolver) }, { annotation.validateParameters(parentResolver) }, { getVisibility().toKModifier() ?: KModifier.PUBLIC }, - ::HookInfo + ::HookInfo, ) } @@ -106,7 +106,7 @@ private fun HookAnnotation.mustBeHookType(parentResolver: TypeParameterResolver) isSuspend, returnType, returnTypeType, - hookFunctionSignatureType + hookFunctionSignatureType, ) } catch (exception: Exception) { raise(HookValidationError.MustBeHookTypeSignature(this)) diff --git a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookPropertyValidations.kt b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookPropertyValidations.kt index b6aeb50..83a3bd5 100644 --- a/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookPropertyValidations.kt +++ b/processor/src/main/kotlin/com/intuit/hooks/plugin/ksp/validation/HookPropertyValidations.kt @@ -10,7 +10,7 @@ import com.intuit.hooks.plugin.ensure context(Raise>) internal fun HookProperty.validate( info: HookInfo, - property: KSPropertyDeclaration, + property: KSPropertyDeclaration ) { when (this) { is HookProperty.Bail -> Unit @@ -30,7 +30,7 @@ private fun HookInfo.validateAsync(property: KSPropertyDeclaration) { context(Raise>) private fun HookProperty.Waterfall.validate( info: HookInfo, - property: KSPropertyDeclaration, + property: KSPropertyDeclaration ) { zipOrAccumulate( { arity(info, property) }, @@ -41,7 +41,7 @@ private fun HookProperty.Waterfall.validate( context(Raise) private fun HookProperty.Waterfall.arity( info: HookInfo, - property: KSPropertyDeclaration, + property: KSPropertyDeclaration ) { ensure(!info.zeroArity) { HookValidationError.WaterfallMustHaveParameters(property) } } @@ -49,7 +49,7 @@ private fun HookProperty.Waterfall.arity( context(Raise) private fun HookProperty.Waterfall.parameters( info: HookInfo, - property: KSPropertyDeclaration, + property: KSPropertyDeclaration ) { ensure(info.hookSignature.returnType == info.params.firstOrNull()?.type) { HookValidationError.WaterfallParameterTypeMustMatch(property) diff --git a/processor/src/test/kotlin/com/intuit/hooks/plugin/HookValidationErrors.kt b/processor/src/test/kotlin/com/intuit/hooks/plugin/HookValidationErrors.kt index 14f4588..894cc7f 100644 --- a/processor/src/test/kotlin/com/intuit/hooks/plugin/HookValidationErrors.kt +++ b/processor/src/test/kotlin/com/intuit/hooks/plugin/HookValidationErrors.kt @@ -16,7 +16,7 @@ class HookValidationErrors { internal abstract class TestHooks : Hooks() { abstract val nonHookProperty: Int } - """ + """, ) val (_, result) = compile(testHooks) @@ -34,7 +34,7 @@ class HookValidationErrors { internal abstract class TestHooks : Hooks() { abstract val syncHook: Hook } - """ + """, ) val (_, result) = compile(testHooks) @@ -55,7 +55,7 @@ class HookValidationErrors { @SyncBail<() -> BailResult> abstract val syncHook: Hook } - """ + """, ) val (_, result) = compile(testHooks) @@ -74,7 +74,7 @@ class HookValidationErrors { @AsyncSeries<() -> Unit> abstract val syncHook: Hook } - """ + """, ) val (_, result) = compile(testHooks) @@ -93,7 +93,7 @@ class HookValidationErrors { @SyncWaterfall<() -> String> abstract val syncHook: Hook } - """ + """, ) val (_, result) = compile(testHooks) @@ -112,7 +112,7 @@ class HookValidationErrors { @SyncWaterfall<(Int, Int) -> Unit> abstract val syncHook: Hook } - """ + """, ) val (_, result) = compile(testHooks) @@ -132,7 +132,7 @@ class HookValidationErrors { abstract val realBad: Hook val state: Int } - """ + """, ) val (_, result) = compile(testHooks) @@ -142,7 +142,7 @@ class HookValidationErrors { "Waterfall hooks must take at least one parameter", "Waterfall hooks must specify the same types for the first parameter and the return type", "Property type (Int) not supported", - "Hooks can only be abstract properties" + "Hooks can only be abstract properties", ) } } diff --git a/processor/src/test/kotlin/com/intuit/hooks/plugin/HooksProcessorTest.kt b/processor/src/test/kotlin/com/intuit/hooks/plugin/HooksProcessorTest.kt index e07925a..14e8846 100644 --- a/processor/src/test/kotlin/com/intuit/hooks/plugin/HooksProcessorTest.kt +++ b/processor/src/test/kotlin/com/intuit/hooks/plugin/HooksProcessorTest.kt @@ -35,7 +35,7 @@ class HooksProcessorTest { public fun call(newSpeed: Int): Unit = super.call { f, context -> f(context, newSpeed) } } } - """ + """, ) val (compilation, result) = compile(testHooks) @@ -43,6 +43,7 @@ class HooksProcessorTest { compilation.assertKspGeneratedSources("TestHooksHooks.kt") result.assertNoKspErrors() } + @Test fun `multiple hook classes in a single file`() { val testHooks = SourceFile.kotlin( "TestHooks.kt", @@ -59,7 +60,7 @@ class HooksProcessorTest { @Sync<(String) -> Unit> abstract val testSyncHook: Hook } - """ + """, ) val assertions = SourceFile.kotlin( @@ -75,7 +76,7 @@ class HooksProcessorTest { hooks.testSyncHook.call("hello") assertTrue(tapCalled) } - """ + """, ) val (compilation, result) = compile(testHooks, assertions) @@ -95,7 +96,7 @@ class HooksProcessorTest { @Sync<(String) -> Unit> abstract val testSyncHook: Hook } - """ + """, ) val assertions = SourceFile.kotlin( @@ -110,7 +111,7 @@ class HooksProcessorTest { hooks.testSyncHook.call("hello") assertTrue(tapCalled) } - """ + """, ) val (compilation, result) = compile(testHooks, assertions) @@ -132,7 +133,7 @@ class HooksProcessorTest { @Sync<(String) -> Unit> abstract val testSyncHook: Hook } - """ + """, ) val (compilation, result) = compile(testHooks) @@ -151,7 +152,7 @@ class HooksProcessorTest { @Sync<(Map, List>) -> Unit> abstract val testSyncHook: Hook } - """ + """, ) val assertions = SourceFile.kotlin( @@ -167,7 +168,7 @@ class HooksProcessorTest { hooks.testSyncHook.call(item) assertEquals(item, tappedItem) } - """ + """, ) val (compilation, result) = compile(testHooks, assertions) @@ -187,7 +188,7 @@ class HooksProcessorTest { @AsyncSeriesWaterfall String> abstract val testAsyncSeriesWaterfallHook: Hook } - """ + """, ) val assertions = SourceFile.kotlin( @@ -208,7 +209,7 @@ class HooksProcessorTest { assertEquals(initialValue, tappedItem) assertEquals("hello world!", result) } - """ + """, ) val (compilation, result) = compile(testHooks, assertions) @@ -238,7 +239,7 @@ class HooksProcessorTest { @AsyncSeriesLoop LoopResult> abstract val asyncSeriesLoop: Hook @AsyncSeriesWaterfall String> abstract val asyncSeriesWaterfall: Hook } - """ + """, ) val (compilation, result) = compile(testHooks) @@ -257,7 +258,7 @@ class HooksProcessorTest { @Sync<(T) -> U> abstract val testSyncHook: Hook } - """ + """, ) val assertions = SourceFile.kotlin( @@ -273,7 +274,7 @@ class HooksProcessorTest { hooks.testSyncHook.call(item) assertEquals(item, tappedValue) } - """ + """, ) val (compilation, result) = compile(testHooks, assertions) @@ -297,7 +298,7 @@ class HooksProcessorTest { val hooks = ControllerHooksImpl() } - """ + """, ) val assertions = SourceFile.kotlin( @@ -313,7 +314,7 @@ class HooksProcessorTest { controller.hooks.testSyncHook.call(item) assertEquals(item, tappedValue) } - """ + """, ) val (compilation, result) = compile(testHooks, assertions) diff --git a/processor/src/test/kotlin/com/intuit/hooks/plugin/KotlinCompilation.kt b/processor/src/test/kotlin/com/intuit/hooks/plugin/KotlinCompilation.kt index 27de56d..684a97d 100644 --- a/processor/src/test/kotlin/com/intuit/hooks/plugin/KotlinCompilation.kt +++ b/processor/src/test/kotlin/com/intuit/hooks/plugin/KotlinCompilation.kt @@ -15,7 +15,7 @@ val KotlinCompilation.kspGeneratedSources get() = fun KotlinCompilation.assertKspGeneratedSources(vararg sources: String) { sources.map { kspSourcesDir.resolve("kotlin").resolve( - it.removeSuffixIfPresent(".kt").replace(".", "/").suffix("kt") + it.removeSuffixIfPresent(".kt").replace(".", "/").suffix("kt"), ) }.forEach { Assertions.assertTrue(kspGeneratedSources.contains(it)) { "KSP processing did not generate file: $it" } diff --git a/settings.gradle.kts b/settings.gradle.kts index d7c4b56..56d0dc1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -78,7 +78,7 @@ dependencyResolutionManagement { "orchid.plugins.snippets", "orchid.plugins.copper", "orchid.plugins.wiki", - ) + ), ) // Testing From babcd782089da5751c3fc5a5686e2b8620b2bf81 Mon Sep 17 00:00:00 2001 From: Jeremiah Zucker Date: Wed, 17 May 2023 18:38:54 -0400 Subject: [PATCH 5/5] cleanup helpers --- .../kotlin/com/intuit/hooks/plugin/Raise.kt | 69 +++---------------- 1 file changed, 9 insertions(+), 60 deletions(-) diff --git a/processor/src/main/kotlin/com/intuit/hooks/plugin/Raise.kt b/processor/src/main/kotlin/com/intuit/hooks/plugin/Raise.kt index 24e068a..468a83c 100644 --- a/processor/src/main/kotlin/com/intuit/hooks/plugin/Raise.kt +++ b/processor/src/main/kotlin/com/intuit/hooks/plugin/Raise.kt @@ -8,76 +8,25 @@ import kotlin.experimental.ExperimentalTypeInference // Collection of [Raise] helpers for accumulating errors from a single error context -/** Helper for accumulating errors from single-error validators */ -@RaiseDSL -internal fun Raise>.ensure(@BuilderInference block: Raise.() -> A): A = - recover(block) { e: Error -> raise(e.nel()) } - -/** Helper for accumulating errors from single-error validators */ -@RaiseDSL -public inline fun Raise>.ensure(condition: Boolean, raise: () -> Error) { - recover({ ensure(condition, raise) }) { e: Error -> raise(e.nel()) } -} - /** Raise a _logical failure_ of type [Error] in a multi-[Error] accumulator */ @RaiseDSL -public inline fun Raise>.raise(r: Error): Nothing { - raise(r.nel()) -} - -@RaiseDSL -public inline fun Raise>.raiseAll( - iterable: Iterable, - @BuilderInference transform: Raise>.(A) -> Unit -): List = mapOrAccumulate(iterable) { arg -> - recover, Unit>({ transform(arg) }) { errors -> - this@raiseAll.raise(errors) - } -} +public inline fun Raise>.raise(r: Error): Nothing = raise(r.nel()) -/** Explicitly accumulate errors that may have been raised while processing each element */ -context(Raise>) +/** Execute [block] in singular `Raise` context such that singular [Error]s are re-raised in [this] scope as a [NonEmptyList] */ @RaiseDSL -public inline fun Iterable.accumulate( - @BuilderInference operation: Raise>.(A) -> Unit -) { - flatMap { - recover({ - operation(it); emptyList() - },) { it } - }.toNonEmptyListOrNull()?.let { raise(it) } -} +internal fun Raise>.ensure(@BuilderInference block: Raise.() -> A): A = + recover(block, ::raise) /** [mapOrAccumulate] variant that accumulates errors from a validator that may raise multiple errors */ -context(Raise>) +context(Raise>) @RaiseDSL -public inline fun Sequence.mapOrAccumulate( // TODO: Consider renaming +public inline fun Sequence.mapOrAccumulate( @BuilderInference operation: Raise>.(A) -> B ): List = toList().mapOrAccumulate(operation) /** [mapOrAccumulate] variant that accumulates errors from a validator that may raise multiple errors */ -context(Raise>) -@RaiseDSL -public inline fun Iterable.mapOrAccumulate( // TODO: Consider renaming - @BuilderInference operation: Raise>.(A) -> B -): List = recover({ - mapOrAccumulate(this@mapOrAccumulate) { operation(it) } -},) { errors -> raise(errors.flatMap { it }) } - -/** [mapOrAccumulate] variant that accumulates errors from a validator that may raise multiple errors */ -context(Raise>) -@RaiseDSL -public inline fun NonEmptyList.mapOrAccumulate( // TODO: Consider renaming - @BuilderInference operation: Raise>.(A) -> B -): NonEmptyList = recover({ - mapOrAccumulate(this@mapOrAccumulate) { operation(it) } -},) { errors -> raise(errors.flatMap { it }) } - -/** [mapOrAccumulate] variant that accumulates errors from a validator that may raise multiple errors */ -context(Raise>) +context(Raise>) @RaiseDSL -public inline fun NonEmptySet.mapOrAccumulate( // TODO: Consider renaming +public inline fun Iterable.mapOrAccumulate( @BuilderInference operation: Raise>.(A) -> B -): NonEmptySet = recover({ - mapOrAccumulate(this@mapOrAccumulate) { operation(it) } -},) { errors -> raise(errors.flatMap { it }) } +): List = mapOrAccumulate(this, NonEmptyList::plus) { operation(it) }