aboutsummaryrefslogtreecommitdiff
path: root/core/src/main/kotlin/Kotlin/DescriptorDocumentationParser.kt
blob: 098a17f97be4613d409c6c38457276ea1d85568f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
package org.jetbrains.dokka.Kotlin

import com.google.inject.Inject
import com.intellij.psi.PsiDocCommentOwner
import com.intellij.psi.PsiNamedElement
import com.intellij.psi.util.PsiTreeUtil
import org.intellij.markdown.parser.LinkMap
import org.jetbrains.dokka.*
import org.jetbrains.dokka.Samples.SampleProcessingService
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.descriptors.impl.EnumEntrySyntheticClassDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
import org.jetbrains.kotlin.idea.kdoc.findKDoc
import org.jetbrains.kotlin.idea.kdoc.resolveKDocLink
import org.jetbrains.kotlin.incremental.components.NoLookupLocation
import org.jetbrains.kotlin.kdoc.parser.KDocKnownTag
import org.jetbrains.kotlin.kdoc.psi.api.KDoc
import org.jetbrains.kotlin.kdoc.psi.impl.KDocSection
import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag
import org.jetbrains.kotlin.load.java.descriptors.JavaCallableMemberDescriptor
import org.jetbrains.kotlin.load.java.descriptors.JavaClassDescriptor
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtBinaryExpressionWithTypeRHS
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.resolve.DescriptorUtils
import org.jetbrains.kotlin.resolve.annotations.argumentValue
import org.jetbrains.kotlin.resolve.constants.StringValue
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter
import org.jetbrains.kotlin.resolve.scopes.getDescriptorsFiltered
import org.jetbrains.kotlin.resolve.source.PsiSourceElement
import java.util.regex.Pattern

private val REF_COMMAND = "ref"
private val NAME_COMMAND = "name"
private val DESCRIPTION_COMMAND = "description"
private val TEXT = Pattern.compile("(\\S+)\\s*(.*)", Pattern.DOTALL)
private val NAME_TEXT = Pattern.compile("(\\S+)(.*)", Pattern.DOTALL)

