Skip to content

Custom actions support #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 18, 2025
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
37 changes: 30 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,44 @@

> Check annotated functions arguments. If they don't change - return last result.
> Functions could be logged.
> Support extra custom action on function processing

import compiler plugin:

import compiler plugin:
```kotlin
dependencies {
implementation("io.github.stslex:compiler-plugin:0.0.1")
kotlinCompilerPluginClasspath("io.github.stslex:compiler-plugin:0.0.1")
implementation("io.github.stslex:compiler-plugin:$version")
kotlinCompilerPluginClasspath("io.github.stslex:compiler-plugin:$version")
}
```

in code:
in code (all annotation properties are optional):

```kotlin

import io.github.stslex.compiler_plugin.DistinctUntilChangeFun

@DistinctUntilChangeFun
fun setUserName(username: String){
// function logic
@DistinctUntilChangeFun(
logging = true,
singletonAllow = false,
name = "set_user_second_name",
action = TestLogger::class
)
fun setUserName(username: String) {
// function logic
}
```

for custom actions:

```kotlin
class TestLogger : Action {

override fun invoke(
name: String,
isProcess: Boolean
) {
println("test action $name procession: $isProcess")
}
}
```
9 changes: 8 additions & 1 deletion app/src/main/kotlin/com/stslex/compiler_app/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,18 @@ class MainActivity : ComponentActivity() {
printUsernameWithSingletonDistinct(user.name)
}


@DistinctUntilChangeFun(true)
private fun setName(name: String) {
logger.log(Level.INFO, "setName: $name")
findViewById<TextView>(R.id.usernameFieldTextView).text = name
}

