Skip to content

Commit

Permalink
Expand ksp-processor
Browse files Browse the repository at this point in the history
- Add @OtherIfDefault
- Add Either
  • Loading branch information
DRSchlaubi committed Sep 13, 2023
1 parent a5bc7b3 commit c6930c4
Show file tree
Hide file tree
Showing 11 changed files with 216 additions and 22 deletions.
2 changes: 1 addition & 1 deletion .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 54 additions & 9 deletions docs/topics/Annotation-Argument-Processor.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Annotation Argument Processor

Even though the [Annotation Argument API](Annotation-Argument-Processor.md) provides an easier to use API than the
Even though the [Annotation Argument API](Annotation-Argument-Processor.md) provides an easier to use API than the
default KSP API, it can still require some boiler-plate to setup. For this reason an annotation processor is provided
to generate that boiler plate for you

Expand Down Expand Up @@ -51,12 +51,12 @@ Please note that the actual class returned is a "data class representation" of t
```kotlin

public data class InlineConstructor private constructor(
public val forClass: KSType, // KClass turns into KSType
public val functionName: String,
public val nameMapping: List<NameMapping>, // Array turns into List
public val ignoreBuilders: List<String>, // Array turns into list
public val useQualifiedName: Boolean,
public val nameProperty: String?, // This becomes nullable, because it is annotated with @NullIfDefault
public val forClass: KSType, // KClass turns into KSType
public val functionName: String,
public val nameMapping: List<NameMapping>, // Array turns into List
public val ignoreBuilders: List<String>, // Array turns into list
public val useQualifiedName: Boolean,
public val nameProperty: String?, // This becomes nullable, because it is annotated with @NullIfDefault
) {
public data class NameMapping private constructor(
public val originalName: String,
Expand All @@ -65,7 +65,52 @@ public data class InlineConstructor private constructor(
}
```

