Skip to content

Commit

Permalink
mutableなコレクションをサポートする
Browse files Browse the repository at this point in the history
  • Loading branch information
kitakkun committed Dec 31, 2023
1 parent 129d497 commit e146ea5
Show file tree
Hide file tree
Showing 10 changed files with 221 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,29 @@ class ValueContainersScope {
setter = "<set-value>"
}
}

fun collections() {
container {
className = "kotlin.collections.MutableList"
captures = listOf("add", "addAll", "clear", "remove", "removeAll", "removeAt", "set")
getter = "<this>"
setter = "clear,addAll"
serializeItself = true
}
container {
className = "kotlin.collections.MutableSet"
captures = listOf("add", "addAll", "clear", "remove", "removeAll")
getter = "<this>"
setter = "clear,addAll"
serializeItself = true
}
container {
className = "kotlin.collections.MutableMap"
captures = listOf("clear", "put", "putAll", "remove", "set")
getter = "<this>"
setter = "clear,putAll"
serializeItself = true
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class BackInTimeCompilerConfigurationProcessor {
capturedCallableIds = config.captures.map { CallableId(classId, Name.guessByFirstCharacter(it)) },
valueGetter = CallableId(classId, Name.guessByFirstCharacter(config.getter)),
valueSetter = CallableId(classId, Name.guessByFirstCharacter(config.setter)),
serializeItSelf = config.serializeItself,
)
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ data class ValueContainerClassInfo(
val capturedCallableIds: List<CallableId>,
val valueGetter: CallableId,
val valueSetter: CallableId,
val serializeItSelf: Boolean,
)
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class UserDefinedValueContainerAnalyzer private constructor() : IrElementVisitor
capturedCallableIds = captures.map { CallableId(classId, it.name) },
valueGetter = CallableId(classId, getter.name),
valueSetter = CallableId(classId, setter.name),
serializeItSelf = false, // FIXME: doesn't support yet
)

mutableCollectedInfoList.add(containerInfo)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import org.jetbrains.kotlin.ir.types.classOrNull
import org.jetbrains.kotlin.ir.types.typeOrNull
import org.jetbrains.kotlin.ir.util.*
import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid
import org.jetbrains.kotlin.name.Name

/**
* generate DebuggableStateHolderManipulator methods bodies
Expand Down Expand Up @@ -61,11 +62,9 @@ class ManipulatorMethodBodyGenerationTransformer : IrElementTransformerVoid() {
+parentClass.irWhenByProperties(
propertyNameParameter = propertyNameParameter,
buildBranchResultExpression = { property ->
val propertyType = property.getter?.returnType ?: return@irWhenByProperties null
generateSetForProperty(
parentClassReceiver = parentClassReceiver,
property = property,
type = propertyType,
value = value,
)
},
Expand Down Expand Up @@ -149,14 +148,14 @@ class ManipulatorMethodBodyGenerationTransformer : IrElementTransformerVoid() {
}.toList() + irElseBranch(irBlock { +elseBranchExpression(propertyNameParameter) })
)


private fun IrBuilderWithScope.generateSetForProperty(
context(IrBuilderWithScope)
private fun generateSetForProperty(
parentClassReceiver: IrValueParameter,
property: IrProperty,
type: IrType,
value: IrValueParameter,
): IrExpression? {
if (!type.isValueContainer() && property.isVar) {
val propertyType = property.getter?.returnType ?: return null
if (!propertyType.isValueContainer() && property.isVar) {
val setter = property.setter ?: return null
return irCall(setter).apply {
dispatchReceiver = irGet(parentClassReceiver)
Expand All @@ -166,36 +165,80 @@ class ManipulatorMethodBodyGenerationTransformer : IrElementTransformerVoid() {

val propertyClass = property.getter?.returnType?.classOrNull?.owner ?: return null
val setterCallableName = valueContainerClassInfoList.find { it.classId == propertyClass.classId }?.valueSetter?.callableName ?: return null
val valueSetter = if (setterCallableName.isSetterName()) {
propertyClass.getPropertySetterRecursively(setterCallableName.getPropertyName())
} else {
propertyClass.getSimpleFunctionRecursively(setterCallableName.asString())
} ?: return null
val valueSetter = when {
setterCallableName.isSetterName() -> propertyClass.getPropertySetterRecursively(setterCallableName.getPropertyName())
else -> propertyClass.getSimpleFunctionRecursively(setterCallableName.asString())
}

val propertyGetter = property.getter ?: return null
if (valueSetter != null) {
return irCall(valueSetter).apply {
dispatchReceiver = irCall(propertyGetter).apply {
dispatchReceiver = irGet(parentClassReceiver)
}
putValueArgument(0, irGet(value))
}
} else if (setterCallableName.isCombinedCallsName()) {
// FIXME: もうちょっとうまく書きたい
val sequence = setterCallableName.asString().split(",").map { Name.guessByFirstCharacter(it) }
return irComposite {
+sequence.dropLast(1).mapNotNull { name ->
val function = when {
name.isSetterName() -> propertyClass.getPropertySetterRecursively(name.getPropertyName())
else -> propertyClass.getSimpleFunctionRecursively(name.asString())
} ?: return@mapNotNull null

return irCall(valueSetter).apply {
dispatchReceiver = irCall(propertyGetter).apply {
dispatchReceiver = irGet(parentClassReceiver)
irCall(function).apply {
dispatchReceiver = irCall(propertyGetter).apply {
dispatchReceiver = irGet(parentClassReceiver)
}
}
}
val lastFunctionName = sequence.last()
val lastFunction = when {
lastFunctionName.isSetterName() -> propertyClass.getPropertySetterRecursively(lastFunctionName.getPropertyName())
else -> propertyClass.getSimpleFunctionRecursively(lastFunctionName.asString())
} ?: return@irComposite

irCall(lastFunction).apply {
dispatchReceiver = irCall(propertyGetter).apply {
dispatchReceiver = irGet(parentClassReceiver)
}
putValueArgument(0, irGet(value))
}
}
putValueArgument(0, irGet(value))
}
return null
}

private fun Name.isCombinedCallsName(): Boolean {
return this.asString().split(",").size > 1
}

private fun IrType.isValueContainer(): Boolean {
return valueContainerClassInfoList.any { it.classId == this.classOrNull?.owner?.classId }
}

private fun IrType.isSerializableItSelf(): Boolean {
return valueContainerClassInfoList.any { it.classId == this.classOrNull?.owner?.classId && it.serializeItSelf }
}

private fun IrBuilderWithScope.generateSerializeCall(value: IrValueParameter, type: IrType): IrExpression? {
return irReturn(
irCall(encodeToStringFunction).apply {
extensionReceiver = irCall(backInTimeJsonGetter)
putValueArgument(0, irGet(value))
putTypeArgument(0, if (type.isValueContainer()) {
(type as? IrSimpleType)?.arguments?.firstOrNull()?.typeOrNull ?: return null
} else {
type
})
putTypeArgument(
index = 0,
type = when {
type.isSerializableItSelf() -> type
type.isValueContainer() -> {
(type as? IrSimpleType)?.arguments?.firstOrNull()?.typeOrNull ?: return null
}

else -> type
}
)
}
)
}
Expand All @@ -205,11 +248,17 @@ class ManipulatorMethodBodyGenerationTransformer : IrElementTransformerVoid() {
irCall(decodeFromStringFunction).apply {
extensionReceiver = irCall(backInTimeJsonGetter)
putValueArgument(0, irGet(value))
putTypeArgument(0, if (type.isValueContainer()) {
(type as? IrSimpleType)?.arguments?.firstOrNull()?.typeOrNull ?: return null
} else {
type
})
putTypeArgument(
index = 0,
type = when {
type.isValueContainer() -> {
if (type.isSerializableItSelf()) type
else (type as? IrSimpleType)?.arguments?.firstOrNull()?.typeOrNull ?: return null
}

else -> type
}
)
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import org.jetbrains.kotlin.ir.expressions.IrCall
import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol
import org.jetbrains.kotlin.ir.types.classOrNull
import org.jetbrains.kotlin.ir.util.classId
import org.jetbrains.kotlin.name.SpecialNames

context(IrBuilderWithScope, BackInTimePluginContext)
fun generateCaptureValueCall(
Expand Down Expand Up @@ -50,13 +51,19 @@ fun IrProperty.generateCaptureValueCallForValueContainer(
uuidVariable: IrVariable,
): IrCall? {
val getter = getter ?: return null
val valueGetter = getValueHolderValueGetterSymbol() ?: return null
val valueGetterSymbol = getValueHolderValueGetterSymbol() ?: return null
return generateCaptureValueCall(
propertyName = name.asString(),
getValueCall = irCall(valueGetter).apply {
dispatchReceiver = irCall(getter).apply {
getValueCall = if (valueGetterSymbol == getter.symbol) {
irCall(getter.symbol).apply {
dispatchReceiver = irGet(instanceParameter)
}
} else {
irCall(valueGetterSymbol).apply {
dispatchReceiver = irCall(getter).apply {
dispatchReceiver = irGet(instanceParameter)
}
}
},
instanceParameter = instanceParameter,
uuidVariable = uuidVariable,
Expand All @@ -70,9 +77,9 @@ private fun IrProperty.getValueHolderValueGetterSymbol(): IrSimpleFunctionSymbol
.find { it.classId == propertyClass.classId }
?.valueGetter
?.callableName ?: return null
return if (valueGetterCallableName.isGetterName()) {
propertyClass.getPropertyGetterRecursively(valueGetterCallableName.getPropertyName())
} else {
propertyClass.getSimpleFunctionRecursively(valueGetterCallableName.asString())
return when {
valueGetterCallableName == SpecialNames.THIS -> getter?.symbol
valueGetterCallableName.isGetterName() -> propertyClass.getPropertyGetterRecursively(valueGetterCallableName.getPropertyName())
else -> propertyClass.getSimpleFunctionRecursively(valueGetterCallableName.asString())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ data class ValueContainerConfig(
var captures: List<String> = emptyList(),
var getter: String = "",
var setter: String = "",
var serializeItself: Boolean = false,
)

1 change: 1 addition & 0 deletions test/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ backInTime {
enabled = true
valueContainers {
androidValueContainers()
collections()

container {
className = "com.github.kitakkun.backintime.test.GradleConfiguredValueContainer"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.github.kitakkun.backintime.test

import com.github.kitakkun.backintime.annotations.DebuggableStateHolder

@DebuggableStateHolder
class MutableCollectionsHolder {
private val mutableList = mutableListOf<String>()
private val mutableSet = mutableSetOf<String>()
private val mutableMap = mutableMapOf<String, String>()

fun mutableListTest() {
mutableList.add("Hello")
mutableList.addAll(listOf("World", "!"))
mutableList.removeAt(1)
mutableList.remove("!")
mutableList.clear()
}

fun mutableSetTest() {
mutableSet.add("Hello")
mutableSet.addAll(listOf("World", "!"))
mutableSet.remove("!")
mutableSet.clear()
}

fun mutableMapTest() {
mutableMap["Hello"] = "World"
mutableMap["World"] = "!"
mutableMap.remove("!")
mutableMap.clear()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.github.kitakkun.backintime.test

import com.github.kitakkun.backintime.runtime.BackInTimeDebugService
import io.mockk.mockkObject
import io.mockk.unmockkAll
import io.mockk.verify
import org.junit.After
import org.junit.Before
import kotlin.test.Test

class MutableCollectionsHolderTest {
@Before
fun setup() {
mockkObject(BackInTimeDebugService)
}

@After
fun teardown() {
unmockkAll()
}

@Test
fun testMutableListCapture() {
val mutableCollectionsHolder = MutableCollectionsHolder()
mutableCollectionsHolder.mutableListTest()

verify(exactly = 5) {
BackInTimeDebugService.notifyPropertyChanged(any(), "mutableList", any(), any())
}
}

@Test
fun testMutableSetCapture() {
val mutableCollectionsHolder = MutableCollectionsHolder()
mutableCollectionsHolder.mutableSetTest()

verify(exactly = 4) {
BackInTimeDebugService.notifyPropertyChanged(any(), "mutableSet", any(), any())
}
}

@Test
fun testMutableMapCapture() {
val mutableCollectionsHolder = MutableCollectionsHolder()
mutableCollectionsHolder.mutableMapTest()

verify(exactly = 4) {
BackInTimeDebugService.notifyPropertyChanged(any(), "mutableMap", any(), any())
}
}

@Test
fun testMutableListForceSet() {
val mutableCollectionsHolder = MutableCollectionsHolder()
mutableCollectionsHolder.forceSetValue("mutableList", mutableListOf("Hello", "World", "!"))
mutableCollectionsHolder.forceSetValue("mutableList", listOf("Hello", "World"))
}

@Test
fun testMutableSetForceSet() {
val mutableCollectionsHolder = MutableCollectionsHolder()
mutableCollectionsHolder.forceSetValue("mutableSet", mutableSetOf("Hello", "World", "!"))
mutableCollectionsHolder.forceSetValue("mutableSet", setOf("Hello", "World"))
}

@Test
fun testMutableMapForceSet() {
val mutableCollectionsHolder = MutableCollectionsHolder()
mutableCollectionsHolder.forceSetValue("mutableMap", mutableMapOf("Hello" to "World", "World" to "!"))
mutableCollectionsHolder.forceSetValue("mutableMap", mapOf("Hello" to "World", "World" to "!"))
}
}

0 comments on commit e146ea5

Please sign in to comment.