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 // a property with a lazily calculated default value 49 inner class lazyDelegate<T>( 50 val defaultValueProvider: () -> T 51 ) : ReadWriteProperty<PsiItem, T> { 52 private var currentValue: T? = null 53 setValuenull54 override operator fun setValue(thisRef: PsiItem, property: KProperty<*>, value: T) { 55 currentValue = value 56 } getValuenull57 override operator fun getValue(thisRef: PsiItem, property: KProperty<*>): T { 58 if (currentValue == null) { 59 currentValue = defaultValueProvider() 60 } 61 62 return currentValue!! 63 } 64 } 65 <lambda>null66 override var originallyHidden: Boolean by lazyDelegate({ 67 documentation.contains('@') && 68 69 (documentation.contains("@hide") || 70 documentation.contains("@pending") || 71 // KDoc: 72 documentation.contains("@suppress")) || 73 modifiers.hasHideAnnotations() 74 }) 75 <lambda>null76 override var hidden: Boolean by lazyDelegate({ originallyHidden && !modifiers.hasShowAnnotation() }) 77 psinull78 override fun psi(): PsiElement? = element 79 80 // TODO: Consider only doing this in tests! 81 override fun isFromClassPath(): Boolean { 82 return if (element is UElement) { 83 (element.sourcePsi ?: element.javaPsi) is PsiCompiledElement 84 } else { 85 element is PsiCompiledElement 86 } 87 } 88 isClonednull89 override fun isCloned(): Boolean = false 90 91 /** Get a mutable version of modifiers for this item */ 92 override fun mutableModifiers(): MutableModifierList = modifiers 93 94 override fun findTagDocumentation(tag: String): String? { 95 if (element is PsiCompiledElement) { 96 return null 97 } 98 if (documentation.isBlank()) { 99 return null 100 } 101 102 // We can't just use element.docComment here because we may have modified 103 // the comment and then the comment snapshot in PSI isn't up to date with our 104 // latest changes 105 val docComment = codebase.getComment(documentation) 106 val docTag = docComment.findTagByName(tag) ?: return null 107 val text = docTag.text 108 109 // Trim trailing next line (javadoc *) 110 var index = text.length - 1 111 while (index > 0) { 112 val c = text[index] 113 if (!(c == '*' || c.isWhitespace())) { 114 break 115 } 116 index-- 117 } 118 index++ 119 return if (index < text.length) { 120 text.substring(0, index) 121 } else { 122 text 123 } 124 } 125 appendDocumentationnull126 override fun appendDocumentation(comment: String, tagSection: String?, append: Boolean) { 127 if (comment.isBlank()) { 128 return 129 } 130 131 // TODO: Figure out if an annotation should go on the return value, or on the method. 132 // For example; threading: on the method, range: on the return value. 133 // TODO: Find a good way to add or append to a given tag (@param <something>, @return, etc) 134 135 if (this is ParameterItem) { 136 // For parameters, the documentation goes into the surrounding method's documentation! 137 // Find the right parameter location! 138 val parameterName = name() 139 val target = containingMethod() 140 target.appendDocumentation(comment, parameterName) 141 return 142 } 143 144 // Micro-optimization: we're very often going to be merging @apiSince and to a lesser 145 // extend @deprecatedSince into existing comments, since we're flagging every single 146 // public API. Normally merging into documentation has to be done carefully, since 147 // there could be existing versions of the tag we have to append to, and some parts 148 // of the comment needs to be present in certain places. For example, you can't 149 // just append to the description of a method by inserting something right before "*/" 150 // since you could be appending to a javadoc tag like @return. 151 // 152 // However, for @apiSince and @deprecatedSince specifically, in addition to being frequent, 153 // they will (a) never appear in existing docs, and (b) they're separate tags, which means 154 // it's safe to append them at the end. So we'll special case these two tags here, to 155 // help speed up the builds since these tags are inserted 30,000+ times for each framework 156 // API target (there are many), and each time would have involved constructing a full javadoc 157 // AST with lexical tokens using IntelliJ's javadoc parsing APIs. Instead, we'll just 158 // do some simple string heuristics. 159 if (tagSection == "@apiSince" || tagSection == "@deprecatedSince") { 160 documentation = addUniqueTag(documentation, tagSection, comment) 161 return 162 } 163 164 documentation = mergeDocumentation(documentation, element, comment.trim(), tagSection, append) 165 } 166 addUniqueTagnull167 private fun addUniqueTag(documentation: String, tagSection: String, commentLine: String): String { 168 assert(commentLine.indexOf('\n') == -1) // Not meant for multi-line comments 169 170 if (documentation.isBlank()) { 171 return "/** $tagSection $commentLine */" 172 } 173 174 // Already single line? 175 if (documentation.indexOf('\n') == -1) { 176 var end = documentation.lastIndexOf("*/") 177 val s = "/**\n *" + documentation.substring(3, end) + "\n * $tagSection $commentLine\n */" 178 return s 179 } 180 181 var end = documentation.lastIndexOf("*/") 182 while (end > 0 && documentation[end - 1].isWhitespace() && 183 documentation[end - 1] != '\n') { 184 end-- 185 } 186 // The comment ends with: 187 // * some comment here */ 188 var insertNewLine: Boolean = documentation[end - 1] != '\n' 189 190 var indent: String 191 var linePrefix = "" 192 val secondLine = documentation.indexOf('\n') 193 if (secondLine == -1) { 194 // Single line comment 195 indent = "\n * " 196 } else { 197 val indentStart = secondLine + 1 198 var indentEnd = indentStart 199 while (indentEnd < documentation.length) { 200 if (!documentation[indentEnd].isWhitespace()) { 201 break 202 } 203 indentEnd++ 204 } 205 indent = documentation.substring(indentStart, indentEnd) 206 // TODO: If it starts with "* " follow that 207 if (documentation.startsWith("* ", indentEnd)) { 208 linePrefix = "* " 209 } 210 } 211 val s = documentation.substring(0, end) + (if (insertNewLine) "\n" else "") + indent + linePrefix + tagSection + " " + commentLine + "\n" + indent + " */" 212 return s 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 return sb.toString() 244 } else { 245 // Temporary workaround: UAST seems to not return document nodes 246 // https://youtrack.jetbrains.com/issue/KT-22135 247 val first = element.sourcePsiElement?.firstChild 248 if (first is KDoc) { 249 return first.text 250 } 251 } 252 } 253 254 if (element is PsiDocCommentOwner && element.docComment !is PsiCompiledElement) { 255 return element.docComment?.text ?: "" 256 } 257 258 return "" 259 } 260 modifiersnull261 fun modifiers( 262 codebase: PsiBasedCodebase, 263 element: PsiModifierListOwner, 264 documentation: String 265 ): PsiModifierItem { 266 return PsiModifierItem.create(codebase, element, documentation) 267 } 268 isKotlinnull269 fun isKotlin(element: PsiElement): Boolean { 270 return element.language === KotlinLanguage.INSTANCE 271 } 272 } 273 } 274