<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.google.common.io.Files
24 import com.intellij.psi.PsiClass
25 import com.intellij.psi.PsiField
26 import com.intellij.psi.PsiMethod
27 import java.io.File
28 import java.util.HashMap
29 import java.util.regex.Pattern
30 import kotlin.math.min
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 : ApiVisitor() {
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                             // 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                     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     @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 getSdk(): AndroidSdkHandler? {
687                 return null
688             }
689 
690             override fun getCacheDir(name: String?, create: Boolean): File? {
691                 if (create && isUnderTest()) {
692                     // Pick unique directory during unit tests
693                     return Files.createTempDir()
694                 }
695 
696                 val sb = StringBuilder(PROGRAM_NAME)
697                 if (name != null) {
698                     sb.append(File.separator)
699                     sb.append(name)
700                 }
701                 val relative = sb.toString()
702 
703                 val tmp = System.getenv("TMPDIR")
704                 if (tmp != null) {
705                     // Android Build environment: Make sure we're really creating a unique
706                     // temp directory each time since builds could be running in
707                     // parallel here.
708                     val dir = File(tmp, relative)
709                     if (!dir.isDirectory) {
710                         dir.mkdirs()
711                     }
712 
713                     return java.nio.file.Files.createTempDirectory(dir.toPath(), null).toFile()
714                 }
715 
716                 val dir = File(System.getProperty("java.io.tmpdir"), relative)
717                 if (create && !dir.isDirectory) {
718                     dir.mkdirs()
719                 }
720                 return dir
721             }
722         }
723 
724         val apiLookup = ApiLookup.get(client)
725 
726         val pkgApi = HashMap<PackageItem, Int?>(300)
727         codebase.accept(object : ApiVisitor(visitConstructorsAsMethods = true) {
728             override fun visitMethod(method: MethodItem) {
729                 val psiMethod = method.psi() as? PsiMethod ?: return
730                 addApiLevelDocumentation(apiLookup.getMethodVersion(psiMethod), method)
731                 addDeprecatedDocumentation(apiLookup.getMethodDeprecatedIn(psiMethod), method)
732             }
733 
734             override fun visitClass(cls: ClassItem) {
735                 val psiClass = cls.psi() as PsiClass
736                 val since = apiLookup.getClassVersion(psiClass)
737                 if (since != -1) {
738                     addApiLevelDocumentation(since, cls)
739 
740                     // Compute since version for the package: it's the min of all the classes in the package
741                     val pkg = cls.containingPackage()
742                     pkgApi[pkg] = min(pkgApi[pkg] ?: Integer.MAX_VALUE, since)
743                 }
744                 addDeprecatedDocumentation(apiLookup.getClassDeprecatedIn(psiClass), cls)
745             }
746 
747             override fun visitField(field: FieldItem) {
748                 val psiField = field.psi() as PsiField
749                 addApiLevelDocumentation(apiLookup.getFieldVersion(psiField), field)
750                 addDeprecatedDocumentation(apiLookup.getFieldDeprecatedIn(psiField), field)
751             }
752         })
753 
754         val packageDocs = codebase.getPackageDocs()
755         if (packageDocs != null) {
756             for ((pkg, api) in pkgApi.entries) {
757                 val code = api ?: 1
758                 addApiLevelDocumentation(code, pkg)
759             }
760         }
761     }
762 
763     private fun addApiLevelDocumentation(level: Int, item: Item) {
764         if (level > 0) {
765             if (item.originallyHidden) {
766                 // @SystemApi, @TestApi etc -- don't apply API levels here since we don't have accurate historical data
767                 return
768             }
769             val currentCodeName = options.currentCodeName
770             val code: String = if (currentCodeName != null && level > options.currentApiLevel) {
771                 currentCodeName
772             } else {
773                 level.toString()
774             }
775 
776             @Suppress("ConstantConditionIf")
777             if (ADD_API_LEVEL_TEXT) { // See 113933920: Remove "Requires API level" from method comment
778                 val description = if (code == currentCodeName) currentCodeName else describeApiLevel(level)
779                 appendDocumentation("Requires API level $description", item, false)
780             }
781             // Also add @since tag, unless already manually entered.
782             // TODO: Override it everywhere in case the existing doc is wrong (we know
783             // better), and at least for OpenJDK sources we *should* since the since tags
784             // are talking about language levels rather than API levels!
785             if (!item.documentation.contains("@apiSince")) {
786                 item.appendDocumentation(code, "@apiSince")
787             } else {
788                 reporter.report(
789                     Issues.FORBIDDEN_TAG, item, "Documentation should not specify @apiSince " +
790                         "manually; it's computed and injected at build time by $PROGRAM_NAME"
791                 )
792             }
793         }
794     }
795 
796     private fun addDeprecatedDocumentation(level: Int, item: Item) {
797         if (level > 0) {
798             if (item.originallyHidden) {
799                 // @SystemApi, @TestApi etc -- don't apply API levels here since we don't have accurate historical data
800                 return
801             }
802             val currentCodeName = options.currentCodeName
803             val code: String = if (currentCodeName != null && level > options.currentApiLevel) {
804                 currentCodeName
805             } else {
806                 level.toString()
807             }
808 
809             @Suppress("ConstantConditionIf")
810             if (ADD_DEPRECATED_IN_TEXT) {
811                 // TODO: *pre*pend instead!
812                 val description =
813                     "<p class=\"caution\"><strong>This class was deprecated in API level $code.</strong></p>"
814                 item.appendDocumentation(description, "@deprecated", append = false)
815             }
816 
817             if (!item.documentation.contains("@deprecatedSince")) {
818                 item.appendDocumentation(code, "@deprecatedSince")
819             } else {
820                 reporter.report(
821                     Issues.FORBIDDEN_TAG, item, "Documentation should not specify @deprecatedSince " +
822                         "manually; it's computed and injected at build time by $PROGRAM_NAME"
823                 )
824             }
825         }
826     }
827 
828     private fun describeApiLevel(level: Int): String {
829         return "$level (Android ${SdkVersionInfo.getVersionString(level)}, ${SdkVersionInfo.getCodeName(level)})"
830     }
831 }
832 
getClassVersionnull833 fun ApiLookup.getClassVersion(cls: PsiClass): Int {
834     val owner = cls.qualifiedName ?: return -1
835     return getClassVersion(owner)
836 }
837 
838 val defaultEvaluator = DefaultJavaEvaluator(null, null)
839 
ApiLookupnull840 fun ApiLookup.getMethodVersion(method: PsiMethod): Int {
841     val containingClass = method.containingClass ?: return -1
842     val owner = containingClass.qualifiedName ?: return -1
843     val desc = defaultEvaluator.getMethodDescription(
844         method,
845         includeName = false,
846         includeReturn = false
847     )
848     return getMethodVersion(owner, if (method.isConstructor) "<init>" else method.name, desc)
849 }
850 
ApiLookupnull851 fun ApiLookup.getFieldVersion(field: PsiField): Int {
852     val containingClass = field.containingClass ?: return -1
853     val owner = containingClass.qualifiedName ?: return -1
854     return getFieldVersion(owner, field.name)
855 }
856 
ApiLookupnull857 fun ApiLookup.getClassDeprecatedIn(cls: PsiClass): Int {
858     val owner = cls.qualifiedName ?: return -1
859     return getClassDeprecatedIn(owner)
860 }
861 
ApiLookupnull862 fun ApiLookup.getMethodDeprecatedIn(method: PsiMethod): Int {
863     val containingClass = method.containingClass ?: return -1
864     val owner = containingClass.qualifiedName ?: return -1
865     val desc = defaultEvaluator.getMethodDescription(
866         method,
867         includeName = false,
868         includeReturn = false
869     )
870     return getMethodDeprecatedIn(owner, method.name, desc)
871 }
872 
ApiLookupnull873 fun ApiLookup.getFieldDeprecatedIn(field: PsiField): Int {
874     val containingClass = field.containingClass ?: return -1
875     val owner = containingClass.qualifiedName ?: return -1
876     return getFieldDeprecatedIn(owner, field.name)
877 }
878