Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
WIP: Validated -> Raise 2
  • Loading branch information
sugarmanz committed May 17, 2023
commit d0ce3b10872546f06d74fa205020dd2e8be9130e
29 changes: 29 additions & 0 deletions processor/src/main/kotlin/com/intuit/hooks/plugin/Raise.kt
Original file line number Diff line number Diff line change
@@ -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 <Error, A> Raise<Nel<Error>>.ensure(@BuilderInference block: Raise<Error>.() -> A): A =
recover(block) { e: Error -> raise(e.nel()) }

/** Helper for accumulating errors from single-error validators */
@RaiseDSL
public inline fun <Error> Raise<Nel<Error>>.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 <Error> Raise<Nel<Error>>.raise(r: Error): Nothing {
raise(r.nel())
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.*

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Nel<HookValidationError>>)
internal fun KSPropertyDeclaration.validateHookAnnotation(parentResolver: TypeParameterResolver): HookInfo {
val annotation = ensure { onlyHasASingleDslAnnotation() }
Expand All @@ -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<HookValidationError, HookInfo> =
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<HookParameter> ->
HookInfo(hookMember, hookType, hookSignature, hookParameters, propertyVisibility)
}
}

// TODO: This'd be a good smart constructor use case
context(Raise<HookValidationError>) private fun KSPropertyDeclaration.onlyHasASingleDslAnnotation(): HookAnnotation {
val annotations = annotations.filter { it.shortName.asString() in annotationDslMarkers }.toList()
Expand All @@ -85,15 +69,6 @@ context(Raise<HookValidationError>) private fun KSPropertyDeclaration.onlyHasASi
}.let(::HookAnnotation)
}

private fun KSPropertyDeclaration.onlyHasASingleDslAnnotation(): ValidatedNel<HookValidationError, HookAnnotation> {
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<HookValidationError>) private fun HookAnnotation.validateParameters(parentResolver: TypeParameterResolver): List<HookParameter> = try {
hookFunctionSignatureReference.functionParameters.mapIndexed { index: Int, parameter: KSValueParameter ->
val name = parameter.name?.asString()
Expand All @@ -104,25 +79,9 @@ context(Raise<HookValidationError>) private fun HookAnnotation.validateParameter
raise(HookValidationError.MustBeHookTypeSignature(this))
}

private fun validateParameters(annotation: HookAnnotation, parentResolver: TypeParameterResolver): ValidatedNel<HookValidationError, List<HookParameter>> = 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<HookValidationError.NoCodeGenerator>) private fun HookAnnotation.hasCodeGenerator(): HookType = type

private fun hasCodeGenerator(annotation: HookAnnotation): ValidatedNel<HookValidationError, HookType> = try {
annotation.type!!.valid()
} catch (e: Exception) {
HookValidationError.NoCodeGenerator(annotation).invalidNel()
}

/** TODO: Another good smart constructor example */
context(Raise<HookValidationError>)
private fun HookAnnotation.mustBeHookType(parentResolver: TypeParameterResolver): HookSignature = try {
Expand All @@ -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<HookValidationError, HookSignature> = 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<kotlin.Int, kotlin.String>
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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<HookValidationError, HookProperty> = when (this) {
is HookProperty.Bail -> valid()
is HookProperty.Loop -> valid()
is HookProperty.Async -> validate(info, property)
is HookProperty.Waterfall -> validate(info, property)
}

context(Raise<Nel<HookValidationError>>)
internal fun HookProperty.validate(
Expand All @@ -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)
Expand All @@ -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<HookValidationError, HookProperty> =
if (info.hookSignature.isSuspend) valid()
else HookValidationError.AsyncHookWithoutSuspend(property).invalidNel()

private fun HookProperty.Waterfall.validate(
info: HookInfo,
property: KSPropertyDeclaration,
): ValidatedNel<HookValidationError, HookProperty> =
Either.zipOrAccumulate(
arity(info, property).toEither(),
parameters(info, property).toEither()
) { _, _ -> this }.toValidated()

context(Raise<Nel<HookValidationError>>)
private fun HookProperty.Waterfall.validate(
info: HookInfo,
Expand All @@ -68,14 +44,6 @@ private fun HookProperty.Waterfall.validate(
) { _, _ -> }
}

private fun HookProperty.Waterfall.arity(
info: HookInfo,
property: KSPropertyDeclaration,
): ValidatedNel<HookValidationError, HookProperty> {
return if (!info.zeroArity) valid()
else HookValidationError.WaterfallMustHaveParameters(property).invalidNel()
}

context(Raise<HookValidationError.WaterfallMustHaveParameters>)
private fun HookProperty.Waterfall.arity(
info: HookInfo,
Expand All @@ -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<HookValidationError, HookProperty> {
return if (info.hookSignature.returnType == info.params.firstOrNull()?.type) valid()
else HookValidationError.WaterfallParameterTypeMustMatch(property).invalidNel()
}

context(Raise<HookValidationError.WaterfallParameterTypeMustMatch>)
private fun HookProperty.Waterfall.parameters(
info: HookInfo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Nel<HookValidationError>>)
internal fun KSPropertyDeclaration.validateProperty(parentResolver: TypeParameterResolver): HookInfo {
// 1. validate types
Expand All @@ -71,60 +57,15 @@ 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<HookValidationError, HookInfo> = 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<HookValidationError.UnsupportedAbstractPropertyType>)
private fun KSPropertyDeclaration.validateHookType() {
ensure(type.text == "Hook") {
HookValidationError.UnsupportedAbstractPropertyType(this)
}
}

private fun KSPropertyDeclaration.validateHookType(): ValidatedNel<HookValidationError, KSTypeReference> =
if (type.text == "Hook") type.valid()
else HookValidationError.UnsupportedAbstractPropertyType(this).invalidNel()


context(Raise<Nel<HookValidationError>>) private fun KSPropertyDeclaration.validateHookProperties(info: HookInfo) {
info.hookType.properties.map {
it.validate(info, this)
}
}

//private fun KSPropertyDeclaration.validateHookProperties(hookInfo: HookInfo): Validated<NonEmptyList<HookValidationError>, 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 <Error, A> Raise<Nel<Error>>.ensure(@BuilderInference block: Raise<Error>.() -> A): A =
recover(block) { e: Error -> raise(e.nel()) }

/** Helper for accumulating errors from single-error validators */
@RaiseDSL
public inline fun <Error> Raise<Nel<Error>>.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 <Error> Raise<Nel<Error>>.raise(r: Error): Nothing {
raise(r.nel())
}
Original file line number Diff line number Diff line change
@@ -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
}