Skip to content

Commit

Permalink
Introduce ksp.map.annotation.arguments.in.java
Browse files Browse the repository at this point in the history
which defaults to false. When enabled, annotation arguments are mapped
to Kotlin types in Java source files.

(cherry picked from commit a9f2244)
  • Loading branch information
ting-yuan authored and KSP Auto Pick committed Jan 19, 2023
1 parent a3fc613 commit 03b0374
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 33 deletions.
12 changes: 12 additions & 0 deletions common-util/src/main/kotlin/com/google/devtools/ksp/KspOptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class KspOptions(
val commonSources: List<File>,

val excludedProcessors: Set<String>,
val mapAnnotationArgumentsInJava: Boolean,
) {
class Builder {
var projectBaseDir: File? = null
Expand Down Expand Up @@ -93,6 +94,7 @@ class KspOptions(
var commonSources: MutableList<File> = mutableListOf()

var excludedProcessors: MutableSet<String> = mutableSetOf()
var mapAnnotationArgumentsInJava: Boolean = false

fun build(): KspOptions {
return KspOptions(
Expand Down Expand Up @@ -121,6 +123,7 @@ class KspOptions(
compilerVersion,
commonSources,
excludedProcessors,
mapAnnotationArgumentsInJava,
)
}
}
Expand Down Expand Up @@ -299,6 +302,14 @@ enum class KspCliOption(
false,
true
),

MAP_ANNOTATION_ARGUMENTS_IN_JAVA_OPTION(
"mapAnnotationArgumentsInJava",
"<mapAnnotationArgumentsInJava>",
"Map types in annotation arguments in Java sources",
false,
false
),
}

@Suppress("IMPLICIT_CAST_TO_ANY")
Expand Down Expand Up @@ -328,4 +339,5 @@ fun KspOptions.Builder.processOption(option: KspCliOption, value: String) = when
KspCliOption.RETURN_OK_ON_ERROR_OPTION -> returnOkOnError = value.toBoolean()
KspCliOption.COMMON_SOURCES_OPTION -> commonSources.addAll(value.split(File.pathSeparator).map { File(it) })
KspCliOption.EXCLUDED_PROCESSORS_OPTION -> excludedProcessors.addAll(value.split(":"))
KspCliOption.MAP_ANNOTATION_ARGUMENTS_IN_JAVA_OPTION -> mapAnnotationArgumentsInJava = value.toBoolean()
}
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,38 @@ class ResolverImpl(
resolverContext.typeResolver.transformJavaType(javaType, TypeUsage.COMMON.toAttributes())
}

/*
* Don't map Java types in annotation parameters
*
* Users may specify Java types explicitly by instances of `Class<T>`.
* The situation is similar to `getClassDeclarationByName` where we have
* decided to keep those Java types not mapped.
*
* It would be troublesome if users try to use reflection on types that
* were mapped to Kotlin builtins, becuase some of those builtins don't
* even exist in classpath.
*
* Therefore, ResolverImpl.resolveJavaType cannot be used.
*/
fun resolveJavaTypeInAnnotations(psiType: PsiType): KSType = if (options.mapAnnotationArgumentsInJava) {
getKSTypeCached(resolveJavaType(psiType))
} else {
when (psiType) {
is PsiPrimitiveType -> {
getClassDeclarationByName(psiType.boxedTypeName!!)!!.asStarProjectedType()
}
is PsiArrayType -> {
val componentType = resolveJavaTypeInAnnotations(psiType.componentType)
val componentTypeRef = createKSTypeReferenceFromKSType(componentType)
val typeArgs = listOf(getTypeArgument(componentTypeRef, Variance.INVARIANT))
builtIns.arrayType.replace(typeArgs)
}
else -> {
getClassDeclarationByName(psiType.canonicalText)?.asStarProjectedType() ?: KSErrorType
}
}
}

fun KotlinType.expandNonRecursively(): KotlinType =
(constructor.declarationDescriptor as? TypeAliasDescriptor)?.expandedType?.withAbbreviation(this as SimpleType)
?: this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import com.google.devtools.ksp.processing.impl.ResolverImpl
import com.google.devtools.ksp.symbol.*
import com.google.devtools.ksp.symbol.impl.binary.getAbsentDefaultArguments
import com.google.devtools.ksp.symbol.impl.binary.getDefaultConstructorArguments
import com.google.devtools.ksp.symbol.impl.kotlin.KSErrorType
import com.google.devtools.ksp.symbol.impl.kotlin.KSTypeImpl
import com.google.devtools.ksp.symbol.impl.toLocation
import com.intellij.lang.jvm.JvmClassKind
Expand Down Expand Up @@ -103,37 +102,6 @@ class KSAnnotationJavaImpl private constructor(val psi: PsiAnnotation) : KSAnnot
kotlinType?.getDefaultConstructorArguments(emptyList(), this) ?: emptyList()
}

