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