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