• 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.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 }