diff --git a/burst-kotlin-plugin-tests/src/test/kotlin/app/cash/burst/kotlin/BurstKotlinPluginTest.kt b/burst-kotlin-plugin-tests/src/test/kotlin/app/cash/burst/kotlin/BurstKotlinPluginTest.kt index 0dfb27f..6329938 100644 --- a/burst-kotlin-plugin-tests/src/test/kotlin/app/cash/burst/kotlin/BurstKotlinPluginTest.kt +++ b/burst-kotlin-plugin-tests/src/test/kotlin/app/cash/burst/kotlin/BurstKotlinPluginTest.kt @@ -18,6 +18,7 @@ package app.cash.burst.kotlin import assertk.assertThat import assertk.assertions.contains import assertk.assertions.containsExactly +import assertk.assertions.containsExactlyInAnyOrder import assertk.assertions.isFalse import assertk.assertions.isTrue import com.tschuchort.compiletesting.JvmCompilationResult @@ -306,6 +307,66 @@ class BurstKotlinPluginTest { assertThat(baseLog).containsExactly("running 16") baseLog.clear() } + + @Test + fun burstValuesHasReasonableSymbolName() { + val result = compile( + sourceFile = SourceFile.kotlin( + "CoffeeTest.kt", + """ + import app.cash.burst.Burst + import app.cash.burst.burstValues + import kotlin.math.PI + import kotlin.math.abs + import kotlin.test.Test + + @Burst + class CoffeeTest { + @Test + fun test( + content: Any? = burstValues( + 3, // No name is generated for the first value. + "5".toInt(), + "hello", + "hello".uppercase(), + CoffeeTest::class, + Float.MAX_VALUE, + PI, + String.CASE_INSENSITIVE_ORDER, + abs(1), + null, + ) + ) { + } + } + """, + ), + ) + assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode, result.messages) + + val baseClass = result.classLoader.loadClass("CoffeeTest") + assertThat(baseClass.testSuffixes).containsExactlyInAnyOrder( + "toInt", + "hello", + "uppercase", + "CoffeeTest", + // Would have preferred 'MAX_VALUE', but this is constant is inlined! + "3_4028235E38", + // Would have preferred 'PI', but this is constant is inlined! + "3_141592653589793", + "CASE_INSENSITIVE_ORDER", + "abs", + "null", + ) + } + + private val Class<*>.testSuffixes: List + get() = methods.mapNotNull { + when { + it.name.startsWith("test_") -> it.name.substring(5) + else -> null + } + } } @ExperimentalCompilerApi diff --git a/burst-kotlin-plugin/src/main/kotlin/app/cash/burst/kotlin/Argument.kt b/burst-kotlin-plugin/src/main/kotlin/app/cash/burst/kotlin/Argument.kt index e1c6bf4..bfffb6f 100644 --- a/burst-kotlin-plugin/src/main/kotlin/app/cash/burst/kotlin/Argument.kt +++ b/burst-kotlin-plugin/src/main/kotlin/app/cash/burst/kotlin/Argument.kt @@ -22,6 +22,7 @@ import org.jetbrains.kotlin.ir.declarations.IrClass import org.jetbrains.kotlin.ir.declarations.IrEnumEntry import org.jetbrains.kotlin.ir.declarations.IrValueParameter import org.jetbrains.kotlin.ir.expressions.IrCall +import org.jetbrains.kotlin.ir.expressions.IrClassReference import org.jetbrains.kotlin.ir.expressions.IrConst import org.jetbrains.kotlin.ir.expressions.IrExpression import org.jetbrains.kotlin.ir.expressions.IrGetEnumValue @@ -29,10 +30,12 @@ import org.jetbrains.kotlin.ir.expressions.IrVararg import org.jetbrains.kotlin.ir.expressions.impl.IrGetEnumValueImpl import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI import org.jetbrains.kotlin.ir.types.IrType +import org.jetbrains.kotlin.ir.types.classFqName import org.jetbrains.kotlin.ir.types.getClass import org.jetbrains.kotlin.ir.util.classId import org.jetbrains.kotlin.ir.util.deepCopyWithSymbols import org.jetbrains.kotlin.ir.util.isEnumClass +import org.jetbrains.kotlin.name.NameUtils internal sealed interface Argument { /** True if this argument matches the default parameter value. */ @@ -59,17 +62,9 @@ private class EnumValueArgument( private class BurstValuesArgument( override val isDefault: Boolean, + override val name: String, private val value: IrExpression, - private val index: Int, ) : Argument { - override val name: String - get() { - return when { - value is IrConst<*> -> value.value.toString() - else -> index.toString() - } - } - override fun expression() = value.deepCopyWithSymbols() } @@ -113,26 +108,57 @@ private fun burstValuesArguments( burstApisCall: IrCall, ): List { return buildList { + val defaultExpression = burstApisCall.valueArguments[0] ?: unexpectedParameter(parameter) add( BurstValuesArgument( isDefault = true, - value = burstApisCall.valueArguments[0] ?: unexpectedParameter(parameter), - index = 0, + name = defaultExpression.suggestedName() ?: "0", + value = defaultExpression, ), ) for ((index, element) in (burstApisCall.valueArguments[1] as IrVararg).elements.withIndex()) { + val varargExpression = element as? IrExpression ?: unexpectedParameter(parameter) add( BurstValuesArgument( isDefault = false, - value = element as? IrExpression ?: unexpectedParameter(parameter), - index = index + 1, + name = varargExpression.suggestedName() ?: (index + 1).toString(), + value = varargExpression, ), ) } } } +/** + * Returns a short name for this expression appropriate for use in a generated symbol declaration. + * + * If this is a constant like 'hello' or '3.14', this returns the value as a string. + * + * If this is a call like `String.CASE_INSENSITIVE_ORDER` or `abs(-5)`, this returns the name of the + * called symbol (`CASE_INSENSITIVE_ORDER` or `abs`), discarding the receiver, value parameters, and + * type parameters. + * + * If this is a class reference like `String::class`, this returns the type's simple name. + */ +@UnsafeDuringIrConstructionAPI +private fun IrExpression.suggestedName(): String? { + val raw = when (this) { + is IrConst<*> -> value.toString() + is IrCall -> { + val target = (symbol.owner.correspondingPropertySymbol?.owner ?: symbol.owner) + target.name.asString() + } + + is IrClassReference -> classType.classFqName?.shortName()?.asString() ?: return null + else -> return null + } + + // Calling sanitizeAsJavaIdentifier is necessary but not sufficient. We assume further phases of + // the compiler will make the returned name safe for the ultimate compilation target. + return NameUtils.sanitizeAsJavaIdentifier(raw) +} + @UnsafeDuringIrConstructionAPI private fun enumValueArguments( referenceClass: IrClass,