CodeOrigin is a Kotlin compiler plugin for compile-time source introspection. It provides:
nameOf(...)for rename-safe symbol names.sourceOf(...)for the original text of an expression.evalSourceOf(...)for the original text and evaluated value of an expression.declarationSourceOf(...)for readable declarations in the current compilation.@CallSitefor injecting caller location data into omitted default parameters.
All transformations happen in Kotlin IR and work without runtime reflection. Expressions passed to
nameOf and sourceOf are type-checked but are not evaluated at runtime. evalSourceOf evaluates
its expression exactly once.
import li.songe.codeorigin.CallSite
import li.songe.codeorigin.SourceLocation
import li.songe.codeorigin.declarationSourceOf
import li.songe.codeorigin.evalSourceOf
import li.songe.codeorigin.nameOf
import li.songe.codeorigin.sourceOf
data class User(val displayName: String)
fun log(
message: String,
@CallSite location: SourceLocation = SourceLocation(),
) {
println("${location.file}:${location.line}: $message")
}
fun example(user: User) {
nameOf(user) // "user"
nameOf(user.displayName) // "displayName"
nameOf<User>() // "User"
sourceOf(user.displayName) // "user.displayName"
evalSourceOf(user.displayName) // Pair("user.displayName", user.displayName)
declarationSourceOf<User>() // "data class User(val displayName: String)"
log("loaded") // location is injected from this call
}| Gradle module | Published artifact | Purpose |
|---|---|---|
codeorigin |
li.songe:codeorigin |
Multiplatform public API |
codeorigin-compiler-plugin |
li.songe:codeorigin-compiler-plugin |
K2 compiler plugin |
codeorigin-gradle-plugin |
li.songe:codeorigin-gradle-plugin |
Kotlin Gradle integration |
codeorigin-integration-tests |
not published | End-to-end JVM tests |
The public functions, annotation, and SourceLocation type all use the
li.songe.codeorigin package. The Gradle plugin ID is also li.songe.codeorigin.
# gradle/libs.versions.toml
[versions]
codeorigin = "<version>"
[libraries]
codeorigin = { module = "li.songe:codeorigin", version.ref = "codeorigin" }
[plugins]
codeorigin = { id = "li.songe.codeorigin", version.ref = "codeorigin" }plugins {
kotlin("multiplatform")
alias(libs.plugins.codeorigin)
}
kotlin {
sourceSets.commonMain.dependencies {
implementation(libs.codeorigin)
}
}The Gradle plugin deliberately does not add the public dependency. This lets a library choose
whether CodeOrigin belongs in api, implementation, or a particular KMP source set.
The value overload accepts named references:
val user = User("Ada")
nameOf(user) // "user"
nameOf(user.displayName) // "displayName"
nameOf(User::displayName) // "displayName"
nameOf(::example) // "example"It also supports objects and enum entries. Compound expressions such as function calls, arithmetic, indexing, and safe calls are rejected with a compiler error.
The type overload returns the source-level simple type name, including a type alias used at the call site:
typealias Account = User
nameOf<User>() // "User"
nameOf<Account>() // "Account"Backticks are removed from escaped identifiers.
sourceOf accepts any valid expression and captures its source text without evaluating it:
var count = 0
sourceOf(count++) // "count++"
check(count == 0)Internal whitespace, comments, and literal spelling are preserved. Leading and trailing whitespace is removed, and CRLF/CR line endings are normalized to LF. A generated or in-memory source file that cannot be read produces a compiler error instead of an empty or reconstructed expression.
evalSourceOf captures the same source text but also evaluates the expression exactly once. It
returns a Pair<String, T> whose first value is the source text and whose second value is the
expression result:
var count = 0
val (source, value) = evalSourceOf(count++)
check(source == "count++")
check(value == 0)
check(count == 1)If the expression throws, the exception propagates normally and no pair is returned. Both APIs
type-check their expressions. Use sourceOf when the expression must not run, and evalSourceOf
when both its value and source text are needed.
declarationSourceOf<T>() captures the complete source text of a class, interface, object, enum,
or annotation class whose source is readable in the current compiler invocation. The declaration
may be in the same source file or another one. The reference overload accepts a function or
property reference:
declarationSourceOf<User>()
declarationSourceOf(::example)
declarationSourceOf(User::displayName)The reference expression is not evaluated. Leading KDoc and use-site annotations are included. Common indentation and leading or trailing whitespace are removed, and line endings are normalized to LF.
Cross-file capture is best effort under incremental compilation. It does not register an incremental dependency on the target declaration's source text, so source-only changes such as KDoc, comments, whitespace, or a non-inline function body may not recompile the call site and can leave a previously generated string stale. Some incremental compiler invocations may also omit the unchanged target source from the available IR. If an up-to-date cross-file value is required, the build must force a clean/full compilation or provide its own invalidation mechanism.
Declarations whose source is unavailable in the current compilation, constructor references, generated declarations without readable source, and non-reference arguments produce compiler errors. The plugin does not package source metadata or use runtime reflection.
@CallSite can annotate an omitted default parameter of type String, Int, or
SourceLocation. Explicit arguments always win.
fun trace(
message: String,
@CallSite("{file}:{line}") source: String = "",
@CallSite("{line}") line: Int = -1,
@CallSite location: SourceLocation = SourceLocation(),
) {
println("[$source] $message")
}String formats support:
| Field | Value | Example |
|---|---|---|
{path} |
Source path relative to the Gradle root project, using / |
src/main/kotlin/com/example/Greeter.kt |
{file} |
File name | Greeter.kt |
{package} |
Kotlin package name | com.example |
{name} |
Nearest source declaration name | greet |
{owner} |
Source declaration path without the package | Greeter.greet |
{qualifiedOwner} |
Package plus source declaration path | com.example.Greeter.greet |
{type} |
Enclosing class/interface/object path | Greeter |
{function} |
Enclosing function, constructor, or accessor | greet |
{line} |
1-based line number | 12 |
{column} |
1-based column number | 9 |
{offset} |
0-based source character offset | 180 |
An empty String format uses {qualifiedOwner}({file}:{line}). An Int parameter must use exactly
{line}, {column}, or {offset}. A SourceLocation parameter is structured and does not accept
a custom format. Use {{ and }} for literal braces in a String format.
When a function overrides another declaration, the plugin also recognizes @CallSite on the
corresponding overridden parameter. The override must retain an effective default value. If
multiple inherited annotations specify different formats, compilation fails with a diagnostic;
an annotation written directly on the overriding parameter takes precedence.
- Kotlin 2.4 compiler plugin with K2 support.
- Kotlin/JVM, JS, Native, and Wasm share the same public API and IR transformation.
nameOf,sourceOf, anddeclarationSourceOfbecome constants in generated IR.evalSourceOfretains its expression and pairs the evaluated value with a source-text constant. These intrinsics are not accepted where the Kotlin frontend requires aconstexpression, such as annotation arguments orconst valinitializers.- Java source calls and K1 are not supported.
./gradlew buildThe end-to-end tests compile fixtures separately from their callers, exercising BINARY annotation
retention, compiler-plugin loading, source capture, side-effect removal, structured call-site
construction, Kotlin/JS and Wasm compilation, and relocation-safe Gradle build caching. They also
verify that evalSourceOf evaluates its expression exactly once.