Repository: oshai/kotlin-logging
Pull request #485: Issue #484 - Optimize the KLoggerNameResolver for jvm
Author: login: scrat98
Created: 2025-03-06T19:54:16Z | Merged: 2025-04-16T10:39:09Z

closes #484 

Linked issue #484: Optimize the KLoggerNameResolver for jvm
Part 1 of https://github.com/oshai/kotlin-logging/issues/480

Optimize the KLoggerNameResolver for jvm, since we don't need to search indexOf multiple times.
Linked issue comments: user: login: scrat98
created_at: 2025-03-06T19:59:38Z
body: @oshai Also please note, that KLoggerNameResolver::name(func: () -> Unit) is never used in the tests. Therefore I'm not sure should we apply the same logic for `name(forClass: Class<T>)`. To do the following:

```kotlin
  /** get class name for function by the package of the function */
  internal actual fun name(func: () -> Unit): String {
    return name(func::class)
  }

  /** get class name for java class (that usually represents kotlin class) */
  internal fun name(clazz: KClass<*>): String {
    return clazz.java.name.toCleanClassName()
  }
```

The problem with that tests rely on the old behavior (with enclosingClass reflection), for example:
```kotlin
      Arguments.of(
        "io.github.oshai.kotlinlogging.internal.BaseClass\$InnerClass\$Obj",
        BaseClass.InnerClass.Obj::class.java,
      )
```
author_association: CONTRIBUTOR

Diff:
diff --git a/src/javaMain/kotlin/io/github/oshai/kotlinlogging/internal/KLoggerNameResolver.kt b/src/javaMain/kotlin/io/github/oshai/kotlinlogging/internal/KLoggerNameResolver.kt
index c880446f..cbf1e024 100644
--- a/src/javaMain/kotlin/io/github/oshai/kotlinlogging/internal/KLoggerNameResolver.kt
+++ b/src/javaMain/kotlin/io/github/oshai/kotlinlogging/internal/KLoggerNameResolver.kt
@@ -1,41 +1,28 @@
 package io.github.oshai.kotlinlogging.internal
 
