Skip to content

Commit 1f5863d

Browse files
authored
Add support for multi-dollar stings. (#74) (#86)
1 parent af57313 commit 1f5863d

2 files changed

Lines changed: 146 additions & 8 deletions

File tree

storm-compiler-plugin/src/main/kotlin-transformer-2.2/st/orm/kotlin/plugin/StormTemplateIrTransformer.kt

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -184,12 +184,19 @@ class StormTemplateIrTransformer(
184184
if (sourceStart < 0 || sourceEnd > sourceText.length) return listOf(irConst)
185185
val source = sourceText.substring(sourceStart, sourceEnd)
186186
// Find all ${"..."} patterns in the source and split the merged text.
187+
// For multi-dollar strings (e.g., $$"...$${"value"}..."), the interpolation marker uses multiple $ signs.
188+
// We scan backwards from the found ${" to include any additional $ signs that are part of the marker.
187189
val result = mutableListOf<IrExpression>()
188190
var textPosition = 0
189191
var sourcePosition = 0
190192
while (sourcePosition < source.length) {
191-
val expressionStart = source.indexOf("\${\"", sourcePosition)
192-
if (expressionStart == -1) break
193+
val dollarBraceQuote = source.indexOf("\${\"", sourcePosition)
194+
if (dollarBraceQuote == -1) break
195+
// Scan backwards to find the start of consecutive $ signs (handles $${"..."}, $$${"..."}, etc.).
196+
var expressionStart = dollarBraceQuote
197+
while (expressionStart > sourcePosition && source[expressionStart - 1] == '$') {
198+
expressionStart--
199+
}
193200
// Add the fragment before the expression.
194201
val fragmentSourceLength = expressionStart - sourcePosition
195202
if (fragmentSourceLength > 0) {
@@ -207,8 +214,8 @@ class StormTemplateIrTransformer(
207214
))
208215
textPosition += fragmentSourceLength
209216
}
210-
// Parse the string literal inside ${"..."}.
211-
val contentStart = expressionStart + 3 // position after ${"
217+
// Parse the string literal inside ${"..."} (or $${"..."}, etc.).
218+
val contentStart = dollarBraceQuote + 3 // position after ${"
212219
val closingQuote = findClosingQuote(source, contentStart)
213220
if (closingQuote == -1) return listOf(irConst) // Malformed; leave unchanged.
214221
val expressionSourceContent = source.substring(contentStart, closingQuote)
@@ -225,8 +232,8 @@ class StormTemplateIrTransformer(
225232
}
226233
// Wrap the expression value in t().
227234
val expressionConst = createStringConst(
228-
sourceStart + expressionStart + 2, // position of opening "
229-
sourceStart + closingQuote + 1, // position after closing "
235+
sourceStart + dollarBraceQuote + 2, // position of opening "
236+
sourceStart + closingQuote + 1, // position after closing "
230237
irConst.type,
231238
expressionValue,
232239
)

storm-compiler-plugin/src/test/kotlin/st/orm/kotlin/plugin/StormTemplatePluginTest.kt

Lines changed: 133 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.tschuchort.compiletesting.KotlinCompilation
55
import com.tschuchort.compiletesting.SourceFile
66
import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
77
import org.junit.jupiter.api.Assertions.assertEquals
8+
import org.junit.jupiter.api.Assumptions.assumeTrue
89
import org.junit.jupiter.api.Test
910

1011
/**
@@ -53,14 +54,22 @@ class StormTemplatePluginTest {
5354
""",
5455
)
5556

56-
private fun compile(vararg sources: SourceFile): JvmCompilationResult = KotlinCompilation().apply {
57+
private fun compile(vararg sources: SourceFile, languageVersion: String = "2.0"): JvmCompilationResult = KotlinCompilation().apply {
5758
this.sources = listOf(templateContextStub) + sources.toList()
5859
compilerPluginRegistrars = listOf(StormTemplatePluginRegistrar())
5960
inheritClassPath = true
60-
languageVersion = "2.0"
61+
this.languageVersion = languageVersion
6162
verbose = false
6263
}.compile()
6364

65+
/** Assumes compilation succeeded; skips the test if the compiler does not support the required language features. */
66+
private fun assumeCompilationSuccess(result: JvmCompilationResult) {
67+
assumeTrue(
68+
result.exitCode == KotlinCompilation.ExitCode.OK,
69+
"Compilation failed (compiler may not support the required language features): ${result.messages.lines().take(5).joinToString("\n")}",
70+
)
71+
}
72+
6473
private fun JvmCompilationResult.runMain(): String {
6574
val output = StringBuilder()
6675
val mainClass = classLoader.loadClass("TestKt")
@@ -609,4 +618,126 @@ class StormTemplatePluginTest {
609618
assertEquals("SELECT * FROM users WHERE id IN |", lines[0])
610619
assertEquals("[1, 2, 3]", lines[1])
611620
}
621+
622+
// -- Multi-dollar string interpolation ($$) tests --
623+
624+
@Test
625+
fun `multi-dollar single interpolation is auto-wrapped`() {
626+
val source = SourceFile.kotlin(
627+
"Test.kt",
628+
"""
629+
import st.orm.template.*
630+
631+
fun main() {
632+
val id = 42
633+
val builder: TemplateBuilder = { ${'$'}${'$'}"SELECT * FROM users WHERE id = ${'$'}${'$'}id" }
634+
val result = builder.build()
635+
println(result.fragments.joinToString("|"))
636+
println(result.values.joinToString(","))
637+
}
638+
""",
639+
)
640+
val result = compile(source, languageVersion = "2.2")
641+
assumeCompilationSuccess(result)
642+
val output = result.runMain()
643+
val lines = output.lines()
644+
assertEquals("SELECT * FROM users WHERE id = |", lines[0])
645+
assertEquals("42", lines[1])
646+
}
647+
648+
@Test
649+
fun `multi-dollar literal dollar sign is preserved as fragment`() {
650+
val source = SourceFile.kotlin(
651+
"Test.kt",
652+
"""
653+
import st.orm.template.*
654+
655+
fun main() {
656+
val id = 42
657+
val builder: TemplateBuilder = { ${'$'}${'$'}"SELECT * FROM users WHERE cost > ${'$'}5 AND id = ${'$'}${'$'}id" }
658+
val result = builder.build()
659+
println(result.fragments.joinToString("|"))
660+
println(result.values.joinToString(","))
661+
}
662+
""",
663+
)
664+
val result = compile(source, languageVersion = "2.2")
665+
assumeCompilationSuccess(result)
666+
val output = result.runMain()
667+
val lines = output.lines()
668+
assertEquals("SELECT * FROM users WHERE cost > ${'$'}5 AND id = |", lines[0])
669+
assertEquals("42", lines[1])
670+
}
671+
672+
@Test
673+
fun `multi-dollar multiple interpolations are auto-wrapped`() {
674+
val source = SourceFile.kotlin(
675+
"Test.kt",
676+
"""
677+
import st.orm.template.*
678+
679+
fun main() {
680+
val id = 42
681+
val status = "active"
682+
val builder: TemplateBuilder = { ${'$'}${'$'}"SELECT * FROM users WHERE id = ${'$'}${'$'}id AND status = ${'$'}${'$'}status" }
683+
val result = builder.build()
684+
println(result.fragments.joinToString("|"))
685+
println(result.values.joinToString(","))
686+
}
687+
""",
688+
)
689+
val result = compile(source, languageVersion = "2.2")
690+
assumeCompilationSuccess(result)
691+
val output = result.runMain()
692+
val lines = output.lines()
693+
assertEquals("SELECT * FROM users WHERE id = | AND status = |", lines[0])
694+
assertEquals("42,active", lines[1])
695+
}
696+
697+
@Test
698+
fun `multi-dollar inline string constant is auto-wrapped`() {
699+
val source = SourceFile.kotlin(
700+
"Test.kt",
701+
"""
702+
import st.orm.template.*
703+
704+
fun main() {
705+
val id = 42
706+
val builder: TemplateBuilder = { ${'$'}${'$'}"SELECT * FROM users WHERE id = ${'$'}${'$'}id AND email LIKE ${'$'}${'$'}{"%@gmail.com"}" }
707+
val result = builder.build()
708+
println(result.fragments.joinToString("|"))
709+
println(result.values.joinToString(","))
710+
}
711+
""",
712+
)
713+
val result = compile(source, languageVersion = "2.2")
714+
assumeCompilationSuccess(result)
715+
val output = result.runMain()
716+
val lines = output.lines()
717+
assertEquals("SELECT * FROM users WHERE id = | AND email LIKE |", lines[0])
718+
assertEquals("42,%@gmail.com", lines[1])
719+
}
720+
721+
@Test
722+
fun `multi-dollar plain string literal is unchanged`() {
723+
val source = SourceFile.kotlin(
724+
"Test.kt",
725+
"""
726+
import st.orm.template.*
727+
728+
fun main() {
729+
val builder: TemplateBuilder = { ${'$'}${'$'}"SELECT COUNT(*) FROM users" }
730+
val result = builder.build()
731+
println(result.fragments.joinToString("|"))
732+
println(result.values.size)
733+
}
734+
""",
735+
)
736+
val result = compile(source, languageVersion = "2.2")
737+
assumeCompilationSuccess(result)
738+
val output = result.runMain()
739+
val lines = output.lines()
740+
assertEquals("SELECT COUNT(*) FROM users", lines[0])
741+
assertEquals("0", lines[1])
742+
}
612743
}

0 commit comments

Comments
 (0)