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