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