diff --git a/README.md b/README.md index dca5044..78578fb 100644 --- a/README.md +++ b/README.md @@ -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") + } +} +``` \ No newline at end of file diff --git a/app/src/main/kotlin/com/stslex/compiler_app/MainActivity.kt b/app/src/main/kotlin/com/stslex/compiler_app/MainActivity.kt index aa3359d..9d3c101 100644 --- a/app/src/main/kotlin/com/stslex/compiler_app/MainActivity.kt +++ b/app/src/main/kotlin/com/stslex/compiler_app/MainActivity.kt @@ -54,13 +54,18 @@ class MainActivity : ComponentActivity() { printUsernameWithSingletonDistinct(user.name) } + @DistinctUntilChangeFun(true) private fun setName(name: String) { logger.log(Level.INFO, "setName: $name") findViewById(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(R.id.secondNameFieldTextView).text = name @@ -74,3 +79,5 @@ class MainActivity : ComponentActivity() { private fun printUsernameWithSingletonDistinct(name: String) { println("printUsernameWithSingletonDistinct: $name") } + + diff --git a/app/src/main/kotlin/com/stslex/compiler_app/TestLogger.kt b/app/src/main/kotlin/com/stslex/compiler_app/TestLogger.kt new file mode 100644 index 0000000..3179a2a --- /dev/null +++ b/app/src/main/kotlin/com/stslex/compiler_app/TestLogger.kt @@ -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") + } +} \ No newline at end of file diff --git a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctChangeCache.kt b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctChangeCache.kt index 0d2d1ab..28ffa38 100644 --- a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctChangeCache.kt +++ b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctChangeCache.kt @@ -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, Any?>>() - private val logger = RuntimeLogger.tag("DistinctChangeLogger") + private val logger = runIf(config.logging) { RuntimeLogger.tag("DistinctChangeLogger") } @Suppress("UNCHECKED_CAST") internal operator fun invoke( @@ -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 } } \ No newline at end of file diff --git a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctChangeConfig.kt b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctChangeConfig.kt deleted file mode 100644 index 49f0917..0000000 --- a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctChangeConfig.kt +++ /dev/null @@ -1,5 +0,0 @@ -package io.github.stslex.compiler_plugin - -internal data class DistinctChangeConfig( - val logging: Boolean -) \ No newline at end of file diff --git a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctUntilChangeFun.kt b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctUntilChangeFun.kt index 04997f6..7cd5ef6 100644 --- a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctUntilChangeFun.kt +++ b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctUntilChangeFun.kt @@ -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 = DefaultAction::class ) { public companion object { internal const val LOGGING_DEFAULT = false + internal const val SINGLETON_ALLOW = false + } } \ No newline at end of file diff --git a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/model/DistinctChangeConfig.kt b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/model/DistinctChangeConfig.kt new file mode 100644 index 0000000..5a93806 --- /dev/null +++ b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/model/DistinctChangeConfig.kt @@ -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 +) \ No newline at end of file diff --git a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/transformers/IrFunctionTransformer.kt b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/transformers/IrFunctionTransformer.kt index 1b23139..83d4b04 100644 --- a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/transformers/IrFunctionTransformer.kt +++ b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/transformers/IrFunctionTransformer.kt @@ -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 @@ -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}") } diff --git a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/CompilerExtensions.kt b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/CompilerExtensions.kt index ec5007b..20f1ad5 100644 --- a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/CompilerExtensions.kt +++ b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/CompilerExtensions.kt @@ -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 @@ -46,10 +47,18 @@ internal fun IrPluginContext.createIrBuilder( symbol = declaration.symbol ) -internal inline fun KClass.toClassId(): ClassId = ClassId( - FqName(java.`package`.name), - Name.identifier(java.simpleName) -) +internal val KClass.classId: ClassId + get() = ClassId(fqName, name) + +internal val KClass.callableId: CallableId + get() = CallableId(fqName, name) + + +internal val KClass.fqName: FqName + get() = FqName(java.`package`.name) + +internal val KClass.name: Name + get() = Name.identifier(java.simpleName) internal fun IrPluginContext.buildLambdaForBody( originalBody: IrBody, @@ -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 @@ -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( diff --git a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/CompilerLogger.kt b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/CompilerLogger.kt new file mode 100644 index 0000000..12ad59b --- /dev/null +++ b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/CompilerLogger.kt @@ -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 + ) + +} diff --git a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/DefaultAction.kt b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/DefaultAction.kt new file mode 100644 index 0000000..c01b2f3 --- /dev/null +++ b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/DefaultAction.kt @@ -0,0 +1,9 @@ +package io.github.stslex.compiler_plugin.utils + +internal class DefaultAction : Action { + + override fun invoke( + name: String, + isProcess: Boolean + ) = Unit +} \ No newline at end of file diff --git a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/ReadQualifierUtil.kt b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/ReadQualifierUtil.kt index 6a8d186..1257ce3 100644 --- a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/ReadQualifierUtil.kt +++ b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/ReadQualifierUtil.kt @@ -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( @@ -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") @@ -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 IrSimpleFunction.getQualifierValue( + name: String, + defaultValue: T +): T = getAnnotation( FqName(DistinctUntilChangeFun::class.qualifiedName!!) ) ?.getValueArgument(Name.identifier(name)) - ?.parseValue() - ?: false + ?.parseValue() + ?: defaultValue private inline fun IrExpression.parseValue(): T = when (this) { is IrConst<*> -> when (kind) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 13c03c7..5f507f1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }