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