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