/*
* Don't map Java types in annotation parameters
*
* Users may specify Java types explicitly by instances of `Class<T>`.
* The situation is similar to `getClassDeclarationByName` where we have
* decided to keep those Java types not mapped.
*
* It would be troublesome if users try to use reflection on types that
* were mapped to Kotlin builtins, becuase some of those builtins don't
* even exist in classpath.
*
* Therefore, ResolverImpl.resolveJavaType cannot be used.
*/
private fun resolveJavaTypeSimple(psiType: PsiType): KSType {
return when (psiType) {
is PsiPrimitiveType -> {
ResolverImpl.instance!!.getClassDeclarationByName(psiType.boxedTypeName!!)!!.asStarProjectedType()
}
is PsiArrayType -> {
val componentType = resolveJavaTypeSimple(psiType.componentType)
val componentTypeRef = ResolverImpl.instance!!.createKSTypeReferenceFromKSType(componentType)
val typeArgs = listOf(ResolverImpl.instance!!.getTypeArgument(componentTypeRef, Variance.INVARIANT))
ResolverImpl.instance!!.builtIns.arrayType.replace(typeArgs)
}
else -> {
ResolverImpl.instance!!.getClassDeclarationByName(psiType.canonicalText)?.asStarProjectedType()
?: KSErrorType
}
}
}

private fun calcValue(value: PsiAnnotationMemberValue?): Any? {
if (value is PsiAnnotation) {
return getCached(value)
Expand All @@ -149,7 +117,7 @@ class KSAnnotationJavaImpl private constructor(val psi: PsiAnnotation) : KSAnnot
}
return when (result) {
is PsiType -> {
resolveJavaTypeSimple(result)
ResolverImpl.instance!!.resolveJavaTypeInAnnotations(result)
}
is PsiLiteralValue -> {
result.value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool
"excludedProcessors",
kspExtension.excludedProcessors.joinToString(":")
)
options += SubpluginOption(
"mapAnnotationArgumentsInJava",
project.findProperty("ksp.map.annotation.arguments.in.java")?.toString() ?: "false"
)
commandLineArgumentProviders.get().forEach {
it.asArguments().forEach { argument ->
if (!argument.matches(Regex("\\S+=\\S+"))) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.google.devtools.ksp.test

import org.gradle.testkit.runner.GradleRunner
import org.gradle.testkit.runner.TaskOutcome
import org.junit.Assert
import org.junit.Rule
import org.junit.Test

class MapAnnotationArgumentsIT {
@Rule
@JvmField
val project: TemporaryTestProject = TemporaryTestProject("map-annotation-arguments", "test-processor")

val expectedErrors = listOf(
"e: [ksp] unboxedChar: Char != Character\n",
"e: [ksp] boxedChar: (Char..Char?) != Character\n",
"e: Error occurred in KSP, check log for detail\n",
)

@Test
fun testMapAnnotationArguments() {
val gradleRunner = GradleRunner.create().withProjectDir(project.root)

gradleRunner.withArguments("assemble", "-Pksp.map.annotation.arguments.in.java=true").build().let { result ->
Assert.assertEquals(TaskOutcome.SUCCESS, result.task(":workload:kspKotlin")?.outcome)
Assert.assertEquals(TaskOutcome.SUCCESS, result.task(":workload:assemble")?.outcome)
}

gradleRunner.withArguments("clean", "assemble", "--rerun-tasks").buildAndFail().let { result ->
Assert.assertEquals(TaskOutcome.FAILED, result.task(":workload:kspKotlin")?.outcome)
Assert.assertTrue(expectedErrors.all { it in result.output })
}

gradleRunner.withArguments("clean", "assemble", "-Pksp.map.annotation.arguments.in.java=false", "--rerun-tasks")
.buildAndFail().let { result ->
Assert.assertEquals(TaskOutcome.FAILED, result.task(":workload:kspKotlin")?.outcome)
Assert.assertTrue(expectedErrors.all { it in result.output })
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import com.google.devtools.ksp.getClassDeclarationByName
import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.*

class TestProcessor(
val codeGenerator: CodeGenerator,
val logger: KSPLogger
) : SymbolProcessor {
val expected = mapOf(
"unboxedChar" to "Char",
"boxedChar" to "(Char..Char?)",
)

override fun process(resolver: Resolver): List<KSAnnotated> {
val j = resolver.getClassDeclarationByName("com.example.AnnotationTest")!!
j.annotations.forEach { annotation ->
annotation.arguments.forEach {
val key = it.name?.asString()
val value = it.value.toString()
if (expected[key] != value) {
logger.error("$key: ${expected[key]} != $value")
}
}
}

return emptyList()
}
}

class TestProcessorProvider : SymbolProcessorProvider {
override fun create(
environment: SymbolProcessorEnvironment
): SymbolProcessor {
return TestProcessor(environment.codeGenerator, environment.logger)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example;

@JavaAnnotation(
unboxedChar = char.class,
boxedChar = Character.class
)
public class AnnotationTest {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.example;

public @interface JavaAnnotation {
Class unboxedChar();
Class boxedChar();
}

0 comments on commit 03b0374

Please sign in to comment.