-import java.lang.reflect.Modifier
+import kotlin.reflect.KClass
 
 /** Resolves name of java classes */
 internal actual object KLoggerNameResolver {
 
   /** get class name for function by the package of the function */
   internal actual fun name(func: () -> Unit): String {
-    val name = func.javaClass.name
-    val slicedName =
-      when {
-        name.contains("Kt$") -> name.substringBefore("Kt$")
-        name.contains("$") -> name.substringBefore("$")
-        else -> name
-      }
-    return slicedName
+    return name(func::class)
+  }
+
+  internal fun name(clazz: KClass<*>): String {
+    return clazz.java.name.toCleanClassName()
   }
 
-  /** get class name for java class (that usually represents kotlin class) */
-  internal fun <T : Any> name(forClass: Class<T>): String = unwrapCompanionClass(forClass).name
+  private val classNameEndings = listOf("Kt$", "$")
 
-  /** unwrap companion class to enclosing class given a Java Class */
-  private fun <T : Any> unwrapCompanionClass(clazz: Class<T>): Class<*> {
-    return clazz.enclosingClass?.let { enclosingClass ->
-      try {
-        enclosingClass.declaredFields
-          .find { field ->
-            field.name == clazz.simpleName &&
-              Modifier.isStatic(field.modifiers) &&
-              field.type == clazz
-          }
-          ?.run { enclosingClass }
-      } catch (se: SecurityException) {
-        // The security manager isn't properly set up, so it won't be possible
-        // to search for the target declared field.
-        null
+  private fun String.toCleanClassName(): String {
+    classNameEndings.forEach { ending ->
+      val indexOfEnding = this.indexOf(ending)
+      if (indexOfEnding != -1) {
+        return this.substring(0, indexOfEnding)
       }
-    } ?: clazz
+    }
+    return this
   }
 }
diff --git a/src/jvmTest/kotlin/io/github/oshai/kotlinlogging/internal/KLoggerNameResolverTest.kt b/src/jvmTest/kotlin/io/github/oshai/kotlinlogging/internal/KLoggerNameResolverTest.kt
index 5accfc15..90ef40e0 100644
--- a/src/jvmTest/kotlin/io/github/oshai/kotlinlogging/internal/KLoggerNameResolverTest.kt
+++ b/src/jvmTest/kotlin/io/github/oshai/kotlinlogging/internal/KLoggerNameResolverTest.kt
@@ -1,6 +1,7 @@
 package io.github.oshai.kotlinlogging.internal
 
 import java.util.stream.Stream
+import kotlin.reflect.KClass
 import org.junit.jupiter.api.Assertions.assertEquals
 import org.junit.jupiter.api.TestInstance
 import org.junit.jupiter.params.ParameterizedTest
@@ -12,43 +13,37 @@ class KLoggerNameResolverTest {
 
   @ParameterizedTest
   @MethodSource("testNames")
-  fun testNames(expectedName: String, clazz: Class<*>) {
+  fun testNames(expectedName: String, clazz: KClass<*>) {
     assertEquals(expectedName, KLoggerNameResolver.name(clazz))
   }
 
   private fun testNames(): Stream<Arguments> =
     Stream.of(
-      Arguments.of("io.github.oshai.kotlinlogging.internal.BaseClass", BaseClass::class.java),
-      Arguments.of("io.github.oshai.kotlinlogging.internal.ChildClass", ChildClass::class.java),
-      Arguments.of(
-        "io.github.oshai.kotlinlogging.internal.BaseClass",
-        BaseClass.Companion::class.java,
-      ),
+      Arguments.of("io.github.oshai.kotlinlogging.internal.BaseClass", BaseClass::class),
+      Arguments.of("io.github.oshai.kotlinlogging.internal.ChildClass", ChildClass::class),
+      Arguments.of("io.github.oshai.kotlinlogging.internal.BaseClass", BaseClass.Companion::class),
       Arguments.of(
         "io.github.oshai.kotlinlogging.internal.ChildClass",
-        ChildClass.Companion::class.java,
+        ChildClass.Companion::class,
       ),
-      Arguments.of("io.github.oshai.kotlinlogging.internal.Singleton", Singleton::class.java),
-      Arguments.of("io.github.oshai.kotlinlogging.internal.MyInterface", MyInterface::class.java),
-      Arguments.of("java.lang.Object", Any().javaClass),
+      Arguments.of("io.github.oshai.kotlinlogging.internal.Singleton", Singleton::class),
+      Arguments.of("io.github.oshai.kotlinlogging.internal.MyInterface", MyInterface::class),
+      Arguments.of("java.lang.Object", Any()::class),
       Arguments.of(
-        "io.github.oshai.kotlinlogging.internal.KLoggerNameResolverTest\$testNames$1",
-        object {}.javaClass,
+        "io.github.oshai.kotlinlogging.internal.KLoggerNameResolverTest",
+        object {}::class,
       ),
+      Arguments.of("io.github.oshai.kotlinlogging.internal.BaseClass", BaseClass.InnerClass::class),
       Arguments.of(
-        "io.github.oshai.kotlinlogging.internal.BaseClass\$InnerClass\$Obj",
-        BaseClass.InnerClass.Obj::class.java,
-      ),
-      Arguments.of(
-        "io.github.oshai.kotlinlogging.internal.BaseClass\$InnerClass\$Obj",
-        BaseClass.InnerClass.Obj.javaClass,
+        "io.github.oshai.kotlinlogging.internal.BaseClass",
+        BaseClass.InnerClass.Obj::class,
       ),
       Arguments.of(
-        "io.github.oshai.kotlinlogging.internal.BaseClass\$InnerClass",
-        BaseClass.InnerClass.CmpObj::class.java,
+        "io.github.oshai.kotlinlogging.internal.BaseClass",
+        BaseClass.InnerClass.CmpObj::class,
       ),
-      Arguments.of("io.github.oshai.kotlinlogging.internal.Foo\$Bar", Foo.Bar::class.java),
-      Arguments.of("io.github.oshai.kotlinlogging.internal.Foo\$Bar2", Foo.Bar3.javaClass),
+      Arguments.of("io.github.oshai.kotlinlogging.internal.Foo", Foo.Bar::class),
+      Arguments.of("io.github.oshai.kotlinlogging.internal.Foo", Foo.Bar3::class),
       Arguments.of(
         "io.github.oshai.kotlinlogging.internal.PrivateCompanion",
         PrivateCompanion().companionClass,
@@ -88,7 +83,7 @@ class Foo {
 }
 
 class PrivateCompanion {
-  val companionClass: Class<*> = Companion::class.java
+  val companionClass: KClass<*> = Companion::class
 
   private companion object
 }


Reviews:
user: login: oshai
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-06T19:12:39Z
user: login: scrat98
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T19:33:23Z

Review comments:
diff_hunk: @@ -1,41 +1,28 @@
 package io.github.oshai.kotlinlogging.internal
 
-import java.lang.reflect.Modifier
+import kotlin.reflect.KClass
 
 /** Resolves name of java classes */
 internal actual object KLoggerNameResolver {
 
   /** get class name for function by the package of the function */
   internal actual fun name(func: () -> Unit): String {
-    val name = func.javaClass.name
-    val slicedName =
-      when {
-        name.contains("Kt$") -> name.substringBefore("Kt$")
-        name.contains("$") -> name.substringBefore("$")
-        else -> name
-      }
-    return slicedName
+    return name(func::class)
+  }
+
+  internal fun name(clazz: KClass<*>): String {
path: src/javaMain/kotlin/io/github/oshai/kotlinlogging/internal/KLoggerNameResolver.kt
user: login: oshai
body: I think this method should be private
created_at: 2025-04-06T19:12:39Z
author_association: OWNER
diff_hunk: @@ -1,41 +1,28 @@
 package io.github.oshai.kotlinlogging.internal
 
-import java.lang.reflect.Modifier
+import kotlin.reflect.KClass
 
 /** Resolves name of java classes */
 internal actual object KLoggerNameResolver {
 
   /** get class name for function by the package of the function */
   internal actual fun name(func: () -> Unit): String {
-    val name = func.javaClass.name
-    val slicedName =
-      when {
-        name.contains("Kt$") -> name.substringBefore("Kt$")
-        name.contains("$") -> name.substringBefore("$")
-        else -> name
-      }
-    return slicedName
+    return name(func::class)
+  }
+
+  internal fun name(clazz: KClass<*>): String {
path: src/javaMain/kotlin/io/github/oshai/kotlinlogging/internal/KLoggerNameResolver.kt
user: login: scrat98
body: @oshai historically this method was used in the tests, therefore it's not possible to make it private without rewriting tests structure. https://github.com/oshai/kotlin-logging/blob/master/src/javaMain/kotlin/io/github/oshai/kotlinlogging/internal/KLoggerNameResolver.kt#L21

I would prefer to keep it internal for now and if the changes from https://github.com/oshai/kotlin-logging/pull/481 will be accepted, then signature of the method will be changed from
```kotlin
internal actual fun name(func: () -> Unit): String
```

to
```kotlin
internal actual fun name(ref: Any): String {
```

and then we can update the tests to use instances of the classes, rather then KClass
created_at: 2025-04-06T19:33:23Z
author_association: CONTRIBUTOR

PR comments:
user: login: oshai
created_at: 2025-03-16T16:25:54Z
body: I am not sure what is the optimization here?
author_association: OWNER
user: login: scrat98
created_at: 2025-03-16T16:39:15Z
body: @oshai it's unnecessary to search the same delimiter twice
1. contains = indexOf(delimeter) >= 0
2. substringBefore = substring(0, indexOf(delimeter))
author_association: CONTRIBUTOR
user: login: scrat98
created_at: 2025-03-16T16:43:19Z
body: @oshai also please note, that there is a problem with a tests that're not covering `name(func: () -> Unit): String` at all. The tests cover `name(forClass: Class<T>)` only, that have completely another logic to obtain the logger name https://github.com/oshai/kotlin-logging/issues/484#issuecomment-2704817334
author_association: CONTRIBUTOR
user: login: oshai
created_at: 2025-03-16T19:40:44Z
body: > @oshai it's unnecessary to search the same delimiter twice
> 
> 1. contains = indexOf(delimeter) >= 0
> 2. substringBefore = substring(0, indexOf(delimeter))

got it.

I am not sure wdym by tests not covering func. Anyway, it will be nice if you'll add unit test to the changed class.
author_association: OWNER
user: login: scrat98
created_at: 2025-04-06T17:37:21Z
body: @oshai I'll update the tests, but they will rely on **current** behavior. For example, tests expected to support inner classes

```kotlin
Arguments.of("io.github.oshai.kotlinlogging.internal.Foo\$Bar", Foo.Bar::class.java)
```

But currently Inner classes are not supported already, therefore it may seems that my changes break behavior, but actually not, this behavior was already changed. 

You can try this with the latest library version
```kotlin
class Base {
  class Inner {
    private companion object {
      private val logger = KotlinLogging.logger { }
    }

    init {
        logger.info { "Hello" }
    }
  }
}

fun main() {
  Base.Inner()
}
```

```
18:28:14.314 [main] INFO  io.github.oshai.kotlinlogging.internal.Base - Hello
```

Please, see the comment here: https://github.com/oshai/kotlin-logging/issues/484#issuecomment-2704817334

author_association: CONTRIBUTOR
user: login: scrat98
created_at: 2025-04-06T17:50:38Z
body: @oshai please have a look
author_association: CONTRIBUTOR
user: login: oshai
created_at: 2025-04-06T19:40:10Z
body: ok. checks are now failing - I think on code formatting.
author_association: OWNER
user: login: scrat98
created_at: 2025-04-06T19:44:06Z
body: Applied formatting, I'm sorry, completely forgot about it
author_association: CONTRIBUTOR
user: login: scrat98
created_at: 2025-04-06T19:56:39Z
body: Also I think it's worth to mention somewhere in the documentation that loggers in the inner classes always will have the name of the parent(base) class. Example from here https://github.com/oshai/kotlin-logging/pull/485#issuecomment-2781526598

Or create another issue if it supposed to be fixed
author_association: CONTRIBUTOR
user: login: scrat98
created_at: 2025-04-06T20:04:35Z
body: Created issue about logger name for inner class https://github.com/oshai/kotlin-logging/issues/495
author_association: CONTRIBUTOR
user: login: oshai
created_at: 2025-04-16T10:38:32Z
body: Thanks for the PR!
author_association: OWNER