• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 }