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