> You can find the full code [here](https://github.com/kordlib/codegen-kt/blob/main/ksp-annotations/src/commonMain/kotlin/InlineConstructor.kt)
> You can find the full
> code [here](https://github.com/kordlib/codegen-kt/blob/main/ksp-annotations/src/commonMain/kotlin/InlineConstructor.kt)

Please read the documentation of the `ProcessorAnnotation` annotation [here](https://codegen.kord.dev/api/ksp/dev.kord.codegen.ksp.annotations/-processor-annotation/index.html)
Please read the documentation of the `ProcessorAnnotation`
annotation [here](https://codegen.kord.dev/api/ksp/dev.kord.codegen.ksp.annotations/-processor-annotation/index.html)

### Default values

The processor also allows you to use some other default values than in you annotation, for example `null` or default to
another propery

```kotlin
const val DEFAULT_VALUE = "DEFAULT!"

annotation class TestAnnotation(
@NullIfDefault
val nullable: String = DEFAULT_VALUE // will be represented as "null" if not explicitly defined
// Will be the same as "nullable" if default
// Please note that since "nullable" is annotated with @NullIfDefault other will be nullable too
@OtherIfDefault("nullable")
val other: String = DEFAULT_VALUE
)
```

### Validation

You are also able to validate that certain values are different, let's say we want either an int or a string value,
but not both or none

```kotlin
const val NO_INT = -1
const val NO_STRING = ""

@Eihter(["intValue", "stringValue"], exclusive = true)
annotation class ExpressionValue(
@NullIfDefault
val intValue: Int = NO_INT,
@NullIfDefault
val stringValue: Int = NO_STRING,
)
```

This will throw an `IllegalStateException` in the [factory functions](#use-the-generated-code) when
none or both values are present

> Note that all properties used in an `@Either` annotation must be annotated with `@NullIfDefault`
>
{style="warning"}
2 changes: 0 additions & 2 deletions docs/topics/about.topic
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,4 @@
<a href="https://kotlinlang.org/docs/ksp-quickstart.html" summary="KSP Documentation"/>
</secondary>
</section-starting-page>


</topic>
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ public annotation class InlineConstructor(
val ignoreBuilders: Array<String> = [],
val useQualifiedName: Boolean = false,
@NullIfDefault
val nameProperty: String = NO_DELEGATION
val nameProperty: String = NO_DELEGATION,
@NullIfDefault
val nameProperty2: String = NO_DELEGATION
) {

/**
Expand Down
4 changes: 4 additions & 0 deletions ksp/annotations/api/annotations.api
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
public abstract interface annotation class dev/kord/codegen/ksp/annotations/NullIfDefault : java/lang/annotation/Annotation {
}

public abstract interface annotation class dev/kord/codegen/ksp/annotations/OtherIfDefault : java/lang/annotation/Annotation {
public abstract fun name ()Ljava/lang/String;
}

public abstract interface annotation class dev/kord/codegen/ksp/annotations/ProcessorAnnotation : java/lang/annotation/Annotation {
public abstract fun packageName ()Ljava/lang/String;
}
Expand Down
16 changes: 16 additions & 0 deletions ksp/annotations/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
org.jetbrains.kotlin.multiplatform
`kord-publishing`
com.google.devtools.ksp
}

base {
Expand All @@ -26,4 +27,19 @@ kotlin {
// tvos()
// macosX64()
// macosArm64()

sourceSets {
named("jvmMain") {
kotlin.srcDirs("build/generated/ksp/jvm/jvmMain/kotlin")
dependencies {
compileOnly(libs.ksp.api)
compileOnly(libs.codegen.ksp)
}
}
}
}

dependencies {
"kspJvm"(libs.codegen.ksp.processor)

}
30 changes: 28 additions & 2 deletions ksp/annotations/src/commonMain/kotlin/ProcessorAnnotation.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package dev.kord.codegen.ksp.annotations

private const val packageName = "dev.kord.codegen.ksp.processor"

/**
* This annotation instructs the processor to generate a data class wrapper around the annotation with
* factory functions to instantiate it from a `KSAnnotation`.
Expand Down Expand Up @@ -32,14 +34,38 @@ package dev.kord.codegen.ksp.annotations
@MustBeDocumented
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.ANNOTATION_CLASS)
@ProcessorAnnotation(packageName = "dev.kord.codegen.ksp.processor")
@ProcessorAnnotation(packageName)
public annotation class ProcessorAnnotation(
val packageName: String
)

/**
* Instructs the processor to use null, if [AnnotationArguments.isDefault] returns `true` for this property.
* Instructs the processor to use null, if `AnnotationArguments.isDefault` returns `true` for this property.
*/
@MustBeDocumented
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.FIELD)
public annotation class NullIfDefault

/**
* Instructs the processor to use value of [name], if `AnnotationArguments.isDefault` returns `true` for this property.
*
* @property name the name of the property to use as a default
*/
@MustBeDocumented
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.FIELD)
@ProcessorAnnotation(packageName)
public annotation class OtherIfDefault(val name: String)

/**
* Instructs the processor to validate that either of [names] are present.
*
* @property exclusive whether there should be only once exclusive value
*/
@MustBeDocumented
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.ANNOTATION_CLASS)
@Repeatable
@ProcessorAnnotation(packageName)
public annotation class Either(val names: Array<String>, val exclusive: Boolean = false)
13 changes: 11 additions & 2 deletions ksp/processor/src/main/kotlin/generator/DataClassRepresentation.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package dev.kord.codegen.ksp.processor.generator

import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.getDeclaredProperties
import com.google.devtools.ksp.isAnnotationPresent
import com.google.devtools.ksp.symbol.*
import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.ksp.addOriginatingKSFile
import com.squareup.kotlinpoet.ksp.toClassName
import com.squareup.kotlinpoet.ksp.toTypeName
import dev.kord.codegen.kotlinpoet.*
import dev.kord.codegen.ksp.processor.NULL_IF_DEFAULT
import dev.kord.codegen.ksp.annotations.NullIfDefault
import dev.kord.codegen.ksp.annotations.OtherIfDefault
import dev.kord.codegen.ksp.processor.PROCESSOR_ANNOTATION
import dev.kord.codegen.ksp.processor.getOtherIfDefault

fun KSType.isMappedAnnotation(rootType: KSClassDeclaration): Boolean {
val declaration = declaration
Expand Down Expand Up @@ -47,11 +51,16 @@ private fun KSTypeReference.dataClassType(rootType: KSClassDeclaration): TypeNam
}
}

@OptIn(KspExperimental::class)
fun KSPropertyDeclaration.dataClassType(rootType: KSClassDeclaration): TypeName {
val notNullType = type.dataClassType(rootType)

return if (annotations.any { it.annotationType.resolve().declaration.qualifiedName!!.asString() == NULL_IF_DEFAULT }) {
return if (isAnnotationPresent(NullIfDefault::class)) {
notNullType.copy(nullable = true)
} else if (isAnnotationPresent(OtherIfDefault::class)) {
val annotation = getOtherIfDefault()
val otherField = rootType.getDeclaredProperties().first { it.simpleName.asString() == annotation.name }
otherField.dataClassType(rootType)
} else {
notNullType
}
Expand Down
41 changes: 38 additions & 3 deletions ksp/processor/src/main/kotlin/generator/FactoryFunction.kt
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
package dev.kord.codegen.ksp.processor.generator

import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.getDeclaredProperties
import com.google.devtools.ksp.isAnnotationPresent
import com.google.devtools.ksp.symbol.KSAnnotation
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.ksp.toClassName
import dev.kord.codegen.kotlinpoet.addCode
import dev.kord.codegen.kotlinpoet.addFunction
import dev.kord.codegen.kotlinpoet.addParameter
import dev.kord.codegen.kotlinpoet.withControlFlow
import dev.kord.codegen.ksp.annotations.Either
import dev.kord.codegen.ksp.annotations.NullIfDefault
import dev.kord.codegen.ksp.annotations.OtherIfDefault
import dev.kord.codegen.ksp.processor.ARGUMENTS
import dev.kord.codegen.ksp.processor.ARGUMENTS_NOT_NULL
import dev.kord.codegen.ksp.processor.NULL_IF_DEFAULT
import dev.kord.codegen.ksp.processor.getEithers
import dev.kord.codegen.ksp.processor.getOtherIfDefault

context(TypeSpec.Builder)
@OptIn(KspExperimental::class)
fun KSClassDeclaration.factoryFunction(packageName: String) {
addFunction(simpleName.asString()) {
val type = ClassName(packageName, toClassName().simpleNames)
Expand Down Expand Up @@ -41,17 +49,44 @@ fun KSClassDeclaration.factoryFunction(packageName: String) {
add(".let(%1L.Companion::%1L)", argumentType.declaration.simpleName.asString())
}

if (it.annotations.any { it.annotationType.resolve().declaration.qualifiedName!!.asString() == NULL_IF_DEFAULT }) {
if (it.isAnnotationPresent(NullIfDefault::class) || it.isAnnotationPresent(OtherIfDefault::class)) {
withControlFlow(".takeIf") {
add("!arguments.isDefault(%T::%L)", toClassName(), it.simpleName.asString())
add("\n")
}

if (it.isAnnotationPresent(OtherIfDefault::class)) {
add("?:·%L", it.getOtherIfDefault().name)
}
}
add("\n")
}
addCode(property)
CodeBlock.of("%L", it.simpleName.asString())
}.joinToString(", ")

if (isAnnotationPresent(Either::class)) {
getEithers().forEach { (names, exclusive) ->
addCode {
val nameList = names.map { CodeBlock.of("%N", it) }.joinToCode(", ")
val condition = CodeBlock.of("it != null")
val check = buildCodeBlock {
add("listOf(%L)", nameList)
if (exclusive) {
add(".singleOrNull { %L }", condition)
add("·==·null")
} else {
add(".none { %L }", condition)
}
}
withControlFlow("if (%L)", check) {
add("error(%S)", "Either of these values must be present: $names")
add("\n")
}
add("\n")
}
}
}

addCode("return·%T(%L)", type, valueArguments)
}
}
52 changes: 51 additions & 1 deletion ksp/processor/src/main/kotlin/generator/Generator.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
package dev.kord.codegen.ksp.processor.generator

import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.getDeclaredProperties
import com.google.devtools.ksp.isAnnotationPresent
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSPropertyDeclaration
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.DelicateKotlinPoetApi
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.ksp.toClassName
import com.squareup.kotlinpoet.ksp.writeTo
import dev.kord.codegen.kotlinpoet.FileSpec
import dev.kord.codegen.ksp.annotations.NullIfDefault
import dev.kord.codegen.ksp.annotations.OtherIfDefault
import dev.kord.codegen.ksp.processor.getEithers
import dev.kord.codegen.ksp.processor.getOtherIfDefault
import dev.kord.codegen.ksp.processor.getProcessorAnnotation

data class ProcessingContext(
Expand All @@ -17,8 +25,50 @@ data class ProcessingContext(
val packageName: String
)

@OptIn(DelicateKotlinPoetApi::class)
private data class Property(
val index: Int,
val declaration: KSPropertyDeclaration
) : KSPropertyDeclaration by declaration

@OptIn(DelicateKotlinPoetApi::class, KspExperimental::class)
fun SymbolProcessorEnvironment.processAnnotation(declaration: KSClassDeclaration) {
val propertyTypes = declaration
.getDeclaredProperties()
.mapIndexed { index, it -> it.simpleName.asString() to Property(index, it) }
.toMap()
declaration.getEithers().forEach {
it.names.forEach { name ->
val property = propertyTypes[name] ?: run {
logger.error("Either type does not exist", declaration)
return
}

if (!property.isAnnotationPresent(NullIfDefault::class)) {
logger.error("Either can only be used with @NullIfDefault values", declaration)
return
}
}
}
declaration.getDeclaredProperties().forEachIndexed { index, it ->
if (it.isAnnotationPresent(OtherIfDefault::class)) {
if (it.isAnnotationPresent(NullIfDefault::class)) {
logger.error("Usage of both @NullIfDefault and @OtherIfDefault is not allowed", it)
} else {
val name = it.getOtherIfDefault().name
val property = propertyTypes[name] ?: return run {
logger.error("Specified property name does not exist: $name", it)
}
if (!it.type.resolve().isAssignableFrom(property.type.resolve())) {
logger.error("Specified property has different type! Expected ${it.type} found $property", it)
return
}
if (index < property.index) {
logger.error("Specified property comes after current property", it)
return
}
}
}
}
val packageName = declaration.getProcessorAnnotation().packageName
val fileSpec = FileSpec(packageName, declaration.simpleName.asString()) {
addKotlinDefaultImports(includeJs = false)
Expand Down

0 comments on commit c6930c4

Please sign in to comment.