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.model.DefaultItem
20 import com.android.tools.metalava.model.Item
21 import com.android.tools.metalava.model.MutableModifierList
22 import com.android.tools.metalava.model.PackageItem
23 import com.android.tools.metalava.model.ParameterItem
24 import com.intellij.psi.PsiClass
25 import com.intellij.psi.PsiCompiledElement
26 import com.intellij.psi.PsiDocCommentOwner
27 import com.intellij.psi.PsiElement
28 import com.intellij.psi.PsiMember
29 import com.intellij.psi.PsiModifierListOwner
30 import com.intellij.psi.PsiReference
31 import com.intellij.psi.PsiWhiteSpace
32 import com.intellij.psi.javadoc.PsiDocTag
33 import com.intellij.psi.javadoc.PsiInlineDocTag
34 import org.jetbrains.kotlin.kdoc.psi.api.KDoc
35 import org.jetbrains.uast.UElement
36 import org.jetbrains.uast.sourcePsiElement
37 
38 abstract class PsiItem(
39     override val codebase: PsiBasedCodebase,
40     val element: PsiElement,
41     override val modifiers: PsiModifierItem,
42     override var documentation: String
43 ) : DefaultItem() {
44 
45     override val deprecated: Boolean get() = modifiers.isDeprecated()
46 
47     @Suppress("LeakingThis") // Documentation can change, but we don't want to pick up subsequent @docOnly mutations
48     override var docOnly = documentation.contains("@doconly")
49     @Suppress("LeakingThis")
50     override var removed = documentation.contains("@removed")
51     @Suppress("LeakingThis")
52     override var hidden = (documentation.contains("@hide") || documentation.contains("@pending") ||
53         modifiers.hasHideAnnotations()) && !modifiers.hasShowAnnotation()
54 
psinull55     override fun psi(): PsiElement? = element
56 
57     // TODO: Consider only doing this in tests!
58     override fun isFromClassPath(): Boolean {
59         return if (element is UElement) {
60             (element.sourcePsi ?: element.javaPsi) is PsiCompiledElement
61         } else {
62             element is PsiCompiledElement
63         }
64     }
65 
isClonednull66     override fun isCloned(): Boolean = false
67 
68     /** Get a mutable version of modifiers for this item */
69     override fun mutableModifiers(): MutableModifierList = modifiers
70 
71     override fun findTagDocumentation(tag: String): String? {
72         if (element is PsiCompiledElement) {
73             return null
74         }
75         if (documentation.isBlank()) {
76             return null
77         }
78 
79         // We can't just use element.docComment here because we may have modified
80         // the comment and then the comment snapshot in PSI isn't up to date with our
81         // latest changes
82         val docComment = codebase.getComment(documentation)
83         val docTag = docComment.findTagByName(tag) ?: return null
84         val text = docTag.text
85 
86         // Trim trailing next line (javadoc *)
87         var index = text.length - 1
88         while (index > 0) {
89             val c = text[index]
90             if (!(c == '*' || c.isWhitespace())) {
91                 break
92             }
93             index--
94         }
95         index++
96         return if (index < text.length) {
97             text.substring(0, index)
98         } else {
99             text
100         }
101     }
102 
appendDocumentationnull103     override fun appendDocumentation(comment: String, tagSection: String?, append: Boolean) {
104         if (comment.isBlank()) {
105             return
106         }
107 
108         // TODO: Figure out if an annotation should go on the return value, or on the method.
109         // For example; threading: on the method, range: on the return value.
110         // TODO: Find a good way to add or append to a given tag (@param <something>, @return, etc)
111 
112         if (this is ParameterItem) {
113             // For parameters, the documentation goes into the surrounding method's documentation!
114             // Find the right parameter location!
115             val parameterName = name()
116             val target = containingMethod()
117             target.appendDocumentation(comment, parameterName)
118             return
119         }
120 
121         documentation = mergeDocumentation(documentation, element, comment.trim(), tagSection, append)
122     }
123 
packageNamenull124     private fun packageName(): String? {
125         var curr: Item? = this
126         while (curr != null) {
127             if (curr is PackageItem) {
128                 return curr.qualifiedName()
129             }
130             curr = curr.parent()
131         }
132 
133         return null
134     }
135 
fullyQualifiedDocumentationnull136     override fun fullyQualifiedDocumentation(): String {
137         if (documentation.isBlank()) {
138             return documentation
139         }
140 
141         if (!(documentation.contains("@link") || // includes @linkplain
142                 documentation.contains("@see") ||
143                 documentation.contains("@throws"))
144         ) {
145             // No relevant tags that need to be expanded/rewritten
146             return documentation
147         }
148 
149         val comment =
150             try {
151                 codebase.getComment(documentation, psi())
152             } catch (throwable: Throwable) {
153                 // TODO: Get rid of line comments as documentation
154                 // Invalid comment
155                 if (documentation.startsWith("//") && documentation.contains("/**")) {
156                     documentation = documentation.substring(documentation.indexOf("/**"))
157                 }
158                 codebase.getComment(documentation, psi())
159             }
160         val sb = StringBuilder(documentation.length)
161         var curr = comment.firstChild
162         while (curr != null) {
163             if (curr is PsiDocTag) {
164                 sb.append(getExpanded(curr))
165             } else {
166                 sb.append(curr.text)
167             }
168             curr = curr.nextSibling
169         }
170 
171         return sb.toString()
172     }
173 
getExpandednull174     private fun getExpanded(tag: PsiDocTag): String {
175         val text = tag.text
176         var valueElement = tag.valueElement
177         val reference = extractReference(tag)
178         var resolved = reference?.resolve()
179         var referenceText = reference?.element?.text
180         if (resolved == null && tag.name == "throws") {
181             // Workaround: @throws does not provide a valid reference to the class
182             val dataElements = tag.dataElements
183             if (dataElements.isNotEmpty()) {
184                 if (dataElements[0] is PsiInlineDocTag) {
185                     val innerReference = extractReference(dataElements[0] as PsiInlineDocTag)
186                     resolved = innerReference?.resolve()
187                     if (innerReference != null && resolved == null) {
188                         referenceText = innerReference.canonicalText
189                         resolved = codebase.createReferenceFromText(referenceText, psi()).resolve()
190                     } else {
191                         referenceText = innerReference?.element?.text
192                     }
193                 }
194                 if (resolved == null || referenceText == null) {
195                     val exceptionName = dataElements[0].text
196                     val exceptionReference = codebase.createReferenceFromText(exceptionName, psi())
197                     resolved = exceptionReference.resolve()
198                     referenceText = exceptionName
199                 } else {
200                     // Create a placeholder value since the inline tag
201                     // wipes it out
202                     val t = dataElements[0].text
203                     val index = text.indexOf(t) + t.length
204                     val suffix = text.substring(index)
205                     val dummyTag = codebase.createDocTagFromText("@${tag.name} $suffix")
206                     valueElement = dummyTag.valueElement
207                 }
208             } else {
209                 return text
210             }
211         }
212 
213         if (resolved != null && referenceText != null) {
214             if (referenceText.startsWith("#")) {
215                 // Already a local/relative reference
216                 return text
217             }
218 
219             when (resolved) {
220             // TODO: If not absolute, preserve syntax
221                 is PsiClass -> {
222                     if (samePackage(resolved)) {
223                         return text
224                     }
225                     val qualifiedName = resolved.qualifiedName ?: return text
226                     if (referenceText == qualifiedName) {
227                         // Already absolute
228                         return text
229                     }
230                     return when {
231                         valueElement != null -> {
232                             val start = valueElement.startOffsetInParent
233                             val end = start + valueElement.textLength
234                             text.substring(0, start) + qualifiedName + text.substring(end)
235                         }
236                         tag.name == "see" -> {
237                             val suffix = text.substring(text.indexOf(referenceText) + referenceText.length)
238                             "@see $qualifiedName$suffix"
239                         }
240                         text.startsWith("{") -> "{@${tag.name} $qualifiedName $referenceText}"
241                         else -> "@${tag.name} $qualifiedName $referenceText"
242                     }
243                 }
244                 is PsiMember -> {
245                     val containing = resolved.containingClass ?: return text
246                     if (samePackage(containing)) {
247                         return text
248                     }
249                     val qualifiedName = containing.qualifiedName ?: return text
250                     if (referenceText.startsWith(qualifiedName)) {
251                         // Already absolute
252                         return text
253                     }
254 
255                     val name = containing.name ?: return text
256                     if (valueElement != null) {
257                         val start = valueElement.startOffsetInParent
258                         val close = text.lastIndexOf('}')
259                         if (close == -1) {
260                             return text // invalid javadoc
261                         }
262                         val memberPart = text.substring(text.indexOf(name, start) + name.length, close)
263                         return "${text.substring(0, start)}$qualifiedName$memberPart $referenceText}"
264                     }
265                 }
266             }
267         }
268 
269         return text
270     }
271 
samePackagenull272     private fun samePackage(cls: PsiClass): Boolean {
273         val pkg = packageName() ?: return false
274         return cls.qualifiedName == "$pkg.${cls.name}"
275     }
276 
277     // Copied from UnnecessaryJavaDocLinkInspection
extractReferencenull278     private fun extractReference(tag: PsiDocTag): PsiReference? {
279         val valueElement = tag.valueElement
280         if (valueElement != null) {
281             return valueElement.reference
282         }
283         // hack around the fact that a reference to a class is apparently
284         // not a PsiDocTagValue
285         val dataElements = tag.dataElements
286         if (dataElements.isEmpty()) {
287             return null
288         }
289         val salientElement: PsiElement = dataElements.firstOrNull { it !is PsiWhiteSpace } ?: return null
290         val child = salientElement.firstChild
291         return if (child !is PsiReference) null else child
292     }
293 
294     /** Finish initialization of the item */
finishInitializationnull295     open fun finishInitialization() {
296         modifiers.setOwner(this)
297     }
298 
isKotlinnull299     override fun isKotlin(): Boolean {
300         return isKotlin(element)
301     }
302 
303     companion object {
javadocnull304         fun javadoc(element: PsiElement): String {
305             if (element is PsiCompiledElement) {
306                 return ""
307             }
308 
309             if (element is UElement) {
310                 val comments = element.comments
311                 if (comments.isNotEmpty()) {
312                     val sb = StringBuilder()
313                     comments.asSequence().joinTo(buffer = sb, separator = "\n")
314                     return sb.toString()
315                 } else {
316                     // Temporary workaround: UAST seems to not return document nodes
317                     // https://youtrack.jetbrains.com/issue/KT-22135
318                     val first = element.sourcePsiElement?.firstChild
319                     if (first is KDoc) {
320                         return first.text
321                     }
322                 }
323             }
324 
325             if (element is PsiDocCommentOwner) {
326                 return element.docComment?.text ?: ""
327             }
328 
329             return ""
330         }
331 
modifiersnull332         fun modifiers(
333             codebase: PsiBasedCodebase,
334             element: PsiModifierListOwner,
335             documentation: String
336         ): PsiModifierItem {
337             return PsiModifierItem.create(codebase, element, documentation)
338         }
339 
isKotlinnull340         fun isKotlin(element: PsiElement): Boolean {
341             return element.language.id == "kotlin"
342         }
343     }
344 }
345