1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tools.metalava.model.psi
18 
19 import com.android.tools.metalava.doclava1.Issues
20 import com.android.tools.metalava.model.ClassItem
21 import com.android.tools.metalava.model.Item
22 import com.android.tools.metalava.model.PackageItem
23 import com.android.tools.metalava.reporter
24 import com.intellij.psi.JavaDocTokenType
25 import com.intellij.psi.JavaPsiFacade
26 import com.intellij.psi.PsiClass
27 import com.intellij.psi.PsiElement
28 import com.intellij.psi.PsiJavaCodeReferenceElement
29 import com.intellij.psi.PsiMember
30 import com.intellij.psi.PsiMethod
31 import com.intellij.psi.PsiReference
32 import com.intellij.psi.PsiTypeParameter
33 import com.intellij.psi.PsiWhiteSpace
34 import com.intellij.psi.impl.source.SourceTreeToPsiMap
35 import com.intellij.psi.impl.source.javadoc.PsiDocMethodOrFieldRef
36 import com.intellij.psi.impl.source.tree.CompositePsiElement
37 import com.intellij.psi.impl.source.tree.JavaDocElementType
38 import com.intellij.psi.javadoc.PsiDocComment
39 import com.intellij.psi.javadoc.PsiDocTag
40 import com.intellij.psi.javadoc.PsiDocToken
41 import com.intellij.psi.javadoc.PsiInlineDocTag
42 import org.intellij.lang.annotations.Language
43 
44 /*
45  * Various utilities for handling javadoc, such as
46  * merging comments into existing javadoc sections,
47  * rewriting javadocs into fully qualified references, etc.
48  *
49  * TODO: Handle KDoc
50  */
51 
52 /**
53  * If the reference is to a class in the same package, include the package prefix?
54  * This should not be necessary, but doclava has problems finding classes without
55  * it. Consider turning this off when we switch to Dokka.
56  */
57 const val INCLUDE_SAME_PACKAGE = true
58 
59 /** If documentation starts with hash, insert the implicit class? */
60 const val PREPEND_LOCAL_CLASS = false
61 
62 /**
63  * Whether we should report unresolved symbols. This is typically
64  * a bug in the documentation. It looks like there are a LOT
65  * of mistakes right now, so I'm worried about turning this on
66  * since doclava didn't seem to abort on this.
67  *
68  * Here are some examples I've spot checked:
69  * (1) "Unresolved SQLExceptionif": In java.sql.CallableStatement the
70  * getBigDecimal method contains this, presumably missing a space
71  * before the if suffix: "@exception SQLExceptionif parameterName does not..."
72  * (2) In android.nfc.tech.IsoDep there is "@throws TagLostException if ..."
73  * but TagLostException is not imported anywhere and is not in the same
74  * package (it's in the parent package).
75  */
76 const val REPORT_UNRESOLVED_SYMBOLS = false
77 
78 /**
79  * Merges the given [newText] into the existing documentation block [existingDoc]
80  * (which should be a full documentation node, including the surrounding comment
81  * start and end tokens.)
82  *
83  * If the [tagSection] is null, add the comment to the initial text block
84  * of the description. Otherwise if it is "@return", add the comment
85  * to the return value. Otherwise the [tagSection] is taken to be the
86  * parameter name, and the comment added as parameter documentation
87  * for the given parameter.
88  */
mergeDocumentationnull89 fun mergeDocumentation(
90     existingDoc: String,
91     psiElement: PsiElement,
92     newText: String,
93     tagSection: String?,
94     append: Boolean
95 ): String {
96 
97     if (existingDoc.isBlank()) {
98         // There's no existing comment: Create a new one. This is easy.
99         val content = when {
100             tagSection == "@return" -> "@return $newText"
101             tagSection?.startsWith("@") ?: false -> "$tagSection $newText"
102             tagSection != null -> "@param $tagSection $newText"
103             else -> newText
104         }
105 
106         val inherit =
107             when (psiElement) {
108                 is PsiMethod -> psiElement.findSuperMethods(true).isNotEmpty()
109                 else -> false
110             }
111         val initial = if (inherit) "/**\n* {@inheritDoc}\n */" else "/** */"
112         val new = insertInto(initial, content, initial.indexOf("*/"))
113         if (new.startsWith("/**\n * \n *")) {
114             return "/**\n *" + new.substring(10)
115         }
116         return new
117     }
118 
119     val doc = trimDocIndent(existingDoc)
120 
121     // We'll use the PSI Javadoc support to parse the documentation
122     // to help us scan the tokens in the documentation, such that
123     // we don't have to search for raw substrings like "@return" which
124     // can incorrectly find matches in escaped code snippets etc.
125     val factory = JavaPsiFacade.getElementFactory(psiElement.project)
126         ?: error("Invalid tool configuration; did not find JavaPsiFacade factory")
127     val docComment = factory.createDocCommentFromText(doc)
128 
129     if (tagSection == "@return") {
130         // Add in return value
131         val returnTag = docComment.findTagByName("return")
132         if (returnTag == null) {
133             // Find last tag
134             val lastTag = findLastTag(docComment)
135             val offset = if (lastTag != null) {
136                 findTagEnd(lastTag)
137             } else {
138                 doc.length - 2
139             }
140             return insertInto(doc, "@return $newText", offset)
141         } else {
142             // Add text to the existing @return tag
143             val offset = if (append)
144                 findTagEnd(returnTag)
145             else returnTag.textRange.startOffset + returnTag.name.length + 1
146             return insertInto(doc, newText, offset)
147         }
148     } else if (tagSection != null) {
149         val parameter = if (tagSection.startsWith("@"))
150             docComment.findTagByName(tagSection.substring(1))
151         else findParamTag(docComment, tagSection)
152         if (parameter == null) {
153             // Add new parameter or tag
154             // TODO: Decide whether to place it alphabetically or place it by parameter order
155             // in the signature. Arguably I should follow the convention already present in the
156             // doc, if any
157             // For now just appending to the last tag before the return tag (if any).
158             // This actually works out well in practice where arguments are generally all documented
159             // or all not documented; when none of the arguments are documented these end up appending
160             // exactly in the right parameter order!
161             val returnTag = docComment.findTagByName("return")
162             val anchor = returnTag ?: findLastTag(docComment)
163             val offset = when {
164                 returnTag != null -> returnTag.textRange.startOffset
165                 anchor != null -> findTagEnd(anchor)
166                 else -> doc.length - 2 // "*/
167             }
168             val tagName = if (tagSection.startsWith("@")) tagSection else "@param $tagSection"
169             return insertInto(doc, "$tagName $newText", offset)
170         } else {
171             // Add to existing tag/parameter
172             val offset = if (append)
173                 findTagEnd(parameter)
174             else parameter.textRange.startOffset + parameter.name.length + 1
175             return insertInto(doc, newText, offset)
176         }
177     } else {
178         // Add to the main text section of the comment.
179         val firstTag = findFirstTag(docComment)
180         val startOffset =
181             if (!append) {
182                 4 // "/** ".length
183             } else firstTag?.textRange?.startOffset ?: doc.length - 2
184         // Insert a <br> before the appended docs, unless it's the beginning of a doc section
185         return insertInto(doc, if (startOffset > 4) "<br>\n$newText" else newText, startOffset)
186     }
187 }
188 
findParamTagnull189 fun findParamTag(docComment: PsiDocComment, paramName: String): PsiDocTag? {
190     return docComment.findTagsByName("param").firstOrNull { it.valueElement?.text == paramName }
191 }
192 
findFirstTagnull193 fun findFirstTag(docComment: PsiDocComment): PsiDocTag? {
194     return docComment.tags.asSequence().minBy { it.textRange.startOffset }
195 }
196 
findLastTagnull197 fun findLastTag(docComment: PsiDocComment): PsiDocTag? {
198     return docComment.tags.asSequence().maxBy { it.textRange.startOffset }
199 }
200 
findTagEndnull201 fun findTagEnd(tag: PsiDocTag): Int {
202     var curr: PsiElement? = tag.nextSibling
203     while (curr != null) {
204         if (curr is PsiDocToken && curr.tokenType == JavaDocTokenType.DOC_COMMENT_END) {
205             return curr.textRange.startOffset
206         } else if (curr is PsiDocTag) {
207             return curr.textRange.startOffset
208         }
209 
210         curr = curr.nextSibling
211     }
212 
213     return tag.textRange.endOffset
214 }
215 
trimDocIndentnull216 fun trimDocIndent(existingDoc: String): String {
217     val index = existingDoc.indexOf('\n')
218     if (index == -1) {
219         return existingDoc
220     }
221 
222     return existingDoc.substring(0, index + 1) +
223         existingDoc.substring(index + 1).trimIndent().split('\n').joinToString(separator = "\n") {
224             if (!it.startsWith(" ")) {
225                 " ${it.trimEnd()}"
226             } else {
227                 it.trimEnd()
228             }
229         }
230 }
231 
insertIntonull232 fun insertInto(existingDoc: String, newText: String, initialOffset: Int): String {
233     // TODO: Insert "." between existing documentation and new documentation, if necessary.
234 
235     val offset = if (initialOffset > 4 && existingDoc.regionMatches(initialOffset - 4, "\n * ", 0, 4, false)) {
236         initialOffset - 4
237     } else {
238         initialOffset
239     }
240     val index = existingDoc.indexOf('\n')
241     val prefixWithStar = index == -1 || existingDoc[index + 1] == '*' ||
242         existingDoc[index + 1] == ' ' && existingDoc[index + 2] == '*'
243 
244     val prefix = existingDoc.substring(0, offset)
245     val suffix = existingDoc.substring(offset)
246     val startSeparator = "\n"
247     val endSeparator =
248         if (suffix.startsWith("\n") || suffix.startsWith(" \n")) "" else if (suffix == "*/") "\n" else if (prefixWithStar) "\n * " else "\n"
249 
250     val middle = if (prefixWithStar) {
251         startSeparator + newText.split('\n').joinToString(separator = "\n") { " * $it" } +
252             endSeparator
253     } else {
254         "$startSeparator$newText$endSeparator"
255     }
256 
257     // Going from single-line to multi-line?
258     return if (existingDoc.indexOf('\n') == -1 && existingDoc.startsWith("/** ")) {
259         prefix.substring(0, 3) + "\n *" + prefix.substring(3) + middle +
260             if (suffix == "*/") " */" else suffix
261     } else {
262         prefix + middle + suffix
263     }
264 }
265 
266 /** Converts from package.html content to a package-info.java javadoc string. */
267 @Language("JAVA")
packageHtmlToJavadocnull268 fun packageHtmlToJavadoc(@Language("HTML") packageHtml: String?): String {
269     packageHtml ?: return ""
270     if (packageHtml.isBlank()) {
271         return ""
272     }
273 
274     val body = getBodyContents(packageHtml).trim()
275     if (body.isBlank()) {
276         return ""
277     }
278     // Combine into comment lines prefixed by asterisk, ,and make sure we don't
279     // have end-comment markers in the HTML that will escape out of the javadoc comment
280     val comment = body.lines().joinToString(separator = "\n") { " * $it" }.replace("*/", "&#42;/")
281     @Suppress("DanglingJavadoc")
282     return "/**\n$comment\n */\n"
283 }
284 
285 /**
286  * Returns the body content from the given HTML document.
287  * Attempts to tokenize the HTML properly such that it doesn't
288  * get confused by comments or text that looks like tags.
289  */
290 @Suppress("LocalVariableName")
getBodyContentsnull291 private fun getBodyContents(html: String): String {
292     val length = html.length
293     val STATE_TEXT = 1
294     val STATE_SLASH = 2
295     val STATE_ATTRIBUTE_NAME = 3
296     val STATE_IN_TAG = 4
297     val STATE_BEFORE_ATTRIBUTE = 5
298     val STATE_ATTRIBUTE_BEFORE_EQUALS = 6
299     val STATE_ATTRIBUTE_AFTER_EQUALS = 7
300     val STATE_ATTRIBUTE_VALUE_NONE = 8
301     val STATE_ATTRIBUTE_VALUE_SINGLE = 9
302     val STATE_ATTRIBUTE_VALUE_DOUBLE = 10
303     val STATE_CLOSE_TAG = 11
304     val STATE_ENDING_TAG = 12
305 
306     var bodyStart = -1
307     var htmlStart = -1
308 
309     var state = STATE_TEXT
310     var offset = 0
311     var tagStart = -1
312     var tagEndStart = -1
313     var prev = -1
314     loop@ while (offset < length) {
315         if (offset == prev) {
316             // Purely here to prevent potential bugs in the state machine from looping
317             // infinitely
318             offset++
319             if (offset == length) {
320                 break
321             }
322         }
323         prev = offset
324 
325         val c = html[offset]
326         when (state) {
327             STATE_TEXT -> {
328                 if (c == '<') {
329                     state = STATE_SLASH
330                     offset++
331                     continue@loop
332                 }
333 
334                 // Other text is just ignored
335                 offset++
336             }
337 
338             STATE_SLASH -> {
339                 if (c == '!') {
340                     if (html.startsWith("!--", offset)) {
341                         // Comment
342                         val end = html.indexOf("-->", offset + 3)
343                         if (end == -1) {
344                             offset = length
345                         } else {
346                             offset = end + 3
347                             state = STATE_TEXT
348                         }
349                         continue@loop
350                     } else if (html.startsWith("![CDATA[", offset)) {
351                         val end = html.indexOf("]]>", offset + 8)
352                         if (end == -1) {
353                             offset = length
354                         } else {
355                             state = STATE_TEXT
356                             offset = end + 3
357                         }
358                         continue@loop
359                     } else {
360                         val end = html.indexOf('>', offset + 2)
361                         if (end == -1) {
362                             offset = length
363                             state = STATE_TEXT
364                         } else {
365                             offset = end + 1
366                             state = STATE_TEXT
367                         }
368                         continue@loop
369                     }
370                 } else if (c == '/') {
371                     state = STATE_CLOSE_TAG
372                     offset++
373                     tagEndStart = offset
374                     continue@loop
375                 } else if (c == '?') {
376                     // XML Prologue
377                     val end = html.indexOf('>', offset + 2)
378                     if (end == -1) {
379                         offset = length
380                         state = STATE_TEXT
381                     } else {
382                         offset = end + 1
383                         state = STATE_TEXT
384                     }
385                     continue@loop
386                 }
387                 state = STATE_IN_TAG
388                 tagStart = offset
389             }
390 
391             STATE_CLOSE_TAG -> {
392                 if (c == '>') {
393                     state = STATE_TEXT
394                     if (html.startsWith("body", tagEndStart, true)) {
395                         val bodyEnd = tagEndStart - 2 // </
396                         if (bodyStart != -1) {
397                             return html.substring(bodyStart, bodyEnd)
398                         }
399                     }
400                     if (html.startsWith("html", tagEndStart, true)) {
401                         val htmlEnd = tagEndStart - 2
402                         if (htmlEnd != -1) {
403                             return html.substring(htmlStart, htmlEnd)
404                         }
405                     }
406                 }
407                 offset++
408             }
409 
410             STATE_IN_TAG -> {
411                 val whitespace = Character.isWhitespace(c)
412                 if (whitespace || c == '>') {
413                     if (html.startsWith("body", tagStart, true)) {
414                         bodyStart = html.indexOf('>', offset) + 1
415                     }
416                     if (html.startsWith("html", tagStart, true)) {
417                         htmlStart = html.indexOf('>', offset) + 1
418                     }
419                 }
420 
421                 when {
422                     whitespace -> state = STATE_BEFORE_ATTRIBUTE
423                     c == '>' -> {
424                         state = STATE_TEXT
425                     }
426                     c == '/' -> state = STATE_ENDING_TAG
427                 }
428                 offset++
429             }
430 
431             STATE_ENDING_TAG -> {
432                 if (c == '>') {
433                     if (html.startsWith("body", tagEndStart, true)) {
434                         val bodyEnd = tagEndStart - 1
435                         if (bodyStart != -1) {
436                             return html.substring(bodyStart, bodyEnd)
437                         }
438                     }
439                     if (html.startsWith("html", tagEndStart, true)) {
440                         val htmlEnd = tagEndStart - 1
441                         if (htmlEnd != -1) {
442                             return html.substring(htmlStart, htmlEnd)
443                         }
444                     }
445                     offset++
446                     state = STATE_TEXT
447                 }
448             }
449 
450             STATE_BEFORE_ATTRIBUTE -> {
451                 if (c == '>') {
452                     state = STATE_TEXT
453                 } else if (c == '/') {
454                     // we expect an '>' next to close the tag
455                 } else if (!Character.isWhitespace(c)) {
456                     state = STATE_ATTRIBUTE_NAME
457                 }
458                 offset++
459             }
460             STATE_ATTRIBUTE_NAME -> {
461                 when {
462                     c == '>' -> state = STATE_TEXT
463                     c == '=' -> state = STATE_ATTRIBUTE_AFTER_EQUALS
464                     Character.isWhitespace(c) -> state = STATE_ATTRIBUTE_BEFORE_EQUALS
465                     c == ':' -> {
466                     }
467                 }
468                 offset++
469             }
470             STATE_ATTRIBUTE_BEFORE_EQUALS -> {
471                 if (c == '=') {
472                     state = STATE_ATTRIBUTE_AFTER_EQUALS
473                 } else if (c == '>') {
474                     state = STATE_TEXT
475                 } else if (!Character.isWhitespace(c)) {
476                     // Attribute value not specified (used for some boolean attributes)
477                     state = STATE_ATTRIBUTE_NAME
478                 }
479                 offset++
480             }
481 
482             STATE_ATTRIBUTE_AFTER_EQUALS -> {
483                 if (c == '\'') {
484                     // a='b'
485                     state = STATE_ATTRIBUTE_VALUE_SINGLE
486                 } else if (c == '"') {
487                     // a="b"
488                     state = STATE_ATTRIBUTE_VALUE_DOUBLE
489                 } else if (!Character.isWhitespace(c)) {
490                     // a=b
491                     state = STATE_ATTRIBUTE_VALUE_NONE
492                 }
493                 offset++
494             }
495 
496             STATE_ATTRIBUTE_VALUE_SINGLE -> {
497                 if (c == '\'') {
498                     state = STATE_BEFORE_ATTRIBUTE
499                 }
500                 offset++
501             }
502             STATE_ATTRIBUTE_VALUE_DOUBLE -> {
503                 if (c == '"') {
504                     state = STATE_BEFORE_ATTRIBUTE
505                 }
506                 offset++
507             }
508             STATE_ATTRIBUTE_VALUE_NONE -> {
509                 if (c == '>') {
510                     state = STATE_TEXT
511                 } else if (Character.isWhitespace(c)) {
512                     state = STATE_BEFORE_ATTRIBUTE
513                 }
514                 offset++
515             }
516             else -> assert(false) { state }
517         }
518     }
519 
520     return html
521 }
522 
containsLinkTagsnull523 fun containsLinkTags(documentation: String): Boolean {
524     var index = 0
525     while (true) {
526         index = documentation.indexOf('@', index)
527         if (index == -1) {
528             return false
529         }
530         if (!documentation.startsWith("@code", index) &&
531             !documentation.startsWith("@literal", index) &&
532             !documentation.startsWith("@param", index) &&
533             !documentation.startsWith("@deprecated", index) &&
534             !documentation.startsWith("@inheritDoc", index) &&
535             !documentation.startsWith("@return", index)
536         ) {
537             return true
538         }
539 
540         index++
541     }
542 }
543 
544 // ------------------------------------------------------------------------------------
545 // Expanding javadocs into fully qualified documentation
546 // ------------------------------------------------------------------------------------
547 
toFullyQualifiedDocumentationnull548 fun toFullyQualifiedDocumentation(owner: PsiItem, documentation: String): String {
549     if (documentation.isBlank() || !containsLinkTags(documentation)) {
550         return documentation
551     }
552 
553     val codebase = owner.codebase
554     val comment =
555         try {
556             codebase.getComment(documentation, owner.psi())
557         } catch (throwable: Throwable) {
558             // TODO: Get rid of line comments as documentation
559             // Invalid comment
560             if (documentation.startsWith("//") && documentation.contains("/**")) {
561                 return toFullyQualifiedDocumentation(owner, documentation.substring(documentation.indexOf("/**")))
562             }
563             codebase.getComment(documentation, owner.psi())
564         }
565     val sb = StringBuilder(documentation.length)
566     expand(owner, comment, sb)
567 
568     return sb.toString()
569 }
570 
reportUnresolvedDocReferencenull571 private fun reportUnresolvedDocReference(owner: Item, unresolved: String) {
572     @Suppress("ConstantConditionIf")
573     if (!REPORT_UNRESOLVED_SYMBOLS) {
574         return
575     }
576 
577     if (unresolved.startsWith("{@") && !unresolved.startsWith("{@link")) {
578         return
579     }
580 
581     // References are sometimes split across lines and therefore have newlines, leading asterisks
582     // etc in the middle: clean this up before emitting reference into error message
583     val cleaned = unresolved.replace("\n", "").replace("*", "")
584         .replace("  ", " ")
585 
586     reporter.report(Issues.UNRESOLVED_LINK, owner, "Unresolved documentation reference: $cleaned")
587 }
588 
expandnull589 private fun expand(owner: PsiItem, element: PsiElement, sb: StringBuilder) {
590     when {
591         element is PsiWhiteSpace -> {
592             sb.append(element.text)
593         }
594         element is PsiDocToken -> {
595             assert(element.firstChild == null)
596             val text = element.text
597             // Auto-fix some docs in the framework which starts with R.styleable in @attr
598             if (text.startsWith("R.styleable#") && owner.documentation.contains("@attr")) {
599                 sb.append("android.")
600             }
601 
602             sb.append(text)
603         }
604         element is PsiDocMethodOrFieldRef -> {
605             val text = element.text
606             var resolved = element.reference?.resolve()
607 
608             // Workaround: relative references doesn't work from a class item to its members
609             if (resolved == null && owner is ClassItem) {
610                 // For some reason, resolving relative methods and field references at the root
611                 // level isn't working right.
612                 if (PREPEND_LOCAL_CLASS && text.startsWith("#")) {
613                     var end = text.indexOf('(')
614                     if (end == -1) {
615                         // definitely a field
616                         end = text.length
617                         val fieldName = text.substring(1, end)
618                         val field = owner.findField(fieldName)
619                         if (field != null) {
620                             resolved = field.psi()
621                         }
622                     }
623                     if (resolved == null) {
624                         val methodName = text.substring(1, end)
625                         resolved = (owner.psi() as PsiClass).findMethodsByName(methodName, true).firstOrNull()
626                     }
627                 }
628             }
629 
630             if (resolved is PsiMember) {
631                 val containingClass = resolved.containingClass
632                 if (containingClass != null && !samePackage(owner, containingClass)) {
633                     val referenceText = element.reference?.element?.text ?: text
634                     if (!PREPEND_LOCAL_CLASS && referenceText.startsWith("#")) {
635                         sb.append(text)
636                         return
637                     }
638 
639                     var className = containingClass.qualifiedName
640 
641                     if (element.firstChildNode.elementType === JavaDocElementType.DOC_REFERENCE_HOLDER) {
642                         val firstChildPsi =
643                             SourceTreeToPsiMap.treeElementToPsi(element.firstChildNode.firstChildNode)
644                         if (firstChildPsi is PsiJavaCodeReferenceElement) {
645                             val referenceElement = firstChildPsi as PsiJavaCodeReferenceElement?
646                             val referencedElement = referenceElement!!.resolve()
647                             if (referencedElement is PsiClass) {
648                                 className = referencedElement.qualifiedName
649                             }
650                         }
651                     }
652 
653                     sb.append(className)
654                     sb.append('#')
655                     sb.append(resolved.name)
656                     val index = text.indexOf('(')
657                     if (index != -1) {
658                         sb.append(text.substring(index))
659                     }
660                 } else {
661                     sb.append(text)
662                 }
663             } else {
664                 if (resolved == null) {
665                     val referenceText = element.reference?.element?.text ?: text
666                     if (text.startsWith("#") && owner is ClassItem) {
667                         // Unfortunately resolving references is broken from class javadocs
668                         // to members using just a relative reference, #.
669                     } else {
670                         reportUnresolvedDocReference(owner, referenceText)
671                     }
672                 }
673                 sb.append(text)
674             }
675         }
676         element is PsiJavaCodeReferenceElement -> {
677             val resolved = element.resolve()
678             if (resolved is PsiClass) {
679                 if (samePackage(owner, resolved) || resolved is PsiTypeParameter) {
680                     sb.append(element.text)
681                 } else {
682                     sb.append(resolved.qualifiedName)
683                 }
684             } else if (resolved is PsiMember) {
685                 val text = element.text
686                 sb.append(resolved.containingClass?.qualifiedName)
687                 sb.append('#')
688                 sb.append(resolved.name)
689                 val index = text.indexOf('(')
690                 if (index != -1) {
691                     sb.append(text.substring(index))
692                 }
693             } else {
694                 val text = element.text
695                 if (resolved == null) {
696                     reportUnresolvedDocReference(owner, text)
697                 }
698                 sb.append(text)
699             }
700         }
701         element is PsiInlineDocTag -> {
702             val handled = handleTag(element, owner, sb)
703             if (!handled) {
704                 sb.append(element.text)
705             }
706         }
707         element.firstChild != null -> {
708             var curr = element.firstChild
709             while (curr != null) {
710                 expand(owner, curr, sb)
711                 curr = curr.nextSibling
712             }
713         }
714         else -> {
715             val text = element.text
716             sb.append(text)
717         }
718     }
719 }
720 
handleTagnull721 fun handleTag(
722     element: PsiInlineDocTag,
723     owner: PsiItem,
724     sb: StringBuilder
725 ): Boolean {
726     val name = element.name
727     if (name == "code" || name == "literal") {
728         // @code: don't attempt to rewrite this
729         sb.append(element.text)
730         return true
731     }
732 
733     val reference = extractReference(element)
734     val referenceText = reference?.element?.text ?: element.text
735     if (!PREPEND_LOCAL_CLASS && referenceText.startsWith("#")) {
736         val suffix = element.text
737         if (suffix.contains("(") && suffix.contains(")")) {
738             expandArgumentList(element, suffix, sb)
739         } else {
740             sb.append(suffix)
741         }
742         return true
743     }
744 
745     // TODO: If referenceText is already absolute, e.g. android.Manifest.permission#BIND_CARRIER_SERVICES,
746     // try to short circuit this?
747 
748     val valueElement = element.valueElement
749     if (valueElement is CompositePsiElement) {
750         if (valueElement.firstChildNode.elementType === JavaDocElementType.DOC_REFERENCE_HOLDER) {
751             val firstChildPsi =
752                 SourceTreeToPsiMap.treeElementToPsi(valueElement.firstChildNode.firstChildNode)
753             if (firstChildPsi is PsiJavaCodeReferenceElement) {
754                 val referenceElement = firstChildPsi as PsiJavaCodeReferenceElement?
755                 val referencedElement = referenceElement!!.resolve()
756                 if (referencedElement is PsiClass) {
757                     var className = PsiClassItem.computeFullClassName(referencedElement)
758                     if (className.indexOf('.') != -1 && !referenceText.startsWith(className)) {
759                         val simpleName = referencedElement.name
760                         if (simpleName != null && referenceText.startsWith(simpleName)) {
761                             className = simpleName
762                         }
763                     }
764                     if (referenceText.startsWith(className)) {
765                         sb.append("{@")
766                         sb.append(element.name)
767                         sb.append(' ')
768                         sb.append(referencedElement.qualifiedName)
769                         val suffix = referenceText.substring(className.length)
770                         if (suffix.contains("(") && suffix.contains(")")) {
771                             expandArgumentList(element, suffix, sb)
772                         } else {
773                             sb.append(suffix)
774                         }
775                         sb.append(' ')
776                         sb.append(referenceText)
777                         sb.append("}")
778                         return true
779                     }
780                 }
781             }
782         }
783     }
784 
785     var resolved = reference?.resolve()
786     if (resolved == null && owner is ClassItem) {
787         // For some reason, resolving relative methods and field references at the root
788         // level isn't working right.
789         if (PREPEND_LOCAL_CLASS && referenceText.startsWith("#")) {
790             var end = referenceText.indexOf('(')
791             if (end == -1) {
792                 // definitely a field
793                 end = referenceText.length
794                 val fieldName = referenceText.substring(1, end)
795                 val field = owner.findField(fieldName)
796                 if (field != null) {
797                     resolved = field.psi()
798                 }
799             }
800             if (resolved == null) {
801                 val methodName = referenceText.substring(1, end)
802                 resolved = (owner.psi() as PsiClass).findMethodsByName(methodName, true).firstOrNull()
803             }
804         }
805     }
806 
807     if (resolved != null) {
808         when (resolved) {
809             is PsiClass -> {
810                 val text = element.text
811                 if (samePackage(owner, resolved)) {
812                     sb.append(text)
813                     return true
814                 }
815                 val qualifiedName = resolved.qualifiedName ?: run {
816                     sb.append(text)
817                     return true
818                 }
819                 if (referenceText == qualifiedName) {
820                     // Already absolute
821                     sb.append(text)
822                     return true
823                 }
824                 val append = when {
825                     valueElement != null -> {
826                         val start = valueElement.startOffsetInParent
827                         val end = start + valueElement.textLength
828                         text.substring(0, start) + qualifiedName + text.substring(end)
829                     }
830                     name == "see" -> {
831                         val suffix = text.substring(text.indexOf(referenceText) + referenceText.length)
832                         "@see $qualifiedName$suffix"
833                     }
834                     text.startsWith("{") -> "{@$name $qualifiedName $referenceText}"
835                     else -> "@$name $qualifiedName $referenceText"
836                 }
837                 sb.append(append)
838                 return true
839             }
840             is PsiMember -> {
841                 val text = element.text
842                 val containing = resolved.containingClass ?: run {
843                     sb.append(text)
844                     return true
845                 }
846                 if (samePackage(owner, containing)) {
847                     sb.append(text)
848                     return true
849                 }
850                 val qualifiedName = containing.qualifiedName ?: run {
851                     sb.append(text)
852                     return true
853                 }
854                 if (referenceText.startsWith(qualifiedName)) {
855                     // Already absolute
856                     sb.append(text)
857                     return true
858                 }
859 
860                 // It may also be the case that the reference is already fully qualified
861                 // but to some different class. For example, the link may be to
862                 // android.os.Bundle#getInt, but the resolved method actually points to
863                 // an inherited method into android.os.Bundle from android.os.BaseBundle.
864                 // In that case we don't want to rewrite the link.
865                 for (c in referenceText) {
866                     if (c == '.') {
867                         // Already qualified
868                         sb.append(text)
869                         return true
870                     } else if (!Character.isJavaIdentifierPart(c)) {
871                         break
872                     }
873                 }
874 
875                 if (valueElement != null) {
876                     val start = valueElement.startOffsetInParent
877 
878                     var nameEnd = -1
879                     var close = start
880                     var balance = 0
881                     while (close < text.length) {
882                         val c = text[close]
883                         if (c == '(') {
884                             if (nameEnd == -1) {
885                                 nameEnd = close
886                             }
887                             balance++
888                         } else if (c == ')') {
889                             balance--
890                             if (balance == 0) {
891                                 close++
892                                 break
893                             }
894                         } else if (c == '}') {
895                             if (nameEnd == -1) {
896                                 nameEnd = close
897                             }
898                             break
899                         } else if (balance == 0 && c == '#') {
900                             if (nameEnd == -1) {
901                                 nameEnd = close
902                             }
903                         } else if (balance == 0 && !Character.isJavaIdentifierPart(c)) {
904                             break
905                         }
906                         close++
907                     }
908                     val memberPart = text.substring(nameEnd, close)
909                     val append = "${text.substring(0, start)}$qualifiedName$memberPart $referenceText}"
910                     sb.append(append)
911                     return true
912                 }
913             }
914         }
915     } else {
916         reportUnresolvedDocReference(owner, referenceText)
917     }
918 
919     return false
920 }
921 
expandArgumentListnull922 private fun expandArgumentList(
923     element: PsiInlineDocTag,
924     suffix: String,
925     sb: StringBuilder
926 ) {
927     val elementFactory = JavaPsiFacade.getElementFactory(element.project)
928     // Try to rewrite the types to fully qualified names as well
929     val begin = suffix.indexOf('(')
930     sb.append(suffix.substring(0, begin + 1))
931     var index = begin + 1
932     var balance = 0
933     var argBegin = index
934     while (index < suffix.length) {
935         val c = suffix[index++]
936         if (c == '<' || c == '(') {
937             balance++
938         } else if (c == '>') {
939             balance--
940         } else if (c == ')' && balance == 0 || c == ',') {
941             // Strip off javadoc header
942             while (argBegin < index) {
943                 val p = suffix[argBegin]
944                 if (p != '*' && !p.isWhitespace()) {
945                     break
946                 }
947                 argBegin++
948             }
949             if (index > argBegin + 1) {
950                 val arg = suffix.substring(argBegin, index - 1).trim()
951                 val space = arg.indexOf(' ')
952                 // Strip off parameter name (shouldn't be there but happens
953                 // in some Android sources sine tools didn't use to complain
954                 val typeString = if (space == -1) {
955                     arg
956                 } else {
957                     if (space < arg.length - 1 && !arg[space + 1].isJavaIdentifierStart()) {
958                         // Example: "String []"
959                         arg
960                     } else {
961                         // Example "String name"
962                         arg.substring(0, space)
963                     }
964                 }
965                 var insert = arg
966                 if (typeString[0].isUpperCase()) {
967                     try {
968                         val type = elementFactory.createTypeFromText(typeString, element)
969                         insert = type.canonicalText
970                     } catch (ignore: com.intellij.util.IncorrectOperationException) {
971                         // Not a valid type - just leave what was in the parameter text
972                     }
973                 }
974                 sb.append(insert)
975                 sb.append(c)
976                 if (c == ')') {
977                     break
978                 }
979             } else if (c == ')') {
980                 sb.append(')')
981                 break
982             }
983             argBegin = index
984         } else if (c == ')') {
985             balance--
986         }
987     }
988     while (index < suffix.length) {
989         sb.append(suffix[index++])
990     }
991 }
992 
samePackagenull993 private fun samePackage(owner: PsiItem, cls: PsiClass): Boolean {
994     @Suppress("ConstantConditionIf")
995     if (INCLUDE_SAME_PACKAGE) {
996         // doclava seems to have REAL problems with this
997         return false
998     }
999     val pkg = packageName(owner) ?: return false
1000     return cls.qualifiedName == "$pkg.${cls.name}"
1001 }
1002 
packageNamenull1003 private fun packageName(owner: PsiItem): String? {
1004     var curr: Item? = owner
1005     while (curr != null) {
1006         if (curr is PackageItem) {
1007             return curr.qualifiedName()
1008         }
1009         curr = curr.parent()
1010     }
1011 
1012     return null
1013 }
1014 
1015 // Copied from UnnecessaryJavaDocLinkInspection and tweaked a bit
extractReferencenull1016 private fun extractReference(tag: PsiDocTag): PsiReference? {
1017     val valueElement = tag.valueElement
1018     if (valueElement != null) {
1019         return valueElement.reference
1020     }
1021     // hack around the fact that a reference to a class is apparently
1022     // not a PsiDocTagValue
1023     val dataElements = tag.dataElements
1024     if (dataElements.isEmpty()) {
1025         return null
1026     }
1027     val salientElement: PsiElement =
1028         dataElements.firstOrNull { it !is PsiWhiteSpace && it !is PsiDocToken } ?: return null
1029     val child = salientElement.firstChild
1030     return if (child !is PsiReference) null else child
1031 }
1032