@DistinctUntilChangeFun(true)
@DistinctUntilChangeFun(
name = "set_user_second_name",
logging = true,
action = TestLogger::class
)
private fun setSecondName(name: String) {
logger.log(Level.INFO, "setSecondName: $name")
findViewById<TextView>(R.id.secondNameFieldTextView).text = name
Expand All @@ -74,3 +79,5 @@ class MainActivity : ComponentActivity() {
private fun printUsernameWithSingletonDistinct(name: String) {
println("printUsernameWithSingletonDistinct: $name")
}


13 changes: 13 additions & 0 deletions app/src/main/kotlin/com/stslex/compiler_app/TestLogger.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.stslex.compiler_app

import io.github.stslex.compiler_plugin.utils.Action

class TestLogger : Action {

override fun invoke(
name: String,
isProcess: Boolean
) {
println("test action $name procession: $isProcess")
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package io.github.stslex.compiler_plugin

import io.github.stslex.compiler_plugin.model.DistinctChangeConfig
import io.github.stslex.compiler_plugin.utils.RuntimeLogger
import org.jetbrains.kotlin.utils.addToStdlib.runIf

internal class DistinctChangeCache(
private val config: DistinctChangeConfig
) {

private val cache = mutableMapOf<String, Pair<List<Any?>, Any?>>()
private val logger = RuntimeLogger.tag("DistinctChangeLogger")
private val logger = runIf(config.logging) { RuntimeLogger.tag("DistinctChangeLogger") }

@Suppress("UNCHECKED_CAST")
internal operator fun <R> invoke(
Expand All @@ -17,19 +19,21 @@ internal class DistinctChangeCache(
): R {
val entry = cache[key]

if (config.logging) {
logger.i("key: $key, config:$config, entry: $entry, args: $args")
}
logger?.i("name: ${config.name} key: $key, config:$config, entry: $entry, args: $args")

config.action(
name = config.name,
isProcess = entry != null && entry.first == args
)

if (entry != null && entry.first == args) {
if (config.logging) {
logger.i("$key not change")
}
logger?.i("${config.name} with key $key not change")
return entry.second as R
}

val result = body()
cache[key] = args to result

return result
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
package io.github.stslex.compiler_plugin

import io.github.stslex.compiler_plugin.utils.Action
import io.github.stslex.compiler_plugin.utils.DefaultAction
import kotlin.reflect.KClass

/**
* @param logging enable logs for Kotlin Compiler Runtime work (useful for debug - don't use in production)
* @param singletonAllow if enable - generates distinction for function without classes (so it's singleton)
* @param name set name for function in compiler logs and put in into action, else take function name
* @param action any action to process when function enter - it invokes with state of processing function body info and [name].
* @suppress [action] shouldn't have properties in it's constructor
* */
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.BINARY)
public annotation class DistinctUntilChangeFun(
val logging: Boolean = LOGGING_DEFAULT,
val singletonAllow: Boolean = false
val singletonAllow: Boolean = SINGLETON_ALLOW,
val name: String = "",
val action: KClass<out Action> = DefaultAction::class
) {

public companion object {

internal const val LOGGING_DEFAULT = false

internal const val SINGLETON_ALLOW = false

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.github.stslex.compiler_plugin.model

import io.github.stslex.compiler_plugin.utils.Action

internal data class DistinctChangeConfig(
val logging: Boolean,
val action: Action,
val name: String
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.github.stslex.compiler_plugin.transformers

import buildArgsListExpression
import io.github.stslex.compiler_plugin.DistinctUntilChangeFun.Companion.SINGLETON_ALLOW
import io.github.stslex.compiler_plugin.utils.CompileLogger.Companion.toCompilerLogger
import io.github.stslex.compiler_plugin.utils.buildLambdaForBody
import io.github.stslex.compiler_plugin.utils.buildSaveInCacheCall
Expand Down Expand Up @@ -29,10 +30,9 @@ internal class IrFunctionTransformer(
val qualifierArgs = pluginContext.readQualifier(declaration, logger)
?: return super.visitSimpleFunction(declaration)

if (
declaration.getQualifierValue("singletonAllow").not() &&
declaration.parentClassOrNull == null
) {
val isSingletonAllow = declaration.getQualifierValue("singletonAllow", SINGLETON_ALLOW)

if (isSingletonAllow.not() && declaration.parentClassOrNull == null) {
error("singleton is not allowed for ${declaration.name} in ${declaration.fileParentOrNull}")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import org.jetbrains.kotlin.ir.util.file
import org.jetbrains.kotlin.ir.util.kotlinFqName
import org.jetbrains.kotlin.ir.util.parentClassOrNull
import org.jetbrains.kotlin.ir.util.patchDeclarationParents
import org.jetbrains.kotlin.name.CallableId
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
Expand All @@ -46,10 +47,18 @@ internal fun IrPluginContext.createIrBuilder(
symbol = declaration.symbol
)

internal inline fun <reified T : Any> KClass<T>.toClassId(): ClassId = ClassId(
FqName(java.`package`.name),
Name.identifier(java.simpleName)
)
internal val <T : Any> KClass<T>.classId: ClassId
get() = ClassId(fqName, name)

internal val <T : Any> KClass<T>.callableId: CallableId
get() = CallableId(fqName, name)


internal val <T : Any> KClass<T>.fqName: FqName
get() = FqName(java.`package`.name)

internal val <T : Any> KClass<T>.name: Name
get() = Name.identifier(java.simpleName)

internal fun IrPluginContext.buildLambdaForBody(
originalBody: IrBody,
Expand Down Expand Up @@ -111,7 +120,7 @@ internal fun IrPluginContext.buildSaveInCacheCall(
): IrExpression {
logger.i("buildSaveInCacheCall for ${function.name}, args: ${argsListExpr.dump()}")

val distinctChangeClassSymbol = referenceClass(DistinctChangeCache::class.toClassId())
val distinctChangeClassSymbol = referenceClass(DistinctChangeCache::class.classId)
?: error("Cannot find DistinctChangeCache")

val invokeFunSymbol = distinctChangeClassSymbol.owner.declarations
Expand Down Expand Up @@ -177,7 +186,7 @@ internal fun IrPluginContext.generateFields(

val fieldSymbol = IrFieldSymbolImpl()

val distinctChangeClass = referenceClass(DistinctChangeCache::class.toClassId())
val distinctChangeClass = referenceClass(DistinctChangeCache::class.classId)
?: error("couldn't find DistinctChangeCache")

val backingField = irFactory.createField(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.github.stslex.compiler_plugin.utils

/**
* Use for any action in runtime plugin
* @suppress shouldn't have properties in it's constructor - cause compiling crush
**/
public fun interface Action {

/**
* @param name setted at [io.github.stslex.compiler_plugin.DistinctUntilChangeFun] property name
* @param isProcess show that function block will process
**/
public operator fun invoke(
name: String,
isProcess: Boolean
)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.github.stslex.compiler_plugin.utils

internal class DefaultAction : Action {

override fun invoke(
name: String,
isProcess: Boolean
) = Unit
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
package io.github.stslex.compiler_plugin.utils

import io.github.stslex.compiler_plugin.DistinctChangeConfig
import io.github.stslex.compiler_plugin.DistinctUntilChangeFun
import io.github.stslex.compiler_plugin.DistinctUntilChangeFun.Companion.LOGGING_DEFAULT
import io.github.stslex.compiler_plugin.model.DistinctChangeConfig
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder
import org.jetbrains.kotlin.ir.builders.irBoolean
import org.jetbrains.kotlin.ir.builders.irCallConstructor
import org.jetbrains.kotlin.ir.builders.irString
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
import org.jetbrains.kotlin.ir.expressions.IrClassReference
import org.jetbrains.kotlin.ir.expressions.IrConst
import org.jetbrains.kotlin.ir.expressions.IrConstKind
import org.jetbrains.kotlin.ir.expressions.IrConstructorCall
import org.jetbrains.kotlin.ir.expressions.IrExpression
import org.jetbrains.kotlin.ir.symbols.IrClassSymbol
import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI
import org.jetbrains.kotlin.ir.types.defaultType
import org.jetbrains.kotlin.ir.util.constructors
import org.jetbrains.kotlin.ir.util.getAnnotation
import org.jetbrains.kotlin.ir.util.getValueArgument
import org.jetbrains.kotlin.ir.util.patchDeclarationParents
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import kotlin.reflect.KClass

@OptIn(UnsafeDuringIrConstructionAPI::class)
internal fun IrPluginContext.readQualifier(
Expand All @@ -30,10 +37,13 @@ internal fun IrPluginContext.readQualifier(

val irBuilder = createIrBuilder(function)

val logging = annotation.getValueArgument(0)
val loggingExpr = annotation.getValueArgument(0)
?: irBuilder.irBoolean(LOGGING_DEFAULT)
val actionName = annotation.getValueArgument(2)
?: irBuilder.irString(function.name.identifier)
val actionInstanceExpr = getQualifierAction(annotation, irBuilder)

val constructorSymbol = referenceClass(DistinctChangeConfig::class.toClassId())
val constructorSymbol = referenceClass(DistinctChangeConfig::class.classId)
?.constructors
?.firstOrNull()
?: error("CheckChangesConfig not found in IR")
Expand All @@ -45,16 +55,43 @@ internal fun IrPluginContext.readQualifier(
)
.also { it.patchDeclarationParents(function) }
.apply {
putValueArgument(0, logging)
putValueArgument(0, loggingExpr)
putValueArgument(1, actionInstanceExpr)
putValueArgument(2, actionName)
}
}

internal fun IrSimpleFunction.getQualifierValue(name: String): Boolean = getAnnotation(
@OptIn(UnsafeDuringIrConstructionAPI::class)
private fun IrPluginContext.getQualifierAction(
annotation: IrConstructorCall,
irBuilder: DeclarationIrBuilder
): IrExpression {
val defaultActionClass = irBuiltIns
.findClass(DefaultAction::class.name, DefaultAction::class.fqName)
?: error("readQualifier ${DefaultAction::class.java.simpleName} not found")

val actionReference = annotation.getValueArgument(3) as? IrClassReference

val actionClassSymbol = actionReference?.symbol as? IrClassSymbol ?: defaultActionClass

val actionConstructorSymbol = actionClassSymbol.constructors.firstOrNull {
it.owner.valueParameters.isEmpty()
} ?: error("No no-arg constructor for action class: ${actionReference?.symbol?.defaultType}")

return irBuilder.irCallConstructor(actionConstructorSymbol, emptyList())
}

public fun getJavaClassNonInline(kClass: KClass<*>): Class<*> = kClass.java

internal inline fun <reified T> IrSimpleFunction.getQualifierValue(
name: String,
defaultValue: T
): T = getAnnotation(
FqName(DistinctUntilChangeFun::class.qualifiedName!!)
)
?.getValueArgument(Name.identifier(name))
?.parseValue<Boolean>()
?: false
?.parseValue<T>()
?: defaultValue

private inline fun <reified T> IrExpression.parseValue(): T = when (this) {
is IrConst<*> -> when (kind) {
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ activity = "1.10.0"
constraintLayout = "2.2.0"
jetbrainsKotlinJvm = "2.0.20"

stslexCompilerPlugin = "0.0.3"
stslexCompilerPlugin = "0.0.4"

[libraries]
android-desugarJdkLibs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" }
Expand Down