Skip to content

Commit

Permalink
[K2] Fix @constructor documentation for primary constructors (#3499)
Browse files Browse the repository at this point in the history
  • Loading branch information
vmishenev authored Feb 26, 2024
1 parent 936cb3f commit d7eba91
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = Fo<caret>o("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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) }

Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}

}

0 comments on commit d7eba91

Please sign in to comment.