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