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