diff --git a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/KDocProvider.kt b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/KDocProvider.kt index b5c3104bf0..7519e92d6b 100644 --- a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/KDocProvider.kt +++ b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/KDocProvider.kt @@ -10,10 +10,7 @@ import org.jetbrains.dokka.analysis.java.parsers.JavadocParser import org.jetbrains.dokka.model.doc.DocumentationNode import org.jetbrains.dokka.utilities.DokkaLogger import org.jetbrains.kotlin.analysis.api.KtAnalysisSession -import org.jetbrains.kotlin.analysis.api.symbols.KtCallableSymbol -import org.jetbrains.kotlin.analysis.api.symbols.KtClassOrObjectSymbol -import org.jetbrains.kotlin.analysis.api.symbols.KtSymbol -import org.jetbrains.kotlin.analysis.api.symbols.KtSymbolOrigin +import org.jetbrains.kotlin.analysis.api.symbols.* import org.jetbrains.kotlin.analysis.api.symbols.markers.KtNamedSymbol import org.jetbrains.kotlin.kdoc.parser.KDocKnownTag import org.jetbrains.kotlin.kdoc.psi.api.KDoc @@ -80,8 +77,28 @@ internal data class KDocContent( ) internal fun KtAnalysisSession.findKDoc(symbol: KtSymbol): KDocContent? { - // for generated function (e.g. `copy`) psi returns class, see test `data class kdocs over generated methods` + // Dokka's HACK: primary constructors can be generated + // so [KtSymbol.psi] is undefined for [KtSymbolOrigin.SOURCE_MEMBER_GENERATED] origin + // we need to get psi of a containing class + if(symbol is KtConstructorSymbol && symbol.isPrimary) { + val containingClass = symbol.originalContainingClassForOverride + if (containingClass?.origin != KtSymbolOrigin.SOURCE) return null + val kdoc = (containingClass.psi as? KtDeclaration)?.docComment ?: return null + val constructorSection = kdoc.findSectionByTag(KDocKnownTag.CONSTRUCTOR) + if (constructorSection != null) { + // if annotated with @constructor tag and the caret is on constructor definition, + // then show @constructor description as the main content, and additional sections + // that contain @param tags (if any), as the most relatable ones + // practical example: val foo = Foo("argument") -- show @constructor and @param content + val paramSections = kdoc.findSectionsContainingTag(KDocKnownTag.PARAM) + return KDocContent(constructorSection, paramSections) + } + } + + // for generated function (e.g. `copy`) [KtSymbol.psi] is undefined (although actually returns a class psi), see test `data class kdocs over generated methods` if (symbol.origin != KtSymbolOrigin.SOURCE) return null + + val ktElement = symbol.psi as? KtElement ktElement?.findKDoc()?.let { return it diff --git a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/DefaultSymbolToDocumentableTranslator.kt b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/DefaultSymbolToDocumentableTranslator.kt index a83c6c0681..c3aa573875 100644 --- a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/DefaultSymbolToDocumentableTranslator.kt +++ b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/translators/DefaultSymbolToDocumentableTranslator.kt @@ -864,7 +864,8 @@ internal class DokkaSymbolVisitor( private fun KtAnalysisSession.getDocumentation(symbol: KtSymbol) = if (symbol.origin == KtSymbolOrigin.SOURCE_MEMBER_GENERATED) - getGeneratedKDocDocumentationFrom(symbol) + // a primary (implicit default) constructor can be generated, so we need KDoc from @constructor tag + getGeneratedKDocDocumentationFrom(symbol) ?: if(symbol is KtConstructorSymbol) getKDocDocumentationFrom(symbol, logger) else null else getKDocDocumentationFrom(symbol, logger) ?: javadocParser?.let { getJavaDocDocumentationFrom(symbol, it) } diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/model/ConstructorsTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/model/ConstructorsTest.kt new file mode 100644 index 0000000000..d2629f307e --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/model/ConstructorsTest.kt @@ -0,0 +1,164 @@ +/* + * Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package model + +import org.jetbrains.dokka.analysis.kotlin.markdown.MARKDOWN_ELEMENT_FILE_NAME +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.model.doc.P +import kotlin.test.Test +import utils.* + + +class ConstructorsTest : AbstractModelTest("/src/main/kotlin/constructors/Test.kt", "constructors") { + + @Test + fun `should have documentation for @constructor tag without parameters`() { + val expectedRootDescription = Description( + CustomDocTag( + emptyList(), + params = emptyMap(), + name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + + val expectedConstructorTag = Constructor( + CustomDocTag( + listOf( + P( + listOf( + Text("some doc"), + ) + ) + ), + params = emptyMap(), + name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + val expectedDescriptionTag = Description( + CustomDocTag( + listOf( + P( + listOf( + Text("some doc"), + ) + ) + ), + params = emptyMap(), + name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + inlineModelTest( + """ + |/** + |* @constructor some doc + |*/ + |class A + """.trimMargin() + ) { + val classlike = packages.flatMap { it.classlikes }.first() as DClass + classlike.name equals "A" + classlike.documentation.values.single() equals DocumentationNode(listOf(expectedRootDescription, expectedConstructorTag)) + val constructor = classlike.constructors.single() + constructor.documentation.values.single() equals DocumentationNode(listOf(expectedDescriptionTag)) + } + } + + @Test + fun `should have documentation for @constructor tag`() { + + val expectedRootDescription = Description( + CustomDocTag( + emptyList(), + params = emptyMap(), + name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + + val expectedConstructorTag = Constructor( + CustomDocTag( + listOf( + P( + listOf( + Text("some doc"), + ) + ) + ), + params = emptyMap(), + name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + val expectedDescriptionTag = Description( + CustomDocTag( + listOf( + P( + listOf( + Text("some doc"), + ) + ) + ), + params = emptyMap(), + name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + inlineModelTest( + """ + |/** + |* @constructor some doc + |*/ + |class A(a: Int) + """.trimMargin() + ) { + val classlike = packages.flatMap { it.classlikes }.first() as DClass + classlike.name equals "A" + classlike.documentation.values.single() equals DocumentationNode(listOf(expectedRootDescription, expectedConstructorTag)) + val constructor = classlike.constructors.single() + constructor.documentation.values.single() equals DocumentationNode(listOf(expectedDescriptionTag)) + } + } + + @Test + fun `should ignore documentation in @constructor tag for a secondary constructor`() { + val expectedRootDescription = Description( + CustomDocTag( + emptyList(), + params = emptyMap(), + name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + + val expectedConstructorTag = Constructor( + CustomDocTag( + listOf( + P( + listOf( + Text("some doc"), + ) + ) + ), + params = emptyMap(), + name = MARKDOWN_ELEMENT_FILE_NAME + ) + ) + + inlineModelTest( + """ + |/** + |* @constructor some doc + |*/ + |class A { + | constructor(a: Int) + |} + """.trimMargin() + ) { + val classlike = packages.flatMap { it.classlikes }.first() as DClass + classlike.name equals "A" + classlike.documentation.values.single() equals DocumentationNode(listOf(expectedRootDescription, expectedConstructorTag)) + val constructor = classlike.constructors.single() + constructor.documentation.isEmpty() equals true + } + } + +}