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.SdkConstants 20 import com.android.tools.metalava.doclava1.ApiPredicate 21 import com.android.tools.metalava.model.ClassItem 22 import com.android.tools.metalava.model.Codebase 23 import com.android.tools.metalava.model.FieldItem 24 import com.android.tools.metalava.model.Item 25 import com.android.tools.metalava.model.MemberItem 26 import com.android.tools.metalava.model.MethodItem 27 import com.android.tools.metalava.model.PackageItem 28 import com.android.tools.metalava.model.ParameterItem 29 import com.android.tools.metalava.model.visitors.ApiVisitor 30 import com.google.common.io.ByteStreams 31 import org.objectweb.asm.ClassReader 32 import org.objectweb.asm.tree.AbstractInsnNode 33 import org.objectweb.asm.tree.ClassNode 34 import org.objectweb.asm.tree.FieldInsnNode 35 import org.objectweb.asm.tree.MethodInsnNode 36 import org.objectweb.asm.tree.MethodNode 37 import java.io.BufferedWriter 38 import java.io.File 39 import java.io.FileWriter 40 import java.io.IOException 41 import java.io.PrintWriter 42 import java.util.zip.ZipFile 43 44 const val CLASS_COLUMN_WIDTH = 60 45 const val COUNT_COLUMN_WIDTH = 16 46 const val USAGE_REPORT_MAX_ROWS = 3000 47 48 /** Sadly gitiles' markdown support doesn't handle tables with top/bottom horizontal edges */ 49 const val INCLUDE_HORIZONTAL_EDGES = false 50 51 class AnnotationStatistics(val api: Codebase) { 52 private val apiFilter = ApiPredicate() 53 54 /** Measure the coverage statistics for the API */ 55 fun count() { 56 // Count of all methods, fields and parameters 57 var allMethods = 0 58 var allFields = 0 59 var allParameters = 0 60 61 // Count of all methods, fields and parameters that requires annotations 62 // (e.g. not primitives, not constructors, etc) 63 var eligibleMethods = 0 64 var eligibleMethodReturns = 0 65 var eligibleFields = 0 66 var eligibleParameters = 0 67 68 // Count of methods, fields and parameters that were actually annotated 69 var annotatedMethods = 0 70 var annotatedMethodReturns = 0 71 var annotatedFields = 0 72 var annotatedParameters = 0 73 74 api.accept(object : ApiVisitor() { 75 override fun skip(item: Item): Boolean { 76 if (options.omitRuntimePackageStats && item is PackageItem) { 77 val name = item.qualifiedName() 78 if (name.startsWith("java.") || 79 name.startsWith("javax.") || 80 name.startsWith("kotlin.") || 81 name.startsWith("kotlinx.") 82 ) { 83 return true 84 } 85 } 86 return super.skip(item) 87 } 88 89 override fun visitParameter(parameter: ParameterItem) { 90 allParameters++ 91 if (!parameter.requiresNullnessInfo()) { 92 return 93 } 94 eligibleParameters++ 95 if (parameter.hasNullnessInfo()) { 96 annotatedParameters++ 97 } 98 } 99 100 override fun visitField(field: FieldItem) { 101 allFields++ 102 if (!field.requiresNullnessInfo()) { 103 return 104 } 105 eligibleFields++ 106 if (field.hasNullnessInfo()) { 107 annotatedFields++ 108 } 109 } 110 111 override fun visitMethod(method: MethodItem) { 112 allMethods++ 113 if (method.requiresNullnessInfo()) { // No, this includes parameter requirements 114 eligibleMethods++ 115 if (method.hasNullnessInfo()) { 116 annotatedMethods++ 117 } 118 } 119 120 // method.requiresNullnessInfo also checks parameters; here we want to consider 121 // only the method modifier list 122 if (!method.isConstructor() && method.returnType()?.primitive != true) { 123 eligibleMethodReturns++ 124 if (method.modifiers.hasNullnessInfo()) { 125 annotatedMethodReturns++ 126 } 127 } 128 } 129 }) 130 131 options.stdout.println() 132 options.stdout.println( 133 """ 134 Nullness Annotation Coverage Statistics: 135 $annotatedFields out of $eligibleFields eligible fields (out of $allFields total fields) were annotated (${percent( 136 annotatedFields, 137 eligibleFields 138 )}%) 139 $annotatedMethods out of $eligibleMethods eligible methods (out of $allMethods total methods) were fully annotated (${percent( 140 annotatedMethods, 141 eligibleMethods 142 )}%) 143 $annotatedMethodReturns out of $eligibleMethodReturns eligible method returns were annotated (${percent( 144 annotatedMethodReturns, 145 eligibleMethodReturns 146 )}%) 147 $annotatedParameters out of $eligibleParameters eligible parameters were annotated (${percent( 148 annotatedParameters, 149 eligibleParameters 150 )}%) 151 """.trimIndent() 152 ) 153 } 154 155 private fun percent(numerator: Int, denominator: Int): Int { 156 return if (denominator == 0) { 157 100 158 } else { 159 numerator * 100 / denominator 160 } 161 } 162 163 fun measureCoverageOf(classPath: List<File>) { 164 val used = HashMap<MemberItem, Int>(1000) 165 166 for (entry in classPath) { 167 recordUsages(used, entry, entry.path) 168 } 169 170 // Keep only those items where there is at least one un-annotated element in the API 171 val filtered = used.keys.filter { 172 !it.hasNullnessInfo() 173 } 174 175 val referenceCount = used.size 176 val missingCount = filtered.size 177 val annotatedCount = used.size - filtered.size 178 179 // Sort by descending usage 180 val sorted = filtered.sortedWith(Comparator { o1, o2 -> 181 // Sort first by descending count, then increasing alphabetical 182 val delta = used[o2]!! - used[o1]!! 183 if (delta != 0) { 184 return@Comparator delta 185 } 186 o1.toString().compareTo(o2.toString()) 187 }) 188 189 // High level summary 190 options.stdout.println() 191 options.stdout.println( 192 "$missingCount methods and fields were missing nullness annotations out of " + 193 "$referenceCount total API references." 194 ) 195 options.stdout.println("API nullness coverage is ${percent(annotatedCount, referenceCount)}%") 196 options.stdout.println() 197 198 reportTopUnannotatedClasses(sorted, used) 199 printMemberTable(sorted, used) 200 } 201 202 private fun reportTopUnannotatedClasses(sorted: List<MemberItem>, used: HashMap<MemberItem, Int>) { 203 // Aggregate class counts 204 val classCount = mutableMapOf<Item, Int>() 205 for (item in sorted) { 206 val containingClass = item.containingClass() 207 val itemCount = used[item]!! 208 val count = classCount[containingClass] 209 if (count == null) { 210 classCount[containingClass] = itemCount 211 } else { 212 classCount[containingClass] = count + itemCount 213 } 214 } 215 216 // Print out top entries 217 val classes = classCount.keys.sortedWith(Comparator { o1, o2 -> 218 // Sort first by descending count, then increasing alphabetical 219 val delta = classCount[o2]!! - classCount[o1]!! 220 if (delta != 0) { 221 return@Comparator delta 222 } 223 o1.toString().compareTo(o2.toString()) 224 }) 225 226 printClassTable(classes, classCount) 227 } 228 229 /** Print table in clean Markdown table syntax */ 230 private fun printTable( 231 labelHeader: String, 232 countHeader: String, 233 items: List<Item>, 234 getLabel: (Item) -> String, 235 getCount: (Item) -> Int, 236 printer: PrintWriter = options.stdout 237 ) { 238 // Print table in clean Markdown table syntax 239 @Suppress("ConstantConditionIf") 240 if (INCLUDE_HORIZONTAL_EDGES) { 241 edge(printer, CLASS_COLUMN_WIDTH + COUNT_COLUMN_WIDTH + 7) 242 } 243 printer.printf( 244 "| %-${CLASS_COLUMN_WIDTH}s | %${COUNT_COLUMN_WIDTH}s |\n", 245 labelHeader, countHeader 246 ) 247 separator(printer, CLASS_COLUMN_WIDTH + 2, COUNT_COLUMN_WIDTH + 2, rightJustify = true) 248 249 for (i in 0 until items.size) { 250 val item = items[i] 251 val label = getLabel(item) 252 val count = getCount(item) 253 printer.printf( 254 "| %-${CLASS_COLUMN_WIDTH}s | %${COUNT_COLUMN_WIDTH}d |\n", 255 truncate(label, CLASS_COLUMN_WIDTH), count 256 ) 257 258 if (i == USAGE_REPORT_MAX_ROWS) { 259 printer.printf( 260 "| %-${CLASS_COLUMN_WIDTH}s | %${COUNT_COLUMN_WIDTH}s |\n", 261 "... (${items.size - USAGE_REPORT_MAX_ROWS} more items", "" 262 ) 263 break 264 } 265 } 266 @Suppress("ConstantConditionIf") 267 if (INCLUDE_HORIZONTAL_EDGES) { 268 edge(printer, CLASS_COLUMN_WIDTH + 2, COUNT_COLUMN_WIDTH + 2) 269 } 270 } 271 272 private fun printClassTable(classes: List<Item>, classCount: MutableMap<Item, Int>) { 273 val reportFile = options.annotationCoverageClassReport 274 val printer = 275 if (reportFile != null) { 276 reportFile.parentFile?.mkdirs() 277 PrintWriter(BufferedWriter(FileWriter(reportFile))) 278 } else { 279 options.stdout 280 } 281 282 // Top APIs 283 printer.println("\nTop referenced un-annotated classes:\n") 284 285 printTable( 286 "Qualified Class Name", 287 "Usage Count", 288 classes, 289 { (it as ClassItem).qualifiedName() }, 290 { classCount[it]!! }, 291 printer 292 ) 293 294 if (reportFile != null) { 295 printer.close() 296 progress("\n$PROGRAM_NAME wrote class annotation coverage report to $reportFile") 297 } 298 } 299 300 private fun printMemberTable( 301 sorted: List<MemberItem>, 302 used: HashMap<MemberItem, Int> 303 ) { 304 val reportFile = options.annotationCoverageMemberReport 305 val printer = 306 if (reportFile != null) { 307 reportFile.parentFile?.mkdirs() 308 PrintWriter(BufferedWriter(FileWriter(reportFile))) 309 } else { 310 options.stdout 311 } 312 313 // Top APIs 314 printer.println("\nTop referenced un-annotated members:\n") 315 316 printTable( 317 "Member", 318 "Usage Count", 319 sorted, 320 { 321 val member = it as MemberItem 322 "${member.containingClass().simpleName()}.${member.name()}${if (member is MethodItem) "(${member.parameters().joinToString { parameter -> 323 parameter.type().toSimpleType() 324 }})" else ""}" 325 }, 326 { used[it]!! }, 327 printer 328 ) 329 330 if (reportFile != null) { 331 printer.close() 332 progress("\n$PROGRAM_NAME wrote member annotation coverage report to $reportFile") 333 } 334 } 335 336 private fun dashes(printer: PrintWriter, max: Int) { 337 for (count in 0 until max) { 338 printer.print('-') 339 } 340 } 341 342 private fun edge(printer: PrintWriter, max: Int) { 343 dashes(printer, max) 344 printer.println() 345 } 346 347 private fun edge(printer: PrintWriter, column1: Int, column2: Int) { 348 printer.print("|") 349 dashes(printer, column1) 350 printer.print("|") 351 dashes(printer, column2) 352 printer.print("|") 353 printer.println() 354 } 355 356 private fun separator(printer: PrintWriter, cell1: Int, cell2: Int, rightJustify: Boolean = false) { 357 printer.print('|') 358 dashes(printer, cell1) 359 printer.print('|') 360 if (rightJustify) { 361 dashes(printer, cell2 - 1) 362 // Markdown syntax to force column to be right justified instead of left justified 363 printer.print(":|") 364 } else { 365 dashes(printer, cell2) 366 printer.print('|') 367 } 368 printer.println() 369 } 370 371 private fun truncate(string: String, maxLength: Int): String { 372 if (string.length < maxLength) { 373 return string 374 } 375 376 return string.substring(0, maxLength - 3) + "..." 377 } 378 379 private fun recordUsages(used: MutableMap<MemberItem, Int>, file: File, path: String) { 380 when { 381 file.name.endsWith(SdkConstants.DOT_JAR) -> try { 382 ZipFile(file).use { jar -> 383 val enumeration = jar.entries() 384 while (enumeration.hasMoreElements()) { 385 val entry = enumeration.nextElement() 386 if (entry.name.endsWith(SdkConstants.DOT_CLASS)) { 387 try { 388 jar.getInputStream(entry).use { `is` -> 389 val bytes = ByteStreams.toByteArray(`is`) 390 if (bytes != null) { 391 recordUsages(used, bytes, path + ":" + entry.name) 392 } 393 } 394 } catch (e: Exception) { 395 options.stdout.println("Could not read jar file entry ${entry.name} from $file: $e") 396 } 397 } 398 } 399 } 400 } catch (e: IOException) { 401 options.stdout.println("Could not read jar file contents from $file: $e") 402 } 403 file.isDirectory -> { 404 val listFiles = file.listFiles() 405 listFiles?.forEach { 406 recordUsages(used, it, it.path) 407 } 408 } 409 file.path.endsWith(SdkConstants.DOT_CLASS) -> { 410 val bytes = file.readBytes() 411 recordUsages(used, bytes, file.path) 412 } 413 else -> options.stdout.println("Ignoring entry $file") 414 } 415 } 416 417 private fun recordUsages(used: MutableMap<MemberItem, Int>, bytes: ByteArray, path: String) { 418 val reader: ClassReader 419 val classNode: ClassNode 420 try { 421 reader = ClassReader(bytes) 422 classNode = ClassNode() 423 reader.accept(classNode, 0) 424 } catch (t: Throwable) { 425 options.stderr.println("Error processing $path: broken class file?") 426 return 427 } 428 429 val skipJava = options.omitRuntimePackageStats 430 431 for (methodObject in classNode.methods) { 432 val method = methodObject as MethodNode 433 val nodes = method.instructions 434 for (i in 0 until nodes.size()) { 435 val instruction = nodes.get(i) 436 val type = instruction.type 437 if (type == AbstractInsnNode.METHOD_INSN) { 438 val call = instruction as MethodInsnNode 439 if (skipJava && isSkippableOwner(call.owner)) { 440 continue 441 } 442 val item = api.findMethod(call, apiFilter) 443 item?.let { 444 val count = used[it] 445 if (count == null) { 446 used[it] = 1 447 } else { 448 used[it] = count + 1 449 } 450 } 451 } else if (type == AbstractInsnNode.FIELD_INSN) { 452 val field = instruction as FieldInsnNode 453 if (skipJava && isSkippableOwner(field.owner)) { 454 continue 455 } 456 val item = api.findField(field, apiFilter) 457 item?.let { 458 val count = used[it] 459 if (count == null) { 460 used[it] = 1 461 } else { 462 used[it] = count + 1 463 } 464 } 465 } 466 } 467 } 468 } 469 470 private fun isSkippableOwner(owner: String) = 471 owner.startsWith("java/") || 472 owner.startsWith("javax/") || 473 owner.startsWith("kotlin") || 474 owner.startsWith("kotlinx/") 475 }