class DescriptorDocumentationParser @Inject constructor(
        val options: DocumentationOptions,
        val logger: DokkaLogger,
        val linkResolver: DeclarationLinkResolver,
        val resolutionFacade: DokkaResolutionFacade,
        val refGraph: NodeReferenceGraph,
        val sampleService: SampleProcessingService,
        val signatureProvider: KotlinElementSignatureProvider,
        val externalDocumentationLinkResolver: ExternalDocumentationLinkResolver
) {
    fun parseDocumentation(descriptor: DeclarationDescriptor, inline: Boolean = false): Content =
            parseDocumentationAndDetails(descriptor, inline).first

    fun parseDocumentationAndDetails(descriptor: DeclarationDescriptor, inline: Boolean = false): Pair<Content, (DocumentationNode) -> Unit> {
        if (descriptor is JavaClassDescriptor || descriptor is JavaCallableMemberDescriptor ||
            descriptor is EnumEntrySyntheticClassDescriptor) {
            return parseJavadoc(descriptor)
        }

        val kdoc = descriptor.findKDoc() ?: findStdlibKDoc(descriptor)
        if (kdoc == null) {
            if (options.effectivePackageOptions(descriptor.fqNameSafe).reportUndocumented && !descriptor.isDeprecated() &&
                    descriptor !is ValueParameterDescriptor && descriptor !is TypeParameterDescriptor &&
                    descriptor !is PropertyAccessorDescriptor && !descriptor.isSuppressWarning()) {
                logger.warn("No documentation for ${descriptor.signatureWithSourceLocation()}")
            }
            return Content.Empty to { node -> }
        }

        val contextDescriptor =
            (PsiTreeUtil.getParentOfType(kdoc, KDoc::class.java)?.context as? KtDeclaration)
                ?.takeIf { it != descriptor.original.sourcePsi() }
                ?.resolveToDescriptorIfAny()
                ?: descriptor

        // This will build the initial node for all content above the tags, however we also sometimes have @Sample
        // tags between content, so we handle that case below
        var kdocText = kdoc.getContent()
        // workaround for code fence parsing problem in IJ markdown parser
        if (kdocText.endsWith("```") || kdocText.endsWith("~~~")) {
            kdocText += "\n"
        }
        val tree = parseMarkdown(kdocText)
        val linkMap = LinkMap.buildLinkMap(tree.node, kdocText)
        val content = buildContent(tree, LinkResolver(linkMap, { href -> linkResolver.resolveContentLink(contextDescriptor, href) }), inline)
        if (kdoc is KDocSection) {
            val tags = kdoc.getTags()
            tags.forEach {
                when (it.knownTag) {
                    KDocKnownTag.SAMPLE -> {
                        content.append(sampleService.resolveSample(contextDescriptor, it.getSubjectName(), it))
                        // If the sample tag has text below it, it will be considered as the child of the tag, so add it
                        val tagSubContent = it.getContent()
                        if (tagSubContent.isNotBlank()) {
                            val markdownNode = parseMarkdown(tagSubContent)
                            buildInlineContentTo(markdownNode, content, LinkResolver(linkMap, { href -> linkResolver.resolveContentLink(contextDescriptor, href) }))
                        }
                    }
                    KDocKnownTag.SEE ->
                        content.addTagToSeeAlso(contextDescriptor, it)
                    KDocKnownTag.PARAM -> {
                        val section = content.addSection(javadocSectionDisplayName(it.name), it.getSubjectName())
                        section.append(ParameterInfoNode {
                            val signature = signatureProvider.signature(descriptor)
                            refGraph.lookupOrWarn(signature, logger)?.details?.find { node ->
                                node.kind == NodeKind.Parameter && node.name == it.getSubjectName()
                            }
                        })
                        val sectionContent = it.getContent()
                        val markdownNode = parseMarkdown(sectionContent)
                        buildInlineContentTo(markdownNode, section, LinkResolver(linkMap, { href -> linkResolver.resolveContentLink(contextDescriptor, href) }))
                    }
                    else -> {
                        val section = content.addSection(javadocSectionDisplayName(it.name), it.getSubjectName())
                        val sectionContent = it.getContent()
                        val markdownNode = parseMarkdown(sectionContent)
                        buildInlineContentTo(markdownNode, section, LinkResolver(linkMap, { href -> linkResolver.resolveContentLink(contextDescriptor, href) }))
                    }
                }
            }
        }
        return content to { node ->
            if (kdoc is KDocSection) {
                val tags = kdoc.getTags()
                node.addExtraTags(tags, descriptor)
            }
        }
    }

    /**
     * Adds @attr tag. There are 3 types of syntax for this:
     * *@attr ref <android.>R.styleable.<attribute_name>
     * *@attr name <attribute_name>
     * *@attr description <attribute_description>
     * This also adds the @since and @apiSince tags.
     */
    private fun DocumentationNode.addExtraTags(tags: Array<KDocTag>, descriptor: DeclarationDescriptor) {
        tags.forEach {
            val name = it.name
            if (name?.toLowerCase() == "attr") {
                it.getAttr(descriptor)?.let { append(it, RefKind.Detail) }
            } else if (name?.toLowerCase() == "since" || name?.toLowerCase() == "apisince") {
                val apiLevel = DocumentationNode(it.getContent(), Content.Empty, NodeKind.ApiLevel)
                append(apiLevel, RefKind.Detail)
            } else if (name?.toLowerCase() == "sdkextsince") {
                val sdkExtSince = DocumentationNode(it.getContent(), Content.Empty, NodeKind.SdkExtSince)
                append(sdkExtSince, RefKind.Detail)
            } else if (name?.toLowerCase() == "deprecatedsince") {
                val deprecatedLevel = DocumentationNode(it.getContent(), Content.Empty, NodeKind.DeprecatedLevel)
                append(deprecatedLevel, RefKind.Detail)
            } else if (name?.toLowerCase() == "artifactid") {
                val artifactId = DocumentationNode(it.getContent(), Content.Empty, NodeKind.ArtifactId)
                append(artifactId, RefKind.Detail)
            }
        }
    }

    private fun DeclarationDescriptor.isSuppressWarning(): Boolean {
        val suppressAnnotation = annotations.findAnnotation(FqName(Suppress::class.qualifiedName!!))
        return if (suppressAnnotation != null) {
            @Suppress("UNCHECKED_CAST")
            (suppressAnnotation.argumentValue("names")?.value as List<StringValue>).any { it.value == "NOT_DOCUMENTED" }
        } else containingDeclaration?.isSuppressWarning() ?: false
    }

    /**
     * Special case for generating stdlib documentation (the Any class to which the override chain will resolve
     * is not the same one as the Any class included in the source scope).
     */
    fun findStdlibKDoc(descriptor: DeclarationDescriptor): KDocTag? {
        if (descriptor !is CallableMemberDescriptor) {
            return null
        }
        val name = descriptor.name.asString()
        if (name == "equals" || name == "hashCode" || name == "toString") {
            var deepestDescriptor: CallableMemberDescriptor = descriptor
            while (!deepestDescriptor.overriddenDescriptors.isEmpty()) {
                deepestDescriptor = deepestDescriptor.overriddenDescriptors.first()
            }
            if (DescriptorUtils.getFqName(deepestDescriptor.containingDeclaration).asString() == "kotlin.Any") {
                val anyClassDescriptors = resolutionFacade.resolveSession.getTopLevelClassifierDescriptors(
                        FqName.fromSegments(listOf("kotlin", "Any")), NoLookupLocation.FROM_IDE)
                anyClassDescriptors.forEach {
                    val anyMethod = (it as ClassDescriptor).getMemberScope(listOf())
                            .getDescriptorsFiltered(DescriptorKindFilter.FUNCTIONS, { it == descriptor.name })
                            .single()
                    val kdoc = anyMethod.findKDoc()
                    if (kdoc != null) {
                        return kdoc
                    }
                }
            }
        }
        return null
    }

    fun parseJavadoc(descriptor: DeclarationDescriptor): Pair<Content, (DocumentationNode) -> Unit> {
        val psi = ((descriptor as? DeclarationDescriptorWithSource)?.source as? PsiSourceElement)?.psi
        if (psi is PsiDocCommentOwner) {
            val parseResult = JavadocParser(
                    refGraph,
                    logger,
                    signatureProvider,
                    externalDocumentationLinkResolver
            ).parseDocumentation(psi as PsiNamedElement)
            return parseResult.content to { node ->
                parseResult.deprecatedContent?.let {
                    val deprecationNode = DocumentationNode("", it, NodeKind.Modifier)
                    node.append(deprecationNode, RefKind.Deprecation)
                }
                if (node.kind in NodeKind.classLike) {
                    parseResult.attributeRefs.forEach {
                        val signature = node.detailOrNull(NodeKind.Signature)
                        val signatureName = signature?.name
                        val classAttrSignature = "${signatureName}:$it"
                        refGraph.register(classAttrSignature, DocumentationNode(node.name, Content.Empty, NodeKind.Attribute))
                        refGraph.link(node, classAttrSignature, RefKind.Detail)
                        refGraph.link(classAttrSignature, node, RefKind.Owner)
                        refGraph.link(classAttrSignature, it, RefKind.AttributeRef)
                    }
                } else if (node.kind in NodeKind.memberLike) {
                    parseResult.attributeRefs.forEach {
                        refGraph.link(node, it, RefKind.HiddenLink)
                    }
                }
                parseResult.apiLevel?.let {
                    node.append(it, RefKind.Detail)
                }
                parseResult.sdkExtSince?.let {
                    node.append(it, RefKind.Detail)
                }
                parseResult.deprecatedLevel?.let {
                    node.append(it, RefKind.Detail)
                }
                parseResult.artifactId?.let {
                    node.append(it, RefKind.Detail)
                }
                parseResult.attribute?.let {
                    val signature = node.detailOrNull(NodeKind.Signature)
                    val signatureName = signature?.name
                    val attrSignature = "AttrMain:$signatureName"
                    refGraph.register(attrSignature, it)
                    refGraph.link(attrSignature, node, RefKind.AttributeSource)
                }
            }
        }
        return Content.Empty to { _ -> }
    }

    fun KDocSection.getTags(): Array<KDocTag> = PsiTreeUtil.getChildrenOfType(this, KDocTag::class.java)
            ?: arrayOf()

    private fun MutableContent.addTagToSeeAlso(descriptor: DeclarationDescriptor, seeTag: KDocTag) {
        addTagToSection(seeTag, descriptor, "See Also")
    }

    private fun MutableContent.addTagToSection(seeTag: KDocTag, descriptor: DeclarationDescriptor, sectionName: String) {
        val subjectName = seeTag.getSubjectName()
        if (subjectName != null) {
            val section = findSectionByTag(sectionName) ?: addSection(sectionName, null)
            val link = linkResolver.resolveContentLink(descriptor, subjectName)
            link.append(ContentText(subjectName))
            val para = ContentParagraph()
            para.append(link)
            section.append(para)
        }
    }

    private fun KDocTag.getAttr(descriptor: DeclarationDescriptor): DocumentationNode? {
        var attribute: DocumentationNode? = null
        val matcher = TEXT.matcher(getContent())
        if (matcher.matches()) {
            val command = matcher.group(1)
            val more = matcher.group(2)
            attribute = when (command) {
                REF_COMMAND -> {
                    val attrRef = more.trim()
                    val qualified = attrRef.split('.', '#')
                    val targetDescriptor = resolveKDocLink(resolutionFacade.resolveSession.bindingContext, resolutionFacade, descriptor, this, qualified)
                    DocumentationNode(attrRef, Content.Empty, NodeKind.Attribute).also {
                        if (targetDescriptor.isNotEmpty()) {
                            refGraph.link(it, targetDescriptor.first().signature(), RefKind.Detail)
                        }
                    }
                }
                NAME_COMMAND -> {
                    val nameMatcher = NAME_TEXT.matcher(more)
                    if (nameMatcher.matches()) {
                        val attrName = nameMatcher.group(1)
                        DocumentationNode(attrName, Content.Empty, NodeKind.Attribute)
                    } else {
                        null
                    }
                }
                DESCRIPTION_COMMAND -> {
                    val attrDescription = more
                    DocumentationNode(attrDescription, Content.Empty, NodeKind.Attribute)
                }
                else -> null
            }
        }
        return attribute
    }

}

/**
 * Lazily executed wrapper node holding a [NodeKind.Parameter] node that will be used to add type
 * and default value information to
 * [org.jetbrains.dokka.Formats.DevsiteLayoutHtmlFormatOutputBuilder].
 *
 * We make this a [ContentBlock] instead of a [ContentNode] so we won't fallback to calling
 * [toString] on this and trying to add it to documentation somewhere - returning an empty list
 * should make this a no-op.
 *
 * @property wrappedNode lazily executable lambda that will return the matching documentation node
 * for this parameter (if it exists)
 */
class ParameterInfoNode(private val wrappedNode: () -> DocumentationNode?) : ContentBlock() {
    private var computed = false

    val parameterContent: NodeRenderContent?
        get() = lazyNode

    private var lazyNode: NodeRenderContent? = null
        get() {
            if (!computed) {
                computed = true

                val node = wrappedNode()
                if (node != null) {
                    field = NodeRenderContent(node, LanguageService.RenderMode.SUMMARY)
                }
            }
        return field
    }

    override val children = arrayListOf<ContentNode>()
}