From e2351ebcb15a745551fef47c707f0e4e65a707cd Mon Sep 17 00:00:00 2001 From: Ignat Beresnev Date: Fri, 10 Feb 2023 15:10:05 +0100 Subject: Sort divergent elements deterministically (#2846) Fixes #2784 --- .../documentables/DefaultPageCreator.kt | 51 ++++++++++++++++++---- 1 file changed, 43 insertions(+), 8 deletions(-) (limited to 'plugins/base/src/main/kotlin') diff --git a/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt b/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt index b8bf87a4..25f3c450 100644 --- a/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt +++ b/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt @@ -9,6 +9,7 @@ import org.jetbrains.dokka.base.transformers.documentables.ClashingDriIdentifier import org.jetbrains.dokka.base.transformers.pages.comments.CommentsToContentConverter import org.jetbrains.dokka.base.transformers.pages.tags.CustomTagContentProvider import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder.DocumentableContentBuilder +import org.jetbrains.dokka.links.Callable import org.jetbrains.dokka.links.DRI import org.jetbrains.dokka.model.* import org.jetbrains.dokka.model.doc.* @@ -524,30 +525,31 @@ open class DefaultPageCreator( .groupBy { it.name } // This groupBy should probably use LocationProvider // This hacks displaying actual typealias signatures along classlike ones .mapValues { if (it.value.any { it is DClasslike }) it.value.filter { it !is DTypeAlias } else it.value } - .toSortedMap(compareBy(nullsLast(String.CASE_INSENSITIVE_ORDER)) { it }) + .entries.sortedBy { it.key } .forEach { (elementName, elements) -> // This groupBy should probably use LocationProvider + val sortedElements = sortDivergentElementsDeterministically(elements) row( - dri = elements.map { it.dri }.toSet(), - sourceSets = elements.flatMap { it.sourceSets }.toSet(), + dri = sortedElements.map { it.dri }.toSet(), + sourceSets = sortedElements.flatMap { it.sourceSets }.toSet(), kind = kind, styles = emptySet(), extra = elementName?.let { name -> extra + SymbolAnchorHint(name, kind) } ?: extra ) { link( text = elementName.orEmpty(), - address = elements.first().dri, + address = sortedElements.first().dri, kind = kind, styles = setOf(ContentStyle.RowTitle), - sourceSets = elements.sourceSets.toSet(), + sourceSets = sortedElements.sourceSets.toSet(), extra = extra ) divergentGroup( ContentDivergentGroup.GroupID(name), - elements.map { it.dri }.toSet(), + sortedElements.map { it.dri }.toSet(), kind = kind, extra = extra ) { - elements.map { + sortedElements.map { instance( setOf(it.dri), it.sourceSets.toSet(), @@ -571,6 +573,26 @@ open class DefaultPageCreator( } } + /** + * Divergent elements, such as extensions for the same receiver, can have identical signatures + * if they are declared in different places. If such elements are shown on the same page together, + * they need to be rendered deterministically to have reproducible builds. + * + * For example, you can have three identical extensions, if they are declared as: + * 1) top-level in package A + * 2) top-level in package B + * 3) inside a companion object in package A/B + * + * @see divergentBlock + * + * @param elements can contain types (annotation/class/interface/object/typealias), functions and properties + * @return the original list if it has one or zero elements + */ + private fun sortDivergentElementsDeterministically(elements: List): List = + elements.takeIf { it.size > 1 } // the majority are single-element lists, but no real benchmarks done + ?.sortedWith(divergentDocumentableComparator) + ?: elements + private fun DocumentableContentBuilder.contentForCustomTagsBrief(documentable: Documentable) { val customTags = documentable.customTags if (customTags.isEmpty()) return @@ -611,6 +633,19 @@ internal val Documentable.customTags: Map(nullsLast()) { it.dri.packageName } + .thenBy(nullsFirst()) { it.dri.classNames } // nullsFirst for top level to be first + .thenBy( + nullsLast( + compareBy { it.params.size } + .thenBy { it.signature() } + ) + ) { it.dri.callable } + @Suppress("UNCHECKED_CAST") private fun T.nameAfterClash(): String = ((this as? WithExtraProperties)?.extra?.get(DriClashAwareName)?.value ?: name).orEmpty() @@ -624,4 +659,4 @@ internal inline fun GroupedTags.withTypeNamed(): M (this[T::class] as List>?) ?.groupByTo(linkedMapOf()) { it.second.name } ?.mapValues { (_, v) -> v.toMap() } - .orEmpty() \ No newline at end of file + .orEmpty() -- cgit