jvmMain.com.bkahlert.kommons.debug.StackTrace.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of kommons-debug Show documentation
Show all versions of kommons-debug Show documentation
Kommons Debug is a Kotlin Multiplatform Library for print debugging.
package com.bkahlert.kommons.debug
import com.bkahlert.kommons.EMPTY
import java.lang.reflect.Method
import java.util.Locale
import kotlin.reflect.KClass
import kotlin.reflect.KClassifier
import kotlin.reflect.KFunction
/** Representation of a stack trace. */
public class StackTrace(elements: List) : List by elements {
override fun toString(): String {
return joinToString("\n at ")
}
public companion object
}
/** Gets the current [StackTrace]. */
@Suppress("NOTHING_TO_INLINE") // inline to avoid impact on stack trace
public inline fun StackTrace.Companion.get(): StackTrace =
Thread.currentThread().stackTrace
.dropWhile { it.className == Thread::class.qualifiedName }
.let { StackTrace(it) }
/**
* Finds the [StackTraceElement] that immediately follows the one
* the specified [predicate] stops returning `true` for the first time.
*
* In other words:
* - The [predicate] is used to drop irrelevant calls for as long as it returns `false`.
* - As soon as it responds `true` calls that are expected to exist are dropped.
* - Finally, the next element which represents the caller of the last matched call is returned.
*/
public fun StackTrace.findOrNull(predicate: (StackTraceElement) -> Boolean): StackTraceElement? =
(if (firstOrNull()?.let(predicate) == false) dropWhile { !predicate(it) } else this).dropWhile { predicate(it) }.firstOrNull()
/**
* Finds the [StackTraceElement] that immediately follows the one
* the specified [predicate] stops returning `true` for the first time.
*
* In other words:
* - The [predicate] is used to drop irrelevant calls for as long as it returns `false`.
* - As soon as it responds `true` calls that are expected to exist are dropped.
* - Finally, the next element which represents the caller of the last matched call is returned.
*/
public fun StackTrace.find(predicate: (StackTraceElement) -> Boolean): StackTraceElement =
findOrNull(predicate) ?: throw NoSuchElementException()
private val functionMangleRegex: Regex = "\\$.*$".toRegex()
private val generatedFunctionRegex = "\\\$.*".toRegex()
/** Returns the specified [function] with mangling information removed. */
public fun StackTrace.Companion.demangleFunction(function: String): String =
function.replace(functionMangleRegex, String.EMPTY)
/** The name of the invoked function with mangling information removed. */
public val StackTraceElement.demangledFunction: String?
get() = StackTrace.demangleFunction(methodName)
/** The [Class] containing the execution point represented by this element. */
public val StackTraceElement.`class`: Class<*> get() = Class.forName(className)
/** The [KClass] containing the execution point represented by this element. */
public val StackTraceElement.kClass: KClass<*> get() = `class`.kotlin
/** The method containing the execution point represented by this element. */
public val StackTraceElement.method: Method get() = `class`.declaredMethods.single { it.name == methodName }
/**
* Finds the [StackTraceElement] that represents the caller
* invoking the [StackTraceElement] matching a call to the specified [functions].
*/
public fun StackTrace.findByLastKnownCallsOrNull(vararg functions: Pair): StackTraceElement? {
var skipInvoke = false
val demangledFunctions = functions.map { (className, function) -> className to StackTrace.demangleFunction(function) }
return findOrNull {
if (demangledFunctions.any { (className, demangledFunction) -> it.className == className && it.demangledFunction == demangledFunction }) {
skipInvoke = true
true
} else {
if (skipInvoke) it.methodName == "invoke" else false
}
}
}
/**
* Finds the [StackTraceElement] that represents the caller
* invoking the [StackTraceElement] matching a call to the specified [className] and [function].
*/
public fun StackTrace.findByLastKnownCallsOrNull(className: KClassifier, function: String): StackTraceElement? =
findByLastKnownCallsOrNull(className.toString() to function)
private fun String.javaBeanMethodToKotlinProperty() = takeUnless {
length >= 4 && substring(0, 3).let { it == "get" || it == "set" } && this[3].isUpperCase()
} ?: substring(3).replaceFirstChar { it.lowercase(Locale.getDefault()) }
private fun String.simplifyFunction() =
replace(generatedFunctionRegex, String.EMPTY).javaBeanMethodToKotlinProperty()
/**
* Finds the [StackTraceElement] that represents the caller
* invoking the [StackTraceElement] matching a call to the specified [functions].
*/
public fun StackTrace.findByLastKnownCallsOrNull(vararg functions: String): StackTraceElement? {
var skipInvoke = false
val simplifiedFunctions = functions.map { StackTrace.demangleFunction(it).simplifyFunction() }
return findOrNull {
if (simplifiedFunctions.contains(it.demangledFunction?.simplifyFunction())) {
skipInvoke = true
true
} else {
if (skipInvoke) it.methodName == "invoke" else false
}
}
}
/**
* Finds the [StackTraceElement] that represents the caller
* invoking the [StackTraceElement] matching a call to the specified [functions].
*/
public fun StackTrace.findByLastKnownCallsOrNull(vararg functions: KFunction<*>): StackTraceElement? =
findByLastKnownCallsOrNull(*functions.map { it.name }.toTypedArray())