1 /*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.tools.metalava.model.psi
18
19 import com.android.tools.metalava.doclava1.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().minBy { it.textRange.startOffset }
195 }
196
findLastTagnull197 fun findLastTag(docComment: PsiDocComment): PsiDocTag? {
198 return docComment.tags.asSequence().maxBy { 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 if (!PREPEND_LOCAL_CLASS && referenceText.startsWith("#")) {
736 val suffix = element.text
737 if (suffix.contains("(") && suffix.contains(")")) {
738 expandArgumentList(element, suffix, sb)
739 } else {
740 sb.append(suffix)
741 }
742 return true
743 }
744
745 // TODO: If referenceText is already absolute, e.g. android.Manifest.permission#BIND_CARRIER_SERVICES,
746 // try to short circuit this?
747
748 val valueElement = element.valueElement
749 if (valueElement is CompositePsiElement) {
750 if (valueElement.firstChildNode.elementType === JavaDocElementType.DOC_REFERENCE_HOLDER) {
751 val firstChildPsi =
752 SourceTreeToPsiMap.treeElementToPsi(valueElement.firstChildNode.firstChildNode)
753 if (firstChildPsi is PsiJavaCodeReferenceElement) {
754 val referenceElement = firstChildPsi as PsiJavaCodeReferenceElement?
755 val referencedElement = referenceElement!!.resolve()
756 if (referencedElement is PsiClass) {
757 var className = PsiClassItem.computeFullClassName(referencedElement)
758 if (className.indexOf('.') != -1 && !referenceText.startsWith(className)) {
759 val simpleName = referencedElement.name
760 if (simpleName != null && referenceText.startsWith(simpleName)) {
761 className = simpleName
762 }
763 }
764 if (referenceText.startsWith(className)) {
765 sb.append("{@")
766 sb.append(element.name)
767 sb.append(' ')
768 sb.append(referencedElement.qualifiedName)
769 val suffix = referenceText.substring(className.length)
770 if (suffix.contains("(") && suffix.contains(")")) {
771 expandArgumentList(element, suffix, sb)
772 } else {
773 sb.append(suffix)
774 }
775 sb.append(' ')
776 sb.append(referenceText)
777 sb.append("}")
778 return true
779 }
780 }
781 }
782 }
783 }
784
785 var resolved = reference?.resolve()
786 if (resolved == null && owner is ClassItem) {
787 // For some reason, resolving relative methods and field references at the root
788 // level isn't working right.
789 if (PREPEND_LOCAL_CLASS && referenceText.startsWith("#")) {
790 var end = referenceText.indexOf('(')
791 if (end == -1) {
792 // definitely a field
793 end = referenceText.length
794 val fieldName = referenceText.substring(1, end)
795 val field = owner.findField(fieldName)
796 if (field != null) {
797 resolved = field.psi()
798 }
799 }
800 if (resolved == null) {
801 val methodName = referenceText.substring(1, end)
802 resolved = (owner.psi() as PsiClass).findMethodsByName(methodName, true).firstOrNull()
803 }
804 }
805 }
806
807 if (resolved != null) {
808 when (resolved) {
809 is PsiClass -> {
810 val text = element.text
811 if (samePackage(owner, resolved)) {
812 sb.append(text)
813 return true
814 }
815 val qualifiedName = resolved.qualifiedName ?: run {
816 sb.append(text)
817 return true
818 }
819 if (referenceText == qualifiedName) {
820 // Already absolute
821 sb.append(text)
822 return true
823 }
824 val append = when {
825 valueElement != null -> {
826 val start = valueElement.startOffsetInParent
827 val end = start + valueElement.textLength
828 text.substring(0, start) + qualifiedName + text.substring(end)
829 }
830 name == "see" -> {
831 val suffix = text.substring(text.indexOf(referenceText) + referenceText.length)
832 "@see $qualifiedName$suffix"
833 }
834 text.startsWith("{") -> "{@$name $qualifiedName $referenceText}"
835 else -> "@$name $qualifiedName $referenceText"
836 }
837 sb.append(append)
838 return true
839 }
840 is PsiMember -> {
841 val text = element.text
842 val containing = resolved.containingClass ?: run {
843 sb.append(text)
844 return true
845 }
846 if (samePackage(owner, containing)) {
847 sb.append(text)
848 return true
849 }
850 val qualifiedName = containing.qualifiedName ?: run {
851 sb.append(text)
852 return true
853 }
854 if (referenceText.startsWith(qualifiedName)) {
855 // Already absolute
856 sb.append(text)
857 return true
858 }
859
860 // It may also be the case that the reference is already fully qualified
861 // but to some different class. For example, the link may be to
862 // android.os.Bundle#getInt, but the resolved method actually points to
863 // an inherited method into android.os.Bundle from android.os.BaseBundle.
864 // In that case we don't want to rewrite the link.
865 for (c in referenceText) {
866 if (c == '.') {
867 // Already qualified
868 sb.append(text)
869 return true
870 } else if (!Character.isJavaIdentifierPart(c)) {
871 break
872 }
873 }
874
875 if (valueElement != null) {
876 val start = valueElement.startOffsetInParent
877
878 var nameEnd = -1
879 var close = start
880 var balance = 0
881 while (close < text.length) {
882 val c = text[close]
883 if (c == '(') {
884 if (nameEnd == -1) {
885 nameEnd = close
886 }
887 balance++
888 } else if (c == ')') {
889 balance--
890 if (balance == 0) {
891 close++
892 break
893 }
894 } else if (c == '}') {
895 if (nameEnd == -1) {
896 nameEnd = close
897 }
898 break
899 } else if (balance == 0 && c == '#') {
900 if (nameEnd == -1) {
901 nameEnd = close
902 }
903 } else if (balance == 0 && !Character.isJavaIdentifierPart(c)) {
904 break
905 }
906 close++
907 }
908 val memberPart = text.substring(nameEnd, close)
909 val append = "${text.substring(0, start)}$qualifiedName$memberPart $referenceText}"
910 sb.append(append)
911 return true
912 }
913 }
914 }
915 } else {
916 reportUnresolvedDocReference(owner, referenceText)
917 }
918
919 return false
920 }
921
expandArgumentListnull922 private fun expandArgumentList(
923 element: PsiInlineDocTag,
924 suffix: String,
925 sb: StringBuilder
926 ) {
927 val elementFactory = JavaPsiFacade.getElementFactory(element.project)
928 // Try to rewrite the types to fully qualified names as well
929 val begin = suffix.indexOf('(')
930 sb.append(suffix.substring(0, begin + 1))
931 var index = begin + 1
932 var balance = 0
933 var argBegin = index
934 while (index < suffix.length) {
935 val c = suffix[index++]
936 if (c == '<' || c == '(') {
937 balance++
938 } else if (c == '>') {
939 balance--
940 } else if (c == ')' && balance == 0 || c == ',') {
941 // Strip off javadoc header
942 while (argBegin < index) {
943 val p = suffix[argBegin]
944 if (p != '*' && !p.isWhitespace()) {
945 break
946 }
947 argBegin++
948 }
949 if (index > argBegin + 1) {
950 val arg = suffix.substring(argBegin, index - 1).trim()
951 val space = arg.indexOf(' ')
952 // Strip off parameter name (shouldn't be there but happens
953 // in some Android sources sine tools didn't use to complain
954 val typeString = if (space == -1) {
955 arg
956 } else {
957 if (space < arg.length - 1 && !arg[space + 1].isJavaIdentifierStart()) {
958 // Example: "String []"
959 arg
960 } else {
961 // Example "String name"
962 arg.substring(0, space)
963 }
964 }
965 var insert = arg
966 if (typeString[0].isUpperCase()) {
967 try {
968 val type = elementFactory.createTypeFromText(typeString, element)
969 insert = type.canonicalText
970 } catch (ignore: com.intellij.util.IncorrectOperationException) {
971 // Not a valid type - just leave what was in the parameter text
972 }
973 }
974 sb.append(insert)
975 sb.append(c)
976 if (c == ')') {
977 break
978 }
979 } else if (c == ')') {
980 sb.append(')')
981 break
982 }
983 argBegin = index
984 } else if (c == ')') {
985 balance--
986 }
987 }
988 while (index < suffix.length) {
989 sb.append(suffix[index++])
990 }
991 }
992
samePackagenull993 private fun samePackage(owner: PsiItem, cls: PsiClass): Boolean {
994 @Suppress("ConstantConditionIf")
995 if (INCLUDE_SAME_PACKAGE) {
996 // doclava seems to have REAL problems with this
997 return false
998 }
999 val pkg = packageName(owner) ?: return false
1000 return cls.qualifiedName == "$pkg.${cls.name}"
1001 }
1002
packageNamenull1003 private fun packageName(owner: PsiItem): String? {
1004 var curr: Item? = owner
1005 while (curr != null) {
1006 if (curr is PackageItem) {
1007 return curr.qualifiedName()
1008 }
1009 curr = curr.parent()
1010 }
1011
1012 return null
1013 }
1014
1015 // Copied from UnnecessaryJavaDocLinkInspection and tweaked a bit
extractReferencenull1016 private fun extractReference(tag: PsiDocTag): PsiReference? {
1017 val valueElement = tag.valueElement
1018 if (valueElement != null) {
1019 return valueElement.reference
1020 }
1021 // hack around the fact that a reference to a class is apparently
1022 // not a PsiDocTagValue
1023 val dataElements = tag.dataElements
1024 if (dataElements.isEmpty()) {
1025 return null
1026 }
1027 val salientElement: PsiElement =
1028 dataElements.firstOrNull { it !is PsiWhiteSpace && it !is PsiDocToken } ?: return null
1029 val child = salientElement.firstChild
1030 return if (child !is PsiReference) null else child
1031 }
1032