diff --git a/examples/calculator/src/jvmTest/kotlin/com/example/calculator/EnumTest.kt b/examples/calculator/src/jvmTest/kotlin/com/example/calculator/EnumTest.kt index dcfe7e0e..93952358 100644 --- a/examples/calculator/src/jvmTest/kotlin/com/example/calculator/EnumTest.kt +++ b/examples/calculator/src/jvmTest/kotlin/com/example/calculator/EnumTest.kt @@ -114,4 +114,61 @@ class EnumTest { } } + @Test + fun `one-line enum has correct entries`() { + assertEquals(listOf("RED", "GREEN", "BLUE"), Color.entries.map { it.name }) + assertEquals(0, Color.RED.ordinal) + assertEquals(1, Color.GREEN.ordinal) + assertEquals(2, Color.BLUE.ordinal) + } + + @Test + fun `enum with constructor - properties match`() { + assertEquals(listOf("SUCCESS", "ERROR", "PENDING"), Status.entries.map { it.name }) + assertEquals(0, Status.SUCCESS.code) + assertEquals(1, Status.ERROR.code) + assertEquals(2, Status.PENDING.code) + } + + @Test + fun `enum with constructor - as return value`() { + Calculator(10).use { calc -> + assertEquals(Status.SUCCESS, calc.getStatus()) + } + Calculator(-5).use { calc -> + assertEquals(Status.ERROR, calc.getStatus()) + } + Calculator(0).use { calc -> + assertEquals(Status.PENDING, calc.getStatus()) + } + } + + @Test + fun `enum with constructor - as mutable property`() { + Calculator(0).use { calc -> + assertEquals(Status.SUCCESS, calc.lastStatus) + calc.lastStatus = Status.PENDING + assertEquals(Status.PENDING, calc.lastStatus) + } + } + + @Test + fun `enum with multi-param constructor - properties match`() { + assertEquals(2, HttpStatus.entries.size) + assertEquals(200, HttpStatus.OK.code) + assertEquals("OK", HttpStatus.OK.label) + assertEquals(404, HttpStatus.NOT_FOUND.code) + assertEquals("Not Found", HttpStatus.NOT_FOUND.label) + } + + @Test + fun `enum with multi-param constructor - as return value`() { + Calculator(1).use { calc -> + assertEquals(HttpStatus.OK, calc.getHttpStatus()) + } + Calculator(-1).use { calc -> + assertEquals(HttpStatus.NOT_FOUND, calc.getHttpStatus()) + } + } + } diff --git a/examples/calculator/src/nativeMain/kotlin/com/example/calculator/Calculator.kt b/examples/calculator/src/nativeMain/kotlin/com/example/calculator/Calculator.kt index c1b06625..7c24b3ee 100644 --- a/examples/calculator/src/nativeMain/kotlin/com/example/calculator/Calculator.kt +++ b/examples/calculator/src/nativeMain/kotlin/com/example/calculator/Calculator.kt @@ -83,6 +83,16 @@ class Calculator(initial: Int = 0) { fun getLastOp(): Operation = lastOperation + var lastStatus: Status = Status.SUCCESS + + fun getStatus(): Status = when { + accumulator > 0 -> Status.SUCCESS + accumulator < 0 -> Status.ERROR + else -> Status.PENDING + } + + fun getHttpStatus(): HttpStatus = if (accumulator >= 0) HttpStatus.OK else HttpStatus.NOT_FOUND + // ── Nullable types ────────────────────────────────────────────────────── var nickname: String? = null diff --git a/examples/calculator/src/nativeMain/kotlin/com/example/calculator/Operation.kt b/examples/calculator/src/nativeMain/kotlin/com/example/calculator/Operation.kt index 505dff02..3ea3c41c 100644 --- a/examples/calculator/src/nativeMain/kotlin/com/example/calculator/Operation.kt +++ b/examples/calculator/src/nativeMain/kotlin/com/example/calculator/Operation.kt @@ -5,3 +5,16 @@ enum class Operation { SUBTRACT, MULTIPLY, } + +enum class Color { RED, GREEN, BLUE } + +enum class Status(val code: Int) { + SUCCESS(0), + ERROR(1), + PENDING(2), +} + +enum class HttpStatus(val code: Int, val label: String) { + OK(200, "OK"), + NOT_FOUND(404, "Not Found"), +} diff --git a/plugin-build/plugin/build.gradle.kts b/plugin-build/plugin/build.gradle.kts index 5ff33291..ff494346 100644 --- a/plugin-build/plugin/build.gradle.kts +++ b/plugin-build/plugin/build.gradle.kts @@ -8,6 +8,7 @@ dependencies { compileOnly(libs.kotlin.compiler.embeddable) compileOnly(libs.kotlin.gradle.plugin) testImplementation(libs.junit) + testImplementation(libs.kotlin.compiler.embeddable) } kotlin { diff --git a/plugin-build/plugin/src/main/kotlin/dev/nucleusframework/nna/plugin/analysis/PsiSourceParser.kt b/plugin-build/plugin/src/main/kotlin/dev/nucleusframework/nna/plugin/analysis/PsiSourceParser.kt index 69182206..a63bab88 100644 --- a/plugin-build/plugin/src/main/kotlin/dev/nucleusframework/nna/plugin/analysis/PsiSourceParser.kt +++ b/plugin-build/plugin/src/main/kotlin/dev/nucleusframework/nna/plugin/analysis/PsiSourceParser.kt @@ -4,6 +4,7 @@ import dev.nucleusframework.nna.plugin.ir.KneClass import dev.nucleusframework.nna.plugin.ir.KneConstructor import dev.nucleusframework.nna.plugin.ir.KneDataClass import dev.nucleusframework.nna.plugin.ir.KneEnum +import dev.nucleusframework.nna.plugin.ir.KneEnumEntry import dev.nucleusframework.nna.plugin.ir.KneFunction import dev.nucleusframework.nna.plugin.ir.KneInterface import dev.nucleusframework.nna.plugin.ir.KneModule @@ -25,6 +26,7 @@ import org.jetbrains.kotlin.psi.KtNullableType import org.jetbrains.kotlin.psi.KtObjectDeclaration import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.kotlin.psi.KtPsiFactory +import org.jetbrains.kotlin.psi.KtSuperTypeCallEntry import org.jetbrains.kotlin.psi.KtTypeElement import org.jetbrains.kotlin.psi.KtTypeReference import org.jetbrains.kotlin.psi.KtUserType @@ -138,7 +140,7 @@ class PsiSourceParser { for (decl in declarations) { if (decl.isPrivateOrInternal()) continue when { - decl is KtClass && decl.isEnum() -> parseEnum(decl, pkg)?.let { enumMap.putIfAbsent(it.fqName, it) } + decl is KtClass && decl.isEnum() -> parseEnum(decl, pkg, typeMaps)?.let { enumMap.putIfAbsent(it.fqName, it) } decl is KtClass && decl.isData() -> { val name = decl.name ?: continue val dcInfo = knownDataClasses[name] ?: continue @@ -337,12 +339,41 @@ class PsiSourceParser { return KneInterface(name, fq, methods, properties, superInterfaces, isCommon = isCommon) } - private fun parseEnum(ktClass: KtClass, pkg: String): KneEnum? { + private fun parseEnum(ktClass: KtClass, pkg: String, typeMaps: TypeMaps): KneEnum? { val name = ktClass.name ?: return null val fq = if (pkg.isNotEmpty()) "$pkg.$name" else name - return KneEnum(name, fq, ktClass.declarations.filterIsInstance().mapNotNull { it.name }) + + // `declarations` can omit enum entries depending on PSI (stubs vs. AST). + // Raw body children always include KtEnumEntry nodes created by the parser. + val psiEntries = ktClass.body?.children?.filterIsInstance().orEmpty().ifEmpty { + ktClass.declarations.filterIsInstance() + } + val entries = parseEnumEntries(psiEntries) + + val rawCtorParams = ktClass.primaryConstructor?.valueParameters ?: emptyList() + val ctorParams = rawCtorParams.map { param -> + val pName = param.name ?: return KneEnum(name, fq, entries) + val type = resolveTypeFromMaps(param.typeReference, typeMaps) + ?: return KneEnum(name, fq, entries) + KneParam(pName, type, hasDefault = param.hasDefaultValue()) + } + + return KneEnum(name, fq, entries, ctorParams) } + private fun parseEnumEntries(psiEntries: List): List = + psiEntries.mapNotNull { entry -> + val entryName = entry.name ?: return@mapNotNull null + val args = entry.superTypeListEntries + .filterIsInstance() + .firstOrNull() + ?.valueArgumentList + ?.arguments + ?.map { it.getArgumentExpression()?.text ?: return@mapNotNull null } + ?: emptyList() + KneEnumEntry(entryName, args) + } + private fun parseFunction(fn: KtNamedFunction, typeMaps: TypeMaps): KneFunction? { val name = fn.name ?: return null if (name == "init") return null diff --git a/plugin-build/plugin/src/main/kotlin/dev/nucleusframework/nna/plugin/codegen/FfmProxyGenerator.kt b/plugin-build/plugin/src/main/kotlin/dev/nucleusframework/nna/plugin/codegen/FfmProxyGenerator.kt index f92fceac..34ec1261 100644 --- a/plugin-build/plugin/src/main/kotlin/dev/nucleusframework/nna/plugin/codegen/FfmProxyGenerator.kt +++ b/plugin-build/plugin/src/main/kotlin/dev/nucleusframework/nna/plugin/codegen/FfmProxyGenerator.kt @@ -2736,10 +2736,21 @@ class FfmProxyGenerator { appendLine("// Auto-generated by kotlin-native-export plugin. Do not modify.") appendLine("package $pkg") appendLine() - appendLine("enum class ${enum.simpleName} {") + val header = if (enum.constructorParams.isEmpty()) { + "enum class ${enum.simpleName} {" + } else { + val params = enum.constructorParams.joinToString(", ") { "val ${it.name}: ${it.type.jvmTypeName}" } + "enum class ${enum.simpleName}($params) {" + } + appendLine(header) enum.entries.forEachIndexed { idx, entry -> val separator = if (idx < enum.entries.size - 1) "," else ";" - appendLine(" $entry$separator") + val call = if (entry.constructorArgs.isEmpty()) { + entry.name + } else { + "${entry.name}(${entry.constructorArgs.joinToString(", ")})" + } + appendLine(" $call$separator") } appendLine("}") } diff --git a/plugin-build/plugin/src/main/kotlin/dev/nucleusframework/nna/plugin/ir/KneIR.kt b/plugin-build/plugin/src/main/kotlin/dev/nucleusframework/nna/plugin/ir/KneIR.kt index 5804f83e..6a0a4905 100644 --- a/plugin-build/plugin/src/main/kotlin/dev/nucleusframework/nna/plugin/ir/KneIR.kt +++ b/plugin-build/plugin/src/main/kotlin/dev/nucleusframework/nna/plugin/ir/KneIR.kt @@ -45,10 +45,16 @@ data class KneClass( val isCommon: Boolean = false, ) : Serializable +data class KneEnumEntry( + val name: String, + val constructorArgs: List = emptyList(), +) : Serializable + data class KneEnum( val simpleName: String, val fqName: String, - val entries: List, + val entries: List, + val constructorParams: List = emptyList(), ) : Serializable data class KneConstructor( diff --git a/plugin-build/plugin/src/test/kotlin/dev/nucleusframework/nna/plugin/analysis/PsiSourceParserEnumTest.kt b/plugin-build/plugin/src/test/kotlin/dev/nucleusframework/nna/plugin/analysis/PsiSourceParserEnumTest.kt new file mode 100644 index 00000000..7b42bc26 --- /dev/null +++ b/plugin-build/plugin/src/test/kotlin/dev/nucleusframework/nna/plugin/analysis/PsiSourceParserEnumTest.kt @@ -0,0 +1,70 @@ +package dev.nucleusframework.nna.plugin.analysis + +import dev.nucleusframework.nna.plugin.ir.KneType +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class PsiSourceParserEnumTest { + + @get:Rule + val tmp = TemporaryFolder() + + @Test + fun `parses simple, one-line, and constructor enum entries`() { + val source = tmp.newFile("Enums.kt") + source.writeText( + """ + package demo + + enum class Operation { + ADD, + SUBTRACT, + MULTIPLY, + } + + enum class Color { RED, GREEN, BLUE } + + enum class Status(val code: Int) { + SUCCESS(0), + ERROR(1), + PENDING(2), + } + + enum class HttpStatus(val code: Int, val label: String) { + OK(200, "OK"), + NOT_FOUND(404, "Not Found"), + } + """.trimIndent(), + ) + + val module = PsiSourceParser().parse(listOf(source), "demo", emptyList()) + val enums = module.enums.associateBy { it.simpleName } + + assertEquals(setOf("Operation", "Color", "Status", "HttpStatus"), enums.keys) + + val operation = enums.getValue("Operation") + assertEquals(listOf("ADD", "SUBTRACT", "MULTIPLY"), operation.entries.map { it.name }) + assertTrue(operation.constructorParams.isEmpty()) + + val color = enums.getValue("Color") + assertEquals(listOf("RED", "GREEN", "BLUE"), color.entries.map { it.name }) + + val status = enums.getValue("Status") + assertEquals(listOf("SUCCESS", "ERROR", "PENDING"), status.entries.map { it.name }) + assertEquals(listOf("0", "1", "2"), status.entries.map { it.constructorArgs.single() }) + assertEquals(1, status.constructorParams.size) + assertEquals("code", status.constructorParams.single().name) + assertEquals(KneType.INT, status.constructorParams.single().type) + + val http = enums.getValue("HttpStatus") + assertEquals(listOf("OK", "NOT_FOUND"), http.entries.map { it.name }) + assertEquals(listOf("200", "\"OK\""), http.entries[0].constructorArgs) + assertEquals(listOf("404", "\"Not Found\""), http.entries[1].constructorArgs) + assertEquals(listOf("code", "label"), http.constructorParams.map { it.name }) + assertEquals(KneType.INT, http.constructorParams[0].type) + assertEquals(KneType.STRING, http.constructorParams[1].type) + } +} diff --git a/plugin-build/plugin/src/test/kotlin/dev/nucleusframework/nna/plugin/codegen/FfmProxyGeneratorEnumTest.kt b/plugin-build/plugin/src/test/kotlin/dev/nucleusframework/nna/plugin/codegen/FfmProxyGeneratorEnumTest.kt new file mode 100644 index 00000000..d6563c29 --- /dev/null +++ b/plugin-build/plugin/src/test/kotlin/dev/nucleusframework/nna/plugin/codegen/FfmProxyGeneratorEnumTest.kt @@ -0,0 +1,102 @@ +package dev.nucleusframework.nna.plugin.codegen + +import dev.nucleusframework.nna.plugin.ir.KneEnum +import dev.nucleusframework.nna.plugin.ir.KneEnumEntry +import dev.nucleusframework.nna.plugin.ir.KneModule +import dev.nucleusframework.nna.plugin.ir.KneParam +import dev.nucleusframework.nna.plugin.ir.KneType +import org.junit.Assert.assertEquals +import org.junit.Test + +class FfmProxyGeneratorEnumTest { + + @Test + fun `generates simple enum entries`() { + val source = generate( + KneEnum( + simpleName = "Operation", + fqName = "demo.Operation", + entries = listOf( + KneEnumEntry("ADD"), + KneEnumEntry("SUBTRACT"), + KneEnumEntry("MULTIPLY"), + ), + ), + ) + + assertEquals( + """ + // Auto-generated by kotlin-native-export plugin. Do not modify. + package demo + + enum class Operation { + ADD, + SUBTRACT, + MULTIPLY; + } + + """.trimIndent(), + source, + ) + } + + @Test + fun `generates constructor enum entries with property values`() { + val source = generate( + KneEnum( + simpleName = "HttpStatus", + fqName = "demo.HttpStatus", + entries = listOf( + KneEnumEntry("OK", listOf("200", "\"OK\"")), + KneEnumEntry("NOT_FOUND", listOf("404", "\"Not Found\"")), + ), + constructorParams = listOf( + KneParam("code", KneType.INT), + KneParam("label", KneType.STRING), + ), + ), + ) + + assertEquals( + """ + // Auto-generated by kotlin-native-export plugin. Do not modify. + package demo + + enum class HttpStatus(val code: Int, val label: String) { + OK(200, "OK"), + NOT_FOUND(404, "Not Found"); + } + + """.trimIndent(), + source, + ) + } + + @Test + fun `empty entries still emit a valid enum class`() { + val source = generate(KneEnum("Empty", "demo.Empty", emptyList())) + assertEquals( + """ + // Auto-generated by kotlin-native-export plugin. Do not modify. + package demo + + enum class Empty { + } + + """.trimIndent(), + source, + ) + } + + private fun generate(enum: KneEnum): String { + val module = KneModule( + libName = "demo", + packages = setOf("demo"), + classes = emptyList(), + dataClasses = emptyList(), + enums = listOf(enum), + functions = emptyList(), + ) + return FfmProxyGenerator().generate(module, "demo").getValue("${enum.simpleName}.kt") + } +}