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