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
Next Next commit
WIP: Validated -> Raise
  • Loading branch information
sugarmanz committed May 17, 2023
commit e46f59f435c52ff0ea96a8b0f810e271b8a55876
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ subprojects {
val configure: KotlinCompile.() -> Unit = {
kotlinOptions {
freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
freeCompilerArgs += "-Xcontext-receivers"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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.*
Expand All @@ -26,62 +30,66 @@ public class HooksProcessor(
return emptyList()
}

private inner class HookPropertyVisitor : KSDefaultVisitor<TypeParameterResolver, ValidatedNel<HookValidationError, HookInfo>>() {
override fun visitPropertyDeclaration(property: KSPropertyDeclaration, parentResolver: TypeParameterResolver): ValidatedNel<HookValidationError, HookInfo> {
return if (property.modifiers.contains(Modifier.ABSTRACT))
validateProperty(property, parentResolver)
else
HookValidationError.NotAnAbstractProperty(property).invalidNel()
private inner class HookPropertyVisitor : KSDefaultVisitor<TypeParameterResolver, HookInfo>() {

context(Raise<Nel<HookValidationError>>)
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<HookValidationError, HookInfo> =
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<KSClassDeclaration>()
.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<LogicalFailure> ->
errors.filterIsInstance<HookValidationError>().forEach(logger::error)
}, { throwable: Throwable ->
logger.error("Uncaught exception while processing file: ${throwable.localizedMessage}", file)
logger.exception(throwable)
})
}
}

private inner class HookContainerVisitor : KSDefaultVisitor<Unit, List<ValidatedNel<HookValidationError, HooksContainer>>>() {
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit): List<ValidatedNel<HookValidationError, HooksContainer>> {
private inner class HookContainerVisitor : KSDefaultVisitor<Raise<Nel<HookValidationError>>, Sequence<HooksContainer>>() {
// TODO: Try with context receiver
override fun visitClassDeclaration(
classDeclaration: KSClassDeclaration,
raise: Raise<Nel<HookValidationError>>
): Sequence<HooksContainer> = 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) {
Expand Down Expand Up @@ -109,8 +117,7 @@ public class HooksProcessor(
)
}

override fun defaultHandler(node: KSNode, data: Unit): List<ValidatedNel<HookValidationError, HooksContainer>> =
TODO("Not yet implemented")
override fun defaultHandler(node: KSNode, data: Raise<Nel<HookValidationError>>) = TODO("Not yet implemented")
}

public class Provider : SymbolProcessorProvider {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<HookValidationError.NoCodeGenerator>) 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<Nel<HookValidationError>>)
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<HookValidationError, HookInfo> =
onlyHasASingleDslAnnotation().andThen { annotation ->
Expand All @@ -46,6 +75,16 @@ internal fun KSPropertyDeclaration.validateHookAnnotation(parentResolver: TypePa
}
}

// 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()
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<HookValidationError, HookAnnotation> {
val annotations = annotations.filter { it.shortName.asString() in annotationDslMarkers }.toList()
return when (annotations.size) {
Expand All @@ -55,6 +94,16 @@ private fun KSPropertyDeclaration.onlyHasASingleDslAnnotation(): ValidatedNel<Ho
}
}

context(Raise<HookValidationError>) private fun HookAnnotation.validateParameters(parentResolver: TypeParameterResolver): List<HookParameter> = 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<HookValidationError, List<HookParameter>> = try {
annotation.hookFunctionSignatureReference.functionParameters.mapIndexed { index: Int, parameter: KSValueParameter ->
val name = parameter.name?.asString()
Expand All @@ -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<HookValidationError.NoCodeGenerator>) private fun HookAnnotation.hasCodeGenerator(): HookType = type

private fun hasCodeGenerator(annotation: HookAnnotation): ValidatedNel<HookValidationError, HookType> = try {
annotation.type.valid()
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 {
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<kotlin.Int, kotlin.String>
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<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>
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -18,6 +21,26 @@ internal fun HookProperty.validate(
is HookProperty.Waterfall -> validate(info, property)
}

context(Raise<Nel<HookValidationError>>)
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<HookValidationError.AsyncHookWithoutSuspend>)
private fun HookInfo.validateAsync(property: KSPropertyDeclaration) {
ensure(hookSignature.isSuspend) { HookValidationError.AsyncHookWithoutSuspend(property) }
}

private fun HookProperty.Async.validate(
info: HookInfo,
property: KSPropertyDeclaration,
Expand All @@ -29,9 +52,21 @@ private fun HookProperty.Waterfall.validate(
info: HookInfo,
property: KSPropertyDeclaration,
): ValidatedNel<HookValidationError, HookProperty> =
arity(info, property).zip(
parameters(info, property),
) { _, _ -> this }
Either.zipOrAccumulate(
arity(info, property).toEither(),
parameters(info, property).toEither()
) { _, _ -> this }.toValidated()

context(Raise<Nel<HookValidationError>>)
private fun HookProperty.Waterfall.validate(
info: HookInfo,
property: KSPropertyDeclaration,
) {
zipOrAccumulate(
{ arity(info, property) },
{ parameters(info, property) },
) { _, _ -> }
}

private fun HookProperty.Waterfall.arity(
info: HookInfo,
Expand All @@ -41,10 +76,29 @@ private fun HookProperty.Waterfall.arity(
else HookValidationError.WaterfallMustHaveParameters(property).invalidNel()
}

context(Raise<HookValidationError.WaterfallMustHaveParameters>)
private fun HookProperty.Waterfall.arity(
info: HookInfo,
property: KSPropertyDeclaration,
) {
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,
property: KSPropertyDeclaration,
) {
ensure(info.hookSignature.returnType == info.params.firstOrNull()?.type) {
HookValidationError.WaterfallParameterTypeMustMatch(property)
}
}
Loading