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.Item 21 import com.android.tools.metalava.model.MutableModifierList 22 import com.android.tools.metalava.model.PackageItem 23 import com.android.tools.metalava.model.ParameterItem 24 import com.intellij.psi.PsiClass 25 import com.intellij.psi.PsiCompiledElement 26 import com.intellij.psi.PsiDocCommentOwner 27 import com.intellij.psi.PsiElement 28 import com.intellij.psi.PsiMember 29 import com.intellij.psi.PsiModifierListOwner 30 import com.intellij.psi.PsiReference 31 import com.intellij.psi.PsiWhiteSpace 32 import com.intellij.psi.javadoc.PsiDocTag 33 import com.intellij.psi.javadoc.PsiInlineDocTag 34 import org.jetbrains.kotlin.kdoc.psi.api.KDoc 35 import org.jetbrains.uast.UElement 36 import org.jetbrains.uast.sourcePsiElement 37 38 abstract class PsiItem( 39 override val codebase: PsiBasedCodebase, 40 val element: PsiElement, 41 override val modifiers: PsiModifierItem, 42 override var documentation: String 43 ) : DefaultItem() { 44 45 override val deprecated: Boolean get() = modifiers.isDeprecated() 46 47 @Suppress("LeakingThis") // Documentation can change, but we don't want to pick up subsequent @docOnly mutations 48 override var docOnly = documentation.contains("@doconly") 49 @Suppress("LeakingThis") 50 override var removed = documentation.contains("@removed") 51 @Suppress("LeakingThis") 52 override var hidden = (documentation.contains("@hide") || documentation.contains("@pending") || 53 modifiers.hasHideAnnotations()) && !modifiers.hasShowAnnotation() 54 psinull55 override fun psi(): PsiElement? = element 56 57 // TODO: Consider only doing this in tests! 58 override fun isFromClassPath(): Boolean { 59 return if (element is UElement) { 60 (element.sourcePsi ?: element.javaPsi) is PsiCompiledElement 61 } else { 62 element is PsiCompiledElement 63 } 64 } 65 isClonednull66 override fun isCloned(): Boolean = false 67 68 /** Get a mutable version of modifiers for this item */ 69 override fun mutableModifiers(): MutableModifierList = modifiers 70 71 override fun findTagDocumentation(tag: String): String? { 72 if (element is PsiCompiledElement) { 73 return null 74 } 75 if (documentation.isBlank()) { 76 return null 77 } 78 79 // We can't just use element.docComment here because we may have modified 80 // the comment and then the comment snapshot in PSI isn't up to date with our 81 // latest changes 82 val docComment = codebase.getComment(documentation) 83 val docTag = docComment.findTagByName(tag) ?: return null 84 val text = docTag.text 85 86 // Trim trailing next line (javadoc *) 87 var index = text.length - 1 88 while (index > 0) { 89 val c = text[index] 90 if (!(c == '*' || c.isWhitespace())) { 91 break 92 } 93 index-- 94 } 95 index++ 96 return if (index < text.length) { 97 text.substring(0, index) 98 } else { 99 text 100 } 101 } 102 appendDocumentationnull103 override fun appendDocumentation(comment: String, tagSection: String?, append: Boolean) { 104 if (comment.isBlank()) { 105 return 106 } 107 108 // TODO: Figure out if an annotation should go on the return value, or on the method. 109 // For example; threading: on the method, range: on the return value. 110 // TODO: Find a good way to add or append to a given tag (@param <something>, @return, etc) 111 112 if (this is ParameterItem) { 113 // For parameters, the documentation goes into the surrounding method's documentation! 114 // Find the right parameter location! 115 val parameterName = name() 116 val target = containingMethod() 117 target.appendDocumentation(comment, parameterName) 118 return 119 } 120 121 documentation = mergeDocumentation(documentation, element, comment.trim(), tagSection, append) 122 } 123 packageNamenull124 private fun packageName(): String? { 125 var curr: Item? = this 126 while (curr != null) { 127 if (curr is PackageItem) { 128 return curr.qualifiedName() 129 } 130 curr = curr.parent() 131 } 132 133 return null 134 } 135 fullyQualifiedDocumentationnull136 override fun fullyQualifiedDocumentation(): String { 137 if (documentation.isBlank()) { 138 return documentation 139 } 140 141 if (!(documentation.contains("@link") || // includes @linkplain 142 documentation.contains("@see") || 143 documentation.contains("@throws")) 144 ) { 145 // No relevant tags that need to be expanded/rewritten 146 return documentation 147 } 148 149 val comment = 150 try { 151 codebase.getComment(documentation, psi()) 152 } catch (throwable: Throwable) { 153 // TODO: Get rid of line comments as documentation 154 // Invalid comment 155 if (documentation.startsWith("//") && documentation.contains("/**")) { 156 documentation = documentation.substring(documentation.indexOf("/**")) 157 } 158 codebase.getComment(documentation, psi()) 159 } 160 val sb = StringBuilder(documentation.length) 161 var curr = comment.firstChild 162 while (curr != null) { 163 if (curr is PsiDocTag) { 164 sb.append(getExpanded(curr)) 165 } else { 166 sb.append(curr.text) 167 } 168 curr = curr.nextSibling 169 } 170 171 return sb.toString() 172 } 173 getExpandednull174 private fun getExpanded(tag: PsiDocTag): String { 175 val text = tag.text 176 var valueElement = tag.valueElement 177 val reference = extractReference(tag) 178 var resolved = reference?.resolve() 179 var referenceText = reference?.element?.text 180 if (resolved == null && tag.name == "throws") { 181 // Workaround: @throws does not provide a valid reference to the class 182 val dataElements = tag.dataElements 183 if (dataElements.isNotEmpty()) { 184 if (dataElements[0] is PsiInlineDocTag) { 185 val innerReference = extractReference(dataElements[0] as PsiInlineDocTag) 186 resolved = innerReference?.resolve() 187 if (innerReference != null && resolved == null) { 188 referenceText = innerReference.canonicalText 189 resolved = codebase.createReferenceFromText(referenceText, psi()).resolve() 190 } else { 191 referenceText = innerReference?.element?.text 192 } 193 } 194 if (resolved == null || referenceText == null) { 195 val exceptionName = dataElements[0].text 196 val exceptionReference = codebase.createReferenceFromText(exceptionName, psi()) 197 resolved = exceptionReference.resolve() 198 referenceText = exceptionName 199 } else { 200 // Create a placeholder value since the inline tag 201 // wipes it out 202 val t = dataElements[0].text 203 val index = text.indexOf(t) + t.length 204 val suffix = text.substring(index) 205 val dummyTag = codebase.createDocTagFromText("@${tag.name} $suffix") 206 valueElement = dummyTag.valueElement 207 } 208 } else { 209 return text 210 } 211 } 212 213 if (resolved != null && referenceText != null) { 214 if (referenceText.startsWith("#")) { 215 // Already a local/relative reference 216 return text 217 } 218 219 when (resolved) { 220 // TODO: If not absolute, preserve syntax 221 is PsiClass -> { 222 if (samePackage(resolved)) { 223 return text 224 } 225 val qualifiedName = resolved.qualifiedName ?: return text 226 if (referenceText == qualifiedName) { 227 // Already absolute 228 return text 229 } 230 return when { 231 valueElement != null -> { 232 val start = valueElement.startOffsetInParent 233 val end = start + valueElement.textLength 234 text.substring(0, start) + qualifiedName + text.substring(end) 235 } 236 tag.name == "see" -> { 237 val suffix = text.substring(text.indexOf(referenceText) + referenceText.length) 238 "@see $qualifiedName$suffix" 239 } 240 text.startsWith("{") -> "{@${tag.name} $qualifiedName $referenceText}" 241 else -> "@${tag.name} $qualifiedName $referenceText" 242 } 243 } 244 is PsiMember -> { 245 val containing = resolved.containingClass ?: return text 246 if (samePackage(containing)) { 247 return text 248 } 249 val qualifiedName = containing.qualifiedName ?: return text 250 if (referenceText.startsWith(qualifiedName)) { 251 // Already absolute 252 return text 253 } 254 255 val name = containing.name ?: return text 256 if (valueElement != null) { 257 val start = valueElement.startOffsetInParent 258 val close = text.lastIndexOf('}') 259 if (close == -1) { 260 return text // invalid javadoc 261 } 262 val memberPart = text.substring(text.indexOf(name, start) + name.length, close) 263 return "${text.substring(0, start)}$qualifiedName$memberPart $referenceText}" 264 } 265 } 266 } 267 } 268 269 return text 270 } 271 samePackagenull272 private fun samePackage(cls: PsiClass): Boolean { 273 val pkg = packageName() ?: return false 274 return cls.qualifiedName == "$pkg.${cls.name}" 275 } 276 277 // Copied from UnnecessaryJavaDocLinkInspection extractReferencenull278 private fun extractReference(tag: PsiDocTag): PsiReference? { 279 val valueElement = tag.valueElement 280 if (valueElement != null) { 281 return valueElement.reference 282 } 283 // hack around the fact that a reference to a class is apparently 284 // not a PsiDocTagValue 285 val dataElements = tag.dataElements 286 if (dataElements.isEmpty()) { 287 return null 288 } 289 val salientElement: PsiElement = dataElements.firstOrNull { it !is PsiWhiteSpace } ?: return null 290 val child = salientElement.firstChild 291 return if (child !is PsiReference) null else child 292 } 293 294 /** Finish initialization of the item */ finishInitializationnull295 open fun finishInitialization() { 296 modifiers.setOwner(this) 297 } 298 isKotlinnull299 override fun isKotlin(): Boolean { 300 return isKotlin(element) 301 } 302 303 companion object { javadocnull304 fun javadoc(element: PsiElement): String { 305 if (element is PsiCompiledElement) { 306 return "" 307 } 308 309 if (element is UElement) { 310 val comments = element.comments 311 if (comments.isNotEmpty()) { 312 val sb = StringBuilder() 313 comments.asSequence().joinTo(buffer = sb, separator = "\n") 314 return sb.toString() 315 } else { 316 // Temporary workaround: UAST seems to not return document nodes 317 // https://youtrack.jetbrains.com/issue/KT-22135 318 val first = element.sourcePsiElement?.firstChild 319 if (first is KDoc) { 320 return first.text 321 } 322 } 323 } 324 325 if (element is PsiDocCommentOwner) { 326 return element.docComment?.text ?: "" 327 } 328 329 return "" 330 } 331 modifiersnull332 fun modifiers( 333 codebase: PsiBasedCodebase, 334 element: PsiModifierListOwner, 335 documentation: String 336 ): PsiModifierItem { 337 return PsiModifierItem.create(codebase, element, documentation) 338 } 339 isKotlinnull340 fun isKotlin(element: PsiElement): Boolean { 341 return element.language.id == "kotlin" 342 } 343 } 344 } 345