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