<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.sdklib.repository.AndroidSdkHandler
6 import com.android.tools.lint.LintCliClient
7 import com.android.tools.lint.checks.ApiLookup
8 import com.android.tools.lint.detector.api.editDistance
9 import com.android.tools.lint.helpers.DefaultJavaEvaluator
10 import com.android.tools.metalava.doclava1.Issues
11 import com.android.tools.metalava.model.AnnotationAttributeValue
12 import com.android.tools.metalava.model.AnnotationItem
13 import com.android.tools.metalava.model.ClassItem
14 import com.android.tools.metalava.model.Codebase
15 import com.android.tools.metalava.model.FieldItem
16 import com.android.tools.metalava.model.Item
17 import com.android.tools.metalava.model.MemberItem
18 import com.android.tools.metalava.model.MethodItem
19 import com.android.tools.metalava.model.PackageItem
20 import com.android.tools.metalava.model.ParameterItem
21 import com.android.tools.metalava.model.psi.containsLinkTags
22 import com.android.tools.metalava.model.visitors.ApiVisitor
23 import com.google.common.io.Files
24 import com.intellij.psi.PsiClass
25 import com.intellij.psi.PsiField
26 import com.intellij.psi.PsiMethod
27 import java.io.File
28 import java.util.HashMap
29 import java.util.regex.Pattern
30 import kotlin.math.min
31
32 /**
33 * Whether to include textual descriptions of the API requirements instead
34 * of just inserting a since-tag. This should be off if there is post-processing
35 * to convert since tags in the documentation tool used.
36 */
37 const val ADD_API_LEVEL_TEXT = false
38 const val ADD_DEPRECATED_IN_TEXT = false
39
40 /**
41 * Walk over the API and apply tweaks to the documentation, such as
42 * - Looking for annotations and converting them to auxiliary tags
43 * that will be processed by the documentation tools later.
44 * - Reading lint's API database and inserting metadata into
45 * the documentation like api levels and deprecation levels.
46 * - Transferring docs from hidden super methods.
47 * - Performing tweaks for common documentation mistakes, such as
48 * ending the first sentence with ", e.g. " where javadoc will sadly
49 * see the ". " and think "aha, that's the end of the sentence!"
50 * (It works around this by replacing the space with .)
51 * This will also attempt to fix common typos (Andriod->Android etc).
52 */
53 class DocAnalyzer(
54 /** The codebase to analyze */
55 private val codebase: Codebase
56 ) {
57
58 /** Computes the visible part of the API from all the available code in the codebase */
59 fun enhance() {
60 // Apply options for packages that should be hidden
61 documentsFromAnnotations()
62
63 tweakGrammar()
64
65 for (docReplacement in options.docReplacements) {
66 codebase.accept(docReplacement)
67 }
68
69 injectArtifactIds()
70
71 // TODO:
72 // insertMissingDocFromHiddenSuperclasses()
73 }
74
75 private fun injectArtifactIds() {
76 val artifacts = options.artifactRegistrations
77 if (!artifacts.any()) {
78 return
79 }
80
81 artifacts.tag(codebase)
82
83 codebase.accept(object : ApiVisitor() {
84 override fun visitClass(cls: ClassItem) {
85 cls.artifact?.let {
86 cls.appendDocumentation(it, "@artifactId")
87 }
88 }
89 })
90 }
91
92 val mentionsNull: Pattern = Pattern.compile("\\bnull\\b")
93
94 /** Hide packages explicitly listed in [Options.hidePackages] */
95 private fun documentsFromAnnotations() {
96 // Note: Doclava1 inserts its own javadoc parameters into the documentation,
97 // which is then later processed by javadoc to insert actual descriptions.
98 // This indirection makes the actual descriptions of the annotations more
99 // configurable from a separate file -- but since this tool isn't hooked
100 // into javadoc anymore (and is going to be used by for example Dokka too)
101 // instead metalava will generate the descriptions directly in-line into the
102 // docs.
103 //
104 // This does mean that you have to update the metalava source code to update
105 // the docs -- but on the other hand all the other docs in the documentation
106 // set also requires updating framework source code, so this doesn't seem
107 // like an unreasonable burden.
108
109 codebase.accept(object : ApiVisitor() {
110 override fun visitItem(item: Item) {
111 val annotations = item.modifiers.annotations()
112 if (annotations.isEmpty()) {
113 return
114 }
115
116 for (annotation in annotations) {
117 handleAnnotation(annotation, item, depth = 0)
118 }
119
120 /* Handled via @memberDoc/@classDoc on the annotations themselves right now.
121 That doesn't handle combinations of multiple thread annotations, but those
122 don't occur yet, right?
123 // Threading annotations: can't look at them one at a time; need to list them
124 // all together
125 if (item is ClassItem || item is MethodItem) {
126 val threads = findThreadAnnotations(annotations)
127 threads?.let {
128 val threadList = it.joinToString(separator = " or ") +
129 (if (it.size == 1) " thread" else " threads")
130 val doc = if (item is ClassItem) {
131 "All methods in this class must be invoked on the $threadList, unless otherwise noted"
132 } else {
133 assert(item is MethodItem)
134 "This method must be invoked on the $threadList"
135 }
136 appendDocumentation(doc, item, false)
137 }
138 }
139 */
140 if (findThreadAnnotations(annotations).size > 1) {
141 reporter.report(
142 Issues.MULTIPLE_THREAD_ANNOTATIONS,
143 item,
144 "Found more than one threading annotation on $item; " +
145 "the auto-doc feature does not handle this correctly"
146 )
147 }
148 }
149
150 private fun findThreadAnnotations(annotations: List<AnnotationItem>): List<String> {
151 var result: MutableList<String>? = null
152 for (annotation in annotations) {
153 val name = annotation.qualifiedName()
154 if (name != null && name.endsWith("Thread") &&
155 (name.startsWith(ANDROID_SUPPORT_ANNOTATION_PREFIX) ||
156 name.startsWith(ANDROIDX_ANNOTATION_PREFIX))
157 ) {
158 if (result == null) {
159 result = mutableListOf()
160 }
161 val threadName = if (name.endsWith("UiThread")) {
162 "UI"
163 } else {
164 name.substring(name.lastIndexOf('.') + 1, name.length - "Thread".length)
165 }
166 result.add(threadName)
167 }
168 }
169 return result ?: emptyList()
170 }
171
172 /** Fallback if field can't be resolved or if an inlined string value is used */
173 private fun findPermissionField(codebase: Codebase, value: Any): FieldItem? {
174 val perm = value.toString()
175 val permClass = codebase.findClass("android.Manifest.permission")
176 permClass?.fields()?.filter {
177 it.initialValue(requireConstant = false)?.toString() == perm
178 }?.forEach { return it }
179 return null
180 }
181
182 private fun handleAnnotation(
183 annotation: AnnotationItem,
184 item: Item,
185 depth: Int
186 ) {
187 val name = annotation.qualifiedName()
188 if (name == null || name.startsWith(JAVA_LANG_PREFIX)) {
189 // Ignore java.lang.Retention etc.
190 return
191 }
192
193 if (item is ClassItem && name == item.qualifiedName()) {
194 // The annotation annotates itself; we shouldn't attempt to recursively
195 // pull in documentation from it; the documentation is already complete.
196 return
197 }
198
199 // Some annotations include the documentation they want inlined into usage docs.
200 // Copy those here:
201
202 handleInliningDocs(annotation, item)
203
204 when (name) {
205 "androidx.annotation.RequiresPermission" -> handleRequiresPermission(annotation, item)
206 "androidx.annotation.IntRange",
207 "androidx.annotation.FloatRange" -> handleRange(annotation, item)
208 "androidx.annotation.IntDef",
209 "androidx.annotation.LongDef",
210 "androidx.annotation.StringDef" -> handleTypeDef(annotation, item)
211 "android.annotation.RequiresFeature" -> handleRequiresFeature(annotation, item)
212 "androidx.annotation.RequiresApi" -> handleRequiresApi(annotation, item)
213 "android.provider.Column" -> handleColumn(annotation, item)
214 "kotlin.Deprecated" -> handleKotlinDeprecation(annotation, item)
215 }
216
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() != annotation.qualifiedName()) {
229 handleAnnotation(nested, item, depth + 1)
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 original
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 getSdk(): AndroidSdkHandler? {
687 return null
688 }
689
690 override fun getCacheDir(name: String?, create: Boolean): File? {
691 if (create && isUnderTest()) {
692 // Pick unique directory during unit tests
693 return Files.createTempDir()
694 }
695
696 val sb = StringBuilder(PROGRAM_NAME)
697 if (name != null) {
698 sb.append(File.separator)
699 sb.append(name)
700 }
701 val relative = sb.toString()
702
703 val tmp = System.getenv("TMPDIR")
704 if (tmp != null) {
705 // Android Build environment: Make sure we're really creating a unique
706 // temp directory each time since builds could be running in
707 // parallel here.
708 val dir = File(tmp, relative)
709 if (!dir.isDirectory) {
710 dir.mkdirs()
711 }
712
713 return java.nio.file.Files.createTempDirectory(dir.toPath(), null).toFile()
714 }
715
716 val dir = File(System.getProperty("java.io.tmpdir"), relative)
717 if (create && !dir.isDirectory) {
718 dir.mkdirs()
719 }
720 return dir
721 }
722 }
723
724 val apiLookup = ApiLookup.get(client)
725
726 val pkgApi = HashMap<PackageItem, Int?>(300)
727 codebase.accept(object : ApiVisitor(visitConstructorsAsMethods = true) {
728 override fun visitMethod(method: MethodItem) {
729 val psiMethod = method.psi() as? PsiMethod ?: return
730 addApiLevelDocumentation(apiLookup.getMethodVersion(psiMethod), method)
731 addDeprecatedDocumentation(apiLookup.getMethodDeprecatedIn(psiMethod), method)
732 }
733
734 override fun visitClass(cls: ClassItem) {
735 val psiClass = cls.psi() as PsiClass
736 val since = apiLookup.getClassVersion(psiClass)
737 if (since != -1) {
738 addApiLevelDocumentation(since, cls)
739
740 // Compute since version for the package: it's the min of all the classes in the package
741 val pkg = cls.containingPackage()
742 pkgApi[pkg] = min(pkgApi[pkg] ?: Integer.MAX_VALUE, since)
743 }
744 addDeprecatedDocumentation(apiLookup.getClassDeprecatedIn(psiClass), cls)
745 }
746
747 override fun visitField(field: FieldItem) {
748 val psiField = field.psi() as PsiField
749 addApiLevelDocumentation(apiLookup.getFieldVersion(psiField), field)
750 addDeprecatedDocumentation(apiLookup.getFieldDeprecatedIn(psiField), field)
751 }
752 })
753
754 val packageDocs = codebase.getPackageDocs()
755 if (packageDocs != null) {
756 for ((pkg, api) in pkgApi.entries) {
757 val code = api ?: 1
758 addApiLevelDocumentation(code, pkg)
759 }
760 }
761 }
762
763 private fun addApiLevelDocumentation(level: Int, item: Item) {
764 if (level > 0) {
765 if (item.originallyHidden) {
766 // @SystemApi, @TestApi etc -- don't apply API levels here since we don't have accurate historical data
767 return
768 }
769 val currentCodeName = options.currentCodeName
770 val code: String = if (currentCodeName != null && level > options.currentApiLevel) {
771 currentCodeName
772 } else {
773 level.toString()
774 }
775
776 @Suppress("ConstantConditionIf")
777 if (ADD_API_LEVEL_TEXT) { // See 113933920: Remove "Requires API level" from method comment
778 val description = if (code == currentCodeName) currentCodeName else describeApiLevel(level)
779 appendDocumentation("Requires API level $description", item, false)
780 }
781 // Also add @since tag, unless already manually entered.
782 // TODO: Override it everywhere in case the existing doc is wrong (we know
783 // better), and at least for OpenJDK sources we *should* since the since tags
784 // are talking about language levels rather than API levels!
785 if (!item.documentation.contains("@apiSince")) {
786 item.appendDocumentation(code, "@apiSince")
787 } else {
788 reporter.report(
789 Issues.FORBIDDEN_TAG, item, "Documentation should not specify @apiSince " +
790 "manually; it's computed and injected at build time by $PROGRAM_NAME"
791 )
792 }
793 }
794 }
795
796 private fun addDeprecatedDocumentation(level: Int, item: Item) {
797 if (level > 0) {
798 if (item.originallyHidden) {
799 // @SystemApi, @TestApi etc -- don't apply API levels here since we don't have accurate historical data
800 return
801 }
802 val currentCodeName = options.currentCodeName
803 val code: String = if (currentCodeName != null && level > options.currentApiLevel) {
804 currentCodeName
805 } else {
806 level.toString()
807 }
808
809 @Suppress("ConstantConditionIf")
810 if (ADD_DEPRECATED_IN_TEXT) {
811 // TODO: *pre*pend instead!
812 val description =
813 "<p class=\"caution\"><strong>This class was deprecated in API level $code.</strong></p>"
814 item.appendDocumentation(description, "@deprecated", append = false)
815 }
816
817 if (!item.documentation.contains("@deprecatedSince")) {
818 item.appendDocumentation(code, "@deprecatedSince")
819 } else {
820 reporter.report(
821 Issues.FORBIDDEN_TAG, item, "Documentation should not specify @deprecatedSince " +
822 "manually; it's computed and injected at build time by $PROGRAM_NAME"
823 )
824 }
825 }
826 }
827
828 private fun describeApiLevel(level: Int): String {
829 return "$level (Android ${SdkVersionInfo.getVersionString(level)}, ${SdkVersionInfo.getCodeName(level)})"
830 }
831 }
832
getClassVersionnull833 fun ApiLookup.getClassVersion(cls: PsiClass): Int {
834 val owner = cls.qualifiedName ?: return -1
835 return getClassVersion(owner)
836 }
837
838 val defaultEvaluator = DefaultJavaEvaluator(null, null)
839
ApiLookupnull840 fun ApiLookup.getMethodVersion(method: PsiMethod): Int {
841 val containingClass = method.containingClass ?: return -1
842 val owner = containingClass.qualifiedName ?: return -1
843 val desc = defaultEvaluator.getMethodDescription(
844 method,
845 includeName = false,
846 includeReturn = false
847 )
848 return getMethodVersion(owner, if (method.isConstructor) "<init>" else method.name, desc)
849 }
850
ApiLookupnull851 fun ApiLookup.getFieldVersion(field: PsiField): Int {
852 val containingClass = field.containingClass ?: return -1
853 val owner = containingClass.qualifiedName ?: return -1
854 return getFieldVersion(owner, field.name)
855 }
856
ApiLookupnull857 fun ApiLookup.getClassDeprecatedIn(cls: PsiClass): Int {
858 val owner = cls.qualifiedName ?: return -1
859 return getClassDeprecatedIn(owner)
860 }
861
ApiLookupnull862 fun ApiLookup.getMethodDeprecatedIn(method: PsiMethod): Int {
863 val containingClass = method.containingClass ?: return -1
864 val owner = containingClass.qualifiedName ?: return -1
865 val desc = defaultEvaluator.getMethodDescription(
866 method,
867 includeName = false,
868 includeReturn = false
869 )
870 return getMethodDeprecatedIn(owner, method.name, desc)
871 }
872
ApiLookupnull873 fun ApiLookup.getFieldDeprecatedIn(field: PsiField): Int {
874 val containingClass = field.containingClass ?: return -1
875 val owner = containingClass.qualifiedName ?: return -1
876 return getFieldDeprecatedIn(owner, field.name)
877 }
878