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 .)
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