Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do not repeat @OptIn annotation for generated class #773

Merged
merged 2 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ ktorfit{

## Fixed
- @Headers annotation produces unexpected newline in generated code by ksp plugin #752
- Generated code containing repeated @OptIn annotation #767

# [2.2.0]()
* Supported Kotlin version: 2.0.0; 2.0.10; 2.0.20, 2.1.0-Beta1; 2.0.21-RC, 2.0.21, 2.1.0-RC, 2.1.0-RC2, 2.1.0, 2.1.10
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package de.jensklingenberg.ktorfit.poetspec

import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.symbol.KSAnnotation
import com.google.devtools.ksp.symbol.KSPropertyDeclaration
import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.KSTypeReference
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
Expand All @@ -10,7 +12,7 @@ import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.ksp.toAnnotationSpec
import com.squareup.kotlinpoet.ksp.toClassName
import com.squareup.kotlinpoet.ksp.toTypeName
import de.jensklingenberg.ktorfit.KtorfitOptions
import de.jensklingenberg.ktorfit.model.ClassData
Expand Down Expand Up @@ -56,25 +58,16 @@ private fun createImplClassTypeSpec(
implClassProperties: List<PropertySpec>,
funSpecs: List<FunSpec>
): TypeSpec {
val optInAnnotations =
classData.annotations.filter { it.shortName.getShortName() == "OptIn" }.map { it.toAnnotationSpec() }
val helperProperty =
PropertySpec
.builder(converterHelper.objectName, converterHelper.toClassName())
.initializer("${converterHelper.name}(${ktorfitClass.objectName})")
.addModifiers(KModifier.PRIVATE)
.build()

val internalApiAnnotation =
AnnotationSpec
.builder(ClassName("kotlin", "OptIn"))
.addMember(
"%T::class",
internalApi,
).build()
return TypeSpec
.classBuilder(implClassName)
.addAnnotations(optInAnnotations + internalApiAnnotation)
.addAnnotation(getOptInAnnotation(classData.annotations))
.addModifiers(classData.modifiers)
.primaryConstructor(
FunSpec
Expand All @@ -94,6 +87,27 @@ private fun createImplClassTypeSpec(
.build()
}

private fun getOptInAnnotation(annotations: List<KSAnnotation>): AnnotationSpec {
val markerClasses =
annotations
.filter { it.shortName.getShortName() == "OptIn" }
.flatMap { annotation ->
@Suppress("UNCHECKED_CAST")
(annotation.arguments[0].value as? List<KSType>)
?.map { it.toClassName() }
.orEmpty()
}
.plus(internalApi)
.toTypedArray<Any>()

val format = (1..markerClasses.size).joinToString { "%T::class" }

return AnnotationSpec
.builder(ClassName("kotlin", "OptIn"))
.addMember(format, *markerClasses)
.build()
}

private fun propertySpec(property: KSPropertyDeclaration): PropertySpec {
val propBuilder =
PropertySpec
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,16 @@ import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi

@OptIn(ExperimentalCompilerApi::class)
interface TestService {
@Headers(value = ["x:y","a:b"])
@GET("posts")
@OptIn(ExperimentalCompilerApi::class)
suspend fun test(@Header("testHeader") testParameterNonNullable: String?, @Header("testHeader") testParameterNullable: String?, @HeaderMap("testHeaderMap") testParameter2: Map<String,String>): String
@Headers(value = ["x:y","a:b"])
@GET("posts")
@OptIn(ExperimentalCompilerApi::class)
suspend fun test(@Header("testHeader") testParameterNonNullable: String?, @Header("testHeader") testParameterNullable: String?, @HeaderMap("testHeaderMap") testParameter2: Map<String,String>): String
}
""",
)

val expectedHeadersArgumentText =
"""@OptIn(ExperimentalCompilerApi::class)
@OptIn(InternalKtorfitApi::class)
"""@OptIn(ExperimentalCompilerApi::class, InternalKtorfitApi::class)
public class _TestServiceImpl(
private val _ktorfit: Ktorfit,
) : TestService {
Expand All @@ -41,9 +40,52 @@ public class _TestServiceImpl(
override suspend fun test("""

val compilation = getCompilation(listOf(source))
val result = compilation.compile()

val generatedSourcesDir = compilation.kspSourcesDir
val generatedSourcesDir = compilation.apply { compile() }.kspSourcesDir
val generatedFile =
File(
generatedSourcesDir,
"/kotlin/com/example/api/_TestServiceImpl.kt",
)

val actualSource = generatedFile.readText()
assertTrue(actualSource.contains(expectedHeadersArgumentText))
}

@Test
fun `when OptIn annotation are add to the implementation class do not repeat marker classes`() {
val source =
SourceFile.kotlin(
"Source.kt",
"""package com.example.api
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.Headers
import de.jensklingenberg.ktorfit.http.Header
import de.jensklingenberg.ktorfit.http.HeaderMap
import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi

@OptIn(ExperimentalCompilerApi::class)
interface TestService {
@GET("posts")
@OptIn(ExperimentalCompilerApi::class)
suspend fun test1()

@GET("posts")
@OptIn(ExperimentalCompilerApi::class)
suspend fun test2()
}
""",
)

val expectedHeadersArgumentText =
"""@OptIn(ExperimentalCompilerApi::class, InternalKtorfitApi::class)
public class _TestServiceImpl(
private val _ktorfit: Ktorfit,
) : TestService {"""

val compilation = getCompilation(listOf(source))

val generatedSourcesDir = compilation.apply { compile() }.kspSourcesDir
val generatedFile =
File(
generatedSourcesDir,
Expand Down