diff --git a/compiler-utils/src/main/kotlin/com/r0adkll/kimchi/util/ksp/ClassDeclarations.kt b/compiler-utils/src/main/kotlin/com/r0adkll/kimchi/util/ksp/ClassDeclarations.kt index edc7056..7281387 100644 --- a/compiler-utils/src/main/kotlin/com/r0adkll/kimchi/util/ksp/ClassDeclarations.kt +++ b/compiler-utils/src/main/kotlin/com/r0adkll/kimchi/util/ksp/ClassDeclarations.kt @@ -4,6 +4,7 @@ package com.r0adkll.kimchi.util.ksp import com.google.devtools.ksp.getAllSuperTypes import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSName import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.ksp.toClassName @@ -16,3 +17,6 @@ public fun KSClassDeclaration.implements(className: ClassName): Boolean { it.toClassName() == className } } + +public val KSClassDeclaration.requireQualifiedName: KSName + get() = qualifiedName!! diff --git a/compiler-utils/src/main/kotlin/com/r0adkll/kimchi/util/ksp/KSName.kt b/compiler-utils/src/main/kotlin/com/r0adkll/kimchi/util/ksp/KSName.kt new file mode 100644 index 0000000..06d650d --- /dev/null +++ b/compiler-utils/src/main/kotlin/com/r0adkll/kimchi/util/ksp/KSName.kt @@ -0,0 +1,12 @@ +// Copyright (C) 2024 r0adkll +// SPDX-License-Identifier: Apache-2.0 +package com.r0adkll.kimchi.util.ksp + +import com.google.devtools.ksp.symbol.KSName + +/** + * Convert a [KSName] to a URL-safe string that can be used as a file name + * @receiver [KSName] + * @return a url safe name with '.' replaced with '_' characters + */ +public fun KSName.asUrlSafeString(): String = asString().replace(".", "_") diff --git a/compiler/src/main/kotlin/com/r0adkll/kimchi/processors/HintSymbolProcessor.kt b/compiler/src/main/kotlin/com/r0adkll/kimchi/processors/HintSymbolProcessor.kt index ec84e2d..6b1dc78 100644 --- a/compiler/src/main/kotlin/com/r0adkll/kimchi/processors/HintSymbolProcessor.kt +++ b/compiler/src/main/kotlin/com/r0adkll/kimchi/processors/HintSymbolProcessor.kt @@ -10,6 +10,8 @@ import com.google.devtools.ksp.symbol.KSClassDeclaration import com.r0adkll.kimchi.REFERENCE_SUFFIX import com.r0adkll.kimchi.SCOPE_SUFFIX import com.r0adkll.kimchi.util.buildFile +import com.r0adkll.kimchi.util.ksp.asUrlSafeString +import com.r0adkll.kimchi.util.ksp.requireQualifiedName import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.KModifier @@ -65,10 +67,10 @@ internal abstract class HintSymbolProcessor( private fun process( element: KSClassDeclaration, ): FileSpec { - val fileName = element.simpleName.asString() + // TODO: In other examples using the fq name for file names has been known to hit length limits + // so we should look into using a hash instead. + val fileName = element.requireQualifiedName.asUrlSafeString() val className = element.toClassName() - val propertyName = element.qualifiedName!!.asString() - .replace(".", "_") validate(element) @@ -79,7 +81,7 @@ internal abstract class HintSymbolProcessor( addProperty( PropertySpec .builder( - name = propertyName + REFERENCE_SUFFIX, + name = fileName + REFERENCE_SUFFIX, type = KClass::class.asClassName().parameterizedBy(className), ) .initializer("%T::class", className) @@ -91,7 +93,7 @@ internal abstract class HintSymbolProcessor( addProperty( PropertySpec .builder( - name = propertyName + SCOPE_SUFFIX, + name = fileName + SCOPE_SUFFIX, type = KClass::class.asClassName().parameterizedBy(scope), ) .initializer("%T::class", scope) diff --git a/compiler/src/test/kotlin/com/r0adkll/kimchi/TestUtils.kt b/compiler/src/test/kotlin/com/r0adkll/kimchi/TestUtils.kt index 774c5c2..17c3162 100644 --- a/compiler/src/test/kotlin/com/r0adkll/kimchi/TestUtils.kt +++ b/compiler/src/test/kotlin/com/r0adkll/kimchi/TestUtils.kt @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 package com.r0adkll.kimchi +import com.r0adkll.kimchi.util.capitalized import com.squareup.kotlinpoet.asClassName import kotlin.reflect.KClass import kotlin.reflect.KFunction @@ -22,7 +23,19 @@ fun Class<*>.getHintScope(hintPackage: String): KClass<*>? = fun Class<*>.contributedProperties( hintPackageName: String, ): List>? { - val clazz = topLevelClass(hintPackageName) ?: return null + // The capitalize() comes from kotlinc's implicit handling of file names -> class names. It will + // always, unless otherwise instructed via `@file:JvmName`, capitalize its facade class. + val className = kotlin.asClassName() + .canonicalName + .replace(".", "_") + .capitalized() + .plus("Kt") + + val clazz = try { + classLoader.loadClass("$hintPackageName.$className") + } catch (e: ClassNotFoundException) { + return null + } return clazz.declaredFields .sortedBy { it.name } @@ -33,25 +46,20 @@ fun Class<*>.contributedProperties( fun Class<*>.topLevelFunctions( packageName: String, ): List>? { - val clazz = topLevelClass(packageName) ?: return null - - return clazz.declaredMethods - .sortedBy { it.name } - .mapNotNull { method -> method.kotlinFunction } -} - -private fun Class<*>.topLevelClass( - packageName: String, -): Class<*>? { // The capitalize() comes from kotlinc's implicit handling of file names -> class names. It will // always, unless otherwise instructed via `@file:JvmName`, capitalize its facade class. val className = kotlin.asClassName() .simpleName + .capitalized() .plus("Kt") - return try { + val clazz = try { classLoader.loadClass("$packageName.$className") } catch (e: ClassNotFoundException) { - null + return null } + + return clazz.declaredMethods + .sortedBy { it.name } + .mapNotNull { method -> method.kotlinFunction } } diff --git a/compiler/src/test/kotlin/com/r0adkll/kimchi/processors/HintSymbolProcessorTest.kt b/compiler/src/test/kotlin/com/r0adkll/kimchi/processors/HintSymbolProcessorTest.kt index ece56b8..8f25871 100644 --- a/compiler/src/test/kotlin/com/r0adkll/kimchi/processors/HintSymbolProcessorTest.kt +++ b/compiler/src/test/kotlin/com/r0adkll/kimchi/processors/HintSymbolProcessorTest.kt @@ -16,6 +16,8 @@ import com.tschuchort.compiletesting.KotlinCompilation import java.io.File import kotlin.reflect.KClass import org.intellij.lang.annotations.Language +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.CleanupMode import org.junit.jupiter.api.io.TempDir import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource @@ -24,7 +26,7 @@ import strikt.assertions.isEqualTo class HintSymbolProcessorTest { - @TempDir + @TempDir(cleanup = CleanupMode.NEVER) lateinit var workingDir: File @ParameterizedTest @@ -44,6 +46,30 @@ class HintSymbolProcessorTest { } } + @Test + fun `Duplicate class names do NOT cause a collision`() { + println(workingDir.absolutePath) + compileKimchiWithTestSources( + """ + package kimchi + + import com.r0adkll.kimchi.annotations.ContributesTo + + interface Outer1 { + @ContributesTo(TestScope::class) + interface Inner + } + + interface Outer2 { + @ContributesTo(TestScope::class) + interface Inner + } + """.trimIndent(), + workingDir = workingDir, + expectExitCode = KotlinCompilation.ExitCode.OK, + ) + } + companion object { @JvmStatic fun hintTestParameters(): List = listOf(