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