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