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