Skip to content

Commit

Permalink
Sort divergent elements deterministically (#2846)
Browse files Browse the repository at this point in the history
Fixes #2784
  • Loading branch information
IgnatBeresnev authored Feb 10, 2023
1 parent f09b149 commit e2351eb
Show file tree
Hide file tree
Showing 2 changed files with 316 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -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(),
Expand All @@ -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<Documentable>): List<Documentable> =
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
Expand Down Expand Up @@ -611,6 +633,19 @@ internal val Documentable.customTags: Map<String, SourceSetDependent<CustomTagWr
private val Documentable.hasSeparatePage: Boolean
get() = this !is DTypeAlias

/**
* @see DefaultPageCreator.sortDivergentElementsDeterministically for usage
*/
private val divergentDocumentableComparator =
compareBy<Documentable, String?>(nullsLast()) { it.dri.packageName }
.thenBy(nullsFirst()) { it.dri.classNames } // nullsFirst for top level to be first
.thenBy(
nullsLast(
compareBy<Callable> { it.params.size }
.thenBy { it.signature() }
)
) { it.dri.callable }

@Suppress("UNCHECKED_CAST")
private fun <T : Documentable> T.nameAfterClash(): String =
((this as? WithExtraProperties<out Documentable>)?.extra?.get(DriClashAwareName)?.value ?: name).orEmpty()
Expand All @@ -624,4 +659,4 @@ internal inline fun <reified T : NamedTagWrapper> GroupedTags.withTypeNamed(): M
(this[T::class] as List<Pair<DokkaSourceSet, T>>?)
?.groupByTo(linkedMapOf()) { it.second.name }
?.mapValues { (_, v) -> v.toMap() }
.orEmpty()
.orEmpty()
Loading

0 comments on commit e2351eb

Please sign in to comment.