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