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