Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
}
1 change: 1 addition & 0 deletions plugin-build/plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ dependencies {
compileOnly(libs.kotlin.compiler.embeddable)
compileOnly(libs.kotlin.gradle.plugin)
testImplementation(libs.junit)
testImplementation(libs.kotlin.compiler.embeddable)
}

kotlin {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<KtEnumEntry>().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<KtEnumEntry>().orEmpty().ifEmpty {
ktClass.declarations.filterIsInstance<KtEnumEntry>()
}
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<KtEnumEntry>): List<KneEnumEntry> =
psiEntries.mapNotNull { entry ->
val entryName = entry.name ?: return@mapNotNull null
val args = entry.superTypeListEntries
.filterIsInstance<KtSuperTypeCallEntry>()
.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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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("}")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,16 @@ data class KneClass(
val isCommon: Boolean = false,
) : Serializable

data class KneEnumEntry(
val name: String,
val constructorArgs: List<String> = emptyList(),
) : Serializable

data class KneEnum(
val simpleName: String,
val fqName: String,
val entries: List<String>,
val entries: List<KneEnumEntry>,
val constructorParams: List<KneParam> = emptyList(),
) : Serializable

data class KneConstructor(
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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")
}
}
Loading