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