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