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("*/", "*/")
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