• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download

<lambda>null1 package com.android.tools.metalava
2 
3 import com.android.SdkConstants.ATTR_VALUE
4 import com.android.sdklib.SdkVersionInfo
5 import com.android.sdklib.repository.AndroidSdkHandler
6 import com.android.tools.lint.LintCliClient
7 import com.android.tools.lint.checks.ApiLookup
8 import com.android.tools.lint.detector.api.editDistance
9 import com.android.tools.lint.helpers.DefaultJavaEvaluator
10 import com.android.tools.metalava.doclava1.Issues
11 import com.android.tools.metalava.model.AnnotationAttributeValue
12 import com.android.tools.metalava.model.AnnotationItem
13 import com.android.tools.metalava.model.ClassItem
14 import com.android.tools.metalava.model.Codebase
15 import com.android.tools.metalava.model.FieldItem
16 import com.android.tools.metalava.model.Item
17 import com.android.tools.metalava.model.MemberItem
18 import com.android.tools.metalava.model.MethodItem
19 import com.android.tools.metalava.model.PackageItem
20 import com.android.tools.metalava.model.ParameterItem
21 import com.android.tools.metalava.model.psi.containsLinkTags
22 import com.android.tools.metalava.model.visitors.ApiVisitor
23 import com.android.tools.metalava.model.visitors.VisibleItemVisitor
24 import com.google.common.io.Files
25 import com.intellij.psi.PsiClass
26 import com.intellij.psi.PsiField
27 import com.intellij.psi.PsiMethod
28 import java.io.File
29 import java.util.HashMap
30 import java.util.regex.Pattern
31 
32 /**
33  * Whether to include textual descriptions of the API requirements instead
34  * of just inserting a since-tag. This should be off if there is post-processing
35  * to convert since tags in the documentation tool used.
36  */
37 const val ADD_API_LEVEL_TEXT = false
38 const val ADD_DEPRECATED_IN_TEXT = false
39 
40 /**
41  * Walk over the API and apply tweaks to the documentation, such as
42  *     - Looking for annotations and converting them to auxiliary tags
43  *       that will be processed by the documentation tools later.
44  *     - Reading lint's API database and inserting metadata into
45  *       the documentation like api levels and deprecation levels.
46  *     - Transferring docs from hidden super methods.
47  *     - Performing tweaks for common documentation mistakes, such as
48  *       ending the first sentence with ", e.g. " where javadoc will sadly
49  *       see the ". " and think "aha, that's the end of the sentence!"
50  *       (It works around this by replacing the space with &nbsp;.)
51  *       This will also attempt to fix common typos (Andriod->Android etc).
52  */
53 class DocAnalyzer(
54     /** The codebase to analyze */
55     private val codebase: Codebase
56 ) {
57 
58     /** Computes the visible part of the API from all the available code in the codebase */
59     fun enhance() {
60         // Apply options for packages that should be hidden
61         documentsFromAnnotations()
62 
63         tweakGrammar()
64 
65         for (docReplacement in options.docReplacements) {
66             codebase.accept(docReplacement)
67         }
68 
69         injectArtifactIds()
70 
71         // TODO:
72         // insertMissingDocFromHiddenSuperclasses()
73     }
74 
75     private fun injectArtifactIds() {
76         val artifacts = options.artifactRegistrations
77         if (!artifacts.any()) {
78             return
79         }
80 
81         artifacts.tag(codebase)
82 
83         codebase.accept(object : VisibleItemVisitor() {
84             override fun visitClass(cls: ClassItem) {
85                 cls.artifact?.let {
86                     cls.appendDocumentation(it, "@artifactId")
87                 }
88             }
89         })
90     }
91 
92     val mentionsNull: Pattern = Pattern.compile("\\bnull\\b")
93 
94     /** Hide packages explicitly listed in [Options.hidePackages] */
95     private fun documentsFromAnnotations() {
96         // Note: Doclava1 inserts its own javadoc parameters into the documentation,
97         // which is then later processed by javadoc to insert actual descriptions.
98         // This indirection makes the actual descriptions of the annotations more
99         // configurable from a separate file -- but since this tool isn't hooked
100         // into javadoc anymore (and is going to be used by for example Dokka too)
101         // instead metalava will generate the descriptions directly in-line into the
102         // docs.
103         //
104         // This does mean that you have to update the metalava source code to update
105         // the docs -- but on the other hand all the other docs in the documentation
106         // set also requires updating framework source code, so this doesn't seem
107         // like an unreasonable burden.
108 
109         codebase.accept(object : ApiVisitor() {
110             override fun visitItem(item: Item) {
111                 val annotations = item.modifiers.annotations()
112                 if (annotations.isEmpty()) {
113                     return
114                 }
115 
116                 for (annotation in annotations) {
117                     handleAnnotation(annotation, item, depth = 0)
118                 }
119 
120                 /* Handled via @memberDoc/@classDoc on the annotations themselves right now.
121                    That doesn't handle combinations of multiple thread annotations, but those
122                    don't occur yet, right?
123                 // Threading annotations: can't look at them one at a time; need to list them
124                 // all together
125                 if (item is ClassItem || item is MethodItem) {
126                     val threads = findThreadAnnotations(annotations)
127                     threads?.let {
128                         val threadList = it.joinToString(separator = " or ") +
129                                 (if (it.size == 1) " thread" else " threads")
130                         val doc = if (item is ClassItem) {
131                             "All methods in this class must be invoked on the $threadList, unless otherwise noted"
132                         } else {
133                             assert(item is MethodItem)
134                             "This method must be invoked on the $threadList"
135                         }
136                         appendDocumentation(doc, item, false)
137                     }
138                 }
139                 */
140                 if (findThreadAnnotations(annotations).size > 1) {
141                     reporter.report(
142                         Issues.MULTIPLE_THREAD_ANNOTATIONS,
143                         item,
144                         "Found more than one threading annotation on $item; " +
145                             "the auto-doc feature does not handle this correctly"
146                     )
147                 }
148             }
149 
150             private fun findThreadAnnotations(annotations: List<AnnotationItem>): List<String> {
151                 var result: MutableList<String>? = null
152                 for (annotation in annotations) {
153                     val name = annotation.qualifiedName()
154                     if (name != null && name.endsWith("Thread") &&
155                         (name.startsWith(ANDROID_SUPPORT_ANNOTATION_PREFIX) ||
156                             name.startsWith(ANDROIDX_ANNOTATION_PREFIX))
157                     ) {
158                         if (result == null) {
159                             result = mutableListOf()
160                         }
161                         val threadName = if (name.endsWith("UiThread")) {
162                             "UI"
163                         } else {
164                             name.substring(name.lastIndexOf('.') + 1, name.length - "Thread".length)
165                         }
166                         result.add(threadName)
167                     }
168                 }
169                 return result ?: emptyList()
170             }
171 
172             /** Fallback if field can't be resolved or if an inlined string value is used */
173             private fun findPermissionField(codebase: Codebase, value: Any): FieldItem? {
174                 val perm = value.toString()
175                 val permClass = codebase.findClass("android.Manifest.permission")
176                 permClass?.fields()?.filter {
177                     it.initialValue(requireConstant = false)?.toString() == perm
178                 }?.forEach { return it }
179                 return null
180             }
181 
182             private fun handleAnnotation(
183                 annotation: AnnotationItem,
184                 item: Item,
185                 depth: Int
186             ) {
187                 val name = annotation.qualifiedName()
188                 if (name == null || name.startsWith(JAVA_LANG_PREFIX)) {
189                     // Ignore java.lang.Retention etc.
190                     return
191                 }
192 
193                 if (item is ClassItem && name == item.qualifiedName()) {
194                     // The annotation annotates itself; we shouldn't attempt to recursively
195                     // pull in documentation from it; the documentation is already complete.
196                     return
197                 }
198 
199                 // Some annotations include the documentation they want inlined into usage docs.
200                 // Copy those here:
201 
202                 handleInliningDocs(annotation, item)
203 
204                 when (name) {
205                     "androidx.annotation.RequiresPermission" -> handleRequiresPermission(annotation, item)
206                     "androidx.annotation.IntRange",
207                     "androidx.annotation.FloatRange" -> handleRange(annotation, item)
208                     "androidx.annotation.IntDef",
209                     "androidx.annotation.LongDef",
210                     "androidx.annotation.StringDef" -> handleTypeDef(annotation, item)
211                     "android.annotation.RequiresFeature" -> handleRequiresFeature(annotation, item)
212                     "androidx.annotation.RequiresApi" -> handleRequiresApi(annotation, item)
213                     "android.provider.Column" -> handleColumn(annotation, item)
214                     "kotlin.Deprecated" -> handleKotlinDeprecation(annotation, item)
215                 }
216 
217                 // Thread annotations are ignored here because they're handled as a group afterwards
218 
219                 // TODO: Resource type annotations
220 
221                 // Handle inner annotations
222                 annotation.resolve()?.modifiers?.annotations()?.forEach { nested ->
223                     if (depth == 20) { // Temp debugging
224                         throw StackOverflowError(
225                             "Unbounded recursion, processing annotation " +
226                                 "${annotation.toSource()} in $item in ${item.compilationUnit()} "
227                         )
228                     } else if (nested.qualifiedName() != annotation.qualifiedName()) {
229                         handleAnnotation(nested, item, depth + 1)
230                     }
231                 }
232             }
233 
234             private fun handleKotlinDeprecation(annotation: AnnotationItem, item: Item) {
235                 val text = (annotation.findAttribute("message") ?: annotation.findAttribute(ATTR_VALUE))
236                     ?.value?.value()?.toString() ?: return
237                 if (text.isBlank() || item.documentation.contains(text)) {
238                     return
239                 }
240 
241                 item.appendDocumentation(text, "@deprecated")
242             }
243 
244             private fun handleInliningDocs(
245                 annotation: AnnotationItem,
246                 item: Item
247             ) {
248                 if (annotation.isNullable() || annotation.isNonNull()) {
249                     // Some docs already specifically talk about null policy; in that case,
250                     // don't include the docs (since it may conflict with more specific conditions
251                     // outlined in the docs).
252                     if (item.documentation.contains("null") &&
253                         mentionsNull.matcher(item.documentation).find()
254                     ) {
255                         return
256                     }
257                 }
258 
259                 when (item) {
260                     is FieldItem -> {
261                         addDoc(annotation, "memberDoc", item)
262                     }
263                     is MethodItem -> {
264                         addDoc(annotation, "memberDoc", item)
265                         addDoc(annotation, "returnDoc", item)
266                     }
267                     is ParameterItem -> {
268                         addDoc(annotation, "paramDoc", item)
269                     }
270                     is ClassItem -> {
271                         addDoc(annotation, "classDoc", item)
272                     }
273                 }
274             }
275 
276             private fun handleRequiresPermission(
277                 annotation: AnnotationItem,
278                 item: Item
279             ) {
280                 if (item !is MemberItem) {
281                     return
282                 }
283                 var values: List<AnnotationAttributeValue>? = null
284                 var any = false
285                 var conditional = false
286                 for (attribute in annotation.attributes()) {
287                     when (attribute.name) {
288                         "value", "allOf" -> {
289                             values = attribute.leafValues()
290                         }
291                         "anyOf" -> {
292                             any = true
293                             values = attribute.leafValues()
294                         }
295                         "conditional" -> {
296                             conditional = attribute.value.value() == true
297                         }
298                     }
299                 }
300 
301                 if (values != null && values.isNotEmpty() && !conditional) {
302                     // Look at macros_override.cs for the usage of these
303                     // tags. In particular, search for def:dump_permission
304 
305                     val sb = StringBuilder(100)
306                     sb.append("Requires ")
307                     var first = true
308                     for (value in values) {
309                         when {
310                             first -> first = false
311                             any -> sb.append(" or ")
312                             else -> sb.append(" and ")
313                         }
314 
315                         val resolved = value.resolve()
316                         val field = if (resolved is FieldItem)
317                             resolved
318                         else {
319                             val v: Any = value.value() ?: value.toSource()
320                             if (v == CARRIER_PRIVILEGES_MARKER) {
321                                 // TODO: Warn if using allOf with carrier
322                                 sb.append("{@link android.telephony.TelephonyManager#hasCarrierPrivileges carrier privileges}")
323                                 continue
324                             }
325                             findPermissionField(codebase, v)
326                         }
327                         if (field == null) {
328                             val v = value.value()?.toString() ?: value.toSource()
329                             if (editDistance(CARRIER_PRIVILEGES_MARKER, v, 3) < 3) {
330                                 reporter.report(
331                                     Issues.MISSING_PERMISSION, item,
332                                     "Unrecognized permission `$v`; did you mean `$CARRIER_PRIVILEGES_MARKER`?"
333                                 )
334                             } else {
335                                 reporter.report(
336                                     Issues.MISSING_PERMISSION, item,
337                                     "Cannot find permission field for $value required by $item (may be hidden or removed)"
338                                 )
339                             }
340                             sb.append(value.toSource())
341                         } else {
342                             if (filterReference.test(field)) {
343                                 sb.append("{@link ${field.containingClass().qualifiedName()}#${field.name()}}")
344                             } else {
345                                 reporter.report(
346                                     Issues.MISSING_PERMISSION, item,
347                                     "Permission $value required by $item is hidden or removed"
348                                 )
349                                 sb.append("${field.containingClass().qualifiedName()}.${field.name()}")
350                             }
351                         }
352                     }
353 
354                     appendDocumentation(sb.toString(), item, false)
355                 }
356             }
357 
358             private fun handleRange(
359                 annotation: AnnotationItem,
360                 item: Item
361             ) {
362                 val from: String? = annotation.findAttribute("from")?.value?.toSource()
363                 val to: String? = annotation.findAttribute("to")?.value?.toSource()
364                 // TODO: inclusive/exclusive attributes on FloatRange!
365                 if (from != null || to != null) {
366                     val args = HashMap<String, String>()
367                     if (from != null) args["from"] = from
368                     if (from != null) args["from"] = from
369                     if (to != null) args["to"] = to
370                     val doc = if (from != null && to != null) {
371                         "Value is between $from and $to inclusive"
372                     } else if (from != null) {
373                         "Value is $from or greater"
374                     } else if (to != null) {
375                         "Value is $to or less"
376                     } else {
377                         null
378                     }
379                     appendDocumentation(doc, item, true)
380                 }
381             }
382 
383             private fun handleTypeDef(
384                 annotation: AnnotationItem,
385                 item: Item
386             ) {
387                 val values = annotation.findAttribute("value")?.leafValues() ?: return
388                 val flag = annotation.findAttribute("flag")?.value?.toSource() == "true"
389 
390                 // Look at macros_override.cs for the usage of these
391                 // tags. In particular, search for def:dump_int_def
392 
393                 val sb = StringBuilder(100)
394                 sb.append("Value is ")
395                 if (flag) {
396                     sb.append("either <code>0</code> or ")
397                     if (values.size > 1) {
398                         sb.append("a combination of ")
399                     }
400                 }
401 
402                 values.forEachIndexed { index, value ->
403                     sb.append(
404                         when (index) {
405                             0 -> {
406                                 ""
407                             }
408                             values.size - 1 -> {
409                                 if (flag) {
410                                     ", and "
411                                 } else {
412                                     ", or "
413                                 }
414                             }
415                             else -> {
416                                 ", "
417                             }
418                         }
419                     )
420 
421                     val field = value.resolve()
422                     if (field is FieldItem)
423                         if (filterReference.test(field)) {
424                             sb.append("{@link ${field.containingClass().qualifiedName()}#${field.name()}}")
425                         } else {
426                             // Typdef annotation references field which isn't part of the API: don't
427                             // try to link to it.
428                             reporter.report(
429                                 Issues.HIDDEN_TYPEDEF_CONSTANT, item,
430                                 "Typedef references constant which isn't part of the API, skipping in documentation: " +
431                                     "${field.containingClass().qualifiedName()}#${field.name()}"
432                             )
433                             sb.append(field.containingClass().qualifiedName() + "." + field.name())
434                         }
435                     else {
436                         sb.append(value.toSource())
437                     }
438                 }
439                 appendDocumentation(sb.toString(), item, true)
440             }
441 
442             private fun handleRequiresFeature(
443                 annotation: AnnotationItem,
444                 item: Item
445             ) {
446                 val value = annotation.findAttribute("value")?.leafValues()?.firstOrNull() ?: return
447                 val sb = StringBuilder(100)
448                 val resolved = value.resolve()
449                 val field = resolved as? FieldItem
450                 sb.append("Requires the ")
451                 if (field == null) {
452                     reporter.report(
453                         Issues.MISSING_PERMISSION, item,
454                         "Cannot find feature field for $value required by $item (may be hidden or removed)"
455                     )
456                     sb.append("{@link ${value.toSource()}}")
457                 } else {
458                     if (filterReference.test(field)) {
459                         sb.append("{@link ${field.containingClass().qualifiedName()}#${field.name()} ${field.containingClass().simpleName()}#${field.name()}} ")
460                     } else {
461                         reporter.report(
462                             Issues.MISSING_PERMISSION, item,
463                             "Feature field $value required by $item is hidden or removed"
464                         )
465                         sb.append("${field.containingClass().simpleName()}#${field.name()} ")
466                     }
467                 }
468 
469                 sb.append("feature which can be detected using ")
470                 sb.append("{@link android.content.pm.PackageManager#hasSystemFeature(String) ")
471                 sb.append("PackageManager.hasSystemFeature(String)}.")
472                 appendDocumentation(sb.toString(), item, false)
473             }
474 
475             private fun handleRequiresApi(
476                 annotation: AnnotationItem,
477                 item: Item
478             ) {
479                 val level = run {
480                     val api = annotation.findAttribute("api")?.leafValues()?.firstOrNull()?.value()
481                     if (api == null || api == 1) {
482                         annotation.findAttribute("value")?.leafValues()?.firstOrNull()?.value() ?: return
483                     } else {
484                         api
485                     }
486                 }
487 
488                 if (level is Int) {
489                     addApiLevelDocumentation(level, item)
490                 }
491             }
492 
493             private fun handleColumn(
494                 annotation: AnnotationItem,
495                 item: Item
496             ) {
497                 val value = annotation.findAttribute("value")?.leafValues()?.firstOrNull() ?: return
498                 val readOnly = annotation.findAttribute("readOnly")?.leafValues()?.firstOrNull()?.value() == true
499                 val sb = StringBuilder(100)
500                 val resolved = value.resolve()
501                 val field = resolved as? FieldItem
502                 sb.append("This constant represents a column name that can be used with a ")
503                 sb.append("{@link android.content.ContentProvider}")
504                 sb.append(" through a ")
505                 sb.append("{@link android.content.ContentValues}")
506                 sb.append(" or ")
507                 sb.append("{@link android.database.Cursor}")
508                 sb.append(" object. The values stored in this column are ")
509                 sb.append("")
510                 if (field == null) {
511                     reporter.report(
512                         Issues.MISSING_COLUMN, item,
513                         "Cannot find feature field for $value required by $item (may be hidden or removed)"
514                     )
515                     sb.append("{@link ${value.toSource()}}")
516                 } else {
517                     if (filterReference.test(field)) {
518                         sb.append("{@link ${field.containingClass().qualifiedName()}#${field.name()} ${field.containingClass().simpleName()}#${field.name()}} ")
519                     } else {
520                         reporter.report(
521                             Issues.MISSING_COLUMN, item,
522                             "Feature field $value required by $item is hidden or removed"
523                         )
524                         sb.append("${field.containingClass().simpleName()}#${field.name()} ")
525                     }
526                 }
527 
528                 if (readOnly) {
529                     sb.append(", and are read-only and cannot be mutated")
530                 }
531                 sb.append(".")
532                 appendDocumentation(sb.toString(), item, false)
533             }
534         })
535     }
536 
537     /**
538      * Appends the given documentation to the given item.
539      * If it's documentation on a parameter, it is redirected to the surrounding method's
540      * documentation.
541      *
542      * If the [returnValue] flag is true, the documentation is added to the description text
543      * of the method, otherwise, it is added to the return tag. This lets for example
544      * a threading annotation requirement be listed as part of a method description's
545      * text, and a range annotation be listed as part of the return value description.
546      * */
547     private fun appendDocumentation(doc: String?, item: Item, returnValue: Boolean) {
548         doc ?: return
549 
550         when (item) {
551             is ParameterItem -> item.containingMethod().appendDocumentation(doc, item.name())
552             is MethodItem ->
553                 // Document as part of return annotation, not member doc
554                 item.appendDocumentation(doc, if (returnValue) "@return" else null)
555             else -> item.appendDocumentation(doc)
556         }
557     }
558 
559     private fun addDoc(annotation: AnnotationItem, tag: String, item: Item) {
560         // TODO: Cache: we shouldn't have to keep looking this up over and over
561         // for example for the nullable/non-nullable annotation classes that
562         // are used everywhere!
563         val cls = annotation.resolve() ?: return
564 
565         val documentation = cls.findTagDocumentation(tag)
566         if (documentation != null) {
567             assert(documentation.startsWith("@$tag")) { documentation }
568             // TODO: Insert it in the right place (@return or @param)
569             val section = when {
570                 documentation.startsWith("@returnDoc") -> "@return"
571                 documentation.startsWith("@paramDoc") -> "@param"
572                 documentation.startsWith("@memberDoc") -> null
573                 else -> null
574             }
575 
576             val insert = stripLeadingAsterisks(stripMetaTags(documentation.substring(tag.length + 2)))
577             val qualified = if (containsLinkTags(insert)) {
578                 val original = "/** $insert */"
579                 val qualified = cls.fullyQualifiedDocumentation(original)
580                 if (original != qualified) {
581                     qualified.substring(if (qualified[3] == ' ') 4 else 3, qualified.length - 2)
582                 } else {
583                     original
584                 }
585             } else {
586                 insert
587             }
588 
589             item.appendDocumentation(qualified, section) // 2: @ and space after tag
590         }
591     }
592 
593     private fun stripLeadingAsterisks(s: String): String {
594         if (s.contains("*")) {
595             val sb = StringBuilder(s.length)
596             var strip = true
597             for (c in s) {
598                 if (strip) {
599                     if (c.isWhitespace() || c == '*') {
600                         continue
601                     } else {
602                         strip = false
603                     }
604                 } else {
605                     if (c == '\n') {
606                         strip = true
607                     }
608                 }
609                 sb.append(c)
610             }
611             return sb.toString()
612         }
613 
614         return s
615     }
616 
617     private fun stripMetaTags(string: String): String {
618         // Get rid of @hide and @remove tags etc that are part of documentation snippets
619         // we pull in, such that we don't accidentally start applying this to the
620         // item that is pulling in the documentation.
621         if (string.contains("@hide") || string.contains("@remove")) {
622             return string.replace("@hide", "").replace("@remove", "")
623         }
624         return string
625     }
626 
627     /** Replacements to perform in documentation */
628     val typos = mapOf(
629         "JetPack" to "Jetpack",
630         "Andriod" to "Android",
631         "Kitkat" to "KitKat",
632         "LemonMeringuePie" to "Lollipop",
633         "LMP" to "Lollipop",
634         "KeyLimePie" to "KitKat",
635         "KLP" to "KitKat",
636         "teh" to "the"
637     )
638 
639     private fun tweakGrammar() {
640         codebase.accept(object : VisibleItemVisitor() {
641             override fun visitItem(item: Item) {
642                 var doc = item.documentation
643                 if (doc.isBlank()) {
644                     return
645                 }
646 
647                 if (!reporter.isSuppressed(Issues.TYPO)) {
648                     for (typo in typos.keys) {
649                         if (doc.contains(typo)) {
650                             val replacement = typos[typo] ?: continue
651                             val new = doc.replace(Regex("\\b$typo\\b"), replacement)
652                             if (new != doc) {
653                                 reporter.report(
654                                     Issues.TYPO,
655                                     item,
656                                     "Replaced $typo with $replacement in the documentation for $item"
657                                 )
658                                 doc = new
659                                 item.documentation = doc
660                             }
661                         }
662                     }
663                 }
664 
665                 // Work around javadoc cutting off the summary line after the first ". ".
666                 val firstDot = doc.indexOf(".")
667                 if (firstDot > 0 && doc.regionMatches(firstDot - 1, "e.g. ", 0, 5, false)) {
668                     doc = doc.substring(0, firstDot) + ".g.&nbsp;" + doc.substring(firstDot + 4)
669                     item.documentation = doc
670                 }
671             }
672         })
673     }
674 
675     fun applyApiLevels(applyApiLevelsXml: File) {
676         @Suppress("DEPRECATION") // still using older lint-api when building with soong
677         val client = object : LintCliClient() {
678             override fun findResource(relativePath: String): File? {
679                 if (relativePath == ApiLookup.XML_FILE_PATH) {
680                     return applyApiLevelsXml
681                 }
682                 return super.findResource(relativePath)
683             }
684 
685             override fun getSdk(): AndroidSdkHandler? {
686                 return null
687             }
688 
689             override fun getCacheDir(name: String?, create: Boolean): File? {
690                 if (create && isUnderTest()) {
691                     // Pick unique directory during unit tests
692                     return Files.createTempDir()
693                 }
694 
695                 val sb = StringBuilder(PROGRAM_NAME)
696                 if (name != null) {
697                     sb.append(File.separator)
698                     sb.append(name)
699                 }
700                 val relative = sb.toString()
701 
702                 val tmp = System.getenv("TMPDIR")
703                 if (tmp != null) {
704                     // Android Build environment: Make sure we're really creating a unique
705                     // temp directory each time since builds could be running in
706                     // parallel here.
707                     val dir = File(tmp, relative)
708                     if (!dir.isDirectory) {
709                         dir.mkdirs()
710                     }
711 
712                     return java.nio.file.Files.createTempDirectory(dir.toPath(), null).toFile()
713                 }
714 
715                 val dir = File(System.getProperty("java.io.tmpdir"), relative)
716                 if (create && !dir.isDirectory) {
717                     dir.mkdirs()
718                 }
719                 return dir
720             }
721         }
722 
723         val apiLookup = ApiLookup.get(client)
724 
725         val pkgApi = HashMap<PackageItem, Int?>(300)
726         codebase.accept(object : ApiVisitor(visitConstructorsAsMethods = true) {
727             override fun visitMethod(method: MethodItem) {
728                 val psiMethod = method.psi() as? PsiMethod ?: return
729                 addApiLevelDocumentation(apiLookup.getMethodVersion(psiMethod), method)
730                 addDeprecatedDocumentation(apiLookup.getMethodDeprecatedIn(psiMethod), method)
731             }
732 
733             override fun visitClass(cls: ClassItem) {
734                 val psiClass = cls.psi() as PsiClass
735                 val since = apiLookup.getClassVersion(psiClass)
736                 if (since != -1) {
737                     addApiLevelDocumentation(since, cls)
738 
739                     // Compute since version for the package: it's the min of all the classes in the package
740                     val pkg = cls.containingPackage()
741                     pkgApi[pkg] = Math.min(pkgApi[pkg] ?: Integer.MAX_VALUE, since)
742                 }
743                 addDeprecatedDocumentation(apiLookup.getClassDeprecatedIn(psiClass), cls)
744             }
745 
746             override fun visitField(field: FieldItem) {
747                 val psiField = field.psi() as PsiField
748                 addApiLevelDocumentation(apiLookup.getFieldVersion(psiField), field)
749                 addDeprecatedDocumentation(apiLookup.getFieldDeprecatedIn(psiField), field)
750             }
751         })
752 
753         val packageDocs = codebase.getPackageDocs()
754         if (packageDocs != null) {
755             for ((pkg, api) in pkgApi.entries) {
756                 val code = api ?: 1
757                 addApiLevelDocumentation(code, pkg)
758             }
759         }
760     }
761 
762     private fun addApiLevelDocumentation(level: Int, item: Item) {
763         if (level > 0) {
764             if (item.originallyHidden) {
765                 // @SystemApi, @TestApi etc -- don't apply API levels here since we don't have accurate historical data
766                 return
767             }
768             val currentCodeName = options.currentCodeName
769             val code: String = if (currentCodeName != null && level > options.currentApiLevel) {
770                 currentCodeName
771             } else {
772                 level.toString()
773             }
774 
775             @Suppress("ConstantConditionIf")
776             if (ADD_API_LEVEL_TEXT) { // See 113933920: Remove "Requires API level" from method comment
777                 val description = if (code == currentCodeName) currentCodeName else describeApiLevel(level)
778                 appendDocumentation("Requires API level $description", item, false)
779             }
780             // Also add @since tag, unless already manually entered.
781             // TODO: Override it everywhere in case the existing doc is wrong (we know
782             // better), and at least for OpenJDK sources we *should* since the since tags
783             // are talking about language levels rather than API levels!
784             if (!item.documentation.contains("@apiSince")) {
785                 item.appendDocumentation(code, "@apiSince")
786             } else {
787                 reporter.report(
788                     Issues.FORBIDDEN_TAG, item, "Documentation should not specify @apiSince " +
789                         "manually; it's computed and injected at build time by $PROGRAM_NAME"
790                 )
791             }
792         }
793     }
794 
795     private fun addDeprecatedDocumentation(level: Int, item: Item) {
796         if (level > 0) {
797             if (item.originallyHidden) {
798                 // @SystemApi, @TestApi etc -- don't apply API levels here since we don't have accurate historical data
799                 return
800             }
801             val currentCodeName = options.currentCodeName
802             val code: String = if (currentCodeName != null && level > options.currentApiLevel) {
803                 currentCodeName
804             } else {
805                 level.toString()
806             }
807 
808             @Suppress("ConstantConditionIf")
809             if (ADD_DEPRECATED_IN_TEXT) {
810                 // TODO: *pre*pend instead!
811                 val description =
812                     "<p class=\"caution\"><strong>This class was deprecated in API level $code.</strong></p>"
813                 item.appendDocumentation(description, "@deprecated", append = false)
814             }
815 
816             if (!item.documentation.contains("@deprecatedSince")) {
817                 item.appendDocumentation(code, "@deprecatedSince")
818             } else {
819                 reporter.report(
820                     Issues.FORBIDDEN_TAG, item, "Documentation should not specify @deprecatedSince " +
821                         "manually; it's computed and injected at build time by $PROGRAM_NAME"
822                 )
823             }
824         }
825     }
826 
827     private fun describeApiLevel(level: Int): String {
828         return "$level (Android ${SdkVersionInfo.getVersionString(level)}, ${SdkVersionInfo.getCodeName(level)})"
829     }
830 }
831 
getClassVersionnull832 fun ApiLookup.getClassVersion(cls: PsiClass): Int {
833     val owner = cls.qualifiedName ?: return -1
834     return getClassVersion(owner)
835 }
836 
837 val defaultEvaluator = DefaultJavaEvaluator(null, null)
838 
ApiLookupnull839 fun ApiLookup.getMethodVersion(method: PsiMethod): Int {
840     val containingClass = method.containingClass ?: return -1
841     val owner = containingClass.qualifiedName ?: return -1
842     val desc = defaultEvaluator.getMethodDescription(method, false, false)
843     return getMethodVersion(owner, if (method.isConstructor) "<init>" else method.name, desc)
844 }
845 
ApiLookupnull846 fun ApiLookup.getFieldVersion(field: PsiField): Int {
847     val containingClass = field.containingClass ?: return -1
848     val owner = containingClass.qualifiedName ?: return -1
849     return getFieldVersion(owner, field.name)
850 }
851 
ApiLookupnull852 fun ApiLookup.getClassDeprecatedIn(cls: PsiClass): Int {
853     val owner = cls.qualifiedName ?: return -1
854     return getClassDeprecatedIn(owner)
855 }
856 
ApiLookupnull857 fun ApiLookup.getMethodDeprecatedIn(method: PsiMethod): Int {
858     val containingClass = method.containingClass ?: return -1
859     val owner = containingClass.qualifiedName ?: return -1
860     val desc = defaultEvaluator.getMethodDescription(method, false, false)
861     return getMethodDeprecatedIn(owner, method.name, desc)
862 }
863 
ApiLookupnull864 fun ApiLookup.getFieldDeprecatedIn(field: PsiField): Int {
865     val containingClass = field.containingClass ?: return -1
866     val owner = containingClass.qualifiedName ?: return -1
867     return getFieldDeprecatedIn(owner, field.name)
868 }
869