1 /* <lambda>null2 * 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 18 19 import com.android.tools.lint.annotations.Extractor 20 import com.android.tools.metalava.model.ANDROIDX_ANNOTATION_PREFIX 21 import com.android.tools.metalava.model.ANDROID_ANNOTATION_PREFIX 22 import com.android.tools.metalava.model.ANNOTATION_ATTR_VALUE 23 import com.android.tools.metalava.model.AnnotationItem 24 import com.android.tools.metalava.model.AnnotationRetention 25 import com.android.tools.metalava.model.AnnotationTarget 26 import com.android.tools.metalava.model.CallableItem 27 import com.android.tools.metalava.model.ClassItem 28 import com.android.tools.metalava.model.Codebase 29 import com.android.tools.metalava.model.FieldItem 30 import com.android.tools.metalava.model.Item 31 import com.android.tools.metalava.model.JAVA_LANG_PREFIX 32 import com.android.tools.metalava.model.MemberItem 33 import com.android.tools.metalava.model.MethodItem 34 import com.android.tools.metalava.model.PackageItem 35 import com.android.tools.metalava.model.ParameterItem 36 import com.android.tools.metalava.model.findAnnotation 37 import com.android.tools.metalava.model.psi.CodePrinter 38 import com.android.tools.metalava.model.psi.report 39 import com.android.tools.metalava.model.psi.uAnnotation 40 import com.android.tools.metalava.model.visitors.ApiVisitor 41 import com.android.tools.metalava.reporter.Issues 42 import com.android.tools.metalava.reporter.Reporter 43 import com.google.common.xml.XmlEscapers 44 import com.intellij.psi.PsiAnnotation 45 import com.intellij.psi.PsiNameValuePair 46 import java.io.BufferedOutputStream 47 import java.io.File 48 import java.io.FileOutputStream 49 import java.io.PrintWriter 50 import java.io.StringWriter 51 import java.util.jar.JarEntry 52 import java.util.jar.JarOutputStream 53 import kotlin.text.Charsets.UTF_8 54 import org.jetbrains.uast.UAnnotation 55 import org.jetbrains.uast.UCallExpression 56 import org.jetbrains.uast.UExpression 57 import org.jetbrains.uast.UastEmptyExpression 58 import org.jetbrains.uast.UastFacade 59 import org.jetbrains.uast.toUElement 60 61 // Like the tools/base Extractor class, but limited to our own (mapped) AnnotationItems, 62 // and only those with source retention (and in particular right now that just means the 63 // typedef annotations.) 64 class ExtractAnnotations( 65 private val codebase: Codebase, 66 private val reporter: Reporter, 67 private val outputFile: File, 68 ) : 69 ApiVisitor( 70 apiPredicateConfig = @Suppress("DEPRECATION") options.apiPredicateConfig, 71 ) { 72 // Used linked hash map for order such that we always emit parameters after their surrounding 73 // method etc 74 private val packageToAnnotationPairs = 75 LinkedHashMap<PackageItem, MutableList<Pair<Item, AnnotationItem>>>() 76 77 private val fieldNamePrinter = 78 CodePrinter( 79 codebase = codebase, 80 reporter = reporter, 81 filterReference = filterReference, 82 inlineFieldValues = false, 83 skipUnknown = true, 84 ) 85 86 private val fieldValuePrinter = 87 CodePrinter( 88 codebase = codebase, 89 reporter = reporter, 90 filterReference = filterReference, 91 inlineFieldValues = true, 92 skipUnknown = true, 93 ) 94 95 private val classToAnnotationHolder = mutableMapOf<String, AnnotationItem>() 96 97 fun extractAnnotations() { 98 codebase.accept(this) 99 100 // Write external annotations 101 FileOutputStream(outputFile).use { fileOutputStream -> 102 JarOutputStream(BufferedOutputStream(fileOutputStream)).use { zos -> 103 val sortedPackages = 104 packageToAnnotationPairs.keys 105 .asSequence() 106 .sortedBy { it.qualifiedName() } 107 .toList() 108 109 for (pkg in sortedPackages) { 110 // Note: Using / rather than File.separator: jar lib requires it 111 val name = pkg.qualifiedName().replace('.', '/') + "/annotations.xml" 112 113 val outEntry = JarEntry(name) 114 outEntry.time = 0 115 zos.putNextEntry(outEntry) 116 117 val pairs = packageToAnnotationPairs[pkg] ?: continue 118 119 // Ensure stable output 120 if (pairs.size > 1) { 121 pairs.sortBy { it.first.getExternalAnnotationSignature() } 122 } 123 124 StringPrintWriter.create().use { writer -> 125 writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>") 126 127 var open = false 128 var previousSignature: String? = null 129 for ((item, annotation) in pairs) { 130 val signature = item.getExternalAnnotationSignature() 131 if (signature != previousSignature) { 132 if (open) { 133 writer.print(" </item>") 134 writer.println() 135 } 136 writer.print(" <item name=\"") 137 writer.print(signature) 138 writer.println("\">") 139 open = true 140 } 141 previousSignature = signature 142 143 writeAnnotation(writer, item, annotation) 144 } 145 if (open) { 146 writer.print(" </item>") 147 writer.println() 148 } 149 writer.println("</root>\n") 150 writer.close() 151 val bytes = writer.contents.toByteArray(UTF_8) 152 zos.write(bytes) 153 zos.closeEntry() 154 } 155 } 156 } 157 } 158 } 159 160 private fun addItem(item: Item, annotation: AnnotationItem) { 161 val pkg = 162 when (item) { 163 is ClassItem -> item.containingPackage() 164 is MemberItem -> item.containingClass().containingPackage() 165 is ParameterItem -> item.containingCallable().containingClass().containingPackage() 166 else -> return 167 } 168 169 val list = 170 packageToAnnotationPairs[pkg] 171 ?: run { 172 val new = mutableListOf<Pair<Item, AnnotationItem>>() 173 packageToAnnotationPairs[pkg] = new 174 new 175 } 176 list.add(Pair(item, annotation)) 177 } 178 179 override fun visitClass(cls: ClassItem) { 180 checkItem(cls) 181 } 182 183 override fun visitField(field: FieldItem) { 184 checkItem(field) 185 } 186 187 override fun visitCallable(callable: CallableItem) { 188 checkItem(callable) 189 } 190 191 override fun visitParameter(parameter: ParameterItem) { 192 checkItem(parameter) 193 } 194 195 /** For a given item, extract the relevant annotations for that item */ 196 private fun checkItem(item: Item) { 197 for (annotation in item.modifiers.annotations()) { 198 val qualifiedName = annotation.qualifiedName 199 if ( 200 qualifiedName.startsWith(JAVA_LANG_PREFIX) || 201 qualifiedName.startsWith(ANDROIDX_ANNOTATION_PREFIX) || 202 qualifiedName.startsWith(ANDROID_ANNOTATION_PREFIX) 203 ) { 204 if (annotation.isTypeDefAnnotation()) { 205 // Imported typedef 206 addItem(item, annotation) 207 } else if ( 208 annotation.targets.contains(AnnotationTarget.EXTERNAL_ANNOTATIONS_FILE) 209 ) { 210 addItem(item, annotation) 211 } 212 213 continue 214 } else if ( 215 qualifiedName.startsWith(ORG_JETBRAINS_ANNOTATIONS_PREFIX) || 216 qualifiedName.startsWith(ORG_INTELLIJ_LANG_ANNOTATIONS_PREFIX) 217 ) { 218 // Externally merged metadata, like @Contract and @Language 219 addItem(item, annotation) 220 continue 221 } 222 223 val typeDefClass = annotation.resolve() ?: continue 224 val className = typeDefClass.qualifiedName() 225 if (typeDefClass.isAnnotationType()) { 226 val cached = classToAnnotationHolder[className] 227 if (cached != null) { 228 addItem(item, cached) 229 continue 230 } 231 232 val typeDefAnnotation = 233 typeDefClass.modifiers.findAnnotation(AnnotationItem::isTypeDefAnnotation) 234 if (typeDefAnnotation != null) { 235 // Make sure it has the right retention 236 if (typeDefClass.getRetention() != AnnotationRetention.SOURCE) { 237 reporter.report( 238 Issues.ANNOTATION_EXTRACTION, 239 typeDefClass, 240 "This typedef annotation class should have @Retention(RetentionPolicy.SOURCE)" 241 ) 242 } 243 244 if (filterEmit.test(typeDefClass)) { 245 reporter.report( 246 Issues.ANNOTATION_EXTRACTION, 247 typeDefClass, 248 "This typedef annotation class should be marked @hide or should not be marked public" 249 ) 250 } 251 252 classToAnnotationHolder[className] = typeDefAnnotation 253 addItem(item, typeDefAnnotation) 254 255 if ( 256 item is MethodItem && 257 !reporter.isSuppressed(Issues.RETURNING_UNEXPECTED_CONSTANT) 258 ) { 259 item.body.verifyReturnedConstants(typeDefAnnotation, typeDefClass) 260 } 261 } 262 } 263 } 264 } 265 266 /** 267 * A writer which stores all its contents into a string and has the ability to mark a certain 268 * freeze point and then reset back to it 269 */ 270 private class StringPrintWriter constructor(private val stringWriter: StringWriter) : 271 PrintWriter(stringWriter) { 272 private var mark: Int = 0 273 274 val contents: String 275 get() = stringWriter.toString() 276 277 fun mark() { 278 flush() 279 mark = stringWriter.buffer.length 280 } 281 282 fun reset() { 283 stringWriter.buffer.setLength(mark) 284 } 285 286 override fun toString(): String { 287 return contents 288 } 289 290 companion object { 291 fun create(): StringPrintWriter { 292 return StringPrintWriter(StringWriter(1000)) 293 } 294 } 295 } 296 297 private fun escapeXml(unescaped: String): String { 298 return XmlEscapers.xmlAttributeEscaper().escape(unescaped) 299 } 300 301 private fun Item.getExternalAnnotationSignature(): String? { 302 when (this) { 303 is PackageItem -> { 304 return escapeXml(qualifiedName()) 305 } 306 is ClassItem -> { 307 return escapeXml(qualifiedName()) 308 } 309 is CallableItem -> { 310 val sb = StringBuilder(100) 311 sb.append(escapeXml(containingClass().qualifiedName())) 312 sb.append(' ') 313 314 if (isConstructor()) { 315 sb.append(escapeXml(containingClass().simpleName())) 316 } else { 317 sb.append(escapeXml(returnType().toTypeString())) 318 sb.append(' ') 319 sb.append(escapeXml(name())) 320 } 321 322 sb.append('(') 323 324 // The signature must match *exactly* the formatting used by IDEA, 325 // since it looks up external annotations in a map by this key. 326 // Therefore, it is vital that the parameter list uses exactly one 327 // space after each comma between parameters, and *no* spaces between 328 // generics variables, e.g. foo(Map<A,B>, int) 329 var i = 0 330 val parameterList = parameters() 331 val n = parameterList.size 332 while (i < n) { 333 if (i > 0) { 334 sb.append(',').append(' ') 335 } 336 val type = 337 parameterList[i] 338 .type() 339 .toTypeString() 340 .replace(" ", "") 341 .replace("?extends", "? extends ") 342 .replace("?super", "? super ") 343 sb.append(escapeXml(type)) 344 i++ 345 } 346 sb.append(')') 347 return sb.toString() 348 } 349 is FieldItem -> { 350 return escapeXml(containingClass().qualifiedName()) + " " + name() 351 } 352 is ParameterItem -> { 353 return containingCallable().getExternalAnnotationSignature() + 354 " " + 355 this.parameterIndex 356 } 357 } 358 359 return null 360 } 361 362 private fun writeAnnotation( 363 writer: StringPrintWriter, 364 item: Item, 365 annotationItem: AnnotationItem 366 ) { 367 val uAnnotation = annotationItem.uAnnotation ?: return 368 val qualifiedName = annotationItem.qualifiedName 369 370 writer.mark() 371 writer.print(" <annotation name=\"") 372 writer.print(qualifiedName) 373 374 var attributes = 375 // Ensure consistent ordering. 376 uAnnotation.attributeValues.sortedWith( 377 compareBy( 378 // Ensure that the value attribute is written first 379 { (it.name ?: ANNOTATION_ATTR_VALUE) != ANNOTATION_ATTR_VALUE }, 380 { it.name } 381 ) 382 ) 383 384 if (attributes.isEmpty()) { 385 writer.print("\"/>") 386 writer.println() 387 return 388 } 389 390 writer.print("\">") 391 writer.println() 392 393 if (attributes.size == 1 && Extractor.REQUIRES_PERMISSION.isPrefix(qualifiedName, true)) { 394 val expression = attributes[0].expression 395 if (expression is UAnnotation) { 396 // The external annotations format does not allow for nested/complex annotations. 397 // However, these special annotations (@RequiresPermission.Read, 398 // @RequiresPermission.Write, etc.) are known to only be simple containers with a 399 // single permission child, so instead we "inline" the content: 400 // @Read(@RequiresPermission(allOf={P1,P2},conditional=true) 401 // => 402 // @RequiresPermission.Read(allOf({P1,P2},conditional=true) 403 // That's setting attributes that don't actually exist on the container permission, 404 // but we'll counteract that on the read-annotations side. 405 val annotation = expression as UAnnotation 406 attributes = annotation.attributeValues 407 } else if (expression is UCallExpression) { 408 val nestedPsi = expression.sourcePsi as? PsiAnnotation 409 val annotation = 410 nestedPsi?.let { 411 UastFacade.convertElement(it, expression, UAnnotation::class.java) 412 } as? UAnnotation 413 annotation?.attributeValues?.let { attributes = it } 414 } else if ( 415 expression is UastEmptyExpression && attributes[0].sourcePsi is PsiNameValuePair 416 ) { 417 val memberValue = (attributes[0].sourcePsi as PsiNameValuePair).value 418 if (memberValue is PsiAnnotation) { 419 val annotation = memberValue.toUElement(UAnnotation::class.java) 420 annotation?.attributeValues?.let { attributes = it } 421 } 422 } 423 } 424 425 val inlineConstants = isInlinedConstant(annotationItem) 426 var empty = true 427 for (pair in attributes) { 428 val expression = pair.expression 429 val value = attributeString(expression, inlineConstants) ?: continue 430 empty = false 431 var name = pair.name 432 if (name == null) { 433 name = ANNOTATION_ATTR_VALUE // default name 434 } 435 436 // Platform typedef annotations declare prefix/suffix attributes for historical reasons, 437 // and they are no longer necessary; they should also not be part of the extracted 438 // metadata. 439 if (("prefix" == name || "suffix" == name) && annotationItem.isTypeDefAnnotation()) { 440 reporter.report( 441 Issues.SUPERFLUOUS_PREFIX, 442 item, 443 "Superfluous $name attribute on typedef" 444 ) 445 continue 446 } 447 448 // The value could contain fully qualified references to enum values that are in the 449 // android.annotation package. If so, then replace them with references in the 450 // androidx.annotation package. 451 val normalizedValue = 452 value.replace(ANDROID_ANNOTATION_PREFIX, ANDROIDX_ANNOTATION_PREFIX) 453 454 writer.print(" <val name=\"") 455 writer.print(name) 456 writer.print("\" val=\"") 457 writer.print(escapeXml(normalizedValue)) 458 writer.println("\" />") 459 } 460 461 if (empty && attributes.isNotEmpty()) { 462 // All items were filtered out: don't write the annotation at all 463 writer.reset() 464 return 465 } 466 467 writer.println(" </annotation>") 468 } 469 470 private fun attributeString(value: UExpression?, inlineConstants: Boolean): String? { 471 val printer = 472 if (inlineConstants) { 473 fieldValuePrinter 474 } else { 475 fieldNamePrinter 476 } 477 478 return printer.toSourceString(value) 479 } 480 481 private fun isInlinedConstant(annotationItem: AnnotationItem): Boolean { 482 return annotationItem.isTypeDefAnnotation() 483 } 484 } 485