blob: 97455a16e2e693b2fd82ccaf942012fef25f4106 [file] [log] [blame]
/*
* 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.common.truth.Truth.assertThat
import com.google.devsite.components.ContextFreeComponent
import com.google.devsite.components.DescriptionComponent
import com.google.devsite.components.Link
import com.google.devsite.components.Raw
import com.google.devsite.components.impl.DefaultDescriptionComponent
import com.google.devsite.components.impl.UndocumentedSymbolDescriptionComponent
import com.google.devsite.components.symbols.LambdaTypeProjectionComponent
import com.google.devsite.components.symbols.ParameterComponent
import com.google.devsite.components.symbols.TypeParameterComponent
import com.google.devsite.components.table.SummaryList
import com.google.devsite.components.testing.NoopContextFreeComponent
import com.google.devsite.renderer.Language
import com.google.devsite.renderer.converters.testing.description
import com.google.devsite.renderer.converters.testing.generics
import com.google.devsite.renderer.converters.testing.isAtNonNull
import com.google.devsite.renderer.converters.testing.item
import com.google.devsite.renderer.converters.testing.items
import com.google.devsite.renderer.converters.testing.link
import com.google.devsite.renderer.converters.testing.name
import com.google.devsite.renderer.converters.testing.projectionName
import com.google.devsite.renderer.converters.testing.single
import com.google.devsite.renderer.converters.testing.size
import com.google.devsite.renderer.converters.testing.text
import com.google.devsite.renderer.converters.testing.title
import com.google.devsite.renderer.converters.testing.typeName
import com.google.devsite.renderer.impl.DocumentablesHolder
import com.google.devsite.testing.ConverterTestBase
import kotlinx.coroutines.runBlocking
import kotlinx.html.body
import kotlinx.html.stream.createHTML
import org.jetbrains.dokka.links.Callable
import org.jetbrains.dokka.model.DClass
import org.jetbrains.dokka.model.DModule
import org.jetbrains.dokka.model.Documentable
import org.jetbrains.dokka.model.doc.DocumentationLink
import org.jetbrains.dokka.model.doc.Img
import org.jetbrains.dokka.model.doc.Text
import org.jetbrains.dokka.model.doc.Pre
import org.jetbrains.dokka.model.properties.WithExtraProperties
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import java.io.ByteArrayOutputStream
import java.io.PrintStream
import kotlin.test.assertFails
@RunWith(Parameterized::class)
internal class DocTagConverterTest(
private val language: Language
) : ConverterTestBase(language) {
@Test
fun `Empty description isn't documented`() {
val description = """
|class Foo
""".render().description()
assertThat(description.javaClass)
.isAssignableTo(UndocumentedSymbolDescriptionComponent::class.java)
}
@Test
fun `Basic summary description has correct flags`() {
val description = """
|/** Hello World! */
|class Foo
""".render().description()
assertThat(description.data.summary).isTrue()
assertThat(description.data.deprecation).isNull()
}
@Suppress("unused") // TODO: fix deprecated class details b/183420241
@Test
fun `Deprecated class summary and detail description flags correct in 4x Kotlin and Java`() {
val codeK = """
|/**
| * class_description
| */
|@Deprecated("Bye")
|class Foo
""".render()
val summarykK = codeK.description()
val detailsK = codeK.documentation()
val codeJ = """
|/**
| * class_description
| * @deprecated Bye
| */
|@Deprecated
|public class Foo {}
""".render(java = true)
val summaryJ = codeJ.description { this.clazz() }
val detailsJ = codeJ.documentation(doc = { this.clazz() })
for (summary in listOf(summarykK, summaryJ)) {
assertThat(summary.data.summary).isTrue()
assertThat(summary.text()).isEqualTo("Bye")
assertThat(summary.data.deprecation).isEqualTo("This class is deprecated.")
}
/* TODO: fix deprecated class details b/183420241
for (details in listOf(detailsK, detailsJ)) {
val detail = (details.single() as Description)
assertThat(detail.data.summary).isFalse()
assertThat(detail.text()).isEqualTo("Bye")
assertThat(detail.data.deprecation).isEqualTo("This class is deprecated.")
}*/
}
/**
* This test largely serves as a regression test for https://github.com/Kotlin/dokka/issues/1939
*/
@Test
fun `Full summary and description work with multiline in 4x Kotlin and Java`() {
val moduleK = """
|/**
| * Hello World! Docs with period issue, e.g. this.
| *
| * A second line of desc. A third line of desc.
| */
|class Foo
""".render()
val moduleJ = """
|/**
| * Hello World! Docs with period issue, e.g. this.
| *
| * A second line of desc. A third line of desc.
| */
|public class Foo() {}
""".render(java = true)
for (module in listOf(moduleJ, moduleK)) {
val summary = module.description(doc = { this.clazz() })
assertThat(summary.data.summary).isTrue()
val detail = module.documentation(doc = { this.clazz() }).item() as DescriptionComponent
assertThat(detail.data.summary).isFalse()
val separator = if (module == moduleK) "</p>\n <p>" else " "
assertThat(detail.render()).isEqualTo(
"""
<body>
<p>Hello World! Docs with period issue, e.g. this.${separator}A second line of desc. A third line of desc.</p>
</body>
""".trim()
)
val dComponents = detail.data.components
val sComponents = summary.data.components
for (components in listOf(dComponents, sComponents)) {
val size = if (module == moduleJ) 1 else 2
assertThat(components.size).isEqualTo(size)
}
}
}
@Test // b/192714584
fun `th tag can be nested in a tr tag (with a thead tag)`() {
val module = """
|/**
| * This is a table with a thead tag.
| * <table>
| * <thead>
| * <tr>
| * <th>FooHead1</th>
| * <th>BarHead1</th>
| * </tr>
| * </thead>
| * <tr>
| * <td>FooCell1</td>
| * <td>BarCell1</td>
| * </tr>
| * </table>
| */
|public class Foo() {}
""".render(java = true)
val detail = module.documentation(doc = { this.clazz() }).item() as DescriptionComponent
assertThat(detail.render()).isEqualTo(
"""
<body>
<p>This is a table with a thead tag. </p>
<table>
<thead>
<tr>
<th>FooHead1</th>
<th>BarHead1</th>
</tr>
</thead>
<tbody>
<tr>
<td>FooCell1</td>
<td>BarCell1</td>
</tr>
</tbody>
</table>
</body>
""".trim()
)
}
@Test // b/192714584
fun `th tag can be nested in a tr tag (without a thead tag)`() {
val module = """
|/**
| * This is a table without a thead tag.
| * <table>
| * <tr>
| * <th>FooHead2</th>
| * <th>BarHead2</th>
| * </tr>
| * <tr>
| * <td>FooCell2</td>
| * <td>BarCell2</td>
| * </tr>
| * </table>
| */
|public class Foo() {}
""".render(java = true)
val detail = module.documentation(doc = { this.clazz() }).item() as DescriptionComponent
assertThat(detail.render()).isEqualTo(
"""
<body>
<p>This is a table without a thead tag. </p>
<table>
<tbody>
<tr>
<th>FooHead2</th>
<th>BarHead2</th>
</tr>
<tr>
<td>FooCell2</td>
<td>BarCell2</td>
</tr>
</tbody>
</table>
</body>
""".trim()
)
}
@Test // NOTE: upstream dokka does not support @param <Baz> documentation style in kotlin
fun `Class type parameters can be documented with @param with or without angle brackets`() {
val documentationK = """
|/**
| * Hello World!
| * @param Bar A type of bar
| * @param Baz Bazzy baz
| */
|class <Bar: String, Baz> Foo: List<Bar>
""".render().documentation(doc = { this.clazz() })
val documentationJ = """
|/**
| * Hello World!
| * @param Bar A type of bar
| * @param <Baz> Bazzy baz
| */
|public class Foo<Bar extends String, Baz> extends java.util.List<Bar> {
|}
""".render(java = true).documentation(doc = { this.clazz() })
for (documentation in listOf(documentationK, documentationJ)) {
val classParams = documentation.first {
(it as? SummaryList)?.title() == "Parameters" } as SummaryList
assertThat(classParams.size()).isEqualTo(2)
val barParam = classParams.items().first().data
val bazParam = classParams.items().last().data
val barTypeParam = barParam.title as TypeParameterComponent
assertThat(barTypeParam.data.name).isEqualTo("Bar")
assertThat(barTypeParam.projectionName()).isEqualTo("String")
assertThat((barParam.description as DescriptionComponent).text())
.isEqualTo("A type of bar")
val bazTypeParam = bazParam.title as TypeParameterComponent
assertThat(bazTypeParam.data.name).isEqualTo("Baz")
assertThat((bazParam.description as DescriptionComponent).text())
.isEqualTo("Bazzy baz")
}
}
@Test // NOTE: upstream dokka does not support @param <Baz> documentation style in kotlin
fun `Function type parameters can be documented with @param with or without angle brackets`() {
val documentationK = """
|/**
| * Hello World!
| * @param Bar A type of bar
| * @param Baz Bazzy baz
| * @blamaram Wam wham
| */
|fun <Bar: String, Baz> foo(): List<Bar>
""".render().documentation(doc = { this.function("foo")!! })
val documentationJ = """
|/**
| * Hello World!
| * @param Bar A type of bar
| * @param <Baz> Bazzy baz
| */
|public <Bar extends String, Baz> java.util.List<Bar> foo() {
|}
""".render(java = true).documentation(doc = { this.function("foo")!! })
for (documentation in listOf(documentationK, documentationJ)) {
val params = documentation.first {
(it as? SummaryList)?.title() == "Parameters" } as SummaryList
assertThat(params.size()).isEqualTo(2)
val barParam = params.items().first().data
val bazParam = params.items().last().data
val barTypeParam = barParam.title as TypeParameterComponent
assertThat(barTypeParam.data.name).isEqualTo("Bar")
assertThat(barTypeParam.projectionName()).isEqualTo("String")
assertThat((barParam.description as DescriptionComponent)
.text()).isEqualTo("A type of bar")
val bazTypeParam = bazParam.title as TypeParameterComponent
assertThat(bazTypeParam.data.name).isEqualTo("Baz")
assertThat((bazParam.description as DescriptionComponent)
.text()).isEqualTo("Bazzy baz")
}
}
@Test
fun `Property parameter docs propagate correctly`() {
// Property parameters are kotlin-exclusive
val module = """
|/**
| * Class docs
| * @property bar AtPropertyParameter docs
| * @param bar AtParameterProperty docs
| */
|class Foo(val bar: String)
""".render()
val propDoc = module.documentation({ this.property()!! }).single() as DescriptionComponent
val classDoc = module.documentation({ this.clazz() }).single() as DescriptionComponent
val constructorDoc = module.documentation({ this.constructor() })
val conParamDoc = module.documentation({ this.constructor().parameters.single() }).single()
// An odd propagation system, but it seems to work out to properly document everything?
assertThat((propDoc).text()).isEqualTo("AtPropertyParameter docs")
assertThat((classDoc).text()).isEqualTo("Class docs")
assertThat(constructorDoc.size).isEqualTo(2)
assertThat(constructorDoc.first())
.isInstanceOf(UndocumentedSymbolDescriptionComponent::class.java)
assertThat((constructorDoc.last() as SummaryList).title()).isEqualTo("Parameters")
assertThat((constructorDoc.last() as SummaryList).single().name()).isEqualTo("bar")
assertThat((constructorDoc.last() as SummaryList).single().description().text())
.isEqualTo("AtParameterProperty docs")
assertThat((conParamDoc as DescriptionComponent).text())
.isEqualTo("AtPropertyParameter docs")
}
@Test
fun `@constructor docs are applied`() {
val withAnnotation = """
|/**
| * The amount by which the text is shifted up or down from current the baseline.
| * @constructor Primary constructor docs
| */
|class BaselineShift(val multiplier: Float) {
| /** Secondary constructor docs */
| constructor(multiplier: Int) : this(multiplier)
|
|}
""".render()
val constructorDoc1 = withAnnotation.documentation({ this.constructors().first() })
.single() as DescriptionComponent
val constructorDoc2 = withAnnotation.documentation({ this.constructors().last() })
.single() as DescriptionComponent
assertThat(constructorDoc1.text()).isEqualTo("Secondary constructor docs")
assertThat(constructorDoc2.text()).isEqualTo("Primary constructor docs")
}
@Test
fun `Class property parameters can be documented with @property on the class`() {
val module = """
|/**
| * Hello World!
| * @param baz Buzzbuzzbuzz
| * @property bar A vary bary name
| */
|class Foo(val bar: String) {
|
|}
""".render()
val propertyDoc = module.documentation({ this.property("bar")!! })
// Upstream dokka does not propagates @param documentation on property parameters to the
// property. We think this is what we want.
assertThat((propertyDoc.single() as DescriptionComponent).text())
.isEqualTo("A vary bary name")
// Upstream dokka does not propagate @property documentation on property parameters to the
// constructor. We think this is what we want.
assertFails { val constructorDoc = module.documentation({ this.constructor() }) }
}
@Test
fun `Property parameter can be @suppress-ed as property only`() {
val module = """
|/**
| * @param context context_docs
| */
|public open class NavController(
| /** @suppress */
| @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
| public val context: Context
|) {
""".render()
assertThat(module.properties()).isEmpty()
assertThat(module.constructor().parameters.single().name).isEqualTo("context")
val constructorParams = module.documentation({ this.constructor() })[1] as SummaryList
assertThat(constructorParams.title()).isEqualTo("Parameters")
assertThat(constructorParams.item().name()).isEqualTo("context")
assertThat(constructorParams.item().description().text()).isEqualTo("context_docs")
}
@Test
fun `Full documentation has img tag in 4x Kotlin and Java`() {
val documentationK = """
|/**
| ![Alt text](/path/to/img.jpg)
|*/
|fun foo(a: Int)
""".render().documentation()
val documentationJ = """
|/**
| * <img src="/path/to/img.jpg" alt="Alt text"/>
| */
|public fun foo(Integer a)
""".render(java = true).documentation()
for (documentation in listOf(documentationJ, documentationK)) {
val description = documentation.last() as DescriptionComponent
val img = description.data.components.first().children.item() as Img
assertThat(img.params["href"]).isEqualTo("/path/to/img.jpg")
assertThat(img.params["alt"]).isEqualTo("Alt text")
}
}
@Test // Developers sometimes use white space to line up documentation
fun `Parameter documentation works and ignores whitespace`() {
val documentationK = """
|/**
| * @param a blah
| * @param b bblargh
| */
|fun foo(a: Int, b: String)
""".render().documentation()
val documentationJ = """
|/**
| * @param a blah
| * @param b bblargh
| */
|public void foo(Int a, String b)
""".render(java = true).documentation()
for (documentation in listOf(documentationK, documentationJ)) {
val paramSummary = documentation.last() as SummaryList
assertThat(paramSummary.items().size).isEqualTo(2)
val param1 = paramSummary.items().first().data.title as ParameterComponent
val param2 = paramSummary.items().last().data.title as ParameterComponent
assertThat(paramSummary.title()).isEqualTo("Parameters")
assertThat(param1.data.name).isEqualTo("a")
assertThat(param2.data.name).isEqualTo("b")
}
}
@Ignore // Upstream dokka does not support inheriting docs from hidden components
@Test
fun `Sealed class constructor docs inherit`() {
val rendered = """
|/**
| * @param foo seele_foo_docs
| */
|sealed class Seele(foo: String) {
| /**
| * seele_constructor_docs
| */
| constructor {}
|}
|class Ba: Seele
""".render()
// sealed classes have hidden constructors
assertThat((rendered.explicitClasslike("Seele") as DClass).constructors.isEmpty())
val constructorDoc = rendered.documentation(
{ (this.explicitClasslike("Ba")!! as DClass).constructors.single() })
val conParamDoc = rendered.documentation(
{ (this.explicitClasslike("Ba")!! as DClass).constructors.single().parameters.single() }
).single()
assertThat(constructorDoc.size).isEqualTo(2)
assertThat((constructorDoc.first() as DescriptionComponent).text())
.isEqualTo("seele_constructor_docs")
assertThat((constructorDoc.last() as SummaryList).title()).isEqualTo("Parameters")
assertThat((constructorDoc.last() as SummaryList).single().name()).isEqualTo("foo")
assertThat((constructorDoc.last() as SummaryList).single().description().text())
.isEqualTo("seele_foo_docs")
assertThat((conParamDoc as DescriptionComponent).text()).isEqualTo("seele_foo_docs")
}
@Test
fun `Full documentation has properties`() {
val description = """
|/** @property bar a barber */
|val bar: String = "barbarbar
""".render().documentation(doc = {
this.packages.single()
.properties.single()
}).single() as DescriptionComponent
assertThat(description.text()).isEqualTo("a barber")
}
@Test
fun `Full class documentation has properties`() {
val description = """
|class Foo {
| /** @property bar a barber */
| val bar: String = "barbarbar
|}
""".render().documentation(doc = {
this.packages.single().classlikes.single()
.properties.single()
}).single() as DescriptionComponent
assertThat(description.text()).isEqualTo("a barber")
}
@Test
fun `@param throws exception or prints warning for invalid parameter`() {
val exception = assertFails {
"""
|/**
| * @param NOT_A_REAL_PARAM aaaaaa
| */
|fun foo()
""".render().documentation()
}
assertThat(exception.localizedMessage).contains("with contents: aaaaaa")
val standardOut = System.out
val outputStreamCaptor = ByteArrayOutputStream()
System.setOut(PrintStream(outputStreamCaptor))
"""
|/**
| * @param NOT_A_REAL_PARAM aaaaaa
| */
|class Foo { }
""".render().documentation() // for type params and property params
val expected = "WARN: Unable to find what is referred to by" +
"\n\t@param NOT_A_REAL_PARAM" +
"\nin DClass Foo" +
"\nDid you make a typo? Are you trying to refer to something not visible to users?"
assertThat(outputStreamCaptor.toString()).contains(expected)
System.setOut(standardOut)
assertFails { // for @param in the wrong place
"""
|/**
| * @param NOT_A_REAL_PARAM aaaaaa
| */
|val foo = "bbb"
""".render().documentation()
}
}
@Test
fun `@property throws exception for invalid property`() {
val standardOut = System.out
val outputStreamCaptor = ByteArrayOutputStream()
System.setOut(PrintStream(outputStreamCaptor))
"""
|/**
| * @property NOT_A_REAL_PROPERTY aaaaaa
| */
|class Foo(NOT_A_REAL_PROPERTY: String) { }
""".render().documentation()
var expected = "WARN: Unable to find what is referred to by" +
"\n\t@property NOT_A_REAL_PROPERTY" +
"\nin DClass Foo" +
"\nDid you make a typo? Are you trying to refer to something not visible to users?"
assertThat(outputStreamCaptor.toString()).contains(expected)
System.setOut(PrintStream(outputStreamCaptor))
"""
|/**
| * @property NO_PROPERTIES_HERE aaaaaa
| */
|class Foo() { }
""".render().documentation()
expected = "WARN: Unable to find what is referred to by" +
"\n\t@property NO_PROPERTIES_HERE" +
"\nin DClass Foo" +
"\nDid you make a typo? Are you trying to refer to something not visible to users?"
assertThat(outputStreamCaptor.toString()).contains(expected)
assertFails {
"""
|class Foo() {
| /**
| * @property NO_PROPERTIES_HERE aaaaaa
| */
| fun doAThing()
|}
""".render().documentation() // can't @property on a function
}
assertFails {
"""
|/**
| * @property NO_PROPERTIES_HERE aaaaaa
| */
|fun doAThing()
""".render().documentation() // can't @property on a function
}
assertFails {
"""
|/** @property a
|val b
|val a
""".render().documentation() // @property must be on correct property
}
System.setOut(standardOut)
}
@Test
fun `Full documentation parameters table has types with nullability annotations`() {
if (language == Language.KOTLIN) return
val documentationK = """
|/**
| * @param T a type
| */
|interface PagedListListener<T : Any> {
| /**
| * Called after the current PagedList has been updated.
| *
| * @param previousList The previous list of Ts, may be null.
| * @param currentList The new current list of nullable Ts, may not be null.
| */
| fun onCurrentListChanged(
| @Suppress("DEPRECATION") previousList: List<T>?,
| @Suppress("DEPRECATION") currentList: List<T?>
| )
|}
""".render().documentation()
val documentationJ = """
|/**
| * Called after the current PagedList has been updated.
| *
| * @param previousList The previous list of Ts, may be null.
| * @param currentList The new current list of nullable Ts, may not be null.
| */
|public <T> void onCurrentListChanged(
| java.util.List<@NonNull T> previousList,
| @NonNull java.util.List<T> currentList) {}
""".render(java = true).documentation()
for (documentation in listOf(documentationK, documentationJ)) {
val paramTable = documentation.first { (it as? SummaryList)?.title() == "Parameters" }
as SummaryList
assertThat(paramTable.size()).isEqualTo(2)
val param0 = paramTable.items().first()
val param0Left = (param0.data.title as ParameterComponent)
val param0Generic = param0Left.data.type.data.generics.single()
val param1 = paramTable.items().last()
val param1Left = (param1.data.title as ParameterComponent)
val param1Generic = param1Left.data.type.data.generics.single()
assertThat(param0.name()).isEqualTo("previousList")
assertThat(param0.description().text())
.isEqualTo("The previous list of Ts, may be null.")
assertThat(param0Left.typeName()).isEqualTo("List")
assertThat(param0Generic.name()).isEqualTo("T")
assertThat(param1.name()).isEqualTo("currentList")
assertThat(param1.description().text())
.isEqualTo("The new current list of nullable Ts, may not be null.")
assertThat(param0Left.typeName()).isEqualTo("List")
assertThat(param1Generic.name()).isEqualTo("T")
javaOnly {
assertThat(param0Left.data.annotationComponents).isEmpty()
assertThat(param0Left.data.type.data.annotationComponents).isEmpty()
assertThat(param0Generic.data.annotationComponents.single().isAtNonNull).isTrue()
assertThat(param1Left.data.annotationComponents).isEmpty()
assertThat(param1Left.data.type.data.annotationComponents.single().isAtNonNull)
.isTrue()
assertThat(param1Generic.data.annotationComponents.isEmpty())
}
kotlinOnly {
assertThat(param0Left.data.type.nullable).isEqualTo(true)
assertThat(param0Generic.nullable).isEqualTo(false)
assertThat(param1Left.data.type.nullable).isEqualTo(false)
assertThat(param0Generic.nullable).isEqualTo(true)
}
}
}
@Test
fun `Parameter detection works on nested named lambda types`() {
val documentation = """
|/**
| * @param funA a fun
| * @param funB be fun
| * @param unt where units go
| * @param foo a list of foos
| */
|fun mega(funA: (funB: ((unt: Unit, foo: List<Boolean>) -> String) -> Int) -> (Double))
""".render().documentation()
val paramTable = documentation.first {
(it as? SummaryList)?.title() == "Parameters" } as SummaryList
val funADocs = paramTable.items().single { it.name() == "funA" }.description().text()
assertThat(funADocs).isEqualTo("a fun")
val funBDocs = paramTable.items().single { it.name() == "funB" }.description().text()
assertThat(funBDocs).isEqualTo("be fun")
val untDocs = paramTable.items().single { it.name() == "unt" }.description().text()
assertThat(untDocs).isEqualTo("where units go")
val fooDocs = paramTable.items().single { it.name() == "foo" }.description().text()
assertThat(fooDocs).isEqualTo("a list of foos")
}
@Test
fun `Parameter documentation works on generic and lambda types`() {
val documentation = """
|abstract class Factory<Key : Any, Value : Any> {
| /**
| * Applies the given function to each value emitted by DataSources produced by this Factory.
| *
| * Same as mapByPage, but operates on individual items.
| *
| * @param function Function that runs on each loaded item, returning items of a potentially
| * new type.
| * @param ToValue Type of items produced by the new DataSource, from the passed function.
| * @return A new [Factory], which transforms items using the given function.
| *
| * @see mapByPage
| * @see DataSource.map
| * @see DataSource.mapByPage
| */
| open fun <ToValue : String> map(function: (Value) -> ToValue): Factory<Key, ToValue>
|}
""".render().documentation()
val paramTable = documentation.first {
(it as? SummaryList)?.title() == "Parameters" } as SummaryList
// We don't want Value and/or Key to appear listed as parameters for map().
assertThat(paramTable.size()).isEqualTo(2)
val param0 = paramTable.items().first()
val param0Left = param0.data.title as ParameterComponent
val param1 = paramTable.items().last()
val param1Left = param1.data.title as TypeParameterComponent
assertThat(param0Left.data.name).isEqualTo("function")
javaOnly {
assertThat(param0Left.data.type.link().name).isEqualTo("Function1")
val param0LambdaTypes = param0Left.generics().map { it.link().name }
assertThat(param0LambdaTypes).isEqualTo(listOf("Value", "ToValue"))
}
kotlinOnly {
val lambdaSymbol = param0Left.data.type as LambdaTypeProjectionComponent
assertThat(lambdaSymbol.data.receiver).isNull()
assertThat(lambdaSymbol.data.lambdaModifiers).isEmpty()
// The evaluation type of the lambda is ToValue
assertThat(lambdaSymbol.link().name).isEqualTo("ToValue")
val param0LambdaArgumentType = lambdaSymbol.data.lambdaParams
.map { it.data.type.link().name }
assertThat(param0LambdaArgumentType).isEqualTo(listOf("Value"))
}
assertThat(param0.description().text()).isEqualTo("Function that runs on each " +
"loaded item, returning items of a potentially new type."
)
assertThat(param1Left.data.name).isEqualTo("ToValue")
assertThat(param1Left.projectionName()).isEqualTo("String")
assertThat(param1.description().text()).isEqualTo("Type of items produced by the " +
"new DataSource, from the passed function."
)
val seeAlsoTable = documentation.first {
(it as? SummaryList)?.title() == "See also" } as SummaryList
assertThat(seeAlsoTable.size()).isEqualTo(3)
assertThat(seeAlsoTable.items().map { (it.data.title as Link).data.name })
.isEqualTo(listOf("mapByPage", "DataSource.map", "DataSource.mapByPage"))
}
@Test
fun `Copied from PagingData`() {
val documentation = """
|/**
| * Returns a [PagingData] containing only elements matching the given [predicate]
| *
| * @see filter
| */
|@JvmName("filter")
|@CheckResult
|fun filterSync(predicate: (T) -> Boolean): PagingData<T> = transform { event ->
| event.filter { predicate(it) }
|}
""".render().documentation()
val seeAlsoTable = documentation.first {
(it as? SummaryList)?.title() == "See also" } as SummaryList
assertThat((seeAlsoTable.single().data.title as Link).data.name).isEqualTo("filter")
}
@Test
fun `Documentation spacing works over multiple lines`() {
val documentationK = """
|/**
| * This is a multi-line documentation string. There is no space at the end of the
| * first line, but "the first" with no space should not appear in the final documentation.
| */
| fun foo(): String
""".render().documentation().first() as DescriptionComponent
val documentationJ = """
|/**
| * This is a multi-line documentation string. There is no space at the end of the
| * first line, but "the first" with no space should not appear in the final documentation.
| */
| public String foo()
""".render(java = true).documentation().first() as DescriptionComponent
for (documentation in listOf(documentationK, documentationJ)) {
assertThat("thefirst" in documentation.text()).isFalse()
}
}
@Test
fun `Test several odd link choices`() {
val documentation = """
|/**
| * Does bar on [this]
| */
|fun Foo.bar() {}
""".render().documentation().first() as DescriptionComponent
val docChildren = documentation.data.components.single().children
assertThat(docChildren.size).isEqualTo(2)
val link = (docChildren.last() as DocumentationLink)
assertThat((link.children.single() as Text).body).isEqualTo("this")
assertThat(link.dri.packageName).isEqualTo("androidx.example")
assertThat(link.dri.classNames).isEqualTo(null)
assertThat((link.dri.callable as Callable).name).isEqualTo("<this>")
}
@Test
fun `Multiline doc from fragment, with formatting`() {
val module = """
|/**
| * Instantiates a Fragment's view.
| *
| * @param parent The parent that the created view will be placed
| * in; <em>note that this may be null</em>.
| * @param name Tag name to be inflated.
| * @param context The context the view is being created in.
| * @param attrs Inflation attributes as specified in XML file.
| *
| * @return view the newly created view
| */
|@Nullable
|public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context,
| @NonNull AttributeSet attrs) {
| return mHost.mFragmentManager.getLayoutInflaterFactory()
| .onCreateView(parent, name, context, attrs);
|}
""".render(java = true)
val paramDocText = (module.documentation(doc = {
this.function()!!.parameters.single { it.name == "parent" }
}).first() as DescriptionComponent).text()
assertThat("placedin" in paramDocText).isFalse()
}
@Test
fun `@deprecated description works over multiple lines`() {
val documentation = """
|/**
| * Return the target fragment set by {@link #setTargetFragment}.
| *
| * @deprecated Instead of using a target fragment to pass results, use
| * {@link androidx.fragment.app.FragmentManager#setFragmentResult(java.lang.String,android.os.Bundle) FragmentManager#setFragmentResult(String, Bundle)} to deliver results to
| * {@link androidx.fragment.app.FragmentResultListener FragmentResultListener} instances registered by other fragments via
| * {@link androidx.fragment.app.FragmentManager#setFragmentResultListener(java.lang.String,androidx.lifecycle.LifecycleOwner,androidx.fragment.app.FragmentResultListener) FragmentManager#setFragmentResultListener(String, LifecycleOwner,
| * LastLine)}.
| */
| @Deprecated
|public void foo(){}
""".render(java = true)
val doc = documentation.documentation()
val functionDesc = doc.first() as DefaultDescriptionComponent
assertThat(functionDesc.data.deprecation).isNotNull()
// Checking the root for LastLine is somewhat testing Dokka
// but this was broken in a previous version
assertThat(functionDesc.text()).contains("LastLine")
assertThat(functionDesc.text()).contains("Instead of using")
assertThat(functionDesc.data.components.single().children.size).isEqualTo(7)
}
@Test // NOTE: render-to-text puts two spaces where the <pre> was. Is this correct?
fun `Test code blocks with @literals and nesting`() {
val documentation = """
|/**
| * Below is a sample of a simple database.
| * <pre>
| * // File: Song.java
| * {@literal @}Entity
| * public class Song {
| */
|public void foo(){}
""".render(java = true)
val doc = documentation.documentation()
val functionDesc = doc.first() as DefaultDescriptionComponent
assertThat(functionDesc.text()).isEqualTo("Below is a sample of a simple database." +
" // File: Song.java\n @ Entity\npublic class Song {")
assertThat(functionDesc.data.components.last()).isInstanceOf(Pre::class.java)
}
@Test
fun `Full documentation has receiver param`() {
val documentation = """
|/** @receiver blah */
|fun Int.foo()
""".render().documentation()
val paramSummary = documentation.last() as SummaryList
val paramParam = (paramSummary.item().data.title as ParameterComponent).data
assertThat(paramSummary.title()).isEqualTo("Parameters")
javaOnly {
assertThat(paramParam.name).isEqualTo("receiver")
}
kotlinOnly {
assertThat(paramParam.name).isEqualTo("")
}
}
@Test
fun `Full documentation has return type`() {
val documentation = """
|/** @return blah */
|fun foo() = Unit
""".render().documentation()
val paramSummary = documentation.last() as SummaryList
val returns = paramSummary.item()
assertThat(paramSummary.title()).isEqualTo("Returns")
assertThat(returns.data.title).isSameInstanceAs(NoopContextFreeComponent)
}
@Test
fun `Full Kotlin documentation has thrown exceptions`() {
val documentation = """
|/** @throws IllegalStateException if it fails */
|fun foo()
""".render().documentation()
val throwsSummary = documentation.first { (it as? SummaryList)?.title() == "Throws" }
as SummaryList
val throwsLeft = throwsSummary.item().data.title as Raw
val throwsRight = ((throwsSummary.item().data.description as DescriptionComponent)
.data.components.first().children.first() as Text)
assertThat(throwsLeft.data.text).contains("IllegalStateException")
assertThat(throwsRight.body).isEqualTo("if it fails")
}
@Test
fun `Full Java documentation has throws tag`() {
val documentation = """
|/** @throws IllegalStateException if it fails */
|public void foo() {}
""".render(java = true).documentation()
val throwsSummary = documentation.first { (it as? SummaryList)?.title() == "Throws" }
as SummaryList
val throwsLeft = throwsSummary.item().data.title as Raw
val throwsRight = ((throwsSummary.item().data.description as DescriptionComponent)
.data.components.first().children.first() as Text)
assertThat(throwsLeft.data.text).isEqualTo("java.lang.IllegalStateException")
assertThat(throwsRight.body).isEqualTo("if it fails")
}
@Ignore // b/203691421
@Test
fun `Full Java documentation has checked exceptions`() {
val documentation = """
|/** IllegalStateException if it fails */
|public void foo() throws IllegalStateException {}
""".render(java = true).documentation()
val throwsSummary = documentation.first { (it as? SummaryList)?.title() == "Throws" }
as SummaryList
val throwsLeft = throwsSummary.item().data.title as Raw
val throwsRight = ((throwsSummary.item().data.description as DescriptionComponent)
.data.components.first().children.first() as Text)
assertThat(throwsLeft.data.text).isEqualTo("java.lang.IllegalStateException")
}
@Test
fun `Full documentation has see alsos`() {
val documentation = """
|/** @see String blah */
|fun foo()
""".render().documentation()
val paramSummary = documentation.last() as SummaryList
val paramText = paramSummary.item()
assertThat(paramSummary.title()).isEqualTo("See also")
assertThat(paramText.link().name).isEqualTo("String")
assertThat(paramText.description().text()).isEqualTo("blah")
}
@Test
fun `See also parses external link`() {
val documentation = """
|/** @see String */
|class Foo
""".render().documentation()
val paramSummary = documentation.last() as SummaryList
val paramText = paramSummary.item()
assertPath(paramText.link().url, "kotlin/String.html")
assertThat(paramText.description().text()).isEmpty()
}
@Test
fun `See also parses internal link`() {
val documentation = """
|/** @see Bar */
|class Foo { class Bar }
""".render().documentation()
val paramSummary = documentation.last() as SummaryList
val paramText = paramSummary.item()
assertPath(paramText.link().url, "androidx/example/Foo.Bar.html")
assertThat(paramText.description().text()).isEmpty()
}
@Test
fun `See also parses link with Kotlin style function`() {
val documentation = """
|/** @see String.isEmpty */
|class Foo
""".render().documentation()
val paramSummary = documentation.last() as SummaryList
val paramText = paramSummary.item()
// TODO(b/167437580): figure out how to reliably parse links
assertThat(paramText.link().url).contains("isEmpty")
assertThat(paramText.description().text()).isEmpty()
}
@Test
fun `See also parses link with Java style function`() {
val documentation = """
|/** @see String#isEmpty() */
|public void foo() {}
""".render(java = true).documentation()
val paramSummary = documentation.last() as SummaryList
val paramText = paramSummary.item()
assertPath(paramText.link().url, "java/lang/String.html#isEmpty()")
assertThat(paramText.description().text()).isEmpty()
}
@Test
fun `See also parses link with unresolved function`() {
val documentation = """
|/** @see com.example.foo.Foo#bar() */
|public void foo() {}
""".render(java = true).documentation()
val paramSummary = documentation.last() as SummaryList
val paramText = paramSummary.item()
assertPath(paramText.link().url, "com/example/foo/Foo.html#bar()")
}
@Test
fun `See also parses link with class`() {
val documentation = """
|/** @see String */
|public void foo() {}
""".render(java = true).documentation()
val paramSummary = documentation.last() as SummaryList
val paramText = paramSummary.item()
assertPath(paramText.link().url, "java/lang/String.html")
}
@Test
fun `See also parses link with unresolved class`() {
val documentation = """
|/** @see com.example.foo.Foo */
|public void foo() {}
""".render(java = true).documentation()
val paramSummary = documentation.last() as SummaryList
val paramText = paramSummary.item()
assertPath(paramText.link().url, "com/example/foo/Foo.html")
}
@Test
fun `Full documentation sorts tags in pre-defined order`() {
val documentation = """
|/**
| * @see String
| * @param a
| * @return
| */
|fun foo(a: String)
""".render().documentation()
val returnSummary = documentation[1] as SummaryList
val paramSummary = documentation[2] as SummaryList
val seeSummary = documentation[3] as SummaryList
assertThat(returnSummary.title()).isEqualTo("Returns")
assertThat(paramSummary.title()).isEqualTo("Parameters")
assertThat(seeSummary.title()).isEqualTo("See also")
}
@Test
fun `Full documentation sorts params in given order`() {
val expected = listOf("a", "b", "c")
val documentation = """
|/**
| * @param b
| * @param c
| * @param a
| */
|fun foo(a: String, b: String, c: String)
""".render().documentation(paramNames = expected)
val paramSummary = documentation.last() as SummaryList
val params = paramSummary.items(3)
for ((i, param) in params.withIndex()) {
assertThat((param.data.title as ParameterComponent).data.name).isEqualTo(expected[i])
}
}
@Test
fun `@sample annotation in kotlin fails if the target samples doesn't exist`() {
val documentation = """
|/**
| * a very foo description
| *
| * @sample foo.samples.fooSample
| */
|fun foo(a: String, b: String, c: String)
""".trimIndent()
assertFails { documentation.render().documentation() }
}
@Test
fun `Verify that several real code samples don't give warnings`() {
val standardOut = System.out
val outputStreamCaptor = ByteArrayOutputStream()
System.setOut(PrintStream(outputStreamCaptor))
val module = """
|/**
| * DSL for constructing a new [DynamicGraphNavigator.DynamicNavGraph]
| *
| * @param provider [NavigatorProvider] to use.
| * @param id NavGraph id.
| * @param startDestination Id start destination in the graph
| */
|@NavDestinationDsl
|public class DynamicNavGraphBuilder(
| provider: NavigatorProvider,
| @IdRes id: Int,
| @IdRes private var startDestination: Int
|) {}
|
| /**
| * ParcelableArrayType is used for [NavArgument]s which hold arrays of Parcelables.
| *
| * Null values are supported.
| * Default values in Navigation XML files are not supported.
| *
| * @param type the type of Parcelable component class of the array
| */
| public class ParcelableArrayType<D : Parcelable>(type: Class<D>) : NavType<Array<D>?>(true) {
| /**
| * Constructs a NavType that supports arrays of a given Parcelable type.
| */
| init {
| require(Parcelable::class.java.isAssignableFrom(type)) {
| " type | does not implement Parcelable."
| }
| val arrayType: Class<Array<D>> = try {
| @Suppress("UNCHECKED_CAST")
| Class.forName("[L |type.name |;") as Class<Array<D>>
| } catch (e: ClassNotFoundException) {
| throw RuntimeException(e) // should never happen
| }
| this.arrayType = arrayType
| }
| }
""".render()
val holder = runBlocking { DocumentablesHolder(module, this) }
val classGraph = runBlocking { holder.classGraph() }
val classConverter1 = ClasslikeDocumentableConverter(language,
module.explicitClasslike("DynamicNavGraphBuilder")!!,
pathProvider(classGraph = classGraph),
holder
)
val documentedClass1 = runBlocking { classConverter1.classlike() }
val classConverter2 = ClasslikeDocumentableConverter(language,
module.explicitClasslike(
"ParcelableArrayType")!!,
pathProvider(classGraph = classGraph),
holder)
val documentedClass2 = runBlocking { classConverter2.classlike() }
assertThat(outputStreamCaptor.toString()).doesNotContain("WARNING")
System.setOut(standardOut)
}
@Test
fun `Test @return on property parameters`() {
val module = """
|public class Foo private constructor(
| /**
| * The arguments used for this entry
| * @return The arguments used when this entry was created
| *
| */
| @set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
| public var arguments: List<String>? = null
|) {}
""".render()
val documentation = module.documentation({ this.property("arguments")!! })
val description = (documentation.first() as DescriptionComponent).text()
assertThat(description == "The arguments used for this entry")
val table = (documentation.last() as SummaryList)
assertThat(table.title()).isEqualTo("Returns")
val tableEntry = table.item().description().text()
assertThat(tableEntry).isEqualTo("The arguments used when this entry was created")
}
@Test // Note: this tests upstream behavior.
fun `Test spacing around line wraps including inside tags`() {
val documentation = """
|/**
| * blah blah blah blah {@link #longFunctionName(java.lang.Object, java.lang.String, int,
| * java.lang.Integer) LongContainingClassName#longFunctionName(Object, String, int,
| * Integer)}
| */
|public void longFunctionName(Object arg1, String arg2, int arg3, Integer arg4) {}
|
""".render(java = true).documentation({ this.function("longFunctionName")!! })
val components = (documentation.single() as DescriptionComponent)
.data.components.single().children
val link = components[1] as DocumentationLink
assertThat(link.text()).doesNotContain("int,Integer") // should be a space here
}
@Test
fun `from ExoPlayer2, @link split across lines works`() {
val documentation = """
|public @interface Mega {
| /**
| * Reason why playback is suppressed even though {@link #getPlayWhenReady()} is {@code true}. One
| * of {@link #PLAYBACK_SUPPRESSION_REASON_NONE} or {@link
| * #PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS}.
| */
| @Documented
| @Retention(RetentionPolicy.SOURCE)
| @IntDef({
| PLAYBACK_SUPPRESSION_REASON_NONE,
| PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS
| })
| @interface PlaybackSuppressionReason {}
| /** Playback is not suppressed. */
| int PLAYBACK_SUPPRESSION_REASON_NONE = 0;
| /** Playback is suppressed due to transient audio focus loss. */
| int PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS = 1;
|
| public void longFunctionName(Object arg1, String arg2, int arg3, Integer arg4)
|}
""".render(java = true)
.documentation({ this.explicitClasslike("PlaybackSuppressionReason") })
val components = (documentation.single() as DescriptionComponent)
.data.components.single().children
val link1 = components[5] as DocumentationLink
val link2 = components[7] as DocumentationLink
// assertThat(link1.dri.packageName).isEqualTo("androidx.example")
// assertThat(link2.dri.packageName).isEqualTo("androidx.example")
assertThat(link1.dri.classNames).isEqualTo("Test.Mega")
assertThat(link2.dri.classNames).isEqualTo("Test.Mega")
assertThat(link1.dri.callable!!.name).isEqualTo("PLAYBACK_SUPPRESSION_REASON_NONE")
assertThat(link2.dri.callable!!.name)
.isEqualTo("PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS")
assertThat(link1.text()).isEqualTo("PLAYBACK_SUPPRESSION_REASON_NONE")
assertThat(link2.text())
.isEqualTo("PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS")
}
private fun DModule.description(doc: DModule.() -> Documentable = ::smartDoc):
DescriptionComponent {
val holder = runBlocking { DocumentablesHolder(this@description, this) }
val classGraph = runBlocking { holder.classGraph() }
val converter = DocTagConverter(language, pathProvider(classGraph = classGraph), holder)
val annotations = (this.doc() as? WithExtraProperties<*>)?.annotations().orEmpty()
return converter.summaryDescription(this.doc(), annotations)
}
private fun DModule.documentation(
doc: DModule.() -> Documentable = ::smartDoc,
paramNames: List<String> = emptyList()
): List<ContextFreeComponent> {
val holder = runBlocking { DocumentablesHolder(this@documentation, this) }
val classGraph = runBlocking { holder.classGraph() }
val converter = DocTagConverter(language, pathProvider(classGraph = classGraph), holder)
return converter.metadata(
doc(),
returnType = NoopContextFreeComponent,
paramNames = paramNames
)
}
private fun DModule.clazz(name: String? = null): DClass {
val topClass = name?.let { this.explicitClasslike(name) } ?: this.classlike()!!
if (topClass.classlikes.isNotEmpty()) return topClass.classlikes.single() as DClass
return topClass as DClass
}
/** In case you aren't explicit, our best guess at what you want docs for. */
private fun smartDoc(module: DModule): Documentable {
return module.function() ?: module.classlike()!!
}
private fun ContextFreeComponent.render() = createHTML().body {
this@render.render(this)
}.trim()
companion object {
@JvmStatic
@Parameterized.Parameters(name = "{0}")
fun data() = listOf(
arrayOf(Language.JAVA),
arrayOf(Language.KOTLIN)
)
}
}