• 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.tools.lint.LintCliClient
6 import com.android.tools.lint.checks.ApiLookup
7 import com.android.tools.lint.detector.api.editDistance
8 import com.android.tools.lint.helpers.DefaultJavaEvaluator
9 import com.android.tools.metalava.model.AnnotationAttributeValue
10 import com.android.tools.metalava.model.AnnotationItem
11 import com.android.tools.metalava.model.ClassItem
12 import com.android.tools.metalava.model.Codebase
13 import com.android.tools.metalava.model.FieldItem
14 import com.android.tools.metalava.model.Item
15 import com.android.tools.metalava.model.MemberItem
16 import com.android.tools.metalava.model.MethodItem
17 import com.android.tools.metalava.model.PackageItem
18 import com.android.tools.metalava.model.ParameterItem
19 import com.android.tools.metalava.model.psi.containsLinkTags
20 import com.android.tools.metalava.model.visitors.ApiVisitor
21 import com.google.common.io.Files
22 import com.intellij.psi.PsiClass
23 import com.intellij.psi.PsiField
24 import com.intellij.psi.PsiMethod
25 import java.io.File
26 import java.util.HashMap
27 import java.util.regex.Pattern
28 import kotlin.math.min
29 
30 /**
31  * Whether to include textual descriptions of the API requirements instead
32  * of just inserting a since-tag. This should be off if there is post-processing
33  * to convert since tags in the documentation tool used.
34  */
35 const val ADD_API_LEVEL_TEXT = false
36 const val ADD_DEPRECATED_IN_TEXT = false
37 
38 /**
39  * Walk over the API and apply tweaks to the documentation, such as
40  *     - Looking for annotations and converting them to auxiliary tags
41  *       that will be processed by the documentation tools later.
42  *     - Reading lint's API database and inserting metadata into
43  *       the documentation like api levels and deprecation levels.
44  *     - Transferring docs from hidden super methods.
45  *     - Performing tweaks for common documentation mistakes, such as
46  *       ending the first sentence with ", e.g. " where javadoc will sadly
47  *       see the ". " and think "aha, that's the end of the sentence!"
48  *       (It works around this by replacing the space with &nbsp;.)
49  *       This will also attempt to fix common typos (Andriod->Android etc).
50  */
51 class DocAnalyzer(
52     /** The codebase to analyze */
53     private val codebase: Codebase
54 ) {
55 
56     /** Computes the visible part of the API from all the available code in the codebase */
57     fun enhance() {
58         // Apply options for packages that should be hidden
59         documentsFromAnnotations()
60 
61         tweakGrammar()
62 
63         for (docReplacement in options.docReplacements) {
64             codebase.accept(docReplacement)
65         }
66 
67         injectArtifactIds()
68 
69         // TODO:
70         // insertMissingDocFromHiddenSuperclasses()
71     }
72 
73     private fun injectArtifactIds() {
74         val artifacts = options.artifactRegistrations
75         if (!artifacts.any()) {
76             return
77         }
78 
79         artifacts.tag(codebase)
80 
81         codebase.accept(object : ApiVisitor() {
82             override fun visitClass(cls: ClassItem) {
83                 cls.artifact?.let {
84                     cls.appendDocumentation(it, "@artifactId")
85                 }
86             }
87         })
88     }
89 
90     val mentionsNull: Pattern = Pattern.compile("\\bnull\\b")
91 
92     /** Hide packages explicitly listed in [Options.hidePackages] */
93     private fun documentsFromAnnotations() {
94         // Note: Doclava1 inserts its own javadoc parameters into the documentation,
95         // which is then later processed by javadoc to insert actual descriptions.
96         // This indirection makes the actual descriptions of the annotations more
97         // configurable from a separate file -- but since this tool isn't hooked
98         // into javadoc anymore (and is going to be used by for example Dokka too)
99         // instead metalava will generate the descriptions directly in-line into the
100         // docs.
101         //
102         // This does mean that you have to update the metalava source code to update
103         // the docs -- but on the other hand all the other docs in the documentation
104         // set also requires updating framework source code, so this doesn't seem
105         // like an unreasonable burden.
106 
107         codebase.accept(object : ApiVisitor() {
108             override fun visitItem(item: Item) {
109                 val annotations = item.modifiers.annotations()
110                 if (annotations.isEmpty()) {
111                     return
112                 }
113 
114                 for (annotation in annotations) {
115                     handleAnnotation(annotation, item, depth = 0)
116                 }
117 
118                 /* Handled via @memberDoc/@classDoc on the annotations themselves right now.
119                    That doesn't handle combinations of multiple thread annotations, but those
120                    don't occur yet, right?
121                 // Threading annotations: can't look at them one at a time; need to list them
122                 // all together
123                 if (item is ClassItem || item is MethodItem) {
124                     val threads = findThreadAnnotations(annotations)
125                     threads?.let {
126                         val threadList = it.joinToString(separator = " or ") +
127                                 (if (it.size == 1) " thread" else " threads")
128                         val doc = if (item is ClassItem) {
129                             "All methods in this class must be invoked on the $threadList, unless otherwise noted"
130                         } else {
131                             assert(item is MethodItem)
132                             "This method must be invoked on the $threadList"
133                         }
134                         appendDocumentation(doc, item, false)
135                     }
136                 }
137                 */
138                 if (findThreadAnnotations(annotations).size > 1) {
139                     reporter.report(
140                         Issues.MULTIPLE_THREAD_ANNOTATIONS,
141                         item,
142                         "Found more than one threading annotation on $item; " +
143                             "the auto-doc feature does not handle this correctly"
144                     )
145                 }
146             }
147 
148             private fun findThreadAnnotations(annotations: List<AnnotationItem>): List<String> {
149                 var result: MutableList<String>? = null
150                 for (annotation in annotations) {
151                     val name = annotation.qualifiedName()
152                     if (name != null && name.endsWith("Thread") &&
153                         (name.startsWith(ANDROID_SUPPORT_ANNOTATION_PREFIX) ||
154                             name.startsWith(ANDROIDX_ANNOTATION_PREFIX))
155                     ) {
156                         if (result == null) {
157                             result = mutableListOf()
158                         }
159                         val threadName = if (name.endsWith("UiThread")) {
160                             "UI"
161                         } else {
162                             name.substring(name.lastIndexOf('.') + 1, name.length - "Thread".length)
163                         }
164                         result.add(threadName)
165                     }
166                 }
167                 return result ?: emptyList()
168             }
169 
170             /** Fallback if field can't be resolved or if an inlined string value is used */
171             private fun findPermissionField(codebase: Codebase, value: Any): FieldItem? {
172                 val perm = value.toString()
173                 val permClass = codebase.findClass("android.Manifest.permission")
174                 permClass?.fields()?.filter {
175                     it.initialValue(requireConstant = false)?.toString() == perm
176                 }?.forEach { return it }
177                 return null
178             }
179 
180             private fun handleAnnotation(
181                 annotation: AnnotationItem,
182                 item: Item,
183                 depth: Int,
184                 visitedClasses: MutableSet<String> = mutableSetOf()
185             ) {
186                 val name = annotation.qualifiedName()
187                 if (name == null || name.startsWith(JAVA_LANG_PREFIX)) {
188                     // Ignore java.lang.Retention etc.
189                     return
190                 }
191 
192                 if (item is ClassItem && name == item.qualifiedName()) {
193                     // The annotation annotates itself; we shouldn't attempt to recursively
194                     // pull in documentation from it; the documentation is already complete.
195                     return
196                 }
197 
198                 // Some annotations include the documentation they want inlined into usage docs.
199                 // Copy those here:
200 
201                 handleInliningDocs(annotation, item)
202 
203                 when (name) {
204                     "androidx.annotation.RequiresPermission" -> handleRequiresPermission(annotation, item)
205                     "androidx.annotation.IntRange",
206                     "androidx.annotation.FloatRange" -> handleRange(annotation, item)
207                     "androidx.annotation.IntDef",
208                     "androidx.annotation.LongDef",
209                     "androidx.annotation.StringDef" -> handleTypeDef(annotation, item)
210                     "android.annotation.RequiresFeature" -> handleRequiresFeature(annotation, item)
211                     "androidx.annotation.RequiresApi" -> handleRequiresApi(annotation, item)
212                     "android.provider.Column" -> handleColumn(annotation, item)
213                     "kotlin.Deprecated" -> handleKotlinDeprecation(annotation, item)
214                 }
215 
216                 visitedClasses.add(name)
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() !in visitedClasses) {
229                         handleAnnotation(nested, item, depth + 1, visitedClasses)
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                             // Typedef 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                     insert
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     @Suppress("SpellCheckingInspection")
629     val typos = mapOf(
630         "JetPack" to "Jetpack",
631         "Andriod" to "Android",
632         "Kitkat" to "KitKat",
633         "LemonMeringuePie" to "Lollipop",
634         "LMP" to "Lollipop",
635         "KeyLimePie" to "KitKat",
636         "KLP" to "KitKat",
637         "teh" to "the"
638     )
639 
640     private fun tweakGrammar() {
641         codebase.accept(object : ApiVisitor() {
642             override fun visitItem(item: Item) {
643                 var doc = item.documentation
644                 if (doc.isBlank()) {
645                     return
646                 }
647 
648                 if (!reporter.isSuppressed(Issues.TYPO)) {
649                     for (typo in typos.keys) {
650                         if (doc.contains(typo)) {
651                             val replacement = typos[typo] ?: continue
652                             val new = doc.replace(Regex("\\b$typo\\b"), replacement)
653                             if (new != doc) {
654                                 reporter.report(
655                                     Issues.TYPO,
656                                     item,
657                                     "Replaced $typo with $replacement in the documentation for $item"
658                                 )
659                                 doc = new
660                                 item.documentation = doc
661                             }
662                         }
663                     }
664                 }
665 
666                 // Work around javadoc cutting off the summary line after the first ". ".
667                 val firstDot = doc.indexOf(".")
668                 if (firstDot > 0 && doc.regionMatches(firstDot - 1, "e.g. ", 0, 5, false)) {
669                     doc = doc.substring(0, firstDot) + ".g.&nbsp;" + doc.substring(firstDot + 4)
670                     item.documentation = doc
671                 }
672             }
673         })
674     }
675 
676     fun applyApiLevels(applyApiLevelsXml: File) {
677         @Suppress("DEPRECATION") // still using older lint-api when building with soong
678         val client = object : LintCliClient() {
679             override fun findResource(relativePath: String): File? {
680                 if (relativePath == ApiLookup.XML_FILE_PATH) {
681                     return applyApiLevelsXml
682                 }
683                 return super.findResource(relativePath)
684             }
685 
686             override fun getCacheDir(name: String?, create: Boolean): File? {
687                 if (create && isUnderTest()) {
688                     // Pick unique directory during unit tests
689                     return Files.createTempDir()
690                 }
691 
692                 val sb = StringBuilder(PROGRAM_NAME)
693                 if (name != null) {
694                     sb.append(File.separator)
695                     sb.append(name)
696                 }
697                 val relative = sb.toString()
698 
699                 val tmp = System.getenv("TMPDIR")
700                 if (tmp != null) {
701                     // Android Build environment: Make sure we're really creating a unique
702                     // temp directory each time since builds could be running in
703                     // parallel here.
704                     val dir = File(tmp, relative)
705                     if (!dir.isDirectory) {
706                         dir.mkdirs()
707                     }
708 
709                     return java.nio.file.Files.createTempDirectory(dir.toPath(), null).toFile()
710                 }
711 
712                 val dir = File(System.getProperty("java.io.tmpdir"), relative)
713                 if (create && !dir.isDirectory) {
714                     dir.mkdirs()
715                 }
716                 return dir
717             }
718         }
719 
720         val apiLookup = ApiLookup.get(client)
721 
722         val pkgApi = HashMap<PackageItem, Int?>(300)
723         codebase.accept(object : ApiVisitor(visitConstructorsAsMethods = true) {
724             override fun visitMethod(method: MethodItem) {
725                 val psiMethod = method.psi() as? PsiMethod ?: return
726                 addApiLevelDocumentation(apiLookup.getMethodVersion(psiMethod), method)
727                 addDeprecatedDocumentation(apiLookup.getMethodDeprecatedIn(psiMethod), method)
728             }
729 
730             override fun visitClass(cls: ClassItem) {
731                 val psiClass = cls.psi() as PsiClass
732                 val since = apiLookup.getClassVersion(psiClass)
733                 if (since != -1) {
734                     addApiLevelDocumentation(since, cls)
735 
736                     // Compute since version for the package: it's the min of all the classes in the package
737                     val pkg = cls.containingPackage()
738                     pkgApi[pkg] = min(pkgApi[pkg] ?: Integer.MAX_VALUE, since)
739                 }
740                 addDeprecatedDocumentation(apiLookup.getClassDeprecatedIn(psiClass), cls)
741             }
742 
743             override fun visitField(field: FieldItem) {
744                 val psiField = field.psi() as PsiField
745                 addApiLevelDocumentation(apiLookup.getFieldVersion(psiField), field)
746                 addDeprecatedDocumentation(apiLookup.getFieldDeprecatedIn(psiField), field)
747             }
748         })
749 
750         val packageDocs = codebase.getPackageDocs()
751         if (packageDocs != null) {
752             for ((pkg, api) in pkgApi.entries) {
753                 val code = api ?: 1
754                 addApiLevelDocumentation(code, pkg)
755             }
756         }
757     }
758 
759     private fun addApiLevelDocumentation(level: Int, item: Item) {
760         if (level > 0) {
761             if (item.originallyHidden) {
762                 // @SystemApi, @TestApi etc -- don't apply API levels here since we don't have accurate historical data
763                 return
764             }
765             val currentCodeName = options.currentCodeName
766             val code: String = if (currentCodeName != null && level > options.currentApiLevel) {
767                 currentCodeName
768             } else {
769                 level.toString()
770             }
771 
772             @Suppress("ConstantConditionIf")
773             if (ADD_API_LEVEL_TEXT) { // See 113933920: Remove "Requires API level" from method comment
774                 val description = if (code == currentCodeName) currentCodeName else describeApiLevel(level)
775                 appendDocumentation("Requires API level $description", item, false)
776             }
777             // Also add @since tag, unless already manually entered.
778             // TODO: Override it everywhere in case the existing doc is wrong (we know
779             // better), and at least for OpenJDK sources we *should* since the since tags
780             // are talking about language levels rather than API levels!
781             if (!item.documentation.contains("@apiSince")) {
782                 item.appendDocumentation(code, "@apiSince")
783             } else {
784                 reporter.report(
785                     Issues.FORBIDDEN_TAG, item, "Documentation should not specify @apiSince " +
786                         "manually; it's computed and injected at build time by $PROGRAM_NAME"
787                 )
788             }
789         }
790     }
791 
792     private fun addDeprecatedDocumentation(level: Int, item: Item) {
793         if (level > 0) {
794             if (item.originallyHidden) {
795                 // @SystemApi, @TestApi etc -- don't apply API levels here since we don't have accurate historical data
796                 return
797             }
798             val currentCodeName = options.currentCodeName
799             val code: String = if (currentCodeName != null && level > options.currentApiLevel) {
800                 currentCodeName
801             } else {
802                 level.toString()
803             }
804 
805             @Suppress("ConstantConditionIf")
806             if (ADD_DEPRECATED_IN_TEXT) {
807                 // TODO: *pre*pend instead!
808                 val description =
809                     "<p class=\"caution\"><strong>This class was deprecated in API level $code.</strong></p>"
810                 item.appendDocumentation(description, "@deprecated", append = false)
811             }
812 
813             if (!item.documentation.contains("@deprecatedSince")) {
814                 item.appendDocumentation(code, "@deprecatedSince")
815             } else {
816                 reporter.report(
817                     Issues.FORBIDDEN_TAG, item, "Documentation should not specify @deprecatedSince " +
818                         "manually; it's computed and injected at build time by $PROGRAM_NAME"
819                 )
820             }
821         }
822     }
823 
824     private fun describeApiLevel(level: Int): String {
825         return "$level (Android ${SdkVersionInfo.getVersionString(level)}, ${SdkVersionInfo.getCodeName(level)})"
826     }
827 }
828 
getClassVersionnull829 fun ApiLookup.getClassVersion(cls: PsiClass): Int {
830     val owner = cls.qualifiedName ?: return -1
831     return getClassVersion(owner)
832 }
833 
834 val defaultEvaluator = DefaultJavaEvaluator(null, null)
835 
ApiLookupnull836 fun ApiLookup.getMethodVersion(method: PsiMethod): Int {
837     val containingClass = method.containingClass ?: return -1
838     val owner = containingClass.qualifiedName ?: return -1
839     val desc = defaultEvaluator.getMethodDescription(
840         method,
841         includeName = false,
842         includeReturn = false
843     )
844     return getMethodVersion(owner, if (method.isConstructor) "<init>" else method.name, desc)
845 }
846 
ApiLookupnull847 fun ApiLookup.getFieldVersion(field: PsiField): Int {
848     val containingClass = field.containingClass ?: return -1
849     val owner = containingClass.qualifiedName ?: return -1
850     return getFieldVersion(owner, field.name)
851 }
852 
ApiLookupnull853 fun ApiLookup.getClassDeprecatedIn(cls: PsiClass): Int {
854     val owner = cls.qualifiedName ?: return -1
855     return getClassDeprecatedIn(owner)
856 }
857 
ApiLookupnull858 fun ApiLookup.getMethodDeprecatedIn(method: PsiMethod): Int {
859     val containingClass = method.containingClass ?: return -1
860     val owner = containingClass.qualifiedName ?: return -1
861     val desc = defaultEvaluator.getMethodDescription(
862         method,
863         includeName = false,
864         includeReturn = false
865     )
866     return getMethodDeprecatedIn(owner, method.name, desc)
867 }
868 
ApiLookupnull869 fun ApiLookup.getFieldDeprecatedIn(field: PsiField): Int {
870     val containingClass = field.containingClass ?: return -1
871     val owner = containingClass.qualifiedName ?: return -1
872     return getFieldDeprecatedIn(owner, field.name)
873 }
874