Skip to content

Commit

Permalink
Add support for KSAnnotation.toAnnotationSpec() (#1196)
Browse files Browse the repository at this point in the history
  • Loading branch information
ZacSweers committed Dec 9, 2021
1 parent 1df289e commit 5d9a3e2
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 18 deletions.
120 changes: 120 additions & 0 deletions interop/ksp/src/main/kotlin/com/squareup/kotlinpoet/ksp/annotations.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Copyright (C) 2021 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.kotlinpoet.ksp

import com.google.devtools.ksp.symbol.AnnotationUseSiteTarget
import com.google.devtools.ksp.symbol.ClassKind
import com.google.devtools.ksp.symbol.KSAnnotation
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSName
import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.KSTypeAlias
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.ParameterizedTypeName

/** Returns an [AnnotationSpec] representation of this [KSAnnotation] instance. */
@KotlinPoetKspPreview
public fun KSAnnotation.toAnnotationSpec(): AnnotationSpec {
val builder = when (val type = annotationType.resolve().unwrapTypeAlias().toTypeName()) {
is ClassName -> AnnotationSpec.builder(type)
is ParameterizedTypeName -> AnnotationSpec.builder(type)
else -> error("This is never possible.")
}
useSiteTarget?.let { builder.useSiteTarget(it.kpAnalog) }
// TODO support type params once they're exposed https://github.com/google/ksp/issues/753
for (argument in arguments) {
val member = CodeBlock.builder()
val name = argument.name!!.getShortName()
member.add("%L = ", name)
addValueToBlock(argument.value!!, member)
builder.addMember(member.build())
}
return builder.build()
}

private val AnnotationUseSiteTarget.kpAnalog: UseSiteTarget get() = when (this) {
AnnotationUseSiteTarget.FILE -> UseSiteTarget.FILE
AnnotationUseSiteTarget.PROPERTY -> UseSiteTarget.PROPERTY
AnnotationUseSiteTarget.FIELD -> UseSiteTarget.FIELD
AnnotationUseSiteTarget.GET -> UseSiteTarget.GET
AnnotationUseSiteTarget.SET -> UseSiteTarget.SET
AnnotationUseSiteTarget.RECEIVER -> UseSiteTarget.RECEIVER
AnnotationUseSiteTarget.PARAM -> UseSiteTarget.PARAM
AnnotationUseSiteTarget.SETPARAM -> UseSiteTarget.SETPARAM
AnnotationUseSiteTarget.DELEGATE -> UseSiteTarget.DELEGATE
}

internal fun KSType.unwrapTypeAlias(): KSType {
return if (this.declaration is KSTypeAlias) {
(this.declaration as KSTypeAlias).type.resolve()
} else {
this
}
}

@KotlinPoetKspPreview
private fun addValueToBlock(value: Any, member: CodeBlock.Builder) {
when (value) {
is List<*> -> {
// Array type
member.add("arrayOf(⇥⇥")
value.forEachIndexed { index, innerValue ->
if (index > 0) member.add(", ")
addValueToBlock(innerValue!!, member)
}
member.add("⇤⇤)")
}
is KSType -> {
val unwrapped = value.unwrapTypeAlias()
val isEnum = (unwrapped.declaration as KSClassDeclaration).classKind == ClassKind.ENUM_ENTRY
if (isEnum) {
val parent = unwrapped.declaration.parentDeclaration as KSClassDeclaration
val entry = unwrapped.declaration.simpleName.getShortName()
member.add("%T.%L", parent.toClassName(), entry)
} else {
member.add("%T::class", unwrapped.toClassName())
}
}
is KSName ->
member.add(
"%T.%L", ClassName.bestGuess(value.getQualifier()),
value.getShortName()
)
is KSAnnotation -> member.add("%L", value.toAnnotationSpec())
else -> member.add(memberForValue(value))
}
}

/**
* Creates a [CodeBlock] with parameter `format` depending on the given `value` object.
* Handles a number of special cases, such as appending "f" to `Float` values, and uses
* `%L` for other types.
*/
internal fun memberForValue(value: Any) = when (value) {
is Class<*> -> CodeBlock.of("%T::class", value)
is Enum<*> -> CodeBlock.of("%T.%L", value.javaClass, value.name)
is String -> CodeBlock.of("%S", value)
is Float -> CodeBlock.of("%Lf", value)
is Double -> CodeBlock.of("%L", value)
is Char -> CodeBlock.of("$value.toChar()")
is Byte -> CodeBlock.of("$value.toByte()")
is Short -> CodeBlock.of("$value.toShort()")
// Int or Boolean
else -> CodeBlock.of("%L", value)
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import com.squareup.kotlinpoet.ksp.KotlinPoetKspPreview
import com.squareup.kotlinpoet.ksp.addOriginatingKSFile
import com.squareup.kotlinpoet.ksp.kspDependencies
import com.squareup.kotlinpoet.ksp.originatingKSFiles
import com.squareup.kotlinpoet.ksp.toAnnotationSpec
import com.squareup.kotlinpoet.ksp.toKModifier
import com.squareup.kotlinpoet.ksp.toTypeName
import com.squareup.kotlinpoet.ksp.toTypeParameterResolver
Expand Down Expand Up @@ -62,6 +63,11 @@ class TestProcessor(private val env: SymbolProcessorEnvironment) : SymbolProcess
.apply {
decl.getVisibility().toKModifier()?.let { addModifiers(it) }
addModifiers(decl.modifiers.mapNotNull { it.toKModifier() })
addAnnotations(
decl.annotations
.filterNot { it.shortName.getShortName() == "ExampleAnnotation" }
.map { it.toAnnotationSpec() }.asIterable()
)
}
val classTypeParams = decl.typeParameters.toTypeParameterResolver()
classBuilder.addTypeVariables(
Expand Down Expand Up @@ -94,6 +100,10 @@ class TestProcessor(private val env: SymbolProcessorEnvironment) : SymbolProcess
.apply {
property.getVisibility().toKModifier()?.let { addModifiers(it) }
addModifiers(property.modifiers.mapNotNull { it.toKModifier() })
addAnnotations(
property.annotations
.map { it.toAnnotationSpec() }.asIterable()
)
}
.build()
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2021 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.kotlinpoet.ksp.test.processor

import kotlin.reflect.KClass

annotation class ExampleAnnotation

annotation class ComprehensiveAnnotation<T : CharSequence>(
val boolean: Boolean,
val booleanArray: BooleanArray,
val byte: Byte,
val byteArray: ByteArray,
val short: Short,
val shortArray: ShortArray,
val int: Int,
val intArray: IntArray,
val long: Long,
val longArray: LongArray,
val float: Float,
val floatArray: FloatArray,
val double: Double,
val doubleArray: DoubleArray,
val string: String,
val stringArray: Array<String>,
val someClass: KClass<*>,
val someClasses: Array<KClass<*>>,
val enumValue: AnnotationEnumValue,
val enumValueArray: Array<AnnotationEnumValue>,
val anotherAnnotation: AnotherAnnotation,
val anotherAnnotationArray: Array<AnotherAnnotation>,
// This is still included even when the argument is omitted until https://github.com/google/ksp/issues/674
val defaultingString: String = "defaultValue",
)

annotation class AnotherAnnotation(val input: String)

enum class AnnotationEnumValue {
ONE, TWO, THREE
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,42 @@ class TestProcessorTest {
"""
package test
import com.squareup.kotlinpoet.ksp.test.processor.AnnotationEnumValue
import com.squareup.kotlinpoet.ksp.test.processor.AnotherAnnotation
import com.squareup.kotlinpoet.ksp.test.processor.ComprehensiveAnnotation
import com.squareup.kotlinpoet.ksp.test.processor.ExampleAnnotation
typealias TypeAliasName = String
typealias GenericTypeAlias = List<String>
typealias ParameterizedTypeAlias<T> = List<T>
@ComprehensiveAnnotation<String>(
true, // Omit the name intentionally here to test names are still picked up
booleanArray = [true],
byte = 0.toByte(),
byteArray = [0.toByte()],
short = 0.toShort(),
shortArray = [0.toShort()],
int = 0,
intArray = [0],
long = 0L,
longArray = [0L],
float = 0f,
floatArray = [0f],
double = 0.0,
doubleArray = [0.0],
string = "Hello",
stringArray = ["Hello"],
someClass = String::class,
someClasses = [String::class, Int::class],
enumValue = AnnotationEnumValue.ONE,
enumValueArray = [AnnotationEnumValue.ONE, AnnotationEnumValue.TWO],
anotherAnnotation = AnotherAnnotation("Hello"),
anotherAnnotationArray = [AnotherAnnotation("Hello")]
)
@ExampleAnnotation
class SmokeTestClass<T, R : Any, E : Enum<E>> {
@field:AnotherAnnotation("siteTargeting")
private val propA: String = ""
internal val propB: String = ""
val propC: Int = 0
Expand Down Expand Up @@ -117,6 +145,9 @@ class TestProcessorTest {
"""
package test
import com.squareup.kotlinpoet.ksp.test.processor.AnnotationEnumValue
import com.squareup.kotlinpoet.ksp.test.processor.AnotherAnnotation
import com.squareup.kotlinpoet.ksp.test.processor.ComprehensiveAnnotation
import kotlin.Any
import kotlin.Array
import kotlin.Boolean
Expand All @@ -133,7 +164,33 @@ class TestProcessorTest {
import kotlin.collections.MutableList
import kotlin.collections.Set
@ComprehensiveAnnotation<String>(
boolean = true,
booleanArray = arrayOf(true),
byte = 0.toByte(),
byteArray = arrayOf(0.toByte()),
short = 0.toShort(),
shortArray = arrayOf(0.toShort()),
int = 0,
intArray = arrayOf(0),
long = 0,
longArray = arrayOf(0),
float = 0.0f,
floatArray = arrayOf(0.0f),
double = 0.0,
doubleArray = arrayOf(0.0),
string = "Hello",
stringArray = arrayOf("Hello"),
someClass = String::class,
someClasses = arrayOf(String::class, Int::class),
enumValue = AnnotationEnumValue.ONE,
enumValueArray = arrayOf(AnnotationEnumValue.ONE, AnnotationEnumValue.TWO),
anotherAnnotation = AnotherAnnotation(input = "Hello"),
anotherAnnotationArray = arrayOf(AnotherAnnotation(input = "Hello")),
defaultingString = "defaultValue"
)
public class SmokeTestClass<T, R : Any, E : Enum<E>> {
@field:AnotherAnnotation(input = "siteTargeting")
private val propA: String
internal val propB: String
Expand Down

0 comments on commit 5d9a3e2

Please sign in to comment.