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