1 /*
<lambda>null2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.tools.metalava.model.psi
18
19 import com.android.tools.metalava.model.AbstractItemDocumentation
20 import com.android.tools.metalava.model.ClassItem
21 import com.android.tools.metalava.model.Item
22 import com.android.tools.metalava.model.ItemDocumentation
23 import com.android.tools.metalava.model.ItemDocumentation.Companion.toItemDocumentationFactory
24 import com.android.tools.metalava.model.ItemDocumentationFactory
25 import com.android.tools.metalava.model.PackageItem
26 import com.android.tools.metalava.reporter.Issues
27 import com.intellij.psi.JavaPsiFacade
28 import com.intellij.psi.PsiClass
29 import com.intellij.psi.PsiComment
30 import com.intellij.psi.PsiCompiledElement
31 import com.intellij.psi.PsiDocCommentOwner
32 import com.intellij.psi.PsiElement
33 import com.intellij.psi.PsiJavaCodeReferenceElement
34 import com.intellij.psi.PsiMember
35 import com.intellij.psi.PsiReference
36 import com.intellij.psi.PsiTypeParameter
37 import com.intellij.psi.PsiWhiteSpace
38 import com.intellij.psi.impl.source.SourceTreeToPsiMap
39 import com.intellij.psi.impl.source.javadoc.PsiDocMethodOrFieldRef
40 import com.intellij.psi.impl.source.tree.CompositePsiElement
41 import com.intellij.psi.impl.source.tree.JavaDocElementType
42 import com.intellij.psi.javadoc.PsiDocComment
43 import com.intellij.psi.javadoc.PsiDocTag
44 import com.intellij.psi.javadoc.PsiDocToken
45 import com.intellij.psi.javadoc.PsiInlineDocTag
46 import org.jetbrains.kotlin.kdoc.psi.api.KDoc
47 import org.jetbrains.kotlin.psi.KtDeclaration
48 import org.jetbrains.uast.UElement
49 import org.jetbrains.uast.sourcePsiElement
50
51 /** A Psi specialization of [ItemDocumentation]. */
52 internal class PsiItemDocumentation(
53 private val item: PsiItem,
54 private val psi: PsiElement,
55 private val extraDocs: String?,
56 ) : AbstractItemDocumentation() {
57
58 /** Lazily initialized backing property for [text]. */
59 private lateinit var _text: String
60
61 override var text: String
62 get() = if (::_text.isInitialized) _text else initializeText()
63 set(value) {
64 _text = value
65 }
66
67 /** Lazy initializer for [_text]. */
68 private fun initializeText(): String {
69 _text = javadoc(psi).let { if (extraDocs != null) it + "\n$extraDocs" else it }
70 return _text
71 }
72
73 override fun duplicate(item: Item) =
74 if (item is PsiItem) PsiItemDocumentation(item, psi, extraDocs)
75 else text.toItemDocumentationFactory()(item)
76
77 override fun snapshot(item: Item) = this
78
79 override fun findTagDocumentation(tag: String, value: String?): String? {
80 if (psi is PsiCompiledElement) {
81 return null
82 }
83 if (text.isBlank()) {
84 return null
85 }
86
87 // We can't just use element.docComment here because we may have modified the comment and
88 // then the comment snapshot in PSI isn't up-to-date with our latest changes
89 val docComment = item.codebase.psiAssembler.getComment(text)
90 val tagComment =
91 if (value == null) {
92 docComment.findTagByName(tag)
93 } else {
94 docComment.findTagsByName(tag).firstOrNull { it.valueElement?.text == value }
95 }
96
97 if (tagComment == null) {
98 return null
99 }
100
101 val text = tagComment.text
102 // Trim trailing next line (javadoc *)
103 var index = text.length - 1
104 while (index > 0) {
105 val c = text[index]
106 if (!(c == '*' || c.isWhitespace())) {
107 break
108 }
109 index--
110 }
111 index++
112 return if (index < text.length) {
113 text.substring(0, index)
114 } else {
115 text
116 }
117 }
118
119 override fun mergeDocumentation(comment: String, tagSection: String?) {
120 text = mergeDocumentation(text, psi, comment, tagSection, append = true)
121 }
122
123 override fun findMainDocumentation(): String {
124 if (text == "") return text
125 val comment = item.codebase.psiAssembler.getComment(text)
126 val end = findFirstTag(comment)?.textRange?.startOffset ?: text.length
127 return comment.text.substring(0, end)
128 }
129
130 override fun fullyQualifiedDocumentation(documentation: String): String {
131 if (documentation.isBlank() || !containsLinkTags(documentation)) {
132 return documentation
133 }
134
135 val assembler = item.codebase.psiAssembler
136 val comment = assembler.getComment(documentation, psi)
137 return buildString(documentation.length) { expand(comment, this) }
138 }
139
140 private fun reportUnresolvedDocReference(unresolved: String) {
141 if (!REPORT_UNRESOLVED_SYMBOLS) {
142 return
143 }
144
145 if (unresolved.startsWith("{@") && !unresolved.startsWith("{@link")) {
146 return
147 }
148
149 // References are sometimes split across lines and therefore have newlines, leading
150 // asterisks etc. in the middle: clean this up before emitting reference into error message
151 val cleaned = unresolved.replace("\n", "").replace("*", "").replace(" ", " ")
152
153 item.codebase.reporter.report(
154 Issues.UNRESOLVED_LINK,
155 item,
156 "Unresolved documentation reference: $cleaned"
157 )
158 }
159
160 private fun expand(element: PsiElement, sb: StringBuilder) {
161 when {
162 element is PsiWhiteSpace -> {
163 sb.append(element.text)
164 }
165 element is PsiDocToken -> {
166 assert(element.firstChild == null)
167 val text = element.text
168 sb.append(text)
169 }
170 element is PsiDocMethodOrFieldRef -> {
171 val text = element.text
172 val resolved = element.reference?.resolve()
173 if (resolved is PsiMember) {
174 val containingClass = resolved.containingClass
175 if (containingClass != null && !samePackage(containingClass)) {
176 val referenceText = element.reference?.element?.text ?: text
177 if (referenceText.startsWith("#")) {
178 sb.append(text)
179 return
180 }
181
182 var className = containingClass.classQualifiedName
183
184 if (
185 element.firstChildNode.elementType ===
186 JavaDocElementType.DOC_REFERENCE_HOLDER
187 ) {
188 val firstChildPsi =
189 SourceTreeToPsiMap.treeElementToPsi(
190 element.firstChildNode.firstChildNode
191 )
192 if (firstChildPsi is PsiJavaCodeReferenceElement) {
193 val referenceElement = firstChildPsi as PsiJavaCodeReferenceElement?
194 val referencedElement = referenceElement!!.resolve()
195 if (referencedElement is PsiClass) {
196 className = referencedElement.classQualifiedName
197 }
198 }
199 }
200
201 sb.append(className)
202 sb.append('#')
203 sb.append(resolved.name)
204 val index = text.indexOf('(')
205 if (index != -1) {
206 sb.append(text.substring(index))
207 }
208 } else {
209 sb.append(text)
210 }
211 } else {
212 if (resolved == null) {
213 val referenceText = element.reference?.element?.text ?: text
214 if (text.startsWith("#") && item is ClassItem) {
215 // Unfortunately resolving references is broken from class javadocs
216 // to members using just a relative reference, #.
217 } else {
218 reportUnresolvedDocReference(referenceText)
219 }
220 }
221 sb.append(text)
222 }
223 }
224 element is PsiJavaCodeReferenceElement -> {
225 val resolved = element.resolve()
226 if (resolved is PsiClass) {
227 if (samePackage(resolved) || resolved is PsiTypeParameter) {
228 sb.append(element.text)
229 } else {
230 sb.append(resolved.classQualifiedName)
231 }
232 } else if (resolved is PsiMember) {
233 val text = element.text
234 sb.append(resolved.containingClass?.classQualifiedName)
235 sb.append('#')
236 sb.append(resolved.name)
237 val index = text.indexOf('(')
238 if (index != -1) {
239 sb.append(text.substring(index))
240 }
241 } else {
242 val text = element.text
243 if (resolved == null) {
244 reportUnresolvedDocReference(text)
245 }
246 sb.append(text)
247 }
248 }
249 element is PsiInlineDocTag -> {
250 val handled = handleTag(element, sb)
251 if (!handled) {
252 sb.append(element.text)
253 }
254 }
255 element.firstChild != null -> {
256 var curr = element.firstChild
257 while (curr != null) {
258 expand(curr, sb)
259 curr = curr.nextSibling
260 }
261 }
262 else -> {
263 val text = element.text
264 sb.append(text)
265 }
266 }
267 }
268
269 private fun handleTag(element: PsiInlineDocTag, sb: StringBuilder): Boolean {
270 val name = element.name
271 if (name == "code" || name == "literal") {
272 // @code: don't attempt to rewrite this
273 sb.append(element.text)
274 return true
275 }
276
277 val reference = extractReference(element)
278 val referenceText = reference?.element?.text ?: element.text
279 val customLinkText = extractCustomLinkText(element)
280 val displayText = customLinkText?.text ?: referenceText.replaceFirst('#', '.')
281 if (referenceText.startsWith("#")) {
282 val suffix = element.text
283 if (suffix.contains("(") && suffix.contains(")")) {
284 expandArgumentList(element, suffix, sb)
285 } else {
286 sb.append(suffix)
287 }
288 return true
289 }
290
291 // TODO: If referenceText is already absolute, e.g.
292 // android.Manifest.permission#BIND_CARRIER_SERVICES,
293 // try to short circuit this?
294
295 val valueElement = element.valueElement
296 if (valueElement is CompositePsiElement) {
297 if (
298 valueElement.firstChildNode.elementType === JavaDocElementType.DOC_REFERENCE_HOLDER
299 ) {
300 val firstChildPsi =
301 SourceTreeToPsiMap.treeElementToPsi(valueElement.firstChildNode.firstChildNode)
302 if (firstChildPsi is PsiJavaCodeReferenceElement) {
303 val referenceElement = firstChildPsi as PsiJavaCodeReferenceElement?
304 val referencedElement = referenceElement!!.resolve()
305 if (referencedElement is PsiClass) {
306 var className = computeFullClassName(referencedElement)
307 if (className.indexOf('.') != -1 && !referenceText.startsWith(className)) {
308 val simpleName = referencedElement.name
309 if (simpleName != null && referenceText.startsWith(simpleName)) {
310 className = simpleName
311 }
312 }
313 if (referenceText.startsWith(className)) {
314 sb.append("{@")
315 sb.append(element.name)
316 sb.append(' ')
317 sb.append(referencedElement.classQualifiedName)
318 val suffix = referenceText.substring(className.length)
319 if (suffix.contains("(") && suffix.contains(")")) {
320 expandArgumentList(element, suffix, sb)
321 } else {
322 sb.append(suffix)
323 }
324 sb.append(' ')
325 sb.append(displayText)
326 sb.append("}")
327 return true
328 }
329 }
330 }
331 }
332 }
333
334 val resolved = reference?.resolve()
335 if (resolved != null) {
336 when (resolved) {
337 is PsiClass -> {
338 val text = element.text
339 if (samePackage(resolved)) {
340 sb.append(text)
341 return true
342 }
343 val qualifiedName =
344 resolved.qualifiedName
345 ?: run {
346 sb.append(text)
347 return true
348 }
349 if (referenceText == qualifiedName) {
350 // Already absolute
351 sb.append(text)
352 return true
353 }
354 val append =
355 when {
356 valueElement != null -> {
357 val start = valueElement.startOffsetInParent
358 val end = start + valueElement.textLength
359 text.substring(0, start) + qualifiedName + text.substring(end)
360 }
361 name == "see" -> {
362 val suffix =
363 text.substring(
364 text.indexOf(referenceText) + referenceText.length
365 )
366 "@see $qualifiedName$suffix"
367 }
368 text.startsWith("{") -> "{@$name $qualifiedName $displayText}"
369 else -> "@$name $qualifiedName $displayText"
370 }
371 sb.append(append)
372 return true
373 }
374 is PsiMember -> {
375 val text = element.text
376 val containing =
377 resolved.containingClass
378 ?: run {
379 sb.append(text)
380 return true
381 }
382 if (samePackage(containing)) {
383 sb.append(text)
384 return true
385 }
386 val qualifiedName =
387 containing.qualifiedName
388 ?: run {
389 sb.append(text)
390 return true
391 }
392 if (referenceText.startsWith(qualifiedName)) {
393 // Already absolute
394 sb.append(text)
395 return true
396 }
397
398 // It may also be the case that the reference is already fully qualified
399 // but to some different class. For example, the link may be to
400 // android.os.Bundle#getInt, but the resolved method actually points to
401 // an inherited method into android.os.Bundle from android.os.BaseBundle.
402 // In that case we don't want to rewrite the link.
403 for (c in referenceText) {
404 if (c == '.') {
405 // Already qualified
406 sb.append(text)
407 return true
408 } else if (!Character.isJavaIdentifierPart(c)) {
409 break
410 }
411 }
412
413 if (valueElement != null) {
414 val start = valueElement.startOffsetInParent
415
416 var nameEnd = -1
417 var close = start
418 var balance = 0
419 while (close < text.length) {
420 val c = text[close]
421 if (c == '(') {
422 if (nameEnd == -1) {
423 nameEnd = close
424 }
425 balance++
426 } else if (c == ')') {
427 balance--
428 if (balance == 0) {
429 close++
430 break
431 }
432 } else if (c == '}') {
433 if (nameEnd == -1) {
434 nameEnd = close
435 }
436 break
437 } else if (balance == 0 && c == '#') {
438 if (nameEnd == -1) {
439 nameEnd = close
440 }
441 } else if (balance == 0 && !Character.isJavaIdentifierPart(c)) {
442 break
443 }
444 close++
445 }
446 val memberPart = text.substring(nameEnd, close)
447 val append =
448 "${text.substring(0, start)}$qualifiedName$memberPart $displayText}"
449 sb.append(append)
450 return true
451 }
452 }
453 }
454 } else {
455 reportUnresolvedDocReference(referenceText)
456 }
457
458 return false
459 }
460
461 private fun expandArgumentList(element: PsiInlineDocTag, suffix: String, sb: StringBuilder) {
462 val elementFactory = JavaPsiFacade.getElementFactory(element.project)
463 // Try to rewrite the types to fully qualified names as well
464 val begin = suffix.indexOf('(')
465 sb.append(suffix.substring(0, begin + 1))
466 var index = begin + 1
467 var balance = 0
468 var argBegin = index
469 while (index < suffix.length) {
470 val c = suffix[index++]
471 if (c == '<' || c == '(') {
472 balance++
473 } else if (c == '>') {
474 balance--
475 } else if (c == ')' && balance == 0 || c == ',') {
476 // Strip off javadoc header
477 while (argBegin < index) {
478 val p = suffix[argBegin]
479 if (p != '*' && !p.isWhitespace()) {
480 break
481 }
482 argBegin++
483 }
484 if (index > argBegin + 1) {
485 val arg = suffix.substring(argBegin, index - 1).trim()
486 val space = arg.indexOf(' ')
487 // Strip off parameter name (shouldn't be there but happens
488 // in some Android sources sine tools didn't use to complain
489 val typeString =
490 if (space == -1) {
491 arg
492 } else {
493 if (space < arg.length - 1 && !arg[space + 1].isJavaIdentifierStart()) {
494 // Example: "String []"
495 arg
496 } else {
497 // Example "String name"
498 arg.substring(0, space)
499 }
500 }
501 var insert = arg
502 if (typeString[0].isUpperCase()) {
503 try {
504 val type = elementFactory.createTypeFromText(typeString, element)
505 insert = type.canonicalText
506 } catch (ignore: com.intellij.util.IncorrectOperationException) {
507 // Not a valid type - just leave what was in the parameter text
508 }
509 }
510 sb.append(insert)
511 sb.append(c)
512 if (c == ')') {
513 break
514 }
515 } else if (c == ')') {
516 sb.append(')')
517 break
518 }
519 argBegin = index
520 } else if (c == ')') {
521 balance--
522 }
523 }
524 while (index < suffix.length) {
525 sb.append(suffix[index++])
526 }
527 }
528
529 private fun samePackage(cls: PsiClass): Boolean {
530 if (INCLUDE_SAME_PACKAGE) {
531 // doclava seems to have REAL problems with this
532 return false
533 }
534 val pkg = packageName() ?: return false
535 return cls.qualifiedName == "$pkg.${cls.name}"
536 }
537
538 private fun packageName(): String? {
539 var curr: Item? = item
540 while (curr != null) {
541 if (curr is PackageItem) {
542 return curr.qualifiedName()
543 }
544 curr = curr.parent()
545 }
546
547 return null
548 }
549
550 // Copied from UnnecessaryJavaDocLinkInspection and tweaked a bit
551 private fun extractReference(tag: PsiDocTag): PsiReference? {
552 val valueElement = tag.valueElement
553 if (valueElement != null) {
554 return valueElement.reference
555 }
556 // hack around the fact that a reference to a class is apparently
557 // not a PsiDocTagValue
558 val dataElements = tag.dataElements
559 if (dataElements.isEmpty()) {
560 return null
561 }
562 val salientElement: PsiElement =
563 dataElements.firstOrNull { it !is PsiWhiteSpace && it !is PsiDocToken } ?: return null
564 val child = salientElement.firstChild
565 return if (child !is PsiReference) null else child
566 }
567
568 private fun extractCustomLinkText(tag: PsiDocTag): PsiDocToken? {
569 val dataElements = tag.dataElements
570 if (dataElements.isEmpty()) {
571 return null
572 }
573 val salientElement: PsiElement =
574 dataElements.lastOrNull { it !is PsiWhiteSpace && it !is PsiDocMethodOrFieldRef }
575 ?: return null
576 return if (salientElement !is PsiDocToken) null else salientElement
577 }
578
579 companion object {
580 /**
581 * Get an [ItemDocumentationFactory] for the [psi].
582 *
583 * If [PsiBasedCodebase.allowReadingComments] is `true` then this will return a factory that
584 * creates a [PsiItemDocumentation] instance. If [extraDocs] is not-null then this will
585 * return a factory that will create an [ItemDocumentation] wrapper around [extraDocs],
586 * otherwise it will return [ItemDocumentation.NONE_FACTORY].
587 *
588 * @param psi the underlying element from which the documentation will be retrieved.
589 * Although this is usually accessible through the [PsiItem.psi] property, that is not
590 * true within the [ItemDocumentationFactory] as that is called during initialization of
591 * the [PsiItem] before [PsiItem.psi] has been initialized.
592 */
593 internal fun factory(
594 psi: PsiElement,
595 codebase: PsiBasedCodebase,
596 extraDocs: String? = null,
597 ) =
598 if (codebase.allowReadingComments) {
599 // When reading comments provide full access to them.
600 { item ->
601 val psiItem = item as PsiItem
602 PsiItemDocumentation(psiItem, psi, extraDocs)
603 }
604 } else {
605 // If extraDocs are provided then they most likely contain documentation for the
606 // package from a `package-info.java` or `package.html` file. Make sure that they
607 // are included in the `ItemDocumentation`, otherwise package hiding will not work.
608 extraDocs?.toItemDocumentationFactory()
609 // Otherwise, there is no documentation to use.
610 ?: ItemDocumentation.NONE_FACTORY
611 }
612
613 // Gets the javadoc of the current element
614 private fun javadoc(element: PsiElement): String {
615 if (element is PsiCompiledElement) {
616 return ""
617 }
618
619 if (element is KtDeclaration) {
620 return element.docComment?.text.orEmpty()
621 }
622
623 if (element is UElement) {
624 val comments = element.comments
625 if (comments.isNotEmpty()) {
626 return comments.firstNotNullOfOrNull {
627 val text = it.text
628 if (text.startsWith("/**")) text else null
629 }
630 ?: ""
631 } else {
632 // Temporary workaround: UAST seems to not return document nodes
633 // https://youtrack.jetbrains.com/issue/KT-22135
634 val first = element.sourcePsiElement?.firstChild
635 if (first is KDoc) {
636 return first.text
637 }
638 }
639 }
640
641 if (element is PsiDocCommentOwner) {
642 val docComment = element.docComment
643 if (docComment != null && docComment !is PsiCompiledElement) {
644 val text = docComment.text
645 // Make sure that the text is a doc comment, i.e. starts with /**.
646 if (text != null) {
647 if (text.startsWith("/**")) {
648 return text
649 } else {
650 // Workaround for b/391104222.
651 //
652 // Scan through the previous nodes for the first real doc comment up to
653 // the first non-white space node. The latter ensures it does not find a
654 // doc comment that belongs to another item.
655 var node = element.node
656 while (true) {
657 node = node.treePrev ?: break
658
659 // Ignore white space or empty marker nodes, e.g. ImportListElement,
660 // that are inserted to mark semantically significant locations but
661 // do not actually have any content. They may be added between an
662 // item like a class and its corresponding doc comment.
663 if (node is PsiWhiteSpace || node.textLength == 0) continue
664
665 // Stop searching as soon as the first non PsiComment is found.
666 val psiComment = node as? PsiComment ?: break
667
668 // If the comment is not a doc comment (with the correct type AND
669 // content) then ignore it.
670 if (
671 psiComment !is PsiDocComment ||
672 !psiComment.text.startsWith("/**")
673 )
674 continue
675
676 return psiComment.text
677 }
678 }
679 }
680 }
681 }
682
683 return ""
684 }
685 }
686 }
687
688 /**
689 * Computes the "full" class name; this is not the qualified class name (e.g. with package) but for
690 * a nested class it includes all the outer classes
691 */
computeFullClassNamenull692 private fun computeFullClassName(cls: PsiClass): String {
693 if (cls.containingClass == null) {
694 val name = cls.name
695 return name!!
696 } else {
697 val list = mutableListOf<String>()
698 var curr: PsiClass? = cls
699 while (curr != null) {
700 val name = curr.name
701 curr =
702 if (name != null) {
703 list.add(name)
704 curr.containingClass
705 } else {
706 break
707 }
708 }
709 return list.asReversed().joinToString(separator = ".") { it }
710 }
711 }
712