
Customize string interpolation via a single annotated function; compiler-generated facades split literals into surroundings and holes, enabling escaping, SQL-safe substitution, extension-property processors, and optional fallbacks.
A Kotlin compiler plugin that lets you define custom string interpolation by writing a single function.
Kotlin's built-in string templates ("$variable") are powerful but there's no way to customize what happens to each
interpolated value. Common use cases that are hard to express today:
"SELECT * FROM users WHERE id = $userId" into a prepared statement call
automatically.With this plugin you write a function that describes how to process the template, and the compiler takes care of splitting the string literal into parts and calling your function at compile time.
You write a function (or extension property) that receives a StringTemplate. The StringTemplate interface
exposes two members:
surroundings: List<String> – the constant parts of the literal, one more than the number of holes.holes: List<Any?> – the interpolated expressions.You annotate that function (or property) with @TemplateProcessor.
The plugin generates a sibling function with the same name that accepts regular String parameters instead of
StringTemplate parameters. Inside that generated function the plugin constructs a StringTemplate from the string
literal and forwards it to your original function.
Because the generated function is the one that compiler actually resolves at call sites, the call looks like an ordinary string literal:
val result: String = FOO("Hello, $name!")A processor that wraps every interpolated value in double quotes:
@TemplateProcessor
fun StringBuilder.appendQuoted(string: StringTemplate): StringBuilder {
val partsIterator = string.surroundings.iterator()
append(partsIterator.next())
for (param in string.holes) {
append('"').append(param).append('"')
append(partsIterator.next())
}
return this
}The plugin generates:
fun StringBuilder.appendQuoted(string: String): StringBuilderCall site:
val builder = StringBuilder()
builder.appendQuoted("Hello, $name!")
// Surroundings: ["Hello, ", "!"]
// Holes: [name]
// Result: builder.appendQuoted with each hole wrapped in quotesTemplate processors also work as extension properties with a StringTemplate receiver. The getter receives the
template and returns the processed result:
@TemplateProcessor
val StringTemplate.quoted: String
get() = buildString {
val partsIterator = surroundings.iterator()
append(partsIterator.next())
for (param in holes) {
append('"').append(param).append('"')
append(partsIterator.next())
}
}Usage:
val result: String = "Hello, $name!".quotedContext parameters are supported as well, but it must be a string or string template literal, which is only possible with explicit context arguments.
Template processor functions and properties should use SCREAMING_SNAKE_CASE (e.g. FOO, HTML_DIV, SQL_QUERY).
val result: String = FOO("Hello, $name!")SCREAMING_SNAKE_CASE makes it easy to distinguish template processors from regular functions at a glance inside implementation files where both kinds coexist.
To use it in your project, add this to build.gradle.kts:
plugins {
id("io.github.mimimishkin.custom-string-template") version "2.4.10-0.2.0"
}Note that while this is enough to compile and work properly, you will get a false error in the IDE -
Argument type mismatch: actual type is 'String', but 'StringTemplate' was expected. This is due to the fact that
IntelliJ IDEA runs only bundled compiler plugins (e.g. 'serialization', 'all-open') for code analysis. To enable
external plugins, you need to install
KEFS and add an artifact
io.github.mimimishkin:custom-string-template-compiler-plugin located in Maven Central.
Here you can find more info about working with third-party compiler plugins. I will maintain compatibility only with compiler version that the latest stable IntelliJ IDEA uses.
StringTemplate parameters – every StringTemplate parameter (value, context, or receiver) must be
non-nullable.$default function.val only – @TemplateProcessor cannot be applied to var properties.The surroundings and holes lists always follow this pattern:
surrounding₀ hole₀ surrounding₁ hole₁ ... holeₙ surroundingₙ₊₁
There is always one more surrounding than hole. Both the leading and trailing surrounding may be empty strings.
Example: "$number + 2 = ${number + 2}"
| Part | Value |
|---|---|
| surroundings | ["", " + 2 = ", ""] |
| holes | [number, number + 2] |
The generated facade function carries the @FacadeInterpolatorCall annotation, which is an @OptIn-level marker.
This prevents calling the facade directly from code that doesn't opt in, which would bypass the plugin and produce
incorrect results at runtime. Normal call sites that use string literals are resolved by the compiler and do not
trigger this restriction.
By default, the generated facade throws an AssertionError if the plugin isn't enabled.
If you want call sites to keep working (with your own logic) even without the plugin (or throw you own error), you can
write your own facade for the same callable:
@TemplateProcessor
fun FOO(string: StringTemplate): String = string.reconstruct()
@FacadeInterpolatorCall
fun FOO(string: String): String = "fallback: $string"When the plugin is enabled, it reuses your @FacadeInterpolatorCall function instead of generating a duplicate, and
string-literal calls still route through the template processor. When the plugin is disabled, the call falls back to
your custom implementation instead of throwing an AssertionError.
A Kotlin compiler plugin that lets you define custom string interpolation by writing a single function.
Kotlin's built-in string templates ("$variable") are powerful but there's no way to customize what happens to each
interpolated value. Common use cases that are hard to express today:
"SELECT * FROM users WHERE id = $userId" into a prepared statement call
automatically.With this plugin you write a function that describes how to process the template, and the compiler takes care of splitting the string literal into parts and calling your function at compile time.
You write a function (or extension property) that receives a StringTemplate. The StringTemplate interface
exposes two members:
surroundings: List<String> – the constant parts of the literal, one more than the number of holes.holes: List<Any?> – the interpolated expressions.You annotate that function (or property) with @TemplateProcessor.
The plugin generates a sibling function with the same name that accepts regular String parameters instead of
StringTemplate parameters. Inside that generated function the plugin constructs a StringTemplate from the string
literal and forwards it to your original function.
Because the generated function is the one that compiler actually resolves at call sites, the call looks like an ordinary string literal:
val result: String = FOO("Hello, $name!")A processor that wraps every interpolated value in double quotes:
@TemplateProcessor
fun StringBuilder.appendQuoted(string: StringTemplate): StringBuilder {
val partsIterator = string.surroundings.iterator()
append(partsIterator.next())
for (param in string.holes) {
append('"').append(param).append('"')
append(partsIterator.next())
}
return this
}The plugin generates:
fun StringBuilder.appendQuoted(string: String): StringBuilderCall site:
val builder = StringBuilder()
builder.appendQuoted("Hello, $name!")
// Surroundings: ["Hello, ", "!"]
// Holes: [name]
// Result: builder.appendQuoted with each hole wrapped in quotesTemplate processors also work as extension properties with a StringTemplate receiver. The getter receives the
template and returns the processed result:
@TemplateProcessor
val StringTemplate.quoted: String
get() = buildString {
val partsIterator = surroundings.iterator()
append(partsIterator.next())
for (param in holes) {
append('"').append(param).append('"')
append(partsIterator.next())
}
}Usage:
val result: String = "Hello, $name!".quotedContext parameters are supported as well, but it must be a string or string template literal, which is only possible with explicit context arguments.
Template processor functions and properties should use SCREAMING_SNAKE_CASE (e.g. FOO, HTML_DIV, SQL_QUERY).
val result: String = FOO("Hello, $name!")SCREAMING_SNAKE_CASE makes it easy to distinguish template processors from regular functions at a glance inside implementation files where both kinds coexist.
To use it in your project, add this to build.gradle.kts:
plugins {
id("io.github.mimimishkin.custom-string-template") version "2.4.10-0.2.0"
}Note that while this is enough to compile and work properly, you will get a false error in the IDE -
Argument type mismatch: actual type is 'String', but 'StringTemplate' was expected. This is due to the fact that
IntelliJ IDEA runs only bundled compiler plugins (e.g. 'serialization', 'all-open') for code analysis. To enable
external plugins, you need to install
KEFS and add an artifact
io.github.mimimishkin:custom-string-template-compiler-plugin located in Maven Central.
Here you can find more info about working with third-party compiler plugins. I will maintain compatibility only with compiler version that the latest stable IntelliJ IDEA uses.
StringTemplate parameters – every StringTemplate parameter (value, context, or receiver) must be
non-nullable.$default function.val only – @TemplateProcessor cannot be applied to var properties.The surroundings and holes lists always follow this pattern:
surrounding₀ hole₀ surrounding₁ hole₁ ... holeₙ surroundingₙ₊₁
There is always one more surrounding than hole. Both the leading and trailing surrounding may be empty strings.
Example: "$number + 2 = ${number + 2}"
| Part | Value |
|---|---|
| surroundings | ["", " + 2 = ", ""] |
| holes | [number, number + 2] |
The generated facade function carries the @FacadeInterpolatorCall annotation, which is an @OptIn-level marker.
This prevents calling the facade directly from code that doesn't opt in, which would bypass the plugin and produce
incorrect results at runtime. Normal call sites that use string literals are resolved by the compiler and do not
trigger this restriction.
By default, the generated facade throws an AssertionError if the plugin isn't enabled.
If you want call sites to keep working (with your own logic) even without the plugin (or throw you own error), you can
write your own facade for the same callable:
@TemplateProcessor
fun FOO(string: StringTemplate): String = string.reconstruct()
@FacadeInterpolatorCall
fun FOO(string: String): String = "fallback: $string"When the plugin is enabled, it reuses your @FacadeInterpolatorCall function instead of generating a duplicate, and
string-literal calls still route through the template processor. When the plugin is disabled, the call falls back to
your custom implementation instead of throwing an AssertionError.