| /* |
| * 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.DocsSummaryList |
| import com.google.devsite.LinkDescriptionSummaryList |
| import com.google.devsite.className |
| import com.google.devsite.components.ContextFreeComponent |
| import com.google.devsite.components.DescriptionComponent |
| import com.google.devsite.components.Link |
| import com.google.devsite.components.impl.DefaultAnnotatedLink |
| import com.google.devsite.components.impl.DefaultDescriptionComponent |
| import com.google.devsite.components.impl.DefaultKmpTableRowSummaryItem |
| import com.google.devsite.components.impl.DefaultLink |
| import com.google.devsite.components.impl.DefaultParameterComponent |
| import com.google.devsite.components.impl.DefaultPlatformComponent |
| import com.google.devsite.components.impl.DefaultSummaryList |
| import com.google.devsite.components.impl.DefaultTableRowSummaryItem |
| import com.google.devsite.components.impl.DefaultTableTitle |
| import com.google.devsite.components.impl.DefaultTypeProjectionComponent |
| import com.google.devsite.components.impl.DefaultUnlink |
| import com.google.devsite.components.impl.UndocumentedSymbolDescriptionComponent |
| import com.google.devsite.components.symbols.AnnotatedLink |
| import com.google.devsite.components.symbols.ParameterComponent |
| import com.google.devsite.components.symbols.TypeProjectionComponent |
| import com.google.devsite.components.table.KmpTableRowSummaryItem |
| import com.google.devsite.components.table.SummaryList |
| import com.google.devsite.components.table.TableRowSummaryItem |
| import com.google.devsite.components.table.TableTitle |
| import com.google.devsite.renderer.Language |
| import com.google.devsite.renderer.impl.DocumentablesHolder |
| import com.google.devsite.renderer.impl.paths.FilePathProvider |
| import com.google.devsite.strictSingleOrNull |
| import org.jetbrains.dokka.DokkaConfiguration |
| import org.jetbrains.dokka.links.DRI |
| import org.jetbrains.dokka.model.Annotations |
| import org.jetbrains.dokka.model.Callable |
| import org.jetbrains.dokka.model.DClasslike |
| import org.jetbrains.dokka.model.DFunction |
| import org.jetbrains.dokka.model.DParameter |
| import org.jetbrains.dokka.model.DProperty |
| import org.jetbrains.dokka.model.DTypeAlias |
| import org.jetbrains.dokka.model.DTypeParameter |
| import org.jetbrains.dokka.model.DefinitelyNonNullable |
| import org.jetbrains.dokka.model.Documentable |
| import org.jetbrains.dokka.model.Dynamic |
| import org.jetbrains.dokka.model.JavaObject |
| import org.jetbrains.dokka.model.Nullable |
| import org.jetbrains.dokka.model.PrimitiveJavaType |
| import org.jetbrains.dokka.model.Projection |
| import org.jetbrains.dokka.model.Star |
| import org.jetbrains.dokka.model.StringValue |
| import org.jetbrains.dokka.model.TypeAliased |
| import org.jetbrains.dokka.model.TypeConstructor |
| import org.jetbrains.dokka.model.TypeParameter |
| import org.jetbrains.dokka.model.UnresolvedBound |
| import org.jetbrains.dokka.model.Variance |
| import org.jetbrains.dokka.model.Void |
| import org.jetbrains.dokka.model.WithChildren |
| import org.jetbrains.dokka.model.WithConstructors |
| import org.jetbrains.dokka.model.WithGenerics |
| import org.jetbrains.dokka.model.WithSources |
| import org.jetbrains.dokka.model.doc.Author |
| import org.jetbrains.dokka.model.doc.Constructor |
| import org.jetbrains.dokka.model.doc.CustomTagWrapper |
| import org.jetbrains.dokka.model.doc.Deprecated |
| import org.jetbrains.dokka.model.doc.Description |
| import org.jetbrains.dokka.model.doc.DocTag |
| import org.jetbrains.dokka.model.doc.DocumentationLink |
| import org.jetbrains.dokka.model.doc.NamedTagWrapper |
| import org.jetbrains.dokka.model.doc.P |
| import org.jetbrains.dokka.model.doc.Param |
| import org.jetbrains.dokka.model.doc.Pre |
| import org.jetbrains.dokka.model.doc.Property |
| import org.jetbrains.dokka.model.doc.Receiver |
| import org.jetbrains.dokka.model.doc.Return |
| import org.jetbrains.dokka.model.doc.Sample |
| import org.jetbrains.dokka.model.doc.See |
| import org.jetbrains.dokka.model.doc.Since |
| import org.jetbrains.dokka.model.doc.Suppress |
| import org.jetbrains.dokka.model.doc.TagWrapper |
| import org.jetbrains.dokka.model.doc.Text |
| import org.jetbrains.dokka.model.doc.Throws |
| import org.jetbrains.dokka.model.doc.Version |
| import java.io.File |
| |
| /** Extracts the handwritten documentation from documentables into the correct components. */ |
| internal class DocTagConverter( |
| private val displayLanguage: Language, |
| private val pathProvider: FilePathProvider, |
| private val docsHolder: DocumentablesHolder, |
| private val paramConverter: ParameterDocumentableConverter, |
| private val annotationConverter: AnnotationDocumentableConverter, |
| ) { |
| // We currently assume all sample are in the common sourceSet. TODO KMP samples b/181224204 |
| // private val analysisMap = runBlocking { docsHolder.analysisMap() } |
| |
| /** |
| * @param documentable the documentable we are getting the documentation of |
| * @param deprecationAnnotation the @Deprecated annotation. Is a parameter because for e.g. |
| * getters, the annotation will be propagated manually from some higher element, so we can't |
| * just use documentable.deprecationAnnotation() in such a case. |
| * @return the in-source hand-written element documentation |
| */ |
| fun summaryDescription( |
| documentable: Documentable, |
| deprecationAnnotation: Annotations.Annotation? = documentable.deprecationAnnotation(), |
| ): DescriptionComponent { |
| return deprecationComponent(documentable, summary = true, deprecationAnnotation) |
| ?: documentable.getDescription(summary = true) |
| } |
| |
| /** metadata() can either take a WithSources Documentable, OR an isFromJava boolean */ |
| fun <T> metadata( |
| documentable: T, |
| returnType: TypeProjectionComponent? = null, |
| paramNames: List<String> = emptyList(), |
| deprecationAnnotation: Annotations.Annotation? = documentable.deprecationAnnotation(), |
| ) where T : Documentable, T : WithSources = |
| metadataImpl(documentable, returnType, paramNames, deprecationAnnotation) |
| |
| fun metadata( |
| documentable: Documentable, |
| returnType: TypeProjectionComponent? = null, |
| paramNames: List<String> = emptyList(), |
| deprecationAnnotation: Annotations.Annotation? = documentable.deprecationAnnotation(), |
| isFromJava: Boolean, |
| ) = metadataImpl(documentable, returnType, paramNames, deprecationAnnotation, isFromJava) |
| |
| /** |
| * Returns a breakdown of the different metadata as a deprecation warning, description, and then |
| * separate summaries. Examples include the list of parameters, return type, see also, throws, |
| * etc. |
| */ |
| private fun metadataImpl( |
| documentable: Documentable, |
| returnType: TypeProjectionComponent? = null, |
| paramNames: List<String> = emptyList(), |
| deprecationAnnotation: Annotations.Annotation? = documentable.deprecationAnnotation(), |
| isFromJavaParam: Boolean? = null, |
| ): List<ContextFreeComponent> { |
| val isFromJava = if (isFromJavaParam != null) { |
| isFromJavaParam |
| } else { |
| assert(documentable is WithSources) |
| (documentable as WithSources).isFromJava() |
| } |
| val description = documentable.getDescription(summary = false) |
| val deprecation = deprecationComponent(documentable, summary = false, deprecationAnnotation) |
| val receiverParam = documentable.find<Receiver>()?.let { |
| Param(it.root, "receiver") |
| } |
| val generics = if (documentable is WithGenerics) documentable.generics else emptyList() |
| // Filter out tags which should instead be put into the Description, which is handled in the |
| // getDescription method. |
| val metadataTags = (listOfNotNull(receiverParam) + documentable.tags()) |
| .filter { !it.belongsInDescriptionOf(documentable) } |
| val tagsByType = metadataTags.sortedWith(tagOrder(paramNames)).groupBy { it.javaClass } |
| val tables = tagsByType.mapNotNull { (_, rawTags) -> |
| var tags = handleUpstreamTagDuplication(documentable, rawTags, generics) |
| if (tags.isEmpty()) return@mapNotNull null |
| val firstTag = tags.first() |
| if (documentable is DFunction && firstTag is Param) { |
| val propertyClass = Property::class.java as Class<*> |
| tags = tags + tagsByType[propertyClass].orEmpty() |
| } |
| // We know all the elements in `tags` will be of the same type, so we pick an arbitrary |
| // one to do the switching and then cast the list to its type. |
| @kotlin.Suppress("UNCHECKED_CAST") |
| try { |
| when (firstTag) { |
| is Param -> params( |
| tags as List<NamedTagWrapper>, |
| generics, |
| documentable, |
| isFromJava, |
| documentable.getExpectOrCommonSourceSet(), |
| ) |
| is Return -> returnType(tags as List<Return>, checkNotNull(returnType)) |
| is Throws -> throws(tags as List<Throws>, documentable) |
| is See -> see(tags as List<See>, documentable) |
| is Sample -> null // Samples are handled in the description |
| is Property -> |
| throw RuntimeException("Should have been consumed in description!") |
| is CustomTagWrapper -> docsHolder.printWarningFor( |
| "unrecognized javadoc tag @", |
| documentable, |
| brokenDocTag = firstTag, |
| ).let { null } |
| is Since -> docsHolder.printWarningFor( |
| "unsupported javadoc tag @", |
| documentable, |
| brokenDocTag = firstTag, |
| additionalContext = ". Instead, autogenerate per go/dackka#api-since.", |
| ).let { null } |
| is Constructor -> null // TODO("b/179999964: constructor") |
| is Description, is Deprecated -> null // Documented separately in getDescription |
| is Receiver -> null // gets merged with @params |
| is Suppress -> throw RuntimeException( |
| "Reaching the documentation generation step on a suppressed member should" + |
| "be impossible! If you see this, file a bug on dackka. $documentable", |
| ) |
| // These aren't tags we believe it is necessary to support |
| is Version, is Author -> null |
| } |
| } catch (e: Exception) { |
| throw RuntimeException( |
| "Exception thrown while handling ${firstTag.className} tags $tags.", |
| e, |
| ) |
| } |
| } |
| |
| return listOfNotNull(deprecation, description, *tables.toTypedArray()) |
| } |
| |
| // Turn both "E" and "<E>" to "E" |
| private fun ungenerify(name: String): String { |
| if (name.startsWith('<') && name.endsWith('>')) return name.drop(1).dropLast(1) |
| return name |
| } |
| private fun TagWrapper.name() = ungenerify((this as NamedTagWrapper).name) |
| private fun List<TagWrapper>.names() = this.map { it.name() } |
| |
| /* Tags, in particular for property parameters, are propagated multiple times in upstream. |
| * For example, @param t t_doc class Foo(val t) has Parameter(t, t_doc) duplicated many times. |
| * On the class itself, on each property parameter of that class, and on the constructor. |
| * This function filters out inappropriately propagated docs, and enforces that documentation |
| * applies only to existing properties and parameters. |
| */ |
| private fun handleUpstreamTagDuplication( |
| documentable: Documentable, |
| tags: List<TagWrapper>, |
| generics: List<DTypeParameter>, |
| ): List<TagWrapper> { |
| if (tags.first() is Param) { |
| // This one is very strange, and I haven't been able to reproduce it in unit tests |
| // A generic called "ToValue" is instead registered as being named "V" |
| // TODO: Fix |
| if (tags.names() == listOf("ToValue", "function") && generics.single().name == "V") { |
| return tags.filter { (it as Param).name == "function" } |
| } |
| |
| // Handle parameter properties, e.g. class AClass<Gen>(val propParam) |
| when (documentable) { |
| // doc is DClasslike. DClasslike's only valid @params are type params |
| is DClasslike -> { |
| val genericNames = generics.map { it.name } |
| val (forGenerics, otherAtParams) = tags.partition { it.name() in genericNames } |
| if (otherAtParams.isEmpty()) return forGenerics |
| // Enforce that the propagated documentation makes sense somewhere. Specifically |
| // documentation primarily aimed at a constructor may wind up on the DClass |
| // if the parameter being documented is a primary constructor property parameter |
| val constructorParamNames = (documentable as? WithConstructors)?.constructors |
| ?.map { constructor -> constructor.parameters.map { it.name!! } }?.flatten() |
| .orEmpty() |
| val otherNames = |
| (documentable.properties.map { it.name } + constructorParamNames) |
| val (validTags, badTags) = otherAtParams.partition { it.name() in otherNames } |
| badTags.forEach { |
| docsHolder.printWarningFor( |
| "Unable to find reference @", |
| documentable, |
| brokenDocTag = it, |
| additionalContext = |
| ". Are you trying to refer to something not visible to users?", |
| ) |
| } |
| // Use only docs for type parameters in the parameter documentation table |
| return forGenerics |
| } |
| // doc is Property. It is possible that a parameter property is documented on the |
| // class as @param. That doc is used as though it were @property. Handled there. |
| // Properties can also have @param documentation for type parameters |
| is DProperty -> { |
| val genericNames = generics.map { it.name } |
| val (forGenericsOrThis, badTags) = tags |
| .partition { it.name() in genericNames || it.name() == documentable.name } |
| badTags.forEach { |
| docsHolder.printWarningFor( |
| "Unable to find reference @", |
| documentable, |
| brokenDocTag = it, |
| additionalContext = |
| ". Are you trying to refer to something not visible to users?", |
| ) |
| } |
| return forGenericsOrThis |
| } |
| is DFunction -> {} |
| else -> |
| throw RuntimeException("Can't apply @param to a ${documentable.className}") |
| } |
| } else if (tags.first() is Property) { |
| // A DClasslike with @property applying to property parameters may have Parameter tags |
| // In such a case, none of these tags should become docs *on the DClasslike itself* |
| return when (documentable) { |
| is DClasslike -> { |
| val propertyNames = documentable.properties.map { it.name } |
| tags.filter { it.name() !in propertyNames }.forEach { |
| docsHolder.printWarningFor( |
| "Unable to find reference @", |
| documentable, |
| brokenDocTag = it, |
| ) |
| } |
| emptyList() |
| } |
| |
| is DParameter, is DProperty -> tags |
| else -> |
| throw RuntimeException("Can't apply @property to a ${documentable.className}") |
| } |
| } |
| return tags |
| } |
| |
| private fun params( |
| tags: List<NamedTagWrapper>, |
| dGenerics: List<DTypeParameter>, |
| documentable: Documentable, |
| isFromJava: Boolean, |
| sourceSet: DokkaConfiguration.DokkaSourceSet, |
| ): DocsSummaryList { |
| val tagged = tags.map { it.name() }.toSet() |
| |
| // @param can refer to parameters, lambda parameters, type parameters, or receivers. |
| val allOptions = mutableMapOf<String, ParameterComponent>() |
| if (documentable is DFunction) { |
| allOptions.putAll( |
| documentable.parameters.mapNotNull { |
| val name = it.name!! |
| if (!tagged.contains(name)) { |
| // Synthetic receiver params don't need @param documentation |
| if (name == "receiver") return@mapNotNull null |
| docsHolder.printWarningFor( |
| "Missing @param tag for parameter `$name`", |
| documentable, |
| ) |
| } |
| name to paramConverter.componentForParameter( |
| param = it, |
| isSummary = false, |
| isFromJava = isFromJava, |
| parent = documentable, |
| ) |
| }, |
| ) |
| allOptions.putAll( |
| recursivelyGetLambdaParamNames(documentable.parameters.map { it.type }).map { |
| (it.presentableName ?: "") to paramConverter |
| .componentForLambdaParameter(it, isFromJava, sourceSet) |
| }, |
| ) |
| } |
| allOptions.putAll( |
| dGenerics.map { |
| it.name to paramConverter.componentForTypeParameter(it, isFromJava) |
| }, |
| ) |
| if (documentable is Callable && documentable.receiver != null) { |
| allOptions[documentable.receiver!!.name ?: "receiver"] = |
| paramConverter.componentForParameter( |
| param = documentable.receiver!!, |
| isSummary = false, |
| isFromJava = isFromJava, |
| parent = documentable, |
| ) |
| } |
| val params = tags.map { tag -> |
| if (allOptions[tag.name()] == null) { |
| throw RuntimeException( |
| "Unable to find what is referred to by \"@param " + |
| "${tag.name()}\" in ${documentable.className} " + |
| "${documentable.name}, with contents: ${tag.text()}", |
| ) |
| } |
| val title = allOptions[tag.name()]!! |
| DefaultTableRowSummaryItem( |
| TableRowSummaryItem.Params( |
| title = title, |
| description = description(tag), |
| ), |
| ) |
| } |
| |
| return DefaultSummaryList( |
| SummaryList.Params( |
| header = DefaultTableTitle(TableTitle.Params("Parameters")), |
| items = params, |
| ), |
| ) |
| } |
| |
| /** For example, "a" and "b" in `fun foo((a: (b: String) -> int)) -> Unit)` */ |
| private fun recursivelyGetLambdaParamNames( |
| argumentTypes: List<Projection>, |
| ): List<TypeConstructor> { |
| val result = mutableListOf<TypeConstructor>() |
| for (argumentType in argumentTypes) { |
| when (argumentType) { |
| is TypeConstructor -> { |
| if (argumentType.presentableName != null) result += argumentType |
| result += recursivelyGetLambdaParamNames(argumentType.projections) |
| } |
| is Variance<*> -> { |
| result += recursivelyGetLambdaParamNames(listOf(argumentType.inner)) |
| } |
| is Nullable -> { |
| result += recursivelyGetLambdaParamNames(listOf(argumentType.inner)) |
| } |
| is DefinitelyNonNullable -> { |
| result += recursivelyGetLambdaParamNames(listOf(argumentType.inner)) |
| } |
| is TypeParameter -> { |
| /* Type parameters can't be lambdas, and are fully squashed to strings. */ |
| } |
| is TypeAliased -> { // No clear way to decide which |
| result += recursivelyGetLambdaParamNames( |
| setOf(argumentType.inner, argumentType.typeAlias).toList(), |
| ) |
| } |
| is PrimitiveJavaType, is JavaObject, Void, Dynamic, Star -> { /* Do nothing */ } |
| is UnresolvedBound -> { /* Nothing we can do. We warn elsewhere for this case. */ } |
| } |
| } |
| return result |
| } |
| |
| private fun returnType(tags: List<Return>, returnType: TypeProjectionComponent): |
| SummaryList<TableRowSummaryItem<TypeProjectionComponent, DescriptionComponent>> { |
| val params = tags.map { tag -> |
| DefaultTableRowSummaryItem( |
| TableRowSummaryItem.Params( |
| title = returnType, |
| description = description(tag), |
| ), |
| ) |
| } |
| |
| return DefaultSummaryList( |
| SummaryList.Params( |
| header = DefaultTableTitle(TableTitle.Params("Returns")), |
| items = params, |
| ), |
| ) |
| } |
| |
| private fun throws(tags: List<Throws>, parent: Documentable): DocsSummaryList { |
| val params = tags.map { tag -> |
| DefaultTableRowSummaryItem( |
| TableRowSummaryItem.Params( |
| title = throwsToParameterComponent(tag, parent), |
| description = description(tag), |
| ), |
| ) |
| } |
| |
| return DefaultSummaryList( |
| SummaryList.Params( |
| header = DefaultTableTitle(TableTitle.Params("Throws")), |
| items = params, |
| ), |
| ) |
| } |
| |
| private fun String.firstWord() = substring(0, indexOfFirst { it == ' ' }) |
| |
| private fun throwsToParameterComponent( |
| throws: Throws, |
| parent: Documentable, |
| ): ParameterComponent { |
| val name = throws.name |
| var dri: DRI? = throws.exceptionAddress |
| if (throws.name in listOf("a", "an")) { |
| throw RuntimeException( |
| "Do not use '${throws.name}' before the exception type in an @throws statement. " + |
| "This is against jdoc spec. Your exception is not being linked and looks bad", |
| ) |
| } else if ("{@link" in name) { |
| throw RuntimeException( |
| "Do not {@link the exception type in an @throws statement. @throws state" + |
| "ments are automatically linked. Manually java-linking them is against jdoc s" + |
| "pec, and breaks linking behavior causing them to actually *not* be linked.", |
| ) |
| } else if (throws.exceptionAddress == null) { |
| docsHolder.printWarningFor( |
| "Link does not resolve for @", |
| parent, |
| brokenDocTag = throws, |
| additionalContext = ". Is it from a package that the containing file does not " + |
| "import? Are docs inherited by an un-documented override function, but the " + |
| "exception class is not in scope in the inheriting class? The general fix for" + |
| " these is to fully qualify the exception name, e.g. " + |
| "`@throws java.io.IOException under some conditions`.", |
| ) |
| dri = null |
| } |
| val link = if (dri == null) { |
| DefaultLink(Link.Params(name, url = "")) |
| } else pathProvider.linkForReference(throws.exceptionAddress!!, name) |
| |
| return DefaultParameterComponent( |
| ParameterComponent.Params( |
| name = "", |
| type = DefaultTypeProjectionComponent( |
| TypeProjectionComponent.Params( |
| type = link, |
| nullability = Nullability.DONT_CARE, |
| displayLanguage = displayLanguage, |
| ), |
| ), |
| displayLanguage = displayLanguage, |
| ), |
| ) |
| } |
| |
| private fun see(tags: List<See>, parent: Documentable): LinkDescriptionSummaryList { |
| val params = tags.map { tag -> |
| DefaultTableRowSummaryItem( |
| TableRowSummaryItem.Params( |
| title = tag.toLink(parent), |
| description = description(tag), |
| ), |
| ) |
| } |
| |
| return DefaultSummaryList( |
| SummaryList.Params( |
| header = DefaultTableTitle(TableTitle.Params("See also")), |
| items = params, |
| ), |
| ) |
| } |
| |
| /** |
| * Gets a Description for the Documentable, or returns UndocumentedSymbolDescription() |
| * Has special handling to inject @property documentation as a description, if it exists |
| * Also applies to @param documentation that should become a description, i.e. property params |
| */ |
| private fun Documentable.getDescription(summary: Boolean): DescriptionComponent { |
| val components = mutableListOf<DocTag>() |
| tags().forEach { |
| when (it) { |
| is Sample -> { |
| val dri = it.name |
| // val sourceSet = this.getExpectOrCommonSourceSet() |
| /* val analysisEnvironment = analysisMap[sourceSet]!! |
| val sample = try { |
| analysisEnvironment |
| .resolveSample(sourceSet, dri)!! |
| } catch (e: NullPointerException) { |
| throw RuntimeException("Unable to resolve sample $dri!") |
| }*/ |
| // TODO(KMP) we currently have no plan to provide KMP samples b/181224204 |
| // As such, we currently assume that all samples are in common |
| val sample = docsHolder.sampleAnalysisEnvironment |
| .resolveSample(docsHolder.commonSourceSet, dri)!! |
| val imports = processImports(sample) |
| |
| components.add( |
| Pre( |
| params = mapOf("class" to "prettyprint lang-kotlin"), |
| children = listOf(Text(imports + sample.body)), |
| ), |
| ) |
| components.addAll(it.children) |
| } |
| is Description, is NamedTagWrapper -> { |
| if (!it.belongsInDescriptionOf(this)) return@forEach |
| it.children.forEach { child -> |
| try { |
| recursivelyConsiderPsAndTextsForJavaSamples( |
| child, |
| components, |
| this.getExpectOrCommonSourceSet().samples, |
| ) |
| } catch (e: Exception) { |
| throw RuntimeException( |
| "Error when resolving samples when processing $name", |
| e, |
| ) |
| } |
| } |
| } |
| is Author, is Version -> {} // These are not supported |
| is Return, is Receiver, is Constructor -> {} // These become tables in metadata() |
| is Deprecated, is Suppress, is Since -> {} // These are handled elsewhere |
| } |
| } |
| if (components.isEmpty()) return UndocumentedSymbolDescriptionComponent |
| return description(components, summary, null) |
| } |
| |
| /** |
| * Tags with the same name as the documentable should generally go in the description component, |
| * but sometimes the tag refers to something else with the same name, like a parameter that has |
| * the same name as its function. |
| */ |
| private fun TagWrapper.belongsInDescriptionOf(documentable: Documentable): Boolean { |
| if (this is Description) return true |
| if (this !is NamedTagWrapper) return false |
| return when (this) { |
| is Param, is Property -> (documentable is DParameter || documentable is DProperty) && |
| (this.name == documentable.name) |
| is Throws, is See -> false |
| else -> true |
| } |
| } |
| |
| /** annotation-sampled and SampledAnnotationDetector */ |
| private fun DocTag.explicitlyBanLookingForSamples() = |
| "Functions referenced with @sample are annotated with @Sampled" in text() || |
| "Denotes that the annotated function is considered a sample function" in text() || |
| "that functions referred to from KDoc with a @sample tag are annotated" in text() |
| |
| private fun recursivelyConsiderPsAndTextsForJavaSamples( |
| root: DocTag, |
| components: MutableList<DocTag>, |
| samples: Set<File>, |
| ) { |
| if ("@sample" !in root.text() || root.explicitlyBanLookingForSamples()) { |
| components.add(root) |
| return |
| } |
| when (root) { |
| is Text -> { |
| val parts = root.body.split("{", "}") |
| for (part in parts) { |
| if ("@sample" !in part) { |
| if (part.isNotBlank()) components.add(Text(part.trim())) |
| } else components.add(convertTextToJavadocSample(Text(part.trim()), samples)) |
| } |
| } |
| is P -> { |
| for (child in root.children) { |
| recursivelyConsiderPsAndTextsForJavaSamples(child, components, samples) |
| } |
| } |
| // Having non-text components on the same line as a samples is not supported |
| else -> throw RuntimeException("considered invalid type ${root.className} for sample") |
| } |
| } |
| |
| private fun WithChildren<DocTag>.text(): String { |
| return if (this is Text) this.body else children.joinToString(" ") { it.text() } |
| } |
| |
| private fun description( |
| soleComponent: TagWrapper, |
| summary: Boolean = false, |
| deprecation: String? = null, |
| ): DescriptionComponent { |
| return description(soleComponent.children, summary, deprecation) |
| } |
| |
| private fun description( |
| components: List<DocTag> = emptyList(), |
| summary: Boolean = false, |
| deprecation: String? = null, |
| ): DescriptionComponent { |
| return DefaultDescriptionComponent( |
| DescriptionComponent.Params( |
| pathProvider, |
| components, |
| summary, |
| deprecation, |
| docsHolder, |
| ), |
| ) |
| } |
| |
| /** Returns the component for a deprecation. */ |
| private fun deprecationComponent( |
| documentable: Documentable, |
| summary: Boolean, |
| deprecationAnnotation: Annotations.Annotation?, |
| ): DescriptionComponent? { |
| val deprecation = findDeprecation(documentable, deprecationAnnotation) ?: return null |
| return description(deprecation.children, summary, documentable.deprecationText()) |
| } |
| |
| /** |
| * Finds either the javadoc @deprecated tag or the Kotlin @Deprecated annotation [TagWrapper]. |
| */ |
| private fun findDeprecation( |
| documentable: Documentable, |
| deprecationAnnotation: Annotations.Annotation?, |
| ): Deprecated? { |
| val javadocDeprecation = documentable.find<Deprecated>() |
| if (javadocDeprecation != null) { |
| // Prefer javadoc deprecation messages since they allow formatting |
| return javadocDeprecation |
| } |
| |
| val annotationDeprecationMessage = |
| (deprecationAnnotation?.params?.get("message") as? StringValue)?.value ?: return null |
| // Dokka makes message="foo" show up as "\"foo\"" since you typically want to show quotes |
| // when rendering an annotation. Remove those outer quotes. |
| val message = annotationDeprecationMessage.removeSurrounding("\"") |
| |
| return Deprecated(P(children = listOf(Text(message)))) |
| } |
| |
| private fun Documentable.deprecationText() = |
| "This ${this.stringForType(displayLanguage)} is deprecated." |
| |
| /** Retrieves the doc tags of type [T]. */ |
| private inline fun <reified T> Documentable.find() = |
| tags().filterIsInstance<T>().strictSingleOrNull() |
| |
| /** |
| * @return the doc tags (aka human-written javadoc or kdoc) associated with this documentable |
| */ |
| private fun Documentable.tags() = documentation[getExpectOrCommonSourceSet()]?.children |
| ?: emptyList() |
| |
| private fun tagOrder(paramNames: List<String>) = compareBy<TagWrapper> { tag -> |
| when (tag) { |
| is Deprecated -> 0 |
| is Description -> 1 |
| is Return -> 2 |
| is Constructor -> 3 |
| is Property -> 4 |
| is Receiver -> 5 |
| is Param -> 6 |
| is Throws -> 7 |
| is See -> 8 |
| is Sample -> 9 |
| is Since -> 10 |
| is Version -> 11 |
| is Author -> 12 |
| is Suppress -> 13 |
| is CustomTagWrapper -> 14 |
| } |
| }.thenBy { tag -> |
| when (tag) { |
| is Param -> paramNames.indexOf(tag.name) |
| else -> -1 |
| } |
| } |
| |
| /** |
| * Extract the see tag's reference into a link. |
| * |
| * This one is painful. An address is only sometimes there, other times there's a docs link |
| * nested somewhere in the tree, and as a last resort the name is always present with whatever |
| * a developer writes which could either be a fully qualified reference or just the URL |
| * fragment. |
| */ |
| private fun See.toLink(parent: Documentable): Link { |
| val address = address |
| if (address != null) { |
| return pathProvider.linkForReference(address) |
| } |
| val docsLink = root.explodedChildren.filterIsInstance<DocumentationLink>().singleOrNull() |
| if (docsLink != null) { |
| return pathProvider.linkForReference(docsLink.dri) |
| } |
| |
| // TODO(b/167437580): figure out how to reliably parse links |
| val segments = name.split("#") |
| return if (segments.size == 1) { |
| val (packageName, typeName) = typeToPackageNameAndType(segments.single()) |
| if (typeName.isEmpty()) { |
| var additionalContext = "" |
| // Maybe the link is `package.Class.aFunction` instead of `package.Class#aFunction`? |
| val last = name.substringAfterLast(".") |
| val rest = name.substringBeforeLast(".") |
| if (last.firstOrNull()?.isLowerCase() == true && rest.any { it.isUpperCase() }) { |
| additionalContext = ". Did you mean $rest#$last?" |
| val (packageN, typeN) = typeToPackageNameAndType(rest) |
| val url = pathProvider.forType(packageN, typeN) |
| DefaultLink(Link.Params(typeN, "$url#$last")) |
| } |
| docsHolder.printWarningFor( |
| "Failed to resolve ", |
| parent, |
| brokenDocTag = this, |
| additionalContext = additionalContext, |
| ) |
| DefaultLink(Link.Params(name, url = "")) |
| } else if (packageName.isEmpty()) { |
| // This is a same-package type link, though we sadly can't prove it's correct |
| pathProvider.linkForReference(DRI("", typeName)) |
| } else { |
| pathProvider.linkForReference(DRI(packageName, typeName)) |
| } |
| } else if (segments.size == 2) { |
| val (type, anchor) = segments |
| if (type.isEmpty()) { |
| // Self link |
| DefaultLink(Link.Params(anchor, anchor)) |
| } else { |
| // Assume link with anchor |
| val (packageName, typeName) = typeToPackageNameAndType(type) |
| val url = pathProvider.forType(packageName, typeName) |
| DefaultLink(Link.Params(typeName, "$url#$anchor")) |
| } |
| } else { |
| throw RuntimeException("Could not understand path: $name") |
| } |
| } |
| |
| /** Horrible guess-work to try and extract the package and type names. */ |
| private fun typeToPackageNameAndType(full: String): Pair<String, String> { |
| val parts = full.split(".") |
| |
| if (parts.size == 1) return "" to full |
| |
| val packageName = parts.takeWhile { it.all(Char::isLowerCase) }.joinToString(".") |
| val typeName = parts.takeLastWhile { |
| if (it.isEmpty()) { |
| throw RuntimeException("empty element in TTPNAT. Full: $full") |
| } else it.first().isUpperCase() |
| }.joinToString(".") |
| return packageName to typeName |
| } |
| |
| internal fun docsToSummaryDefault(documentables: List<Documentable>) = |
| docsToSummary(documentables, false) |
| |
| /** |
| * Converts a generic List<Documentable> to a SummaryList. |
| * Does nothing clever; only converts Documentables to links (by default with annotations) |
| */ |
| private fun docsToSummary( |
| documentables: List<Documentable>, |
| showAnnotations: Boolean, |
| ) = DefaultSummaryList( |
| SummaryList.Params( |
| items = documentables |
| .map { summaryForDocumentable(it, showAnnotations) }, |
| ), |
| ) |
| |
| /** |
| * Converts generic Documentables to TableRowSummaryItems, as simple maybe-annotated links |
| * This is used for mini-signatures, e.g. nested types list, subclasses list, package summary |
| */ |
| internal fun summaryForDocumentable( |
| documentable: Documentable, |
| showAnnotations: Boolean = false, |
| ): TableRowSummaryItem<Link, DescriptionComponent> { |
| val link = if (documentable is DTypeAlias) { |
| // typealiases have no pages |
| DefaultUnlink(Link.Params(documentable.name, "")) |
| } else pathProvider.linkForReference(documentable.dri) |
| val maybeAnnotatedLink = if (showAnnotations) { |
| DefaultAnnotatedLink( |
| AnnotatedLink.Params( |
| annotations = annotationConverter.annotationComponents( |
| documentable.annotations(documentable.getExpectOrCommonSourceSet()), |
| nullability = Nullability.DONT_CARE, // Not useful for these cases |
| ), |
| link = link, |
| ), |
| ) |
| } else { |
| link |
| } |
| return DefaultTableRowSummaryItem( |
| TableRowSummaryItem.Params( |
| title = maybeAnnotatedLink, |
| description = summaryDescription( |
| documentable, |
| documentable.deprecationAnnotation(), |
| ), |
| ), |
| ) |
| } |
| |
| /** |
| * Converts a generic List<Documentable> to a SummaryList. |
| * Does nothing clever; only converts Documentables to links (by default with annotations) |
| */ |
| internal fun docsToSummaryKmp( |
| documentables: List<Documentable>, |
| ) = DefaultSummaryList( |
| SummaryList.Params( |
| items = documentables |
| .map { summaryForDocumentableKmp(it) }, |
| ), |
| ) |
| |
| /** |
| * Converts generic Documentables to TableRowSummaryItems, as simple maybe-annotated links |
| * This is used for mini-signatures, e.g. nested types list, subclasses list, package summary |
| */ |
| private fun summaryForDocumentableKmp( |
| documentable: Documentable, |
| ): TableRowSummaryItem<Link, DescriptionComponent> { |
| return DefaultKmpTableRowSummaryItem( |
| KmpTableRowSummaryItem.Params( |
| title = pathProvider.linkForReference(documentable.dri), |
| description = summaryDescription(documentable), |
| platforms = DefaultPlatformComponent(documentable.sourceSets), |
| ), |
| ) |
| } |
| } |