| /* |
| * 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.capitalize |
| import com.google.devsite.components.DescriptionComponent |
| import com.google.devsite.components.Raw |
| import com.google.devsite.components.pages.Classlike |
| import com.google.devsite.components.pages.DevsitePage |
| import com.google.devsite.components.symbols.FunctionSignature |
| import com.google.devsite.components.symbols.SymbolDetail |
| import com.google.devsite.components.symbols.SymbolSummary |
| import com.google.devsite.components.table.SingleColumnSummaryItem |
| import com.google.devsite.components.table.SummaryList |
| import com.google.devsite.components.table.TableTitle |
| import com.google.devsite.renderer.Language |
| import com.google.devsite.renderer.converters.testing.content |
| import com.google.devsite.renderer.converters.testing.description |
| 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.size |
| import com.google.devsite.renderer.converters.testing.summary |
| import com.google.devsite.renderer.converters.testing.summaryItemsFor |
| import com.google.devsite.renderer.converters.testing.symbolsFor |
| import com.google.devsite.renderer.converters.testing.text |
| import com.google.devsite.renderer.converters.testing.title |
| import com.google.devsite.testing.ConverterTestBase |
| import kotlinx.coroutines.runBlocking |
| import org.jetbrains.dokka.model.DModule |
| import org.junit.Ignore |
| import org.junit.Test |
| import org.junit.runner.RunWith |
| import org.junit.runners.Parameterized |
| import kotlin.test.assertFails |
| |
| @RunWith(Parameterized::class) |
| internal class ClasslikeDocumentableConverterTest( |
| private val displayLanguage: Language |
| ) : ConverterTestBase(displayLanguage) { |
| @Test |
| fun `Classlike creates components with correct title`() { |
| val page = """ |
| |class Foo |
| """.render().page() |
| |
| assertThat(page.data.title).isEqualTo("Foo") |
| } |
| |
| @Test |
| fun `Classlike creates components with correct path`() { |
| val page = """ |
| |class Foo |
| """.render().page() |
| |
| assertThat(page.data.path).isEqualTo("androidx/example/Foo.html") |
| } |
| |
| @Test |
| fun `Classlike creates components with correct book path`() { |
| val page = """ |
| |class Foo |
| """.render().page() |
| |
| assertPath(page.data.bookPath, "androidx/_book.yaml") |
| } |
| |
| @Test |
| fun `Empty classlike has no symbols`() { |
| val page = """ |
| |interface Foo |
| """.render().page() |
| |
| val classlike = page.content<Classlike>() |
| |
| for ((summary, symbol) in classlike.data.symbolTypes) { |
| assertThat(summary.hasContent()).isFalse() |
| assertThat(symbol.symbols).isEmpty() |
| } |
| } |
| |
| @Test |
| fun `Public function gets documented`() { |
| val page = """ |
| |class Foo { |
| | fun foo() = Unit |
| |} |
| """.render().page() |
| |
| val classlike = page.content<Classlike>() |
| val summary = classlike.methodSummaryItems() |
| |
| assertThat(summary.single().name()).isEqualTo("foo") |
| } |
| |
| @Test |
| fun `@jvmName functions get documented and sorted by correct name`() { |
| val page = """ |
| |class Foo { |
| | @JvmName("bar") |
| | fun foo() = Unit |
| | |
| | @JvmName("aar") |
| | fun zoo() = Unit |
| |} |
| """.render().page() |
| |
| javaOnly { |
| val classlike = page.content<Classlike>() |
| val (summary) = classlike.symbolsFor("Public methods") |
| assertThat(summary.items().first().summary().name()).isEqualTo("aar") |
| assertThat(summary.items().last().summary().name()).isEqualTo("bar") |
| } |
| kotlinOnly { |
| val classlike = page.content<Classlike>() |
| val (summary) = classlike.symbolsFor("Public functions") |
| assertThat(summary.items().first().summary().name()).isEqualTo("foo") |
| assertThat(summary.items().last().summary().name()).isEqualTo("zoo") |
| } |
| } |
| |
| @Test |
| fun `Properties are documented in sorted order`() { |
| val expected = listOf("a", "b", "c") |
| val documentation = """ |
| |class Foo { |
| | /** @property b b_doc */ |
| | public val b: String |
| | /** @property c c_doc */ |
| | public val c: String |
| | /** @property a a_doc */ |
| | public val a: String |
| |} |
| """.render().page() |
| |
| val propertiesSummary = documentation.content<Classlike>().propertySummaryItems() |
| val props = propertiesSummary.items(3) |
| |
| for ((i, prop) in props.withIndex()) { |
| assertThat((prop.data.description as SymbolSummary).name()).isEqualTo(expected[i]) |
| } |
| } |
| |
| @Test |
| fun `@JvmSynthetic methods are not documented in java`() { |
| val page = """ |
| |class Foo { |
| | fun foo() = Unit |
| | |
| | @JvmSynthetic |
| | fun zoo() = Unit |
| |} |
| """.render().page() |
| |
| val classlike = page.content<Classlike>() |
| val summary = classlike.methodSummaryItems() |
| |
| javaOnly { |
| assertThat(summary.size).isEqualTo(1) |
| } |
| kotlinOnly { |
| assertThat(summary.size).isEqualTo(2) |
| } |
| } |
| |
| @Test |
| fun `Protected function gets documented`() { |
| val page = """ |
| |abstract class Foo { |
| | protected open fun foo() = Unit |
| |} |
| """.render().page() |
| |
| val classlike = page.content<Classlike>() |
| val (summary) = classlike.symbolsFor(protectedMethodsTitle(displayLanguage)) |
| |
| assertThat(summary.item().summary().name()).isEqualTo("foo") |
| } |
| |
| @Test |
| fun `Public property gets documented`() { |
| val page = """ |
| |class Foo { |
| | val foo = Unit |
| |} |
| """.render().page() |
| |
| val classlike = page.content<Classlike>() |
| val summary = classlike.propertySummaryItems() |
| |
| assertThat(summary.single().summary().name()).isEqualTo("foo") |
| } |
| |
| @Test |
| fun `Protected property gets documented`() { |
| val page = """ |
| |abstract class Foo { |
| | protected open val foo = Unit |
| |} |
| """.render().page() |
| |
| val classlike = page.content<Classlike>() |
| val (summary) = classlike.symbolsFor(protectedPropertiesTitle(displayLanguage)) |
| |
| assertThat(summary.item().summary().name()).isEqualTo("foo") |
| } |
| |
| @Test |
| fun `Public constructor gets documented`() { |
| val page = """ |
| |class Foo |
| """.render().page() |
| |
| val classlike = page.content<Classlike>() |
| val (summary) = classlike.symbolsFor("Public constructors") |
| |
| assertThat(summary.constructor().name()).isEqualTo("Foo") |
| } |
| |
| @Test |
| fun `Function summary component hides DeprecationLevel HIDDEN`() { |
| val module = """ |
| |import kotlin.DeprecationLevel.HIDDEN |
| | |
| |class Visible { |
| | public val visible = "v" |
| | @Deprecated("No show!", level = HIDDEN) |
| | public val invisible = "i" |
| | |
| | public fun show() = 7 |
| | @Deprecated("No show!", level = HIDDEN) |
| | public fun noShow() = 5 |
| |} |
| | |
| |@Deprecated("No show!", level = HIDDEN) |
| |class Nope |
| """.render() |
| |
| assertThat(module.packages.single().classlikes.map { it.name }).containsExactly("Visible") |
| val visible = module.page("Visible").content<Classlike>() |
| |
| assertThat(visible.methodSummaryItems().map { it.name() }).containsExactly("show") |
| assertThat(visible.propertySummaryItems().map { it.name() }).containsExactly("visible") |
| } |
| |
| @Test |
| fun `Public constructor does not have @NonNull in 4x Kotlin and Java`() { |
| val constructorsK = """ |
| |class Foo { |
| | constructor() {} |
| |} |
| """.render().page().content<Classlike>().symbolsFor("Public constructors") |
| val constructorsJ = """ |
| |public class Foo { |
| | public Foo() {} |
| |} |
| """.render(java = true).page().content<Classlike>().symbolsFor("Public constructors") |
| |
| for (constructor in listOf(constructorsJ, constructorsK)) { |
| // Constructor summaries are SingleColumnSummaryItems containing SymbolSummaries |
| // They cannot have annotations, enforced by design. |
| |
| val detail = constructor.second.symbols.single() as SymbolDetail |
| val returnAnnotations = detail.data.returnType.data.annotationComponents |
| val annotations = detail.data.annotationComponents |
| val signature = detail.data.signature as FunctionSignature |
| assertThat(returnAnnotations.isEmpty()) |
| assertThat(annotations).isEmpty() |
| assertThat(signature.data.receiver).isNull() |
| } |
| } |
| |
| @Test |
| fun `Protected constructor gets documented`() { |
| val page = """ |
| |open class Foo protected constructor() |
| """.render().page() |
| |
| val classlike = page.content<Classlike>() |
| val (summary) = classlike.symbolsFor("Protected constructors") |
| |
| assertThat(summary.constructor().name()).isEqualTo("Foo") |
| } |
| |
| @Test |
| fun `Nested type gets documented`() { |
| val page = """ |
| |class Foo { |
| | class Bar |
| |} |
| """.render().page() |
| |
| val classlike = page.content<Classlike>() |
| val (summary) = classlike.symbolsFor("Nested types") |
| assertThat(summary.item().link().name).isEqualTo("Foo.Bar") |
| } |
| |
| @Test |
| fun `Companion objects are documented in Java but not Kotlin because they're inlined`() { |
| val page = """ |
| |class Foo { |
| | companion object Bar |
| |} |
| """.render().page() |
| |
| val classlike = page.content<Classlike>() |
| val (summary) = classlike.symbolsFor("Nested types") |
| kotlinOnly { |
| assertThat(summary.items()).hasSize(0) |
| } |
| javaOnly { |
| assertThat(summary.item().link().name).isEqualTo("Foo.Bar") |
| } |
| } |
| |
| @Test |
| fun `Direct subclasses are found`() { |
| val page = """ |
| |abstract class Foo |
| |open class C : B |
| |open class B : Foo |
| |open class A : Foo |
| """.render().page() |
| |
| val classlike = page.content<Classlike>() |
| val subclasses = classlike.data.relatedSymbols.data.directSubclasses.items(2) |
| |
| assertThat(subclasses.first().data.name).isEqualTo("A") |
| assertThat(subclasses.last().data.name).isEqualTo("B") |
| } |
| |
| @Test |
| fun `Indirect subclasses are found`() { |
| val page = """ |
| |abstract class Foo |
| |open class C : Foo |
| |open class B : C |
| |open class A : B |
| """.render().page() |
| |
| val classlike = page.content<Classlike>() |
| val subclasses = classlike.data.relatedSymbols.data.indirectSubclasses.items(2) |
| |
| assertThat(subclasses.first().data.name).isEqualTo("A") |
| assertThat(subclasses.last().data.name).isEqualTo("B") |
| } |
| |
| @Test |
| fun `Class with root object as parent does not have hierarchy`() { |
| val page = """ |
| |class Foo |
| """.render().page() |
| |
| val classlike = page.content<Classlike>() |
| val parents = classlike.data.hierarchy.data.parents |
| |
| assertThat(parents).isEmpty() |
| } |
| |
| @Test |
| fun `Class with single parent has hierarchy with root object, parent, and itself`() { |
| val page = """ |
| |abstract class Parent |
| |class Foo : Parent |
| """.render().page() |
| |
| val classlike = page.content<Classlike>() |
| val parents = classlike.data.hierarchy.data.parents.items(3).toList() |
| |
| javaOnly { assertThat(parents[0].data.name).isEqualTo("Object") } |
| kotlinOnly { assertThat(parents[0].data.name).isEqualTo("Any") } |
| assertThat(parents[1].data.name).isEqualTo("Parent") |
| assertThat(parents[2].data.name).isEqualTo("Foo") |
| } |
| |
| @Test |
| fun `Class with multiple parents has hierarchy with root object and parents`() { |
| val page = """ |
| |abstract class A |
| |abstract class B : A |
| |abstract class C : B |
| |class Foo : C |
| """.render().page() |
| |
| val classlike = page.content<Classlike>() |
| val parents = classlike.data.hierarchy.data.parents.items(5).toList() |
| |
| javaOnly { assertThat(parents[0].data.name).isEqualTo("Object") } |
| kotlinOnly { assertThat(parents[0].data.name).isEqualTo("Any") } |
| assertThat(parents[1].data.name).isEqualTo("A") |
| assertThat(parents[2].data.name).isEqualTo("B") |
| assertThat(parents[3].data.name).isEqualTo("C") |
| assertThat(parents[4].data.name).isEqualTo("Foo") |
| } |
| |
| @Test |
| fun `Class signature appears with extends and implements for internal types in 4x`() { |
| val pageK = """ |
| |interface A |
| |abstract class B |
| |class Foo : A, B |
| """.render().page("Foo") |
| val pageJ = """ |
| |public interface A {} |
| |public abstract class B {} |
| |public class Foo extends Test.B implements Test.A {} |
| """.render(java = true).page("Foo") |
| |
| for (page in listOf(pageJ, pageK)) { |
| val prefix = if (page == pageK) "" else "Test." |
| val classSignature = page.content<Classlike>().data.signature.data |
| assertThat(classSignature.type).isEqualTo("class") |
| assertThat(classSignature.extends.single().data.name).isEqualTo("${prefix}B") |
| assertThat(classSignature.implements.single().data.name).isEqualTo("${prefix}A") |
| } |
| } |
| |
| @Ignore // b/170124934 |
| @Test |
| fun `Class signature appears with extends or implements for external types in 4x`() { |
| val pageExternalK = """ |
| |/** |
| | * An implementation of [Lazy] used by [android.app.Activity.navArgs] and |
| | * [androidx.fragment.app.Fragment.navArgs]. |
| | * |
| | * [argumentProducer] is a lambda that will be called during initialization to provide |
| | * arguments to construct an [Args] instance via reflection. |
| | */ |
| |public class NavArgsLazy<Args : String>( |
| | private val navArgsClass: KClass<Args>, |
| | private val argumentProducer: () -> Bundle |
| |) : Lazy<Args> { |
| """.render().page(name = "NavArgsLazy") |
| val pageExternalJ = """ |
| |public class JavaArgsLazy<Args extends String>() implements Lazy<Args> {} |
| """.render(java = true).page(name = "NavArgsLazy") |
| } |
| |
| @Test |
| fun `Primary constructor can be @suppress-ed without hiding the class itself`() { |
| // Primary constructor suppression is broken upstream |
| // https://github.com/Kotlin/dokka/issues/1953 |
| val modulePrimary = """ |
| |public class BenchmarkState |
| | /** @suppress */ |
| | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) |
| | constructor(val foo: String) { |
| |} |
| """.render() |
| assertFails { |
| val page = modulePrimary.page("BenchmarkState") |
| } |
| // We can convert to equivalent secondary constructor and it works |
| val moduleSecondary = """ |
| |public class BenchmarkState { |
| | val foo: String |
| | /** @suppress */ |
| | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) |
| | constructor(foo: String) { this.foo = foo } |
| |} |
| """.render() |
| val page = moduleSecondary.page("BenchmarkState").content<Classlike>() |
| assertThat(page.symbolsFor("Public constructors").first.size()).isEqualTo(0) |
| } |
| |
| @Test |
| fun `Enum class is rendered and has enum values with types in 4x Kotlin and Java`() { |
| val pageK = """ |
| |/** |
| | * class level docs |
| | */ |
| |enum class AnEnumType { |
| | /** |
| | * content being refreshed, which can be a result of |
| | * invalidation, refresh that may contain content updates, or the initial load. |
| | */ |
| | REFRESH, |
| | /** |
| | * Load at the start |
| | */ |
| | PREPEND, |
| | /** |
| | * Load at the end. |
| | */ |
| | APPEND |
| | |
| | fun foo() |
| |} |
| """.render().page(name = "AnEnumType") |
| val pageJ = """ |
| |/** |
| | * class level docs |
| | */ |
| |public enum AnEnumType { |
| | /** |
| | * content being refreshed, which can be a result of |
| | * invalidation, refresh that may contain content updates, or the initial load. |
| | */ |
| | REFRESH, |
| | /** |
| | * Load at the start |
| | */ |
| | PREPEND, |
| | /** |
| | * Load at the end. |
| | */ |
| | APPEND |
| | |
| | fun foo() |
| |} |
| """.render(java = true).page(name = "AnEnumType") |
| |
| for (page in listOf(pageK, pageJ)) { |
| val classlike = page.content<Classlike>() |
| val signature = classlike.data.signature.data |
| val description = (classlike.data.description.first() as DescriptionComponent) |
| |
| val (enumSummary, enumDetails) = classlike.data.symbolTypes.first { |
| (it.first as? SummaryList)?.title() == "Enum Values" |
| } |
| val enumTable = enumSummary.items(3) as List |
| val enumOne = enumTable[0].data |
| val enumTwo = enumTable[1].data |
| val enumThree = enumTable[2].data |
| |
| assertThat(signature.type).isEqualTo("enum") |
| assertThat(description.text()).isEqualTo("class level docs") |
| |
| assertThat((enumOne.title as Raw).data.text).contains("APPEND") |
| assertThat((enumOne.description as DescriptionComponent).text()) |
| .contains("Load at the end.") |
| assertThat((enumTwo.title as Raw).data.text).contains("PREPEND") |
| assertThat((enumTwo.description as DescriptionComponent).text()) |
| .contains("Load at the start") |
| assertThat((enumThree.title as Raw).data.text).contains("REFRESH") |
| assertThat((enumThree.description as DescriptionComponent).text()) |
| .contains("result of invalidation") |
| |
| val enumName = enumDetails.symbols[0] as SymbolDetail |
| val returnType = enumName.data.returnType.link() |
| assertThat(returnType.name).endsWith("AnEnumType") |
| } |
| } |
| |
| @Test |
| fun `Class component inherits docs from same language in 4x Kotlin and Java`() { |
| val pagesK = """ |
| | /** docs for foo */ |
| |class foo() { |
| | /** dew it */ |
| | fun doit() {} |
| | /** thunderous applause */ |
| | open val democracy = false |
| |} |
| |class bar() : foo { |
| | override fun doit() {} |
| | override val democracy = true |
| |} |
| | /** overriding docs for baz */ |
| |class baz() : foo { |
| | /** overriding function docs */ |
| | override fun doit() {} |
| | /** KotOR */ |
| | override val democracy = true |
| |} |
| |class maz() : foo { |
| | /** {@inheritDoc} */ |
| | override fun doit() {} |
| |} """.render() |
| val pagesJ = """ |
| |public class foo { |
| | /** dew it */ |
| | public void doit() {} |
| |} |
| |public class bar extends foo { |
| | /** {@inheritDoc} */ |
| | @Override |
| | public void doit() {} |
| |} |
| |public class baz extends foo { |
| | /** overriding function docs */ |
| | @Override |
| | public void doit() {} |
| |} |
| """.render(java = true) // Java {@inheritdoc} does not support classes & properties |
| for (pages in listOf(pagesK, pagesJ)) { |
| val fooClass = pages.page("foo").content<Classlike>() |
| val barClass = pages.page("bar").content<Classlike>() |
| val bazClass = pages.page("baz").content<Classlike>() |
| |
| // Function description inheritance does not work properly |
| val fooDoit = fooClass.methodSummaryItems().single() |
| val fooDoitDocs = fooDoit.summary().data.description.text() |
| val barDoit = barClass.methodSummaryItems().single() |
| val barDoitDocs = barDoit.summary().data.description.text() |
| val bazDoit = bazClass.methodSummaryItems().single() |
| val bazDoitDocs = bazDoit.summary().data.description.text() |
| |
| assertThat(fooDoitDocs).isEqualTo("dew it") |
| assertThat(barDoitDocs).isEqualTo("dew it") |
| assertThat(bazDoitDocs).isEqualTo("overriding function docs") |
| |
| if (pages == pagesK) { |
| // overriding class docs is maybe something we want in kotlin, but is not jdoc spec |
| val fooDescription = (fooClass.data.description.first() as DescriptionComponent) |
| val barDescription = (barClass.data.description.first() as DescriptionComponent) |
| val bazDescription = (bazClass.data.description.first() as DescriptionComponent) |
| assertThat(fooDescription.text()).isEqualTo("docs for foo") |
| // assertThat(barDescription.text()).isEqualTo("docs for foo") |
| assertThat(bazDescription.text()).isEqualTo("overriding docs for baz") |
| |
| // overriding properties is kotlin-only, and working |
| val fooDemocracy = fooClass.propertySummaryItems().single().summary() |
| assertThat(fooDemocracy.data.description.text()).isEqualTo("thunderous applause") |
| val barDemocracy = barClass.propertySummaryItems().single().summary() |
| assertThat(barDemocracy.data.description.text()).isEqualTo("thunderous applause") |
| val bazDemocracy = bazClass.propertySummaryItems().single().summary() |
| assertThat(bazDemocracy.data.description.text()).isEqualTo("KotOR") |
| |
| // Using {@inheritDoc} in kotlin is wrong |
| val mazClass = pages.page("maz").content<Classlike>() |
| val mazDoit = mazClass.methodSummaryItems().single() |
| val mazDoitDocs = mazDoit.summary().data.description.text() |
| assertThat(mazDoitDocs).isNotEqualTo("dew it") |
| } |
| } |
| } |
| |
| @Ignore // TODO: patch upstream dokka to implement kotlin documentation inheritance b/184361891 |
| @Test // TODO: add @constructor doc inheritance tests once that is implemented |
| fun `Property parameter documentation inherits properly`() { |
| val pages = """ |
| |/** |
| | * @param param1 param1_docs |
| | * @property property1 property1_docs |
| | */ |
| |class supclaz(val param1: String, val property1: Int) {} |
| |/** |
| | * @param param2 param2_docs |
| | * @property property2 property2_docs |
| | */ |
| |interface interfaz(val param2: String, val property2: Int) {} |
| |/** |
| | * @param param3 param3_docs |
| | * @property property3 property3_docs |
| | */ |
| |sealed class sealclaz(internal val param3: String, protected val property3: Int) {} |
| |class foo(param1: String, property1: Int, param2: String, property2: Int): supclaz(param1, property1), interfaz(param2, property2) |
| |/** |
| | * @param param1 override_param1_docs |
| | * @param param2 override_param2_docs |
| | * @property property1 override_property1_docs |
| | * @property property2 override_property2_docs |
| | */ |
| |class baz(param1: String, property1: Int, param2: String, property2: Int): supclaz(param1, property1), interfaz(param2, property2) |
| |class bar(param3: String, property3: Int): sealclaz(param3, property3) |
| """.render() |
| val fooClass = pages.page("foo").content<Classlike>() |
| val pparam1docs = fooClass.propertySymbol("param1").description() |
| val pparam2docs = fooClass.propertySymbol("param2").description() |
| val prop1docs = fooClass.propertySymbol("property1").description() |
| val prop2docs = fooClass.propertySymbol("property2").description() |
| val param1docs = ( |
| ( |
| fooClass.symbolsFor("Public constructors").second |
| .symbols.single() as SymbolDetail |
| ).data.metadata[1] as SummaryList |
| ) |
| .items().single { it.name() == "param1" }.description() |
| val param2docs = ( |
| ( |
| fooClass.symbolsFor("Public constructors").second |
| .symbols.single() as SymbolDetail |
| ).data.metadata[1] as SummaryList |
| ) |
| .items().single { it.name() == "param2" }.description() |
| assertThat(pparam1docs.text()).isEqualTo("param1_docs") |
| assertThat(prop1docs.text()).isEqualTo("property1_docs") |
| assertThat(pparam2docs.text()).isEqualTo("param2_docs") |
| assertThat(prop2docs.text()).isEqualTo("property2_docs") |
| assertThat(param1docs.text()).isEqualTo("param1_docs") |
| assertThat(param2docs.text()).isEqualTo("param2_docs") |
| val bazClass = pages.page("baz").content<Classlike>() |
| val zpparam1docs = bazClass.propertySymbol("param1").description() |
| val zpparam2docs = bazClass.propertySymbol("param2").description() |
| val zprop1docs = bazClass.propertySymbol("property1").description() |
| val zprop2docs = bazClass.propertySymbol("property2").description() |
| val zparam1docs = ( |
| ( |
| bazClass.symbolsFor("Public constructors").second |
| .symbols.single() as SymbolDetail |
| ).data.metadata[1] as SummaryList |
| ) |
| .items().single { it.name() == "param1" }.description() |
| val zparam2docs = ( |
| ( |
| bazClass.symbolsFor("Public constructors").second |
| .symbols.single() as SymbolDetail |
| ).data.metadata[1] as SummaryList |
| ) |
| .items().single { it.name() == "param2" }.description() |
| assertThat(zpparam1docs.text()).isEqualTo("override_param1_docs") |
| assertThat(zprop1docs.text()).isEqualTo("override_property1_docs") |
| assertThat(zpparam2docs.text()).isEqualTo("override_param2_docs") |
| assertThat(zprop2docs.text()).isEqualTo("override_property2_docs") |
| assertThat(zparam1docs.text()).isEqualTo("override_param1_docs") |
| assertThat(zparam2docs.text()).isEqualTo("override_param2_docs") |
| val barClass = pages.page("bar").content<Classlike>() |
| // TODO: patch upstream? dokka to support inheriting documentation on hidden components |
| val pparam3docs = barClass.propertySymbol("param3").description() |
| val prop3docs = barClass.propertySymbol("property3").description() |
| val param3docs = ( |
| ( |
| barClass.symbolsFor("Public constructors").second |
| .symbols.single() as SymbolDetail |
| ).data.metadata[1] as SummaryList |
| ) |
| .items().single { it.name() == "param3" }.description() |
| assertThat(pparam3docs.text()).isEqualTo("param3_docs") |
| assertThat(prop3docs.text()).isEqualTo("property3_docs") |
| assertThat(param3docs.text()).isEqualTo("param3_docs") |
| } |
| |
| @Test |
| fun `Level-jumping doc inheritance works in 4x Kotlin and Java`() { |
| val pageK = """ |
| | /** docs for foo */ |
| |class foo() { |
| | /** dew it */ |
| | fun doit() {} |
| |} |
| |class bar() : foo {} |
| | /** overriding docs for baz */ |
| |class baz() : bar { |
| | override fun doit() {} |
| |} |
| """.render().page("baz") |
| val pageJ = """ |
| | /** docs for foo */ |
| |public class foo() { |
| | /** dew it */ |
| | public void doit() {} |
| |} |
| |public class bar() extends foo {} |
| | /** overriding docs for baz */ |
| |public class baz() extends bar { |
| | /** {@inheritdoc} */ |
| | override public void doit() {} |
| |} |
| """.render(java = true).page("baz") |
| for (page in listOf(pageJ, pageK)) { |
| val doit = page.content<Classlike>().methodSummaryItems().single() |
| assertThat(doit.summary().data.description.text()).isEqualTo("dew it") |
| } |
| } |
| |
| @Test |
| fun `Inherited methods are sorted by name and arity`() { |
| val childClass = """ |
| |class Parent() { |
| | fun b(input: Int, zinput2: Int) {} |
| | fun c() {} |
| | fun a() {} |
| | fun b(input: Int, input2: Int) {} |
| | fun b() {} |
| | fun b(input: Int) {} |
| |} |
| |class Child() : Parent {} |
| """.render().page("Child").content<Classlike>() |
| val inheritedSummary = childClass.data.inheritedTypes.single().data.inheritedSymbolSummaries |
| val inheritedMethods = inheritedSummary.entries.single().value.data.items |
| val inheritedMethodSignatures = inheritedMethods.map { |
| (it.data.description as SymbolSummary).data.signature as FunctionSignature |
| } |
| assertThat(inheritedMethodSignatures.map { it.data.name.data.name }).isEqualTo( |
| listOf("a", "b", "b", "b", "b", "c") |
| ) |
| assertThat(inheritedMethodSignatures.map { it.data.parameters.size }).isEqualTo( |
| listOf(0, 0, 1, 2, 2, 0) |
| ) |
| val paramNames = inheritedMethodSignatures.map { |
| it.data.parameters.map { it.data.name } |
| } |
| assertThat(paramNames).isEqualTo( |
| listOf( |
| listOf(), |
| listOf(), |
| listOf("input"), |
| listOf("input", "input2"), |
| listOf("input", "zinput2"), |
| listOf() |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Inherited properties are not lost`() { |
| val page = """ |
| |open class Parent { |
| | val b: Int = 8 |
| | val a: String = "9" |
| |} |
| |class Child: Parent() |
| """.render().page("Child").content<Classlike>() |
| |
| val category = page.data.inheritedTypes.single().data.inheritedSymbolSummaries |
| val names = category.values.single().items().map { it.name() } |
| |
| assertThat(names).containsExactly("a", "b").inOrder() |
| } |
| |
| @Test |
| fun `Different categories of symbols inherited from different classes works`() { |
| val page = """ |
| |open class GrandParent { |
| | val grandC: Int = 18 |
| | fun grandA(): String = "9" |
| | fun grandB(): {} |
| |} |
| |open class Parent: GrandParent { |
| | val parentB: Int = 8 |
| | val parentA: String = "9" |
| | fun parentC(): {} |
| |} |
| |class Child: Parent() |
| """.render().page("Child").content<Classlike>() |
| |
| val categoriesNames = page.data.inheritedTypes.map { |
| (it.data.header as TableTitle).data.title |
| } |
| kotlinOnly { |
| assertThat(categoriesNames) |
| .containsExactly("Inherited functions", "Inherited properties").inOrder() |
| } |
| javaOnly { |
| assertThat(categoriesNames) |
| .containsExactly("Inherited methods", "Inherited fields").inOrder() |
| } |
| |
| val functions = page.data.inheritedTypes.first().data.inheritedSymbolSummaries |
| .mapKeys { it.key.data.name } |
| .mapValues { (_, list) -> list.items().map { it.name() } } |
| assertThat(functions).containsExactly( |
| "GrandParent", listOf("grandA", "grandB"), |
| "Parent", listOf("parentC") |
| ) |
| |
| val properties = page.data.inheritedTypes.last().data.inheritedSymbolSummaries |
| .mapKeys { it.key.data.name } |
| .mapValues { (_, list) -> list.items().map { it.name() } } |
| assertThat(properties).containsExactly( |
| "GrandParent", listOf("grandC"), |
| "Parent", listOf("parentA", "parentB") |
| ) |
| } |
| |
| @Test |
| fun `Class component creates inline generics`() { |
| val page = """ |
| |class Foo<T: Number, U>() {} |
| """.render().page() |
| val typeParams = page.content<Classlike>().data.signature.data.typeParameters |
| assertThat(typeParams.first().data.name).isEqualTo("T") |
| assertThat(typeParams.first().projectionName()).isEqualTo("Number") |
| assertThat(typeParams.last().data.name).isEqualTo("U") |
| kotlinOnly { assertThat(typeParams.last().projectionName()).isEqualTo("Any") } |
| javaOnly { assertThat(typeParams.last().projectionName()).isEqualTo("Object") } |
| } |
| |
| @Test |
| fun `Class component creates all symbols in the correct order`() { |
| val page = "class Foo {}".render().page() |
| val symbolTypes = page |
| .content<Classlike>() |
| .data |
| .symbolTypes |
| .map { it.second.title } |
| |
| javaOnly { |
| assertThat(symbolTypes).isEqualTo( |
| listOf( |
| "Nested types", |
| "Enum Values", |
| "Constants", |
| "Public fields", |
| "Protected fields", |
| "Public constructors", |
| "Protected constructors", |
| "Public methods", |
| "Protected methods" |
| ) |
| ) |
| } |
| |
| kotlinOnly { |
| assertThat(symbolTypes).isEqualTo( |
| listOf( |
| "Nested types", |
| "Enum Values", |
| "Constants", |
| "Public companion functions", |
| "Protected companion functions", |
| "Public companion properties", |
| "Protected companion properties", |
| "Public constructors", |
| "Protected constructors", |
| "Public functions", |
| "Protected functions", |
| "Public properties", |
| "Protected properties" |
| ) |
| ) |
| } |
| } |
| |
| @Test |
| fun `classlike companion functions are included in Kotlin and not Java`() { |
| val page = """ |
| |class Foo { |
| | companion object { |
| | fun bar() = Unit |
| | fun baz() = Unit |
| | } |
| |} |
| """.render().page() |
| |
| val classlike = page.content<Classlike>() |
| |
| kotlinOnly { |
| val companionFunctions = classlike.summaryItemsFor(publicCompanionFunctionsTitle()) |
| assertThat(companionFunctions).hasSize(2) |
| } |
| javaOnly { |
| classlike.assertNoSymbolsFor(protectedCompanionFunctionsTitle()) |
| } |
| } |
| |
| @Test |
| fun `protected companion functions are included in Kotlin and not Java`() { |
| val page = """ |
| |class Foo { |
| | companion object { |
| | protected fun bar() = Unit |
| | } |
| |} |
| """.render().page() |
| |
| val classlike = page.content<Classlike>() |
| kotlinOnly { |
| val companionFunctions = classlike.summaryItemsFor(protectedCompanionFunctionsTitle()) |
| assertThat(companionFunctions).hasSize(1) |
| } |
| javaOnly { |
| classlike.assertNoSymbolsFor(protectedCompanionFunctionsTitle()) |
| } |
| } |
| |
| @Test |
| fun `classlike companion properties are included in Kotlin and not Java`() { |
| val page = """ |
| |class Foo { |
| | companion object { |
| | val bar: List<String> = emptyList() |
| | protected val baz: Int = 1 |
| | } |
| |} |
| """.render().page() |
| |
| val classlike = page.content<Classlike>() |
| |
| kotlinOnly { |
| val publicCompanionProperties = classlike |
| .summaryItemsFor(publicCompanionPropertiesTitle()) |
| val protectedCompanionProperties = classlike |
| .summaryItemsFor(protectedCompanionPropertiesTitle()) |
| assertThat(publicCompanionProperties).hasSize(1) |
| assertThat(protectedCompanionProperties).hasSize(1) |
| } |
| javaOnly { |
| classlike.assertNoSymbolsFor(publicCompanionPropertiesTitle()) |
| classlike.assertNoSymbolsFor(protectedCompanionPropertiesTitle()) |
| } |
| } |
| |
| @Test |
| fun `Static and companion functions are treated correctly in both languages`() { |
| val moduleK = """ |
| |class Foo { |
| | companion object { |
| | val baz: Int = 1 |
| | fun bar() = Unit |
| | } |
| |} |
| """.render() |
| val classlikeK = moduleK.page().content<Classlike>() |
| |
| val classlikeJ = """ |
| |public class Foo { |
| | public static void foo() {} |
| | public static String bar = "bar" |
| |} |
| """.render(java = true).page().content<Classlike>() |
| |
| val companionClassK = moduleK.page("Companion") |
| |
| val (kotlinNestedTypeSummary) = classlikeK.symbolsFor("Nested types") |
| |
| val staticJavaMethod = classlikeJ.methodDetailsItems().single().data |
| val staticJavaField = classlikeJ.propertyDetailsItems().single().data |
| |
| // Companion class is included in both modules |
| assertThat(companionClassK).isNotNull() |
| |
| // can see java static methods in both languages |
| assertThat(staticJavaMethod.name).isEqualTo("foo") |
| assertThat(staticJavaField.name).isEqualTo("bar") |
| |
| // can find kotlin companion method in both languages |
| kotlinOnly { |
| // nested companion object is not documented because it is inlined |
| assertThat(kotlinNestedTypeSummary.items()).hasSize(0) |
| val companionFunctions = classlikeK.summaryItemsFor(publicCompanionFunctionsTitle()) |
| val companionProperties = classlikeK.summaryItemsFor(publicCompanionPropertiesTitle()) |
| assertThat(companionFunctions).hasSize(1) |
| assertThat(companionProperties).hasSize(1) |
| } |
| javaOnly { |
| // nested companion object is documented but companion functions are not inlined |
| assertThat(kotlinNestedTypeSummary.items()).hasSize(1) |
| classlikeK.assertNoSymbolsFor(publicCompanionFunctionsTitle()) |
| classlikeK.assertNoSymbolsFor(publicCompanionPropertiesTitle()) |
| assertThat(staticJavaMethod.modifiers).contains("static") |
| assertThat(staticJavaField.modifiers).contains("static") |
| } |
| } |
| |
| @Test |
| fun `Extension functions are included on Java and Kotlin pages`() { |
| val src = """ |
| |class Foo { |
| |} |
| |fun Foo.bar() = Unit |
| |fun Foo.baz() = Unit |
| """ |
| val classlike = src.render().page().content<Classlike>() |
| val extFunctions = classlike.symbolsFor("Extension functions") |
| assertThat(extFunctions.first.data.items).hasSize(2) |
| } |
| |
| @Test |
| fun `Extension functions are ordered by the package they come from`() { |
| val src = listOf( |
| """ |
| |/src/main/kotlin/androidx/example/Foo.kt |
| |package foo |
| |class Foo { |
| |} |
| """, |
| """ |
| |/src/main/kotlin/androidx/example/Second.kt |
| |package second |
| | |
| |import foo.Foo |
| | |
| |fun Foo.baz() = Unit |
| """, |
| """ |
| |/src/main/kotlin/androidx/example/First.kt |
| |package first |
| | |
| |import foo.Foo |
| | |
| |fun Foo.zab() = Unit |
| """ |
| ) |
| val classlike = src.render().page().content<Classlike>() |
| val extFunctions = classlike.symbolsFor("Extension functions") |
| val extFunctionClasses = extFunctions.second.symbols.map { |
| (it as? SymbolDetail)?.data?.extFunctionClass |
| } |
| assertThat(extFunctionClasses).isEqualTo(listOf("FirstKt", "SecondKt")) |
| } |
| |
| @Test |
| fun `Extension functions respect @JvmName for packages`() { |
| val src = listOf( |
| """ |
| |/src/main/kotlin/androidx/example/Foo.kt |
| |package foo |
| |class Foo { |
| |} |
| """, |
| """ |
| |/src/main/kotlin/androidx/example/Second.kt |
| | |
| |@file:JvmName("SecondJvm") |
| |package second |
| | |
| |import foo.Foo |
| | |
| |fun Foo.baz() = Unit |
| """, |
| """ |
| |/src/main/kotlin/androidx/example/First.kt |
| | |
| |@file:JvmName("FirstJvm") |
| |package first |
| | |
| | |
| |import foo.Foo |
| | |
| |fun Foo.zab() = Unit |
| """ |
| ) |
| val classlike = src.render().page().content<Classlike>() |
| val extFunctions = classlike.symbolsFor("Extension functions") |
| val extFunctionClasses = extFunctions.second.symbols.map { |
| (it as? SymbolDetail)?.data?.extFunctionClass |
| } |
| assertThat(extFunctionClasses).isEqualTo(listOf("FirstJvm", "SecondJvm")) |
| } |
| |
| @Ignore // TODO: b/195529157 |
| @Test |
| fun `Annotation types with no parameters have no default constructors`() { |
| // parameterless annotations are invoked as `@NonNull` not `@NonNull()` |
| val documentationJ = """ |
| |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) |
| | @interface PlaybackSuppressionReason {} |
| |} |
| """.render(java = true).page("PlaybackSuppressionReason").content<Classlike>() |
| val documentationK = """ |
| |public annotation class 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}. |
| | */ |
| | annotation class PlaybackSuppressionReason {} |
| |} |
| """.render().page("PlaybackSuppressionReason").content<Classlike>() |
| |
| for (documentation in listOf(documentationJ, documentationK)) { |
| val constructors = documentation.symbolsFor("public constructors") |
| assertThat(constructors.first.size()).isEqualTo(0) |
| } |
| } |
| |
| @Test |
| fun `Java getters and setters are documented`() { |
| val page = """ |
| |public final class Foo { |
| | |
| | public int a; |
| | public int c; // intentional mismatch of field name / getter name |
| | private int d; |
| | protected int e; |
| | |
| | public int getA() { |
| | return a; |
| | } |
| | |
| | public void setA(int a) { |
| | this.a = a; |
| | } |
| | |
| | public int getB() { |
| | return c; |
| | } |
| | |
| | public void setB(int b) { |
| | c = b; |
| | } |
| | |
| | private int getD() { |
| | return d; |
| | } |
| | |
| | private void setD(int d) { |
| | this.d = d; |
| | } |
| | |
| | protected int getE() { |
| | return e; |
| | } |
| | |
| | protected void setE(int e) { |
| | this.e = e; |
| | } |
| |} |
| """.render(java = true).page() |
| |
| val classlike = page.content<Classlike>() |
| val publicMethodSymbols = classlike.symbolsFor(publicMethodsTitle(displayLanguage)) |
| val protectedMethodSymbols = classlike.symbolsFor(protectedMethodsTitle(displayLanguage)) |
| val publicMethodNames = publicMethodSymbols.second.symbols.map { |
| (it as SymbolDetail).data.name |
| } |
| val protectedMethodNames = protectedMethodSymbols.second.symbols.map { |
| (it as SymbolDetail).data.name |
| } |
| |
| kotlinOnly { |
| // in Kotlin, we don't need to show getA / setA because property access is preferred. |
| assertThat(publicMethodNames).isEqualTo(listOf("getB", "setB")) |
| assertThat(protectedMethodNames).isEmpty() |
| } |
| javaOnly { |
| assertThat(publicMethodNames).isEqualTo(listOf("getA", "getB", "setA", "setB")) |
| // TODO(b/165112358): 'e' doesn't show up in the dokka model |
| // assertThat(protectedMethodNames).isEqualTo(listOf("getE", "setE")) |
| } |
| } |
| |
| @Test |
| fun `Java source with public getter and private setter is documented correctly`() { |
| val page = """ |
| |public final class Foo { |
| | |
| | public int a; |
| | |
| | public int getA() { |
| | return a; |
| | } |
| | |
| | private void setA(int a) { |
| | this.a = a; |
| | } |
| |} |
| """.render(java = true).page() |
| |
| val classlike = page.content<Classlike>() |
| val publicMethodSymbols = classlike.methodDetailsItems() |
| val publicMethodNames = publicMethodSymbols.map { it.data.name } |
| |
| kotlinOnly { |
| assertThat(publicMethodNames).isEmpty() |
| } |
| javaOnly { |
| assertThat(publicMethodNames).isEqualTo(listOf("getA")) |
| } |
| } |
| |
| @Test |
| fun `Kotlin generated getters and setters are not documented`() { |
| val page = """ |
| |data class Foo(val a: Int, var b: Int) { |
| | var c: Int |
| | get() = 0 |
| | set(c: Int): Unit |
| |} |
| """.render().page() |
| |
| val classlike = page.content<Classlike>() |
| val methodSymbols = classlike.methodDetailsItems() |
| val methodNames = methodSymbols.map { it.data.name } |
| |
| assertThat(methodNames).isEmpty() |
| } |
| |
| @Test |
| fun `Default no-arg constructors are autogenerated`() { |
| val emptyTestClass = """ |
| public class Foo {} |
| """.trimIndent() |
| |
| for (isJava in listOf(true, false)) { |
| val classlike = emptyTestClass.render(java = isJava).page("Foo").content<Classlike>() |
| val constructorList = classlike.symbolsFor("Public constructors").second.symbols |
| assertThat(constructorList.size).isEqualTo(1) |
| assertThat((constructorList.single() as SymbolDetail).data.name).isEqualTo("Foo") |
| } |
| } |
| |
| @Test |
| fun `Default no-arg constructors are not autogenerated for annotations`() { |
| val classlikeJ = """ |
| public @interface Foo {} |
| """.trimIndent().render(java = true).page("Foo").content<Classlike>() |
| val classlikeK = """ |
| public annotation class Foo {} |
| """.trimIndent().render(java = false).page("Foo").content<Classlike>() |
| |
| for (classlike in listOf(classlikeJ/*, classlikeK*/)) { // TODO: b/195529157 |
| val constructorList = classlike.symbolsFor("Public constructors").second.symbols |
| assertThat(constructorList).isEmpty() |
| } |
| } |
| |
| @Test // Interfaces "extend" other interfaces, while classes "implement" interfaces |
| fun `Interface extending another interface uses correct keyword`() { |
| val signatureJ = """ |
| public interface Foo {} |
| public interface Bar extends Foo {} |
| """.trimIndent().render(java = true).page("Bar").content<Classlike>().data.signature |
| val signatureK = """ |
| public interface Foo {} |
| public interface Bar : Foo {} |
| """.trimIndent().render(java = false).page("Bar").content<Classlike>().data.signature |
| |
| assertThat(signatureJ.data.extends).isEmpty() |
| assertThat(signatureK.data.extends).isEmpty() |
| assertThat(signatureJ.data.implements.single().data.name).isEqualTo("Test.Foo") |
| assertThat(signatureK.data.implements.single().data.name).isEqualTo("Foo") |
| // Now route to DefaultClassSignatureTest.`Interfaces extend other interfaces`() |
| } |
| |
| private fun DModule.page(name: String = "Foo"): DevsitePage { |
| val classlike = explicitClasslike(name) |
| val (holder, pathProvider) = holderAndProvider(this) |
| val extFunctionMap = runBlocking { holder.extensionFunctionMap() } |
| val converter = ClasslikeDocumentableConverter( |
| displayLanguage, |
| classlike, |
| pathProvider, |
| holder, |
| extFunctionMap.getOrDefault(name, emptyList()) |
| ) |
| return runBlocking { converter.classlike() } |
| } |
| |
| private fun String.possiblyAsGetter() = if (displayLanguage == Language.KOTLIN) this |
| else "get" + this.capitalize() |
| |
| private fun Classlike.methodDetailsItems() = ( |
| symbolsFor(publicMethodsTitle(displayLanguage)).second.symbols + |
| symbolsFor(protectedMethodsTitle(displayLanguage)).second.symbols |
| ).map { it as SymbolDetail } |
| |
| private fun Classlike.methodSummaryItems() = |
| summaryItemsFor(publicMethodsTitle(displayLanguage)) + |
| summaryItemsFor(protectedMethodsTitle(displayLanguage)) |
| |
| private fun Classlike.methodSymbol(name: String = "foo") = |
| methodSummaryItems().singleOrNull { it.name() == name } |
| ?: methodSummaryItems().single() |
| |
| private fun Classlike.propertySummaryItems() = |
| summaryItemsFor(publicPropertiesTitle(displayLanguage)) + |
| summaryItemsFor(protectedPropertiesTitle(displayLanguage)) |
| |
| private fun Classlike.propertyDetailsItems() = ( |
| symbolsFor(publicPropertiesTitle(displayLanguage)).second.symbols + |
| symbolsFor(protectedPropertiesTitle(displayLanguage)).second.symbols |
| ).map { it as SymbolDetail } |
| |
| private fun Classlike.propertySymbol(name: String = "foo") = |
| propertySummaryItems().singleOrNull { it.name() == name } |
| ?: propertySummaryItems().single() |
| |
| private fun SummaryList.constructor() = |
| (data.items.item() as SingleColumnSummaryItem).data.description as SymbolSummary |
| |
| private fun Classlike.assertNoSymbolsFor(symbolsName: String) = assertThat( |
| data.symbolTypes.none { it.first.title() == symbolsName } |
| ).isTrue() |
| |
| companion object { |
| @JvmStatic |
| @Parameterized.Parameters(name = "{0}") |
| fun data() = listOf( |
| arrayOf(Language.JAVA), |
| arrayOf(Language.KOTLIN) |
| ) |
| } |
| } |