diff --git a/common-util/src/main/kotlin/com/google/devtools/ksp/KspOptions.kt b/common-util/src/main/kotlin/com/google/devtools/ksp/KspOptions.kt index 6a6aa2af93..c6538aab03 100644 --- a/common-util/src/main/kotlin/com/google/devtools/ksp/KspOptions.kt +++ b/common-util/src/main/kotlin/com/google/devtools/ksp/KspOptions.kt @@ -57,6 +57,7 @@ class KspOptions( val commonSources: List, val excludedProcessors: Set, + val mapAnnotationArgumentsInJava: Boolean, ) { class Builder { var projectBaseDir: File? = null @@ -93,6 +94,7 @@ class KspOptions( var commonSources: MutableList = mutableListOf() var excludedProcessors: MutableSet = mutableSetOf() + var mapAnnotationArgumentsInJava: Boolean = false fun build(): KspOptions { return KspOptions( @@ -121,6 +123,7 @@ class KspOptions( compilerVersion, commonSources, excludedProcessors, + mapAnnotationArgumentsInJava, ) } } @@ -299,6 +302,14 @@ enum class KspCliOption( false, true ), + + MAP_ANNOTATION_ARGUMENTS_IN_JAVA_OPTION( + "mapAnnotationArgumentsInJava", + "", + "Map types in annotation arguments in Java sources", + false, + false + ), } @Suppress("IMPLICIT_CAST_TO_ANY") @@ -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() } diff --git a/compiler-plugin/src/main/kotlin/com/google/devtools/ksp/processing/impl/ResolverImpl.kt b/compiler-plugin/src/main/kotlin/com/google/devtools/ksp/processing/impl/ResolverImpl.kt index e9b8bdf4aa..7c1fade23d 100644 --- a/compiler-plugin/src/main/kotlin/com/google/devtools/ksp/processing/impl/ResolverImpl.kt +++ b/compiler-plugin/src/main/kotlin/com/google/devtools/ksp/processing/impl/ResolverImpl.kt @@ -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`. + * 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 diff --git a/compiler-plugin/src/main/kotlin/com/google/devtools/ksp/symbol/impl/java/KSAnnotationJavaImpl.kt b/compiler-plugin/src/main/kotlin/com/google/devtools/ksp/symbol/impl/java/KSAnnotationJavaImpl.kt index 592748e462..f6ae397b53 100644 --- a/compiler-plugin/src/main/kotlin/com/google/devtools/ksp/symbol/impl/java/KSAnnotationJavaImpl.kt +++ b/compiler-plugin/src/main/kotlin/com/google/devtools/ksp/symbol/impl/java/KSAnnotationJavaImpl.kt @@ -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 @@ -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`. - * 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) @@ -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 diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt index 3b92367404..e242039a82 100644 --- a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt @@ -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+"))) { diff --git a/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/MapAnnotationArgumentsIT.kt b/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/MapAnnotationArgumentsIT.kt new file mode 100644 index 0000000000..870b4e60f8 --- /dev/null +++ b/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/MapAnnotationArgumentsIT.kt @@ -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 }) + } + } +} diff --git a/integration-tests/src/test/resources/map-annotation-arguments/test-processor/src/main/kotlin/TestProcessor.kt b/integration-tests/src/test/resources/map-annotation-arguments/test-processor/src/main/kotlin/TestProcessor.kt new file mode 100644 index 0000000000..7892f0128d --- /dev/null +++ b/integration-tests/src/test/resources/map-annotation-arguments/test-processor/src/main/kotlin/TestProcessor.kt @@ -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 { + 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) + } +} diff --git a/integration-tests/src/test/resources/map-annotation-arguments/workload/src/main/java/com/example/AnnotationTest.java b/integration-tests/src/test/resources/map-annotation-arguments/workload/src/main/java/com/example/AnnotationTest.java new file mode 100644 index 0000000000..18696806a1 --- /dev/null +++ b/integration-tests/src/test/resources/map-annotation-arguments/workload/src/main/java/com/example/AnnotationTest.java @@ -0,0 +1,8 @@ +package com.example; + +@JavaAnnotation( + unboxedChar = char.class, + boxedChar = Character.class +) +public class AnnotationTest { +} diff --git a/integration-tests/src/test/resources/map-annotation-arguments/workload/src/main/java/com/example/JavaAnnotation.java b/integration-tests/src/test/resources/map-annotation-arguments/workload/src/main/java/com/example/JavaAnnotation.java new file mode 100644 index 0000000000..c7c8368fa5 --- /dev/null +++ b/integration-tests/src/test/resources/map-annotation-arguments/workload/src/main/java/com/example/JavaAnnotation.java @@ -0,0 +1,6 @@ +package com.example; + +public @interface JavaAnnotation { + Class unboxedChar(); + Class boxedChar(); +} \ No newline at end of file