Skip to content

Commit a4d5afe

Browse files
committed
Prevent aliased imports from colliding with existing imports
1 parent 2ed2017 commit a4d5afe

File tree

3 files changed

+122
-4
lines changed

3 files changed

+122
-4
lines changed

docs/changelog.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Change Log
44
## Unreleased
55

66
* Fix: Support delegates on anonymous classes. (#2034)
7+
* Fix: Prevent aliased imports from colliding with existing imports (#2046)
78

89
## Version 2.0.0
910

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

+14-4
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,7 @@ internal class CodeWriter(
405405
implicitModifiers = if (omitImplicitModifiers) emptySet() else setOf(KModifier.PUBLIC),
406406
includeKdocTags = true,
407407
)
408+
408409
is TypeAliasSpec -> o.emit(this)
409410
is CodeBlock -> emitCode(o, isConstantContext = isConstantContext)
410411
else -> emit(o.toString())
@@ -751,7 +752,7 @@ internal class CodeWriter(
751752
val imported = mutableMapOf<String, Set<T>>()
752753
forEach { (simpleName, qualifiedNames) ->
753754
val canonicalNamesToQualifiedNames = qualifiedNames.associateBy { it.computeCanonicalName() }
754-
if (canonicalNamesToQualifiedNames.size == 1 && simpleName !in referencedNames) {
755+
if (canonicalNamesToQualifiedNames.size == 1 && simpleName !in referencedNames && simpleName !in imported) {
755756
val canonicalName = canonicalNamesToQualifiedNames.keys.single()
756757
generatedImports[canonicalName] = Import(canonicalName)
757758

@@ -760,7 +761,7 @@ internal class CodeWriter(
760761
// functions declared in the same package. In these cases, a single import will suffice for all of them.
761762
imported[simpleName] = qualifiedNames
762763
} else {
763-
generateImportAliases(simpleName, canonicalNamesToQualifiedNames, capitalizeAliases)
764+
generateImportAliases(simpleName, canonicalNamesToQualifiedNames, capitalizeAliases, imported.keys)
764765
.onEach { (a, qualifiedName) ->
765766
val alias = a.escapeAsAlias()
766767
val canonicalName = qualifiedName.computeCanonicalName()
@@ -777,24 +778,33 @@ internal class CodeWriter(
777778
simpleName: String,
778779
canonicalNamesToQualifiedNames: Map<String, T>,
779780
capitalizeAliases: Boolean,
781+
imported: Set<String>,
780782
): List<Pair<String, T>> {
781783
val canonicalNameSegmentsToQualifiedNames = canonicalNamesToQualifiedNames.mapKeys { (canonicalName, _) ->
782784
canonicalName.split('.')
783785
.dropLast(1) // Last segment of the canonical name is the simple name, drop it to avoid repetition.
784786
.filter { it != "Companion" }
785787
.map { it.replaceFirstChar(Char::uppercaseChar) }
786788
}
789+
val minimumNumberOfSegments = canonicalNameSegmentsToQualifiedNames.keys.minOf { it.size }
787790
val aliasNames = mutableMapOf<String, T>()
788791
var segmentsToUse = 0
789792
// Iterate until we have unique aliases for all names.
790-
while (aliasNames.size != canonicalNamesToQualifiedNames.size) {
793+
outer@ while (aliasNames.size != canonicalNamesToQualifiedNames.size) {
791794
segmentsToUse += 1
792795
aliasNames.clear()
793796
for ((segments, qualifiedName) in canonicalNameSegmentsToQualifiedNames) {
794797
val aliasPrefix = segments.takeLast(min(segmentsToUse, segments.size))
795798
.joinToString(separator = "")
796799
.replaceFirstChar { if (!capitalizeAliases) it.lowercaseChar() else it }
797-
val aliasName = aliasPrefix + simpleName.replaceFirstChar(Char::uppercaseChar)
800+
val aliasSuffix = "_".repeat(segmentsToUse - segments.size)
801+
val aliasName = aliasPrefix + simpleName.replaceFirstChar(Char::uppercaseChar) + aliasSuffix
802+
// If this name has already been imported (e.g. a regular import already exists with this name),
803+
// continue trying with a greater number of segments.
804+
if (aliasName in imported) {
805+
aliasNames.clear()
806+
continue@outer
807+
}
798808
aliasNames[aliasName] = qualifiedName
799809
}
800810
}

kotlinpoet/src/jvmTest/kotlin/com/squareup/kotlinpoet/KotlinPoetTest.kt

+107
Original file line numberDiff line numberDiff line change
@@ -1489,4 +1489,111 @@ class KotlinPoetTest {
14891489
""".trimMargin(),
14901490
)
14911491
}
1492+
1493+
// https://github.com/square/kotlinpoet/issues/2046
1494+
@Test fun importAliasCollisionWithRegularImportWhenAliasesComputedFirst() {
1495+
val class1 = ClassName("com.squareup.taco", "Meal")
1496+
val class2 = ClassName("com.squareup.burrito", "Meal")
1497+
val class3 = ClassName("com.squareup.meal", "TacoMeal")
1498+
val source = FileSpec.builder("com.squareup.food", "Food")
1499+
.addType(
1500+
TypeSpec.classBuilder("Food")
1501+
.addProperty("tacoPackageMeal", class1)
1502+
.addProperty("burritoPackageMeal", class2)
1503+
.addProperty("mealPackageTacoMeal", class3)
1504+
.build(),
1505+
)
1506+
.build()
1507+
assertThat(source.toString()).isEqualTo(
1508+
"""
1509+
|package com.squareup.food
1510+
|
1511+
|import com.squareup.burrito.Meal as BurritoMeal
1512+
|import com.squareup.meal.TacoMeal as MealTacoMeal
1513+
|import com.squareup.taco.Meal as TacoMeal
1514+
|
1515+
|public class Food {
1516+
| public val tacoPackageMeal: TacoMeal
1517+
|
1518+
| public val burritoPackageMeal: BurritoMeal
1519+
|
1520+
| public val mealPackageTacoMeal: MealTacoMeal
1521+
|}
1522+
|
1523+
""".trimMargin(),
1524+
)
1525+
}
1526+
1527+
// https://github.com/square/kotlinpoet/issues/2046
1528+
@Test fun importAliasCollisionWithImportWhenRegularImportComputedFirst() {
1529+
val class1 = ClassName("com.squareup.taco", "Meal")
1530+
val class2 = ClassName("com.squareup.burrito", "Meal")
1531+
val class3 = ClassName("com.squareup.meal", "TacoMeal")
1532+
val source = FileSpec.builder("com.squareup.food", "Food")
1533+
.addType(
1534+
TypeSpec.classBuilder("Food")
1535+
.addProperty("mealPackageTacoMeal", class3)
1536+
.addProperty("tacoPackageMeal", class1)
1537+
.addProperty("burritoPackageMeal", class2)
1538+
.build(),
1539+
)
1540+
.build()
1541+
assertThat(source.toString()).isEqualTo(
1542+
"""
1543+
|package com.squareup.food
1544+
|
1545+
|import com.squareup.meal.TacoMeal
1546+
|import com.squareup.burrito.Meal as SquareupBurritoMeal
1547+
|import com.squareup.taco.Meal as SquareupTacoMeal
1548+
|
1549+
|public class Food {
1550+
| public val mealPackageTacoMeal: TacoMeal
1551+
|
1552+
| public val tacoPackageMeal: SquareupTacoMeal
1553+
|
1554+
| public val burritoPackageMeal: SquareupBurritoMeal
1555+
|}
1556+
|
1557+
""".trimMargin(),
1558+
)
1559+
}
1560+
1561+
// https://github.com/square/kotlinpoet/issues/2046
1562+
@Test fun importAliasCollisionWithImportWhenWeRunOutOfSegments() {
1563+
val class0 = ClassName("squareup", "SquareupTacoMeal")
1564+
val class1 = ClassName("squareup.taco", "Meal")
1565+
val class2 = ClassName("squareup.burrito", "Meal")
1566+
val class3 = ClassName("squareup", "TacoMeal")
1567+
val source = FileSpec.builder("squareup.food", "Food")
1568+
.addType(
1569+
TypeSpec.classBuilder("Food")
1570+
.addProperty("noMoreSegments", class0)
1571+
.addProperty("mealPackageTacoMeal", class3)
1572+
.addProperty("tacoPackageMeal", class1)
1573+
.addProperty("burritoPackageMeal", class2)
1574+
.build(),
1575+
)
1576+
.build()
1577+
assertThat(source.toString()).isEqualTo(
1578+
"""
1579+
|package squareup.food
1580+
|
1581+
|import squareup.SquareupTacoMeal
1582+
|import squareup.TacoMeal
1583+
|import squareup.burrito.Meal as SquareupBurritoMeal_
1584+
|import squareup.taco.Meal as SquareupTacoMeal_
1585+
|
1586+
|public class Food {
1587+
| public val noMoreSegments: SquareupTacoMeal
1588+
|
1589+
| public val mealPackageTacoMeal: TacoMeal
1590+
|
1591+
| public val tacoPackageMeal: SquareupTacoMeal_
1592+
|
1593+
| public val burritoPackageMeal: SquareupBurritoMeal_
1594+
|}
1595+
|
1596+
""".trimMargin(),
1597+
)
1598+
}
14921599
}

0 commit comments

Comments
 (0)