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