Skip to content

Commit 2a157a2

Browse files
blu3berry-whygyenese.matyas
andauthored
fix: stop silently dropping @OptIn markers in generated code (#107)
Two holes in opt-in marker collection made emission classpath-sensitive and the loss silent: - DelegateRegistryGenerator.collectMarkers cast the receiver/return declaration straight to KSClassDeclaration. KSP never auto-expands typealiases, so an aliased type resolved to a KSTypeAlias, the cast failed, and the underlying class's @RequiresOptIn markers were skipped without a warning. Markers are now collected from both the alias declaration and the unaliased class via unwrapTypeAliases(). - OptInMarkerCollector only walked declaration-site annotations, so opt-in expressed as @file:OptIn(...) — a standard codegen emission style — contributed nothing. The containing file's annotations are now walked too. Adds OptInPropagationTest cases for aliased receiver/return types on @KraftConverter delegates and for @file:OptIn on both the mapper and delegate paths; all three reproduced the marker loss before the fix. Fixes #104 Fixes #106 Co-authored-by: gyenese.matyas <gyenese.matyas@novaservices.hu>
1 parent c855763 commit 2a157a2

3 files changed

Lines changed: 119 additions & 3 deletions

File tree

kraft-core/src/main/kotlin/com/blu3berry/kraft/processor/codegen/OptInMarkerCollector.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.blu3berry.kraft.processor.codegen
33
import com.google.devtools.ksp.symbol.KSAnnotated
44
import com.google.devtools.ksp.symbol.KSAnnotation
55
import com.google.devtools.ksp.symbol.KSClassDeclaration
6+
import com.google.devtools.ksp.symbol.KSDeclaration
67
import com.google.devtools.ksp.symbol.KSType
78
import com.blu3berry.kraft.model.descriptor.MapperDescriptor
89
import com.blu3berry.kraft.model.descriptor.MappingSource
@@ -77,7 +78,18 @@ object OptInMarkerCollector {
7778
}
7879

7980
private fun collectFrom(symbol: KSAnnotated, sink: MutableMap<String, OptInMarker>) {
80-
for (annotation in symbol.annotations) {
81+
collectFromAnnotations(symbol.annotations, sink)
82+
// A generator (or user) can express opt-in as `@file:OptIn(...)` instead of
83+
// annotating the declaration; those markers must propagate the same way or
84+
// they are silently dropped (#106).
85+
(symbol as? KSDeclaration)?.containingFile?.let { collectFromAnnotations(it.annotations, sink) }
86+
}
87+
88+
private fun collectFromAnnotations(
89+
annotations: Sequence<KSAnnotation>,
90+
sink: MutableMap<String, OptInMarker>
91+
) {
92+
for (annotation in annotations) {
8193
val annotationType = annotation.annotationType.resolve().declaration as? KSClassDeclaration
8294
?: continue
8395
val annotationFq = annotationType.qualifiedName?.asString() ?: continue

kraft-ksp/src/jvmTest/kotlin/com/blu3berry/kraft/optin/OptInPropagationTest.kt

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,102 @@ class OptInPropagationTest {
122122
assertThat(text).doesNotContain("@OptIn")
123123
}
124124

125+
@Test
126+
fun `markers survive typealiased receiver and return types on delegate registry`() {
127+
val source = SourceFile.kotlin(
128+
"Models.kt",
129+
"""
130+
package models
131+
132+
@RequiresOptIn(level = RequiresOptIn.Level.WARNING)
133+
annotation class ExperimentalId
134+
135+
@RequiresOptIn(level = RequiresOptIn.Level.WARNING)
136+
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPEALIAS)
137+
annotation class ExperimentalAlias
138+
139+
@ExperimentalId
140+
class StampId(val raw: String)
141+
142+
@ExperimentalAlias
143+
typealias AliasedId = StampId
144+
145+
@com.blu3berry.kraft.config.KraftConverter
146+
fun String.toAliasedId(): AliasedId = StampId(this)
147+
148+
@com.blu3berry.kraft.config.KraftConverter
149+
fun AliasedId.toRaw(): String = raw
150+
"""
151+
)
152+
153+
val generated = TestKspRunner.compileAndReturnGenerated(source)
154+
val text = generated.first { it.name.startsWith("Converters_") }.readText()
155+
156+
// The underlying class's marker must be found through the alias, and the
157+
// alias's own marker must be collected too — one delegate per direction.
158+
assertThat("ExperimentalId::class".toRegex().findAll(text).count()).isEqualTo(2)
159+
assertThat("ExperimentalAlias::class".toRegex().findAll(text).count()).isEqualTo(2)
160+
}
161+
162+
@Test
163+
fun `file-level @file OptIn on the source class file propagates to the mapper`() {
164+
val markers = SourceFile.kotlin(
165+
"Markers.kt",
166+
"""
167+
package models
168+
169+
@RequiresOptIn(level = RequiresOptIn.Level.WARNING)
170+
annotation class ExperimentalModel
171+
"""
172+
)
173+
val source = SourceFile.kotlin(
174+
"Models.kt",
175+
"""
176+
@file:OptIn(models.ExperimentalModel::class)
177+
package models
178+
179+
data class Src(val count: Int)
180+
data class Dst(val count: Int)
181+
182+
@com.blu3berry.kraft.config.MapConfig(source = Src::class, target = Dst::class)
183+
object SrcMapper
184+
"""
185+
)
186+
187+
val generated = TestKspRunner.compileAndReturnGenerated(markers, source)
188+
val text = generated.first { it.name.contains("Src") }.readText()
189+
190+
assertThat(text).contains("@OptIn(ExperimentalModel::class)")
191+
}
192+
193+
@Test
194+
fun `file-level @file OptIn on the converter file propagates to the delegate registry`() {
195+
val markers = SourceFile.kotlin(
196+
"Markers.kt",
197+
"""
198+
package models
199+
200+
@RequiresOptIn(level = RequiresOptIn.Level.WARNING)
201+
annotation class ExperimentalConv
202+
"""
203+
)
204+
val source = SourceFile.kotlin(
205+
"Converters.kt",
206+
"""
207+
@file:OptIn(models.ExperimentalConv::class)
208+
package models
209+
210+
@com.blu3berry.kraft.config.KraftConverter
211+
fun Int.toLabel(): String = "n=" + this
212+
"""
213+
)
214+
215+
val generated = TestKspRunner.compileAndReturnGenerated(markers, source)
216+
val text = generated.first { it.name.startsWith("Converters_") }.readText()
217+
218+
assertThat(text).contains("ExperimentalConv::class")
219+
}
220+
125221
@Test
126222
fun `@OptIn on @MapUsing converter propagates to generated function`() {
127223
val source = SourceFile.kotlin(

kraft-ksp/src/main/kotlin/com/blu3berry/kraft/processor/codegen/generator/DelegateRegistryGenerator.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.google.devtools.ksp.symbol.KSClassDeclaration
88
import com.google.devtools.ksp.symbol.KSFile
99
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
1010
import com.google.devtools.ksp.symbol.KSType
11+
import com.google.devtools.ksp.symbol.KSTypeAlias
1112
import com.squareup.kotlinpoet.AnnotationSpec
1213
import com.squareup.kotlinpoet.ClassName
1314
import com.squareup.kotlinpoet.FileSpec
@@ -25,6 +26,7 @@ import com.blu3berry.kraft.processor.codegen.OptInMarkerCollector
2526
import com.blu3berry.kraft.processor.codegen.className
2627
import com.blu3berry.kraft.processor.util.DelegateNaming
2728
import com.blu3berry.kraft.processor.util.KraftKspConstants
29+
import com.blu3berry.kraft.processor.util.unwrapTypeAliases
2830

2931
/**
3032
* Emits a single Kotlin file at `kraft.generated.registry.Converters_<moduleId>.kt`
@@ -186,9 +188,15 @@ class DelegateRegistryGenerator(
186188
receiverType: KSType,
187189
returnType: KSType
188190
): List<OptInMarker> {
191+
// KSP never auto-expands aliases: an aliased receiver/return resolves to
192+
// the KSTypeAlias node, not the class, so markers must be collected from
193+
// both the alias declaration (it can carry its own) and the unaliased
194+
// class, or they are silently dropped (#104/#106).
189195
val targets = mutableListOf<KSAnnotated>(original)
190-
(receiverType.declaration as? KSClassDeclaration)?.let(targets::add)
191-
(returnType.declaration as? KSClassDeclaration)?.let(targets::add)
196+
for (type in listOf(receiverType, returnType)) {
197+
(type.declaration as? KSTypeAlias)?.let(targets::add)
198+
(type.unwrapTypeAliases().declaration as? KSClassDeclaration)?.let(targets::add)
199+
}
192200
return OptInMarkerCollector.collectFromAnnotated(targets)
193201
}
194202

0 commit comments

Comments
 (0)