<lambda>null1 package org.jetbrains.dokka
2
3 import com.intellij.psi.*
4 import com.intellij.psi.impl.source.javadoc.CorePsiDocTagValueImpl
5 import com.intellij.psi.impl.source.tree.JavaDocElementType
6 import com.intellij.psi.javadoc.*
7 import com.intellij.psi.util.PsiTreeUtil
8 import com.intellij.util.IncorrectOperationException
9 import com.intellij.util.containers.isNullOrEmpty
10 import org.jetbrains.dokka.Model.CodeNode
11 import org.jetbrains.kotlin.utils.join
12 import org.jetbrains.kotlin.utils.keysToMap
13 import org.jsoup.Jsoup
14 import org.jsoup.nodes.Element
15 import org.jsoup.nodes.Node
16 import org.jsoup.nodes.TextNode
17 import java.io.File
18 import java.net.URI
19 import java.util.regex.Pattern
20
21 private val NAME_TEXT = Pattern.compile("(\\S+)(.*)", Pattern.DOTALL)
22 private val TEXT = Pattern.compile("(\\S+)\\s*(.*)", Pattern.DOTALL)
23
24 data class JavadocParseResult(
25 val content: Content,
26 val deprecatedContent: Content?,
27 val attributeRefs: List<String>,
28 val apiLevel: DocumentationNode? = null,
29 val deprecatedLevel: DocumentationNode? = null,
30 val artifactId: DocumentationNode? = null,
31 val attribute: DocumentationNode? = null
32 ) {
33 companion object {
34 val Empty = JavadocParseResult(Content.Empty,
35 null,
36 emptyList(),
37 null,
38 null,
39 null
40 )
41 }
42 }
43
44 interface JavaDocumentationParser {
parseDocumentationnull45 fun parseDocumentation(element: PsiNamedElement): JavadocParseResult
46 }
47
48 class JavadocParser(
49 private val refGraph: NodeReferenceGraph,
50 private val logger: DokkaLogger,
51 private val signatureProvider: ElementSignatureProvider,
52 private val externalDocumentationLinkResolver: ExternalDocumentationLinkResolver
53 ) : JavaDocumentationParser {
54
55 private fun ContentSection.appendTypeElement(
56 signature: String,
57 selector: (DocumentationNode) -> DocumentationNode?
58 ) {
59 append(LazyContentBlock {
60 val node = refGraph.lookupOrWarn(signature, logger)?.let(selector)
61 if (node != null) {
62 it.append(NodeRenderContent(node, LanguageService.RenderMode.SUMMARY))
63 it.symbol(":")
64 it.text(" ")
65 }
66 })
67 }
68
69 override fun parseDocumentation(element: PsiNamedElement): JavadocParseResult {
70 val docComment = (element as? PsiDocCommentOwner)?.docComment
71 if (docComment == null) return JavadocParseResult.Empty
72 val result = MutableContent()
73 var deprecatedContent: Content? = null
74 val firstParagraph = ContentParagraph()
75 firstParagraph.convertJavadocElements(
76 docComment.descriptionElements.dropWhile { it.text.trim().isEmpty() },
77 element
78 )
79 val paragraphs = firstParagraph.children.dropWhile { it !is ContentParagraph }
80 firstParagraph.children.removeAll(paragraphs)
81 if (!firstParagraph.isEmpty()) {
82 result.append(firstParagraph)
83 }
84 paragraphs.forEach {
85 result.append(it)
86 }
87
88 if (element is PsiMethod) {
89 val tagsByName = element.searchInheritedTags()
90 for ((tagName, tags) in tagsByName) {
91 for ((tag, context) in tags) {
92 val section = result.addSection(javadocSectionDisplayName(tagName), tag.getSubjectName())
93 val signature = signatureProvider.signature(element)
94 when (tagName) {
95 "param" -> {
96 section.appendTypeElement(signature) {
97 it.details
98 .find { node -> node.kind == NodeKind.Parameter && node.name == tag.getSubjectName() }
99 ?.detailOrNull(NodeKind.Type)
100 }
101 }
102 "return" -> {
103 section.appendTypeElement(signature) { it.detailOrNull(NodeKind.Type) }
104 }
105 }
106 section.convertJavadocElements(tag.contentElements(), context)
107 }
108 }
109 }
110
111 val attrRefSignatures = mutableListOf<String>()
112 var since: DocumentationNode? = null
113 var deprecated: DocumentationNode? = null
114 var artifactId: DocumentationNode? = null
115 var attrName: String? = null
116 var attrDesc: Content? = null
117 var attr: DocumentationNode? = null
118 docComment.tags.forEach { tag ->
119 when (tag.name.toLowerCase()) {
120 "see" -> result.convertSeeTag(tag)
121 "deprecated" -> {
122 deprecatedContent = Content().apply {
123 convertJavadocElements(tag.contentElements(), element)
124 }
125 }
126 "attr" -> {
127 when (tag.valueElement?.text) {
128 "ref" ->
129 tag.getAttrRef(element)?.let {
130 attrRefSignatures.add(it)
131 }
132 "name" -> attrName = tag.getAttrName()
133 "description" -> attrDesc = tag.getAttrDesc(element)
134 }
135 }
136 "since", "apisince" -> {
137 since = DocumentationNode(tag.getApiLevel() ?: "", Content.Empty, NodeKind.ApiLevel)
138 }
139 "deprecatedsince" -> {
140 deprecated = DocumentationNode(tag.getApiLevel() ?: "", Content.Empty, NodeKind.DeprecatedLevel)
141 }
142 "artifactid" -> {
143 artifactId = DocumentationNode(tag.artifactId() ?: "", Content.Empty, NodeKind.ArtifactId)
144 }
145 in tagsToInherit -> {
146 }
147 else -> {
148 val subjectName = tag.getSubjectName()
149 val section = result.addSection(javadocSectionDisplayName(tag.name), subjectName)
150 section.convertJavadocElements(tag.contentElements(), element)
151 }
152 }
153 }
154 attrName?.let { name ->
155 attr = DocumentationNode(name, attrDesc ?: Content.Empty, NodeKind.AttributeRef)
156 }
157 return JavadocParseResult(result, deprecatedContent, attrRefSignatures, since, deprecated, artifactId, attr)
158 }
159
160 private val tagsToInherit = setOf("param", "return", "throws")
161
162 private data class TagWithContext(val tag: PsiDocTag, val context: PsiNamedElement)
163
164 fun PsiDocTag.artifactId(): String? {
165 var artifactName: String? = null
166 if (dataElements.isNotEmpty()) {
167 artifactName = join(dataElements.map { it.text }, "")
168 }
169 return artifactName
170 }
171
172 fun PsiDocTag.getApiLevel(): String? {
173 if (dataElements.isNotEmpty()) {
174 val data = dataElements
175 if (data[0] is CorePsiDocTagValueImpl) {
176 val docTagValue = data[0]
177 if (docTagValue.firstChild != null) {
178 val apiLevel = docTagValue.firstChild
179 return apiLevel.text
180 }
181 }
182 }
183 return null
184 }
185
186 private fun PsiDocTag.getAttrRef(element: PsiNamedElement): String? {
187 if (dataElements.size > 1) {
188 val elementText = dataElements[1].text
189 try {
190 val linkComment = JavaPsiFacade.getInstance(project).elementFactory
191 .createDocCommentFromText("/** {@link $elementText} */", element)
192 val linkElement = PsiTreeUtil.getChildOfType(linkComment, PsiInlineDocTag::class.java)?.linkElement()
193 val signature = resolveInternalLink(linkElement)
194 val attrSignature = "AttrMain:$signature"
195 return attrSignature
196 } catch (e: IncorrectOperationException) {
197 return null
198 }
199 } else return null
200 }
201
202 private fun PsiDocTag.getAttrName(): String? {
203 if (dataElements.size > 1) {
204 val nameMatcher = NAME_TEXT.matcher(dataElements[1].text)
205 if (nameMatcher.matches()) {
206 return nameMatcher.group(1)
207 } else {
208 return null
209 }
210 } else return null
211 }
212
213 private fun PsiDocTag.getAttrDesc(element: PsiNamedElement): Content? {
214 return Content().apply {
215 convertJavadocElementsToAttrDesc(contentElements(), element)
216 }
217 }
218
219 private fun PsiMethod.searchInheritedTags(): Map<String, Collection<TagWithContext>> {
220
221 val output = tagsToInherit.keysToMap { mutableMapOf<String?, TagWithContext>() }
222
223 fun recursiveSearch(methods: Array<PsiMethod>) {
224 for (method in methods) {
225 recursiveSearch(method.findSuperMethods())
226 }
227 for (method in methods) {
228 for (tag in method.docComment?.tags.orEmpty()) {
229 if (tag.name in tagsToInherit) {
230 output[tag.name]!![tag.getSubjectName()] = TagWithContext(tag, method)
231 }
232 }
233 }
234 }
235
236 recursiveSearch(arrayOf(this))
237 return output.mapValues { it.value.values }
238 }
239
240
241 private fun PsiDocTag.contentElements(): Iterable<PsiElement> {
242 val tagValueElements = children
243 .dropWhile { it.node?.elementType == JavaDocTokenType.DOC_TAG_NAME }
244 .dropWhile { it is PsiWhiteSpace }
245 .filterNot { it.node?.elementType == JavaDocTokenType.DOC_COMMENT_LEADING_ASTERISKS }
246 return if (getSubjectName() != null) tagValueElements.dropWhile { it is PsiDocTagValue } else tagValueElements
247 }
248
249 private fun ContentBlock.convertJavadocElements(elements: Iterable<PsiElement>, element: PsiNamedElement) {
250 val doc = Jsoup.parse(expandAllForElements(elements, element))
251 doc.body().childNodes().forEach {
252 convertHtmlNode(it)?.let { append(it) }
253 }
254 doc.head().childNodes().forEach {
255 convertHtmlNode(it)?.let { append(it) }
256 }
257 }
258
259 private fun ContentBlock.convertJavadocElementsToAttrDesc(elements: Iterable<PsiElement>, element: PsiNamedElement) {
260 val doc = Jsoup.parse(expandAllForElements(elements, element))
261 doc.body().childNodes().forEach {
262 convertHtmlNode(it)?.let {
263 var content = it
264 if (content is ContentText) {
265 var description = content.text
266 val matcher = TEXT.matcher(content.text)
267 if (matcher.matches()) {
268 val command = matcher.group(1)
269 if (command == "description") {
270 description = matcher.group(2)
271 content = ContentText(description)
272 }
273 }
274 }
275 append(content)
276 }
277 }
278 }
279
280 private fun expandAllForElements(elements: Iterable<PsiElement>, element: PsiNamedElement): String {
281 val htmlBuilder = StringBuilder()
282 elements.forEach {
283 if (it is PsiInlineDocTag) {
284 htmlBuilder.append(convertInlineDocTag(it, element))
285 } else {
286 htmlBuilder.append(it.text)
287 }
288 }
289 return htmlBuilder.toString().trim()
290 }
291
292 private fun convertHtmlNode(node: Node, isBlockCode: Boolean = false): ContentNode? {
293 if (isBlockCode) {
294 return if (node is TextNode) { // Fixes b/129762453
295 val codeNode = CodeNode(node.wholeText, "")
296 ContentText(codeNode.text().removePrefix("#"))
297 } else { // Fixes b/129857975
298 ContentText(node.toString())
299 }
300 }
301 if (node is TextNode) {
302 return ContentText(node.text().removePrefix("#"))
303 } else if (node is Element) {
304 val childBlock = createBlock(node)
305 node.childNodes().forEach {
306 val child = convertHtmlNode(it, isBlockCode = childBlock is ContentBlockCode)
307 if (child != null) {
308 childBlock.append(child)
309 }
310 }
311 return (childBlock)
312 }
313 return null
314 }
315
316 private fun createBlock(element: Element): ContentBlock = when (element.tagName()) {
317 "p" -> ContentParagraph()
318 "b", "strong" -> ContentStrong()
319 "i", "em" -> ContentEmphasis()
320 "s", "del" -> ContentStrikethrough()
321 "code" -> ContentCode()
322 "pre" -> ContentBlockCode()
323 "ul" -> ContentUnorderedList()
324 "ol" -> ContentOrderedList()
325 "li" -> ContentListItem()
326 "a" -> createLink(element)
327 "br" -> ContentBlock().apply { hardLineBreak() }
328
329 "dl" -> ContentDescriptionList()
330 "dt" -> ContentDescriptionTerm()
331 "dd" -> ContentDescriptionDefinition()
332
333 "table" -> ContentTable()
334 "tbody" -> ContentTableBody()
335 "tr" -> ContentTableRow()
336 "th" -> {
337 val colspan = element.attr("colspan")
338 val rowspan = element.attr("rowspan")
339 ContentTableHeader(colspan, rowspan)
340 }
341 "td" -> {
342 val colspan = element.attr("colspan")
343 val rowspan = element.attr("rowspan")
344 ContentTableCell(colspan, rowspan)
345 }
346
347 "h1" -> ContentHeading(1)
348 "h2" -> ContentHeading(2)
349 "h3" -> ContentHeading(3)
350 "h4" -> ContentHeading(4)
351 "h5" -> ContentHeading(5)
352 "h6" -> ContentHeading(6)
353
354 "div" -> {
355 val divClass = element.attr("class")
356 if (divClass == "special reference" || divClass == "note") ContentSpecialReference()
357 else ContentParagraph()
358 }
359
360 "script" -> {
361
362 // If the `type` attr is an empty string, we want to use null instead so that the resulting generated
363 // Javascript does not contain a `type` attr.
364 //
365 // Example:
366 // type == "" => <script type="" src="...">
367 // type == null => <script src="...">
368 val type = if (element.attr("type").isNotEmpty()) {
369 element.attr("type")
370 } else {
371 null
372 }
373 ScriptBlock(type, element.attr("src"))
374 }
375
376 else -> ContentBlock()
377 }
378
379 private fun createLink(element: Element): ContentBlock {
380 return when {
381 element.hasAttr("docref") -> {
382 val docref = element.attr("docref")
383 ContentNodeLazyLink(docref, { -> refGraph.lookupOrWarn(docref, logger) })
384 }
385 element.hasAttr("href") -> {
386 val href = element.attr("href")
387
388 val uri = try {
389 URI(href)
390 } catch (_: Exception) {
391 null
392 }
393
394 if (uri?.isAbsolute == false) {
395 ContentLocalLink(href)
396 } else {
397 ContentExternalLink(href)
398 }
399 }
400 element.hasAttr("name") -> {
401 ContentBookmark(element.attr("name"))
402 }
403 else -> ContentBlock()
404 }
405 }
406
407 private fun MutableContent.convertSeeTag(tag: PsiDocTag) {
408 val linkElement = tag.linkElement() ?: return
409 val seeSection = findSectionByTag(ContentTags.SeeAlso) ?: addSection(ContentTags.SeeAlso, null)
410
411 val valueElement = tag.referenceElement()
412 val externalLink = resolveExternalLink(valueElement)
413 val text = ContentText(linkElement.text)
414
415 val linkSignature by lazy { resolveInternalLink(valueElement) }
416 val node = when {
417 externalLink != null -> {
418 val linkNode = ContentExternalLink(externalLink)
419 linkNode.append(text)
420 linkNode
421 }
422 linkSignature != null -> {
423 val linkNode =
424 ContentNodeLazyLink(
425 (tag.valueElement ?: linkElement).text,
426 { -> refGraph.lookupOrWarn(linkSignature, logger) }
427 )
428 linkNode.append(text)
429 linkNode
430 }
431 else -> text
432 }
433 seeSection.append(node)
434 }
435
436 private fun convertInlineDocTag(tag: PsiInlineDocTag, element: PsiNamedElement) = when (tag.name) {
437 "link", "linkplain" -> {
438 val valueElement = tag.referenceElement()
439 val externalLink = resolveExternalLink(valueElement)
440 val linkSignature by lazy { resolveInternalLink(valueElement) }
441 if (externalLink != null || linkSignature != null) {
442 val labelText = tag.dataElements.firstOrNull { it is PsiDocToken }?.text ?: valueElement!!.text
443 val linkTarget = if (externalLink != null) "href=\"$externalLink\"" else "docref=\"$linkSignature\""
444 val link = "<a $linkTarget>$labelText</a>"
445 if (tag.name == "link") "<code>$link</code>" else link
446 } else if (valueElement != null) {
447 valueElement.text
448 } else {
449 ""
450 }
451 }
452 "code", "literal" -> {
453 val text = StringBuilder()
454 tag.dataElements.forEach { text.append(it.text) }
455 val escaped = text.toString().trimStart().htmlEscape()
456 if (tag.name == "code") "<code>$escaped</code>" else escaped
457 }
458 "inheritDoc" -> {
459 val result = (element as? PsiMethod)?.let {
460 // @{inheritDoc} is only allowed on functions
461 val parent = tag.parent
462 when (parent) {
463 is PsiDocComment -> element.findSuperDocCommentOrWarn()
464 is PsiDocTag -> element.findSuperDocTagOrWarn(parent)
465 else -> null
466 }
467 }
468 result ?: tag.text
469 }
470 "docRoot" -> {
471 // TODO: fix that
472 "https://developer.android.com/"
473 }
474 "sample" -> {
475 tag.text?.let { tagText ->
476 val (absolutePath, delimiter) = getSampleAnnotationInformation(tagText)
477 val code = retrieveCodeInFile(absolutePath, delimiter)
478 return if (code != null && code.isNotEmpty()) {
479 "<pre is-upgraded>$code</pre>"
480 } else {
481 ""
482 }
483 }
484 }
485
486 // Loads MathJax script from local source, which then updates MathJax HTML code
487 "usesMathJax" -> {
488 "<script src=\"/_static/js/managed/mathjax/MathJax.js?config=TeX-AMS_SVG\"></script>"
489 }
490
491 else -> tag.text
492 }
493
494 private fun PsiDocTag.referenceElement(): PsiElement? =
495 linkElement()?.let {
496 if (it.node.elementType == JavaDocElementType.DOC_REFERENCE_HOLDER) {
497 PsiTreeUtil.findChildOfType(it, PsiJavaCodeReferenceElement::class.java)
498 } else {
499 it
500 }
501 }
502
503 private fun PsiDocTag.linkElement(): PsiElement? =
504 valueElement ?: dataElements.firstOrNull { it !is PsiWhiteSpace }
505
506 private fun resolveExternalLink(valueElement: PsiElement?): String? {
507 val target = valueElement?.reference?.resolve()
508 if (target != null) {
509 return externalDocumentationLinkResolver.buildExternalDocumentationLink(target)
510 }
511 return null
512 }
513
514 private fun resolveInternalLink(valueElement: PsiElement?): String? {
515 val target = valueElement?.reference?.resolve()
516 if (target != null) {
517 return signatureProvider.signature(target)
518 }
519 return null
520 }
521
522 fun PsiDocTag.getSubjectName(): String? {
523 if (name == "param" || name == "throws" || name == "exception") {
524 return valueElement?.text
525 }
526 return null
527 }
528
529 private fun PsiMethod.findSuperDocCommentOrWarn(): String {
530 val method = findFirstSuperMethodWithDocumentation(this)
531 if (method != null) {
532 val descriptionElements = method.docComment?.descriptionElements?.dropWhile {
533 it.text.trim().isEmpty()
534 } ?: return ""
535
536 return expandAllForElements(descriptionElements, method)
537 }
538 logger.warn("No docs found on supertype with {@inheritDoc} method ${this.name} in ${this.containingFile.name}:${this.lineNumber()}")
539 return ""
540 }
541
542
543 private fun PsiMethod.findSuperDocTagOrWarn(elementToExpand: PsiDocTag): String {
544 val result = findFirstSuperMethodWithDocumentationforTag(elementToExpand, this)
545
546 if (result != null) {
547 val (method, tag) = result
548
549 val contentElements = tag.contentElements().dropWhile { it.text.trim().isEmpty() }
550
551 val expandedString = expandAllForElements(contentElements, method)
552
553 return expandedString
554 }
555 logger.warn("No docs found on supertype for @${elementToExpand.name} ${elementToExpand.getSubjectName()} with {@inheritDoc} method ${this.name} in ${this.containingFile.name}:${this.lineNumber()}")
556 return ""
557 }
558
559 private fun findFirstSuperMethodWithDocumentation(current: PsiMethod): PsiMethod? {
560 val superMethods = current.findSuperMethods()
561 for (method in superMethods) {
562 val docs = method.docComment?.descriptionElements?.dropWhile { it.text.trim().isEmpty() }
563 if (!docs.isNullOrEmpty()) {
564 return method
565 }
566 }
567 for (method in superMethods) {
568 val result = findFirstSuperMethodWithDocumentation(method)
569 if (result != null) {
570 return result
571 }
572 }
573
574 return null
575 }
576
577 private fun findFirstSuperMethodWithDocumentationforTag(
578 elementToExpand: PsiDocTag,
579 current: PsiMethod
580 ): Pair<PsiMethod, PsiDocTag>? {
581 val superMethods = current.findSuperMethods()
582 val mappedFilteredTags = superMethods.map {
583 it to it.docComment?.tags?.filter { it.name == elementToExpand.name }
584 }
585
586 for ((method, tags) in mappedFilteredTags) {
587 tags ?: continue
588 for (tag in tags) {
589 val (tagSubject, elementSubject) = when (tag.name) {
590 "throws" -> {
591 // match class names only for throws, ignore possibly fully qualified path
592 // TODO: Always match exactly here
593 tag.getSubjectName()?.split(".")?.last() to elementToExpand.getSubjectName()?.split(".")?.last()
594 }
595 else -> {
596 tag.getSubjectName() to elementToExpand.getSubjectName()
597 }
598 }
599
600 if (tagSubject == elementSubject) {
601 return method to tag
602 }
603 }
604 }
605
606 for (method in superMethods) {
607 val result = findFirstSuperMethodWithDocumentationforTag(elementToExpand, method)
608 if (result != null) {
609 return result
610 }
611 }
612 return null
613 }
614
615 /**
616 * Returns information inside @sample
617 *
618 * Component1 is the absolute path to the file
619 * Component2 is the delimiter if exists in the file
620 */
621 private fun getSampleAnnotationInformation(tagText: String): Pair<String, String> {
622 val pathContent = tagText
623 .trim { it == '{' || it == '}' }
624 .removePrefix("@sample ")
625
626 val formattedPath = pathContent.substringBefore(" ").trim()
627 val potentialDelimiter = pathContent.substringAfterLast(" ").trim()
628
629 val delimiter = if (potentialDelimiter == formattedPath) "" else potentialDelimiter
630 val path = "samples/$formattedPath"
631
632 return Pair(path, delimiter)
633 }
634
635 /**
636 * Retrieves the code inside a file.
637 *
638 * If betweenTag is not empty, it retrieves the code between
639 * BEGIN_INCLUDE($betweenTag) and END_INCLUDE($betweenTag) comments.
640 *
641 * Also, the method will trim every line with the number of spaces in the first line
642 */
643 private fun retrieveCodeInFile(path: String, betweenTag: String = "") = StringBuilder().apply {
644 try {
645 if (betweenTag.isEmpty()) {
646 appendContent(path)
647 } else {
648 appendContentBetweenIncludes(path, betweenTag)
649 }
650 } catch (e: java.lang.Exception) {
651 logger.error("No file found when processing Java @sample. Path to sample: $path")
652 }
653 }
654
655 private fun StringBuilder.appendContent(path: String) {
656 val spaces = InitialSpaceIndent()
657 File(path).forEachLine {
658 appendWithoutInitialIndent(it, spaces)
659 }
660 }
661
662 private fun StringBuilder.appendContentBetweenIncludes(path: String, includeTag: String) {
663 var shouldAppend = false
664 val beginning = "BEGIN_INCLUDE($includeTag)"
665 val end = "END_INCLUDE($includeTag)"
666 val spaces = InitialSpaceIndent()
667 File(path).forEachLine {
668 if (shouldAppend) {
669 if (it.contains(end)) {
670 shouldAppend = false
671 } else {
672 appendWithoutInitialIndent(it, spaces)
673 }
674 } else {
675 if (it.contains(beginning)) shouldAppend = true
676 }
677 }
678 }
679
680 private fun StringBuilder.appendWithoutInitialIndent(it: String, spaces: InitialSpaceIndent) {
681 if (spaces.value == -1) {
682 spaces.value = (it.length - it.trimStart().length).coerceAtLeast(0)
683 appendln(it)
684 } else {
685 appendln(if (it.isBlank()) it else it.substring(spaces.value, it.length))
686 }
687 }
688
689 private data class InitialSpaceIndent(var value: Int = -1)
690 }
691