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 kotlin.properties.ReadWriteProperty
20 import kotlin.reflect.KProperty
21 import com.android.tools.metalava.model.DefaultItem
22 import com.android.tools.metalava.model.MutableModifierList
23 import com.android.tools.metalava.model.ParameterItem
24 import com.intellij.psi.PsiCompiledElement
25 import com.intellij.psi.PsiDocCommentOwner
26 import com.intellij.psi.PsiElement
27 import com.intellij.psi.PsiModifierListOwner
28 import org.jetbrains.kotlin.idea.KotlinLanguage
29 import org.jetbrains.kotlin.kdoc.psi.api.KDoc
30 import org.jetbrains.uast.UElement
31 import org.jetbrains.uast.sourcePsiElement
32 
33 abstract class PsiItem(
34     override val codebase: PsiBasedCodebase,
35     val element: PsiElement,
36     override val modifiers: PsiModifierItem,
37     override var documentation: String
38 ) : DefaultItem() {
39 
40     @Suppress("LeakingThis")
41     override var deprecated: Boolean = modifiers.isDeprecated()
42 
43     @Suppress("LeakingThis") // Documentation can change, but we don't want to pick up subsequent @docOnly mutations
44     override var docOnly = documentation.contains("@doconly")
45     @Suppress("LeakingThis")
46     override var removed = documentation.contains("@removed")
47 
48     override val synthetic = false
49 
50     // a property with a lazily calculated default value
51     inner class LazyDelegate<T>(
52         val defaultValueProvider: () -> T
53     ) : ReadWriteProperty<PsiItem, T> {
54         private var currentValue: T? = null
55 
setValuenull56         override operator fun setValue(thisRef: PsiItem, property: KProperty<*>, value: T) {
57             currentValue = value
58         }
getValuenull59         override operator fun getValue(thisRef: PsiItem, property: KProperty<*>): T {
60             if (currentValue == null) {
61                 currentValue = defaultValueProvider()
62             }
63 
64             return currentValue!!
65         }
66     }
67 
<lambda>null68     override var originallyHidden: Boolean by LazyDelegate {
69         documentation.contains('@') &&
70 
71             (documentation.contains("@hide") ||
72                 documentation.contains("@pending") ||
73                 // KDoc:
74                 documentation.contains("@suppress")) ||
75             modifiers.hasHideAnnotations()
76     }
77 
<lambda>null78     override var hidden: Boolean by LazyDelegate { originallyHidden && !modifiers.hasShowAnnotation() }
79 
psinull80     override fun psi(): PsiElement? = element
81 
82     // TODO: Consider only doing this in tests!
83     override fun isFromClassPath(): Boolean {
84         return if (element is UElement) {
85             (element.sourcePsi ?: element.javaPsi) is PsiCompiledElement
86         } else {
87             element is PsiCompiledElement
88         }
89     }
90 
isClonednull91     override fun isCloned(): Boolean = false
92 
93     /** Get a mutable version of modifiers for this item */
94     override fun mutableModifiers(): MutableModifierList = modifiers
95 
96     override fun findTagDocumentation(tag: String): String? {
97         if (element is PsiCompiledElement) {
98             return null
99         }
100         if (documentation.isBlank()) {
101             return null
102         }
103 
104         // We can't just use element.docComment here because we may have modified
105         // the comment and then the comment snapshot in PSI isn't up to date with our
106         // latest changes
107         val docComment = codebase.getComment(documentation)
108         val docTag = docComment.findTagByName(tag) ?: return null
109         val text = docTag.text
110 
111         // Trim trailing next line (javadoc *)
112         var index = text.length - 1
113         while (index > 0) {
114             val c = text[index]
115             if (!(c == '*' || c.isWhitespace())) {
116                 break
117             }
118             index--
119         }
120         index++
121         return if (index < text.length) {
122             text.substring(0, index)
123         } else {
124             text
125         }
126     }
127 
appendDocumentationnull128     override fun appendDocumentation(comment: String, tagSection: String?, append: Boolean) {
129         if (comment.isBlank()) {
130             return
131         }
132 
133         // TODO: Figure out if an annotation should go on the return value, or on the method.
134         // For example; threading: on the method, range: on the return value.
135         // TODO: Find a good way to add or append to a given tag (@param <something>, @return, etc)
136 
137         if (this is ParameterItem) {
138             // For parameters, the documentation goes into the surrounding method's documentation!
139             // Find the right parameter location!
140             val parameterName = name()
141             val target = containingMethod()
142             target.appendDocumentation(comment, parameterName)
143             return
144         }
145 
146         // Micro-optimization: we're very often going to be merging @apiSince and to a lesser
147         // extend @deprecatedSince into existing comments, since we're flagging every single
148         // public API. Normally merging into documentation has to be done carefully, since
149         // there could be existing versions of the tag we have to append to, and some parts
150         // of the comment needs to be present in certain places. For example, you can't
151         // just append to the description of a method by inserting something right before "*/"
152         // since you could be appending to a javadoc tag like @return.
153         //
154         // However, for @apiSince and @deprecatedSince specifically, in addition to being frequent,
155         // they will (a) never appear in existing docs, and (b) they're separate tags, which means
156         // it's safe to append them at the end. So we'll special case these two tags here, to
157         // help speed up the builds since these tags are inserted 30,000+ times for each framework
158         // API target (there are many), and each time would have involved constructing a full javadoc
159         // AST with lexical tokens using IntelliJ's javadoc parsing APIs. Instead, we'll just
160         // do some simple string heuristics.
161         if (tagSection == "@apiSince" || tagSection == "@deprecatedSince") {
162             documentation = addUniqueTag(documentation, tagSection, comment)
163             return
164         }
165 
166         documentation = mergeDocumentation(documentation, element, comment.trim(), tagSection, append)
167     }
168 
addUniqueTagnull169     private fun addUniqueTag(documentation: String, tagSection: String, commentLine: String): String {
170         assert(commentLine.indexOf('\n') == -1) // Not meant for multi-line comments
171 
172         if (documentation.isBlank()) {
173             return "/** $tagSection $commentLine */"
174         }
175 
176         // Already single line?
177         if (documentation.indexOf('\n') == -1) {
178             val end = documentation.lastIndexOf("*/")
179             return "/**\n *" + documentation.substring(3, end) + "\n * $tagSection $commentLine\n */"
180         }
181 
182         var end = documentation.lastIndexOf("*/")
183         while (end > 0 && documentation[end - 1].isWhitespace() &&
184             documentation[end - 1] != '\n') {
185             end--
186         }
187         // The comment ends with:
188         // * some comment here */
189         val insertNewLine: Boolean = documentation[end - 1] != '\n'
190 
191         val indent: String
192         var linePrefix = ""
193         val secondLine = documentation.indexOf('\n')
194         if (secondLine == -1) {
195             // Single line comment
196             indent = "\n * "
197         } else {
198             val indentStart = secondLine + 1
199             var indentEnd = indentStart
200             while (indentEnd < documentation.length) {
201                 if (!documentation[indentEnd].isWhitespace()) {
202                     break
203                 }
204                 indentEnd++
205             }
206             indent = documentation.substring(indentStart, indentEnd)
207             // TODO: If it starts with "* " follow that
208             if (documentation.startsWith("* ", indentEnd)) {
209                 linePrefix = "* "
210             }
211         }
212         return documentation.substring(0, end) + (if (insertNewLine) "\n" else "") + indent + linePrefix + tagSection + " " + commentLine + "\n" + indent + " */"
213     }
214 
fullyQualifiedDocumentationnull215     override fun fullyQualifiedDocumentation(): String {
216         return fullyQualifiedDocumentation(documentation)
217     }
218 
fullyQualifiedDocumentationnull219     override fun fullyQualifiedDocumentation(documentation: String): String {
220         return toFullyQualifiedDocumentation(this, documentation)
221     }
222 
223     /** Finish initialization of the item */
finishInitializationnull224     open fun finishInitialization() {
225         modifiers.setOwner(this)
226     }
227 
isKotlinnull228     override fun isKotlin(): Boolean {
229         return isKotlin(element)
230     }
231 
232     companion object {
javadocnull233         fun javadoc(element: PsiElement): String {
234             if (element is PsiCompiledElement) {
235                 return ""
236             }
237 
238             if (element is UElement) {
239                 val comments = element.comments
240                 if (comments.isNotEmpty()) {
241                     val sb = StringBuilder()
242                     comments.asSequence().joinTo(buffer = sb, separator = "\n") {
243                         it.text
244                     }
245                     return sb.toString()
246                 } else {
247                     // Temporary workaround: UAST seems to not return document nodes
248                     // https://youtrack.jetbrains.com/issue/KT-22135
249                     val first = element.sourcePsiElement?.firstChild
250                     if (first is KDoc) {
251                         return first.text
252                     }
253                 }
254             }
255 
256             if (element is PsiDocCommentOwner && element.docComment !is PsiCompiledElement) {
257                 return element.docComment?.text ?: ""
258             }
259 
260             return ""
261         }
262 
modifiersnull263         fun modifiers(
264             codebase: PsiBasedCodebase,
265             element: PsiModifierListOwner,
266             documentation: String
267         ): PsiModifierItem {
268             return PsiModifierItem.create(codebase, element, documentation)
269         }
270 
isKotlinnull271         fun isKotlin(element: PsiElement): Boolean {
272             return element.language === KotlinLanguage.INSTANCE
273         }
274     }
275 }
276