Skip to content

Commit 4ee8fc4

Browse files
mitasov-raEgorand
authored andcommitted
Fix KT-18706 in CodeWriter.generateImports
1 parent 946f279 commit 4ee8fc4

File tree

5 files changed

+140
-1
lines changed

5 files changed

+140
-1
lines changed

docs/changelog.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ Change Log
33

44
## Unreleased
55

6+
* Fix: Fix KT-18706: kotlinpoet now generates import aliases without backticks (#1920)
7+
* For example:
8+
```kotlin
9+
// before, doesn't compile due to KT-18706
10+
import com.example.one.`$Foo` as `One$Foo`
11+
import com.example.two.`$Foo` as `Two$Foo`
12+
13+
// now, compiles
14+
import com.example.one.`$Foo` as One__Foo
15+
import com.example.two.`$Foo` as Two__Foo
16+
```
17+
618
## Version 1.18.0
719

820
Thanks to [@DanielGronau][DanielGronau] for contributing to this release.

kotlinpoet/src/commonMain/kotlin/com/squareup/kotlinpoet/CodeWriter.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -761,7 +761,8 @@ internal class CodeWriter(
761761
imported[simpleName] = qualifiedNames
762762
} else {
763763
generateImportAliases(simpleName, canonicalNamesToQualifiedNames, capitalizeAliases)
764-
.onEach { (alias, qualifiedName) ->
764+
.onEach { (a, qualifiedName) ->
765+
val alias = a.escapeAsAlias()
765766
val canonicalName = qualifiedName.computeCanonicalName()
766767
generatedImports[canonicalName] = Import(canonicalName, alias)
767768

kotlinpoet/src/commonMain/kotlin/com/squareup/kotlinpoet/Util.kt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,50 @@ internal fun String.escapeIfNecessary(validate: Boolean = true): String = escape
294294
.escapeIfAllCharactersAreUnderscore()
295295
.apply { if (validate) failIfEscapeInvalid() }
296296

297+
/**
298+
* Because of [KT-18706](https://youtrack.jetbrains.com/issue/KT-18706)
299+
* bug all aliases escaped with backticks are not resolved.
300+
*
301+
* So this method is used instead, which uses custom escape rules:
302+
* - if all characters are underscores, add `'0'` to the end
303+
* - if it's a keyword, prepend it with double underscore `"__"`
304+
* - if first character cannot be used as identifier start (e.g. a number), underscore is prepended
305+
* - all `'$'` replaced with double underscore `"__"`
306+
* - all characters that cannot be used as identifier part (e.g. space or hyphen) are
307+
* replaced with `"_U<code>"` where `code` is 4-digit Unicode character code in hexadecimal form
308+
*/
309+
internal fun String.escapeAsAlias(validate: Boolean = true): String {
310+
if (allCharactersAreUnderscore) {
311+
return "${this}0" // add '0' to make it a valid identifier
312+
}
313+
314+
if (isKeyword) {
315+
return "__$this"
316+
}
317+
318+
val newAlias = StringBuilder("")
319+
320+
if (!Character.isJavaIdentifierStart(first())) {
321+
newAlias.append('_')
322+
}
323+
324+
for (ch in this) {
325+
if (ch == ALLOWED_CHARACTER) {
326+
newAlias.append("__") // all $ replaced with __
327+
continue
328+
}
329+
330+
if (!Character.isJavaIdentifierPart(ch)) {
331+
newAlias.append("_U").append(Integer.toHexString(ch.code).padStart(4, '0'))
332+
continue
333+
}
334+
335+
newAlias.append(ch)
336+
}
337+
338+
return newAlias.toString().apply { if (validate) failIfEscapeInvalid() }
339+
}
340+
297341
private fun String.alreadyEscaped() = startsWith("`") && endsWith("`")
298342

299343
private fun String.escapeIfKeyword() = if (isKeyword && !alreadyEscaped()) "`$this`" else this

kotlinpoet/src/commonTest/kotlin/com/squareup/kotlinpoet/FileSpecTest.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,42 @@ class FileSpecTest {
373373
)
374374
}
375375

376+
@Test fun conflictingImportsEscapedWithoutBackticks() {
377+
val foo1Type = ClassName("com.example.generated.one", "\$Foo")
378+
val foo2Type = ClassName("com.example.generated.another", "\$Foo")
379+
380+
val testFun = FunSpec.builder("testFun")
381+
.addCode(
382+
"""
383+
val foo1 = %T()
384+
val foo2 = %T()
385+
""".trimIndent(),
386+
foo1Type,
387+
foo2Type,
388+
)
389+
.build()
390+
391+
val testFile = FileSpec.builder("com.squareup.kotlinpoet.test", "TestFile")
392+
.addFunction(testFun)
393+
.build()
394+
395+
assertThat(testFile.toString())
396+
.isEqualTo(
397+
"""
398+
|package com.squareup.kotlinpoet.test
399+
|
400+
|import com.example.generated.another.`${'$'}Foo` as Another__Foo
401+
|import com.example.generated.one.`${'$'}Foo` as One__Foo
402+
|
403+
|public fun testFun() {
404+
| val foo1 = One__Foo()
405+
| val foo2 = Another__Foo()
406+
|}
407+
|
408+
""".trimMargin(),
409+
)
410+
}
411+
376412
@Test fun conflictingImportsEscapeKeywords() {
377413
val source = FileSpec.builder("com.squareup.tacos", "Taco")
378414
.addType(

kotlinpoet/src/commonTest/kotlin/com/squareup/kotlinpoet/UtilTest.kt

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,52 @@ class UtilTest {
143143
assertThat("`A`".escapeIfNecessary()).isEqualTo("`A`")
144144
}
145145

146+
@Test
147+
fun `escapeAsAlias all underscores`() {
148+
val input = "____"
149+
val expected = "____0"
150+
assertThat(input.escapeAsAlias()).isEqualTo(expected)
151+
}
152+
153+
@Test
154+
fun `escapeAsAlias keyword`() {
155+
val input = "if"
156+
val expected = "__if"
157+
assertThat(input.escapeAsAlias()).isEqualTo(expected)
158+
}
159+
160+
@Test
161+
fun `escapeAsAlias first character cannot be used as identifier start`() {
162+
val input = "1abc"
163+
val expected = "_1abc"
164+
assertThat(input.escapeAsAlias()).isEqualTo(expected)
165+
}
166+
167+
@Test
168+
fun `escapeAsAlias dollar sign`() {
169+
val input = "\$\$abc"
170+
val expected = "____abc"
171+
assertThat(input.escapeAsAlias()).isEqualTo(expected)
172+
}
173+
174+
@Test
175+
fun `escapeAsAlias characters that cannot be used as identifier part`() {
176+
val input = "a b-c"
177+
val expected = "a_U0020b_U002dc"
178+
assertThat(input.escapeAsAlias()).isEqualTo(expected)
179+
}
180+
181+
@Test
182+
fun `escapeAsAlias double escape does nothing`() {
183+
val input = "1SampleClass_\$Generated "
184+
val expected = "_1SampleClass___Generated_U0020"
185+
186+
assertThat(input.escapeAsAlias())
187+
.isEqualTo(expected)
188+
assertThat(input.escapeAsAlias().escapeAsAlias())
189+
.isEqualTo(expected)
190+
}
191+
146192
private fun stringLiteral(string: String) = stringLiteral(string, string)
147193

148194
private fun stringLiteral(expected: String, value: String) =

0 commit comments

Comments
 (0)