Skip to content

Commit

Permalink
Fix KT-18706 in CodeWriter.generateImports
Browse files Browse the repository at this point in the history
  • Loading branch information
mitasov-ra authored and Egorand committed Jul 11, 2024
1 parent 946f279 commit 4ee8fc4
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 1 deletion.
12 changes: 12 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ Change Log

## Unreleased

* Fix: Fix KT-18706: kotlinpoet now generates import aliases without backticks (#1920)
* For example:
```kotlin
// before, doesn't compile due to KT-18706
import com.example.one.`$Foo` as `One$Foo`
import com.example.two.`$Foo` as `Two$Foo`

// now, compiles
import com.example.one.`$Foo` as One__Foo
import com.example.two.`$Foo` as Two__Foo
```

## Version 1.18.0

Thanks to [@DanielGronau][DanielGronau] for contributing to this release.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -761,7 +761,8 @@ internal class CodeWriter(
imported[simpleName] = qualifiedNames
} else {
generateImportAliases(simpleName, canonicalNamesToQualifiedNames, capitalizeAliases)
.onEach { (alias, qualifiedName) ->
.onEach { (a, qualifiedName) ->
val alias = a.escapeAsAlias()
val canonicalName = qualifiedName.computeCanonicalName()
generatedImports[canonicalName] = Import(canonicalName, alias)

Expand Down
44 changes: 44 additions & 0 deletions kotlinpoet/src/commonMain/kotlin/com/squareup/kotlinpoet/Util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,50 @@ internal fun String.escapeIfNecessary(validate: Boolean = true): String = escape
.escapeIfAllCharactersAreUnderscore()
.apply { if (validate) failIfEscapeInvalid() }

/**
* Because of [KT-18706](https://youtrack.jetbrains.com/issue/KT-18706)
* bug all aliases escaped with backticks are not resolved.
*
* So this method is used instead, which uses custom escape rules:
* - if all characters are underscores, add `'0'` to the end
* - if it's a keyword, prepend it with double underscore `"__"`
* - if first character cannot be used as identifier start (e.g. a number), underscore is prepended
* - all `'$'` replaced with double underscore `"__"`
* - all characters that cannot be used as identifier part (e.g. space or hyphen) are
* replaced with `"_U<code>"` where `code` is 4-digit Unicode character code in hexadecimal form
*/
internal fun String.escapeAsAlias(validate: Boolean = true): String {
if (allCharactersAreUnderscore) {
return "${this}0" // add '0' to make it a valid identifier
}

if (isKeyword) {
return "__$this"
}

val newAlias = StringBuilder("")

if (!Character.isJavaIdentifierStart(first())) {
newAlias.append('_')
}

for (ch in this) {
if (ch == ALLOWED_CHARACTER) {
newAlias.append("__") // all $ replaced with __
continue
}

if (!Character.isJavaIdentifierPart(ch)) {
newAlias.append("_U").append(Integer.toHexString(ch.code).padStart(4, '0'))
continue
}

newAlias.append(ch)
}

return newAlias.toString().apply { if (validate) failIfEscapeInvalid() }
}

private fun String.alreadyEscaped() = startsWith("`") && endsWith("`")

private fun String.escapeIfKeyword() = if (isKeyword && !alreadyEscaped()) "`$this`" else this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,42 @@ class FileSpecTest {
)
}

@Test fun conflictingImportsEscapedWithoutBackticks() {
val foo1Type = ClassName("com.example.generated.one", "\$Foo")
val foo2Type = ClassName("com.example.generated.another", "\$Foo")

val testFun = FunSpec.builder("testFun")
.addCode(
"""
val foo1 = %T()
val foo2 = %T()
""".trimIndent(),
foo1Type,
foo2Type,
)
.build()

val testFile = FileSpec.builder("com.squareup.kotlinpoet.test", "TestFile")
.addFunction(testFun)
.build()

assertThat(testFile.toString())
.isEqualTo(
"""
|package com.squareup.kotlinpoet.test
|
|import com.example.generated.another.`${'$'}Foo` as Another__Foo
|import com.example.generated.one.`${'$'}Foo` as One__Foo
|
|public fun testFun() {
| val foo1 = One__Foo()
| val foo2 = Another__Foo()
|}
|
""".trimMargin(),
)
}

@Test fun conflictingImportsEscapeKeywords() {
val source = FileSpec.builder("com.squareup.tacos", "Taco")
.addType(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,52 @@ class UtilTest {
assertThat("`A`".escapeIfNecessary()).isEqualTo("`A`")
}

@Test
fun `escapeAsAlias all underscores`() {
val input = "____"
val expected = "____0"
assertThat(input.escapeAsAlias()).isEqualTo(expected)
}

@Test
fun `escapeAsAlias keyword`() {
val input = "if"
val expected = "__if"
assertThat(input.escapeAsAlias()).isEqualTo(expected)
}

@Test
fun `escapeAsAlias first character cannot be used as identifier start`() {
val input = "1abc"
val expected = "_1abc"
assertThat(input.escapeAsAlias()).isEqualTo(expected)
}

@Test
fun `escapeAsAlias dollar sign`() {
val input = "\$\$abc"
val expected = "____abc"
assertThat(input.escapeAsAlias()).isEqualTo(expected)
}

@Test
fun `escapeAsAlias characters that cannot be used as identifier part`() {
val input = "a b-c"
val expected = "a_U0020b_U002dc"
assertThat(input.escapeAsAlias()).isEqualTo(expected)
}

@Test
fun `escapeAsAlias double escape does nothing`() {
val input = "1SampleClass_\$Generated "
val expected = "_1SampleClass___Generated_U0020"

assertThat(input.escapeAsAlias())
.isEqualTo(expected)
assertThat(input.escapeAsAlias().escapeAsAlias())
.isEqualTo(expected)
}

private fun stringLiteral(string: String) = stringLiteral(string, string)

private fun stringLiteral(expected: String, value: String) =
Expand Down

0 comments on commit 4ee8fc4

Please sign in to comment.