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