• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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