| /* |
| * Copyright 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.google.devsite.renderer.converters |
| |
| import com.google.devsite.capitalize |
| import com.google.devsite.className |
| import com.google.devsite.not |
| import com.google.devsite.renderer.Language |
| import com.google.devsite.renderer.converters.Memoizers.isFromJavaMap |
| import com.google.devsite.renderer.impl.ClassGraph |
| import com.google.devsite.startsWithAnyOf |
| import org.jetbrains.dokka.DokkaConfiguration |
| import org.jetbrains.dokka.base.signatures.KotlinSignatureUtils.driOrNull |
| import org.jetbrains.dokka.base.transformers.documentables.isException |
| import org.jetbrains.dokka.links.DRI |
| import org.jetbrains.dokka.links.PointingToDeclaration |
| import org.jetbrains.dokka.model.AdditionalModifiers |
| import org.jetbrains.dokka.model.AnnotationTarget |
| import org.jetbrains.dokka.model.Annotations |
| import org.jetbrains.dokka.model.BooleanConstant |
| import org.jetbrains.dokka.model.ComplexExpression |
| import org.jetbrains.dokka.model.DAnnotation |
| import org.jetbrains.dokka.model.DClass |
| import org.jetbrains.dokka.model.DClasslike |
| import org.jetbrains.dokka.model.DEnum |
| import org.jetbrains.dokka.model.DEnumEntry |
| import org.jetbrains.dokka.model.DFunction |
| import org.jetbrains.dokka.model.DInterface |
| import org.jetbrains.dokka.model.DObject |
| import org.jetbrains.dokka.model.DParameter |
| import org.jetbrains.dokka.model.DProperty |
| import org.jetbrains.dokka.model.DTypeAlias |
| import org.jetbrains.dokka.model.Documentable |
| import org.jetbrains.dokka.model.DoubleConstant |
| import org.jetbrains.dokka.model.Expression |
| import org.jetbrains.dokka.model.ExtraModifiers |
| import org.jetbrains.dokka.model.FloatConstant |
| import org.jetbrains.dokka.model.IntegerConstant |
| import org.jetbrains.dokka.model.KotlinModifier |
| import org.jetbrains.dokka.model.KotlinVisibility |
| import org.jetbrains.dokka.model.Modifier |
| import org.jetbrains.dokka.model.SourceSetDependent |
| import org.jetbrains.dokka.model.StringConstant |
| import org.jetbrains.dokka.model.UnresolvedBound |
| import org.jetbrains.dokka.model.Visibility |
| import org.jetbrains.dokka.model.WithAbstraction |
| import org.jetbrains.dokka.model.WithChildren |
| import org.jetbrains.dokka.model.WithGenerics |
| import org.jetbrains.dokka.model.WithSources |
| import org.jetbrains.dokka.model.WithSupertypes |
| import org.jetbrains.dokka.model.WithVisibility |
| import org.jetbrains.dokka.model.doc.Description |
| import org.jetbrains.dokka.model.doc.DocumentationNode |
| import org.jetbrains.dokka.model.doc.Param |
| import org.jetbrains.dokka.model.doc.Property |
| import org.jetbrains.dokka.model.isJvmName |
| import org.jetbrains.dokka.model.properties.ExtraProperty |
| import org.jetbrains.dokka.model.properties.PropertyContainer |
| import org.jetbrains.dokka.model.properties.WithExtraProperties |
| import java.util.concurrent.ConcurrentHashMap |
| import kotlin.reflect.jvm.internal.impl.builtins.jvm.JavaToKotlinClassMap |
| import kotlin.reflect.jvm.internal.impl.name.ClassId |
| import kotlin.reflect.jvm.internal.impl.name.FqName |
| import kotlin.reflect.jvm.jvmName |
| |
| /** For use when generating error messages. Is slow. */ |
| internal fun <T> T.getErrorLocation( |
| sourceSet: DokkaConfiguration.DokkaSourceSet = getExpectOrCommonSourceSet(), |
| ): String where T : WithSources, T : Documentable { |
| val sourceFilePath: String = this.sources[sourceSet]!!.path |
| if (".tmp" in sourceFilePath) return "Error occurred in an unreadable temporary file!" |
| val index = this.sources[sourceSet]!!.computeLineNumber() |
| if (index != null) return "$sourceFilePath:$index" |
| // If this happens, we should probably file an upstream bug. |
| return "$sourceFilePath:UnknownLine" |
| } |
| |
| @JvmName("This is internal and will never be used from JVM") |
| internal fun Documentable.getErrorLocation() = if (this is WithSources) { |
| this.getErrorLocation() |
| } else { |
| "File location could not be determined." |
| } |
| |
| /** Recursively expands all children. */ |
| internal val <T> WithChildren<T>.explodedChildren: List<T> |
| get() = children + children.filterIsInstance<WithChildren<T>>().flatMap { it.explodedChildren } |
| |
| /** |
| * Returns the type's name. Do not use [Documentable.name] as it won't include the outer class. |
| */ |
| internal fun DClasslike.name() = dri.classNames!! |
| |
| internal fun DClasslike.generics() = (this as? WithGenerics)?.generics ?: emptyList() |
| |
| internal fun DClasslike.hasSupertypes(classGraph: ClassGraph) = |
| if (this !is WithSupertypes) { |
| false |
| } else { |
| classGraph.getValue(dri).superClasses.isNotEmpty() || |
| classGraph.getValue(dri).interfaces.isNotEmpty() |
| } |
| |
| internal fun DClasslike.packageName() = dri.packageName!! |
| |
| private val baseClasses = listOf( |
| "kotlin.Any", |
| "java.lang.Object", |
| "kotlin.Enum", |
| "java.lang.Enum", |
| "java.lang.annotation.Annotation", |
| ) |
| |
| /** |
| * Returns true if this dri is from a build in base class like Any, Object, Enum, Annotation |
| */ |
| internal fun DRI.isFromBaseClass(): Boolean { |
| val classAndPackage = packageName?.plus(".").plus(classNames) |
| return baseClasses.contains(classAndPackage) |
| } |
| |
| private object Memoizers { |
| val isFromJavaMap: ConcurrentHashMap<Hashable, Boolean> = |
| ConcurrentHashMap<Hashable, Boolean>() |
| } |
| |
| /** go/dokka-upstream-bug/2620. Because Documentables aren't remotely efficiently hashable. */ |
| internal data class Hashable( |
| val clazz: Class<out WithSources>, |
| val isSynthetic: Boolean?, |
| val dri: DRI?, |
| val visibility: Collection<Visibility>, |
| val modifiers: Collection<Modifier>, |
| val isPsi: Boolean?, |
| ) |
| |
| // TODO(improve isFromJava b/328044424), TODO(improve documentable hashability b/232944038) |
| private fun WithSources.toHashable() = Hashable( |
| clazz = this::class.java, |
| isSynthetic = (this as? DClasslike)?.isSynthetic, |
| dri = if (this is Documentable) this.dri else null, |
| visibility = if (this is WithVisibility) this.visibility.values else emptyList(), |
| modifiers = if (this is WithAbstraction) this.modifier.values else emptyList(), |
| isPsi = this.sources.entries.singleOrNull()?.let { "Psi" in (it.value::class).jvmName }, |
| ) |
| |
| /** |
| * Infer whether this Documentable is from java source, |
| * and thus whether it's nullable if not annotated. |
| * Memoized. |
| */ |
| internal fun WithSources.isFromJava() = this.toHashable().isFromJava() |
| private fun Hashable.isFromJava() = |
| isFromJavaMap.getOrPut(this) { |
| if (isSynthetic == true) { |
| false |
| } else if (visibility.isNotEmpty()) { |
| !visibility.any { it is KotlinVisibility } |
| } else if (modifiers.isNotEmpty()) { |
| !modifiers.any { it is KotlinModifier } |
| } else isPsi == true |
| } |
| |
| // `expect`s cannot be `lateinit`, and `actual`s cannot either because they must match modifiers |
| internal fun DProperty.isLateinit(): Boolean = |
| "lateinit" in modifiers(getExpectOrCommonSourceSet()) |
| |
| internal fun Documentable.isJavaStaticMethod() = this is DFunction && isStaticAnnotated() |
| |
| internal fun Documentable.isStaticAnnotated() = |
| annotations(getAsJavaSourceSet()).any { it.dri == JvmStatic.dri } |
| |
| private val INTERNAL_PACKAGES = listOf("java", "Kotlin", "google", "android") |
| internal fun DRI.isExternal() = !packageName?.startsWithAnyOf(INTERNAL_PACKAGES) ?: true |
| |
| /** |
| * @param displayLanguage the Language of the docs this Documentable will be displayed in |
| * @return the String name that represents this type when displayed |
| */ |
| fun Documentable.stringForType(displayLanguage: Language): String = when (this) { |
| is DClass -> "class" |
| is DInterface -> "interface" |
| is DEnum -> "enum" |
| is DEnumEntry -> "enum value" |
| is DAnnotation -> "annotation" |
| is DFunction -> when (displayLanguage) { |
| Language.JAVA -> "method" |
| Language.KOTLIN -> "function" |
| } |
| is DProperty -> when (displayLanguage) { |
| Language.JAVA -> "field" |
| Language.KOTLIN -> "property" |
| } |
| is DObject -> when (displayLanguage) { |
| Language.KOTLIN -> "object" |
| Language.JAVA -> "class" |
| } |
| is DTypeAlias -> "type alias" |
| is DParameter -> "parameter" |
| else -> error("Unsupported type: $this") |
| } |
| |
| /** |
| * Returns if a class is an Exception or not |
| * isException, the built-in method in Dokka, only considers its supertype, so we also look for |
| * functions that are Throwable |
| * https://github.com/Kotlin/dokka/issues/1557 |
| */ |
| val DClass.isExceptionClass: Boolean |
| get() = isException || functions.any { function -> function.dri.classNames == "Throwable" } |
| |
| /** |
| * Returns whether the java class was synthetically generated from a Kotlin extension function class |
| * Assumes the class this is being called on is Java. |
| */ |
| val DClasslike.isSynthetic: Boolean |
| get() = name().endsWith("Kt") || this.jvmFileName() != null |
| |
| /** |
| * Converts a top level function to its representation under a Java synthetic class. |
| * Replaces the dri to point to the synthetic class and applies the static modifier. |
| * This doesn't change the name to the [jvmName], because that's handled later in |
| * [ClasslikeDocumentableConverter]. |
| */ |
| internal fun DFunction.withJavaSynthetic(syntheticClassName: String): DFunction = |
| copy( |
| // this needs to be the dri IN the synthetic class |
| dri = dri.copy(classNames = syntheticClassName), |
| // put the static annotation on functions in the synthetic class |
| extra = extra.addModifier(ExtraModifiers.JavaOnlyModifiers.Static, sourceSets), |
| ) |
| |
| /** |
| * Converts a top level property to its representation under a Java synthetic class. |
| * Replaces the dri to point to the synthetic class and applies the static modifier, as well as |
| * converting the property's accessors. |
| * This doesn't change the name to the [jvmName], because that's handled later in |
| * [ClasslikeDocumentableConverter]. |
| */ |
| internal fun DProperty.withJavaSynthetic(syntheticClassName: String): DProperty = |
| copy( |
| // this needs to be the dri IN the synthetic class |
| dri = dri.copy(classNames = syntheticClassName), |
| // put the static annotation on properties in the synthetic class |
| extra = extra.addModifier(ExtraModifiers.JavaOnlyModifiers.Static, sourceSets), |
| // convert the getter and setter as well |
| getter = getter?.withJavaSynthetic(syntheticClassName), |
| setter = setter?.withJavaSynthetic(syntheticClassName), |
| ) |
| |
| /** |
| * Adds the [newModifier] to the [PropertyContainer] for each of the [sourceSets] provided. |
| */ |
| internal fun <T> PropertyContainer<T>.addModifier( |
| newModifier: ExtraModifiers, |
| sourceSets: Set<DokkaConfiguration.DokkaSourceSet>, |
| ): PropertyContainer<T> where T : Documentable { |
| val newModifiers = this.allOfType<AdditionalModifiers>().map { modifiers -> |
| AdditionalModifiers( |
| sourceSets.associateWith { sourceSet -> |
| val previous = modifiers.content[sourceSet] ?: emptySet() |
| previous + newModifier |
| }, |
| ) |
| } |
| return addAll(newModifiers) |
| } |
| |
| /** |
| * Converts a level function to its presentation with JvmName |
| */ |
| fun DFunction.withJvmName(): DFunction { |
| val jvmName = jvmName() ?: return this |
| return copy( |
| name = jvmName, |
| dri = dri.copy(callable = dri.callable?.copy(name = jvmName)), |
| extra = extra.plus(OriginalName(name)), |
| ) |
| } |
| |
| /** An [ExtraProperty] to track the original name of an element renamed with @JvmName. */ |
| internal data class OriginalName(val name: String) : ExtraProperty<DFunction> { |
| object PropertyKey : ExtraProperty.Key<DFunction, OriginalName> |
| |
| override val key: ExtraProperty.Key<DFunction, *> = PropertyKey |
| } |
| |
| internal fun DFunction.matches(other: DFunction): Boolean = |
| this.receiver == other.receiver && |
| this.parameters == other.parameters && |
| this.dri.packageName == other.dri.packageName && |
| this.jvmName() == other.jvmName() |
| |
| /** |
| * [Comparator] which sorts [DFunction] by name, then number of params, params names, and then |
| * source sets if necessary. |
| */ |
| val functionSignatureComparator = compareBy<DFunction>( |
| { it.name }, |
| { it.parameters.size }, |
| { it.signatureAsString() }, |
| { it.sourceSets.joinToString { it.displayName } }, |
| ) |
| |
| /** [Comparator] intended for [DClasslike]s known to be name-unique per-platform. */ |
| val simpleDocumentableComparator = compareBy<Documentable>( |
| { it.dri.fullName }, |
| { it.dri.toString() }, |
| { it.sourceSets.joinToString { it.displayName } }, |
| ) |
| |
| private fun DFunction.signatureAsString() = |
| "$name(${parameters.joinToString(separator = ", ") { it.paramAsString() }})" |
| |
| /** Turn a parameter into a string. Used in sorting functions. */ |
| private fun DParameter.paramAsString() = |
| (name ?: "") + ( // Sorting criterion roughly matches rendered text |
| (type as? UnresolvedBound)?.name // driOrNull doesn't handle UnresolvedBound, sadly. |
| ?: type.driOrNull?.let { "${it.classNames} $it" } // by classname, then full dri |
| ) |
| |
| /** |
| * Returns the value of the @JvmName for this function if one exists or null |
| * This is only relevant for as-Java docs |
| */ |
| fun Documentable.jvmName(): String? { |
| return annotations(getAsJavaSourceSet()).firstOrNull { it.isJvmName() }?.nameAsString() |
| } |
| |
| /** |
| * Returns the value of the file:@JvmName if one exists or null |
| */ |
| fun <T> T.jvmFileName() where T : WithSources, T : Documentable = |
| fileLevelAnnotations(getAsJavaSourceSet()).firstOrNull { it.isJvmName() }?.nameAsString() |
| |
| /** |
| * Returns the value of the file:@JvmName if one exists or null |
| */ |
| fun <T> nameForSyntheticClass(entry: T) where T : WithSources, T : Documentable = |
| entry.jvmFileName() ?: entry.sources.let { |
| it.entries.first().value.path.split("/").last().split(".").first() + "Kt" |
| } |
| |
| fun DFunction.driForSyntheticClass() = DRI(dri.packageName, nameForSyntheticClass(this)) |
| |
| /** |
| * Filters out elements that are annotated with @JvmSynthetic |
| */ |
| fun <T : Documentable> List<T>.filterOutJvmSynthetic(): List<T> = this.filterNot { elem -> |
| elem.annotations(elem.getAsJavaSourceSet()).any { it.dri.classNames.equals("JvmSynthetic") } |
| } |
| |
| /** Adds an annotation to a Documentable. Often used for injecting e.g. @JvmStatic. */ |
| internal fun <T> T.addAnnotation(newA: Annotations.Annotation): T |
| where T : Documentable, T : WithExtraProperties<T> { |
| return withNewExtras(extra.addAnnotation(newA, sourceSets)) |
| } |
| |
| /** |
| * Adds [newA] as an annotation for each source set of [sourceSets] (even if the source set did not |
| * previously have any annotations associated with it). |
| */ |
| internal fun <T> PropertyContainer<T>.addAnnotation( |
| newA: Annotations.Annotation, |
| sourceSets: Set<DokkaConfiguration.DokkaSourceSet>, |
| ): PropertyContainer<T> |
| where T : AnnotationTarget { |
| val newAnnotations = this[Annotations]?.let { annotations -> |
| val newDirectAnnotations = |
| sourceSets.associateWith { |
| val previous = annotations.directAnnotations[it] ?: emptyList() |
| previous + newA |
| } |
| annotations.copy(myContent = newDirectAnnotations + annotations.fileLevelAnnotations) |
| } |
| val extraWithoutAnnotations: PropertyContainer<T> = minus(Annotations) |
| |
| return extraWithoutAnnotations.addAll(listOfNotNull(newAnnotations)) |
| } |
| |
| internal val JvmStatic = Annotations.Annotation(DRI("kotlin.jvm", "JvmStatic"), params = emptyMap()) |
| |
| internal fun String?.orNull() = if (this == "") null else this |
| |
| internal val DRI.fullName: String get() = (packageName.orNull()?.let { "$it." }) + |
| (classNames ?: "") |
| |
| internal fun DRI.possiblyConvertMappedType(displayLanguage: Language) = |
| when (displayLanguage) { |
| Language.JAVA -> possiblyAsJava() |
| Language.KOTLIN -> possiblyAsKotlin() |
| } |
| |
| /** |
| * Uses the JavaToKotlinClassMap to possibly convert a dri to its Java equivalent |
| * https://kotlinlang.org/docs/reference/java-interop.html#mapped-types |
| */ |
| internal fun DRI.possiblyAsJava(): DRI { |
| val fullyQualifiedName = packageName?.let { "$it." } + classNames |
| // Use the fully qualified name to look up the class in the map |
| return JavaToKotlinClassMap.INSTANCE.mapKotlinToJava(FqName(fullyQualifiedName).toUnsafe()) |
| ?.let { |
| DRI( |
| packageName = it.packageFqName.asString(), |
| classNames = it.classNames(), |
| callable = this.callable, |
| extra = null, |
| target = PointingToDeclaration, |
| ) |
| } ?: this |
| } |
| |
| /** |
| * Uses the JavaToKotlinClassMap to possibly convert a dri to its Kotlin equivalent |
| * https://kotlinlang.org/docs/reference/java-interop.html#mapped-types |
| */ |
| internal fun DRI.possiblyAsKotlin(): DRI { |
| val fullyQualifiedName = packageName?.let { "$it." } + classNames |
| // Use the fully qualified name to look up the class in the map |
| return JavaToKotlinClassMap.INSTANCE.mapJavaToKotlin(FqName(fullyQualifiedName))?.let { |
| DRI( |
| packageName = it.packageFqName.asString(), |
| classNames = it.classNames(), |
| callable = this.callable, |
| extra = null, |
| target = PointingToDeclaration, |
| ) |
| } ?: this |
| } |
| |
| private fun ClassId.classNames(): String = |
| generateSequence(this) { it.outerClassId } |
| .map { it.shortClassName.identifier } |
| .reduce { acc, pref -> "$pref.$acc" } |
| |
| /** |
| * Returns the string representation of an [Expression] value, mostly relying on the toString |
| * implementation of that type, but wrapping [String] values in quotes for presentation, and |
| * stripping trailing zeros and appending 'f' or 'd' to floats and doubles respectively. |
| */ |
| fun Expression.getValue(): String? = when (this) { |
| is ComplexExpression -> value |
| is IntegerConstant -> "$value" |
| is BooleanConstant -> "$value" |
| is StringConstant -> "\"$value\"" |
| is DoubleConstant -> "$value" |
| is FloatConstant -> "${value}f" |
| else -> null |
| } |
| |
| /** |
| * Whether the property should show up as a class/object property in the Java docs. |
| * Properties that don't fall into one of these categories only appear as accessors. |
| */ |
| fun DProperty.isPropertyInJava() = isJvmField() || isFromJava() || isLateinit() |
| |
| /** Used for when isPropertyInJava's isFromJava is inaccurate. Without accessors, it's a property */ |
| fun DProperty.hasAnAccessor() = getter != null || setter != null |
| |
| /** |
| * Whether the property (belonging to an object) needs to be hoisted to the containing class in the |
| * Java docs. |
| */ |
| fun DProperty.objectPropertyHoistedInJava() = |
| isJvmFieldAnnotated() || isLateinit() || isConstant() |
| |
| /** |
| * If the function has a receiver, converts it to a parameter named "receiver". |
| * Otherwise, returns the original function. |
| */ |
| fun DFunction.convertReceiverForJava() = |
| receiver?.let { receiver -> |
| copy( |
| parameters = listOf(receiver.copy(name = "receiver")) + parameters, |
| receiver = null, |
| ) |
| } ?: this |
| |
| /** |
| * Returns property getters / setters. Includes generated accessors for Kotlin properties, fixing |
| * their names and adding static annotations as needed. |
| */ |
| fun List<DProperty>.gettersAndSetters(): List<DFunction> { |
| return filter { |
| // JvmFields are only accessed as fields, not through accessors |
| !it.isJvmField() |
| }.flatMap { |
| listOf(it to it.getter, it to it.setter) |
| }.mapNotNull { |
| var (property, func) = it |
| // Static properties should also have static accessors |
| if (property.isStaticAnnotated()) { |
| func = func?.addAnnotation(JvmStatic) |
| } |
| val callableName = func?.dri?.callable?.name ?: "" |
| func = if (callableName.startsWith("<get-")) { |
| func!!.fixSyntheticAccessor(property, getter = true) |
| } else if (callableName.startsWith("<set-")) { |
| func!!.fixSyntheticAccessor(property, getter = false) |
| } else { |
| func |
| } |
| func?.withNewExtras(func.extra.plus(SourceProperty(property))) |
| } |
| } |
| |
| /** An [ExtraProperty] for generated accessors to link back to the property they came from. */ |
| internal data class SourceProperty(val property: DProperty) : ExtraProperty<DFunction> { |
| object PropertyKey : ExtraProperty.Key<DFunction, SourceProperty> |
| |
| override val key: ExtraProperty.Key<DFunction, *> = PropertyKey |
| } |
| |
| /** |
| * Fixes issues with the given synthetic accessor [forProperty] to be documented in Java, using |
| * [DRI.withFixedName] and [correctTagsInAccessorDocs]. If [getter] is false, the function is a setter. |
| */ |
| private fun DFunction.fixSyntheticAccessor(forProperty: DProperty, getter: Boolean) = copy( |
| dri = dri.withFixedName(getter), |
| documentation = injectPropertyDocsToAccessor(this, forProperty) |
| .correctTagsInAccessorDocs(forProperty.name, getter), |
| ) |
| |
| /** |
| * Fixes the name of synthetic accessors, e.g. <get-bar> to getBar |
| * If [getter] is true, it is assumed the function name starts with "<get-", otherwise it is |
| * assumes it starts with "<set-". |
| */ |
| private fun DRI.withFixedName(getter: Boolean) = copy( |
| callable = callable!!.copy( |
| name = fixCallableName(callable?.name ?: "", getter), |
| ), |
| ) |
| |
| private fun fixCallableName(badName: String, getter: Boolean) = |
| if (getter) { |
| "get" + badName.removePrefix("<get-").removeSuffix(">").capitalize() |
| } else { |
| "set" + badName.removePrefix("<set-").removeSuffix(">").capitalize() |
| } |
| |
| /** |
| * For each sourceset, if the property has docs but the accessor does not, injects the property docs |
| * to the accessor. This prevents property descriptions from being lost in the as-Java docs. |
| */ |
| private fun injectPropertyDocsToAccessor( |
| accessor: DFunction, |
| property: DProperty, |
| ): SourceSetDependent<DocumentationNode> { |
| val accessorDocs = accessor.documentation.toMutableMap() |
| property.documentation.forEach { (sourceSet, propertyDocs) -> |
| if (!accessorDocs.containsKey(sourceSet)) { |
| accessorDocs[sourceSet] = propertyDocs |
| } |
| } |
| return accessorDocs |
| } |
| |
| /** |
| * This function converts @param and @property tags in synthetic accessor docs, based on whether the |
| * accessor is a [getter] or setter (which is assumed if [getter] is false). |
| * There are a few separate issues here: |
| * |
| * For getters: Some properties are documented with @param tags in a class constructor. When |
| * converted to synthetic accessors, the @param tag stays with the function. However, a synthetic |
| * getter has no parameters, so Dackka won't know how to interpret the tag. In this case, all |
| * [Param] wrappers are removed from the docs, with the text inside left as a [Description]. |
| * |
| * For setters: The generated setter for a Kotlin property will have one parameter, named |
| * [propertyName]. This is true in Dackka even if the setter was explicitly defined with a different |
| * parameter name (b/268236485). To work around this, any [Param] tags for setters are set to have |
| * [propertyName] as the name of the parameter. |
| * |
| * For @property tags, they can be unwrapped for both getters and setters to just descriptions. |
| */ |
| private fun SourceSetDependent<DocumentationNode>.correctTagsInAccessorDocs( |
| propertyName: String, |
| getter: Boolean, |
| ): SourceSetDependent<DocumentationNode> { |
| return this.mapValues { entry -> |
| DocumentationNode( |
| entry.value.children.map { |
| when (it) { |
| is Param -> { |
| // Getters should have no params, move text out into a description |
| if (getter) { |
| Description(it.root) |
| } // Setters have one param, named the same as the property (b/268236485) |
| else { |
| Param(it.root, propertyName) |
| } |
| } |
| is Property -> Description(it.root) |
| else -> it |
| } |
| }, |
| ) |
| } |
| } |
| |
| private fun DRI.isAtJvmField(): Boolean = packageName == "kotlin.jvm" && classNames == "JvmField" |
| |
| private fun Annotations.Annotation.isAtJvmField(): Boolean = dri.isAtJvmField() |
| internal fun DProperty.isJvmFieldAnnotated() = |
| annotations(getAsJavaSourceSet()).any { it.isAtJvmField() } |
| |
| /** |
| * Returns whether property is annotated as @JvmField |
| */ |
| fun DProperty.isJvmField(): Boolean { |
| return isJvmFieldAnnotated() || isConstant() |
| } |
| |
| internal fun List<DFunction>.names() = map { it.name } |
| |
| @JvmName("internalAndThusKotlinOnly") |
| internal fun List<DParameter>.names() = map { it.name } |
| |
| @JvmName("internalAndThusKotlinOnlyAlso") |
| internal fun List<DProperty>.names() = map { it.name } |
| |
| /** |
| * Attempts to get the `expect` source set for a Documentable, or wherever else a sourceset-agnostic |
| * version of the function is defined. This is because we currently pull e.g. description |
| * documentation from this kind of source set, and ignore e.g. `actual`s' documentation. |
| * |
| * Single-platform functions, regardless of function, will return sourceSets.singleOrNull |
| * expect/actual classes and elements will return expectPresentInSet |
| * There are fallbacks for some odd cases following that. |
| */ |
| internal fun Documentable.getExpectOrCommonSourceSet() = |
| sourceSets.singleOrNull() |
| ?: expectPresentInSet |
| ?: sourceSets.singleOrNull { it.analysisPlatform == org.jetbrains.dokka.Platform.common } |
| ?: sourceSets.singleOrNull { it.displayName.equalsPossiblyWithMain("common") } |
| // b/254490320. The one case we expect to see this is package descriptions. |
| // Below is a weak fallback for libraries with no common sourceSet. |
| ?: sourceSets.singleOrNull { it.displayName.equalsIgnoreCase("jvmMain") } |
| ?: sourceSets.singleOrNull { it.displayName.equalsIgnoreCase("androidMain") } |
| ?: sourceSets.singleOrNull { it.displayName.equalsIgnoreCase("desktopMain") } |
| ?: sourceSets.firstOrNull() |
| ?: throw RuntimeException("No sourceSets present for ${this.className} $dri") |
| |
| private fun String.equalsPossiblyWithMain(other: String) = |
| this.equalsIgnoreCase(other) || this.equalsIgnoreCase(other + "main") |
| private fun String.equalsIgnoreCase(other: String) = this.uppercase() == other.uppercase() |
| |
| /** |
| * Used in as-Java docs and when getting JVM-exclusive annotations. |
| * |
| * Returns null if the documentable is not accessible in a jvm context. |
| * |
| * Uses common sourceSet as a fallback, because poorly-implemented JVM-exclusive annotations could |
| * be there, and because java code can access Kotlin common code. |
| */ |
| internal fun Documentable.getAsJavaSourceSet() = |
| sourceSets.singleOrNull { it.analysisPlatform == org.jetbrains.dokka.Platform.jvm } |
| ?: sourceSets.singleOrNull { it.analysisPlatform == org.jetbrains.dokka.Platform.common } |