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