<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.android.tools.metalava.model.visitors.VisibleItemVisitor
24 import com.google.common.io.Files
25 import com.intellij.psi.PsiClass
26 import com.intellij.psi.PsiField
27 import com.intellij.psi.PsiMethod
28 import java.io.File
29 import java.util.HashMap
30 import java.util.regex.Pattern
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 : VisibleItemVisitor() {
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 // Typdef 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 val typos = mapOf(
629 "JetPack" to "Jetpack",
630 "Andriod" to "Android",
631 "Kitkat" to "KitKat",
632 "LemonMeringuePie" to "Lollipop",
633 "LMP" to "Lollipop",
634 "KeyLimePie" to "KitKat",
635 "KLP" to "KitKat",
636 "teh" to "the"
637 )
638
639 private fun tweakGrammar() {
640 codebase.accept(object : VisibleItemVisitor() {
641 override fun visitItem(item: Item) {
642 var doc = item.documentation
643 if (doc.isBlank()) {
644 return
645 }
646
647 if (!reporter.isSuppressed(Issues.TYPO)) {
648 for (typo in typos.keys) {
649 if (doc.contains(typo)) {
650 val replacement = typos[typo] ?: continue
651 val new = doc.replace(Regex("\\b$typo\\b"), replacement)
652 if (new != doc) {
653 reporter.report(
654 Issues.TYPO,
655 item,
656 "Replaced $typo with $replacement in the documentation for $item"
657 )
658 doc = new
659 item.documentation = doc
660 }
661 }
662 }
663 }
664
665 // Work around javadoc cutting off the summary line after the first ". ".
666 val firstDot = doc.indexOf(".")
667 if (firstDot > 0 && doc.regionMatches(firstDot - 1, "e.g. ", 0, 5, false)) {
668 doc = doc.substring(0, firstDot) + ".g. " + doc.substring(firstDot + 4)
669 item.documentation = doc
670 }
671 }
672 })
673 }
674
675 fun applyApiLevels(applyApiLevelsXml: File) {
676 @Suppress("DEPRECATION") // still using older lint-api when building with soong
677 val client = object : LintCliClient() {
678 override fun findResource(relativePath: String): File? {
679 if (relativePath == ApiLookup.XML_FILE_PATH) {
680 return applyApiLevelsXml
681 }
682 return super.findResource(relativePath)
683 }
684
685 override fun getSdk(): AndroidSdkHandler? {
686 return null
687 }
688
689 override fun getCacheDir(name: String?, create: Boolean): File? {
690 if (create && isUnderTest()) {
691 // Pick unique directory during unit tests
692 return Files.createTempDir()
693 }
694
695 val sb = StringBuilder(PROGRAM_NAME)
696 if (name != null) {
697 sb.append(File.separator)
698 sb.append(name)
699 }
700 val relative = sb.toString()
701
702 val tmp = System.getenv("TMPDIR")
703 if (tmp != null) {
704 // Android Build environment: Make sure we're really creating a unique
705 // temp directory each time since builds could be running in
706 // parallel here.
707 val dir = File(tmp, relative)
708 if (!dir.isDirectory) {
709 dir.mkdirs()
710 }
711
712 return java.nio.file.Files.createTempDirectory(dir.toPath(), null).toFile()
713 }
714
715 val dir = File(System.getProperty("java.io.tmpdir"), relative)
716 if (create && !dir.isDirectory) {
717 dir.mkdirs()
718 }
719 return dir
720 }
721 }
722
723 val apiLookup = ApiLookup.get(client)
724
725 val pkgApi = HashMap<PackageItem, Int?>(300)
726 codebase.accept(object : ApiVisitor(visitConstructorsAsMethods = true) {
727 override fun visitMethod(method: MethodItem) {
728 val psiMethod = method.psi() as? PsiMethod ?: return
729 addApiLevelDocumentation(apiLookup.getMethodVersion(psiMethod), method)
730 addDeprecatedDocumentation(apiLookup.getMethodDeprecatedIn(psiMethod), method)
731 }
732
733 override fun visitClass(cls: ClassItem) {
734 val psiClass = cls.psi() as PsiClass
735 val since = apiLookup.getClassVersion(psiClass)
736 if (since != -1) {
737 addApiLevelDocumentation(since, cls)
738
739 // Compute since version for the package: it's the min of all the classes in the package
740 val pkg = cls.containingPackage()
741 pkgApi[pkg] = Math.min(pkgApi[pkg] ?: Integer.MAX_VALUE, since)
742 }
743 addDeprecatedDocumentation(apiLookup.getClassDeprecatedIn(psiClass), cls)
744 }
745
746 override fun visitField(field: FieldItem) {
747 val psiField = field.psi() as PsiField
748 addApiLevelDocumentation(apiLookup.getFieldVersion(psiField), field)
749 addDeprecatedDocumentation(apiLookup.getFieldDeprecatedIn(psiField), field)
750 }
751 })
752
753 val packageDocs = codebase.getPackageDocs()
754 if (packageDocs != null) {
755 for ((pkg, api) in pkgApi.entries) {
756 val code = api ?: 1
757 addApiLevelDocumentation(code, pkg)
758 }
759 }
760 }
761
762 private fun addApiLevelDocumentation(level: Int, item: Item) {
763 if (level > 0) {
764 if (item.originallyHidden) {
765 // @SystemApi, @TestApi etc -- don't apply API levels here since we don't have accurate historical data
766 return
767 }
768 val currentCodeName = options.currentCodeName
769 val code: String = if (currentCodeName != null && level > options.currentApiLevel) {
770 currentCodeName
771 } else {
772 level.toString()
773 }
774
775 @Suppress("ConstantConditionIf")
776 if (ADD_API_LEVEL_TEXT) { // See 113933920: Remove "Requires API level" from method comment
777 val description = if (code == currentCodeName) currentCodeName else describeApiLevel(level)
778 appendDocumentation("Requires API level $description", item, false)
779 }
780 // Also add @since tag, unless already manually entered.
781 // TODO: Override it everywhere in case the existing doc is wrong (we know
782 // better), and at least for OpenJDK sources we *should* since the since tags
783 // are talking about language levels rather than API levels!
784 if (!item.documentation.contains("@apiSince")) {
785 item.appendDocumentation(code, "@apiSince")
786 } else {
787 reporter.report(
788 Issues.FORBIDDEN_TAG, item, "Documentation should not specify @apiSince " +
789 "manually; it's computed and injected at build time by $PROGRAM_NAME"
790 )
791 }
792 }
793 }
794
795 private fun addDeprecatedDocumentation(level: Int, item: Item) {
796 if (level > 0) {
797 if (item.originallyHidden) {
798 // @SystemApi, @TestApi etc -- don't apply API levels here since we don't have accurate historical data
799 return
800 }
801 val currentCodeName = options.currentCodeName
802 val code: String = if (currentCodeName != null && level > options.currentApiLevel) {
803 currentCodeName
804 } else {
805 level.toString()
806 }
807
808 @Suppress("ConstantConditionIf")
809 if (ADD_DEPRECATED_IN_TEXT) {
810 // TODO: *pre*pend instead!
811 val description =
812 "<p class=\"caution\"><strong>This class was deprecated in API level $code.</strong></p>"
813 item.appendDocumentation(description, "@deprecated", append = false)
814 }
815
816 if (!item.documentation.contains("@deprecatedSince")) {
817 item.appendDocumentation(code, "@deprecatedSince")
818 } else {
819 reporter.report(
820 Issues.FORBIDDEN_TAG, item, "Documentation should not specify @deprecatedSince " +
821 "manually; it's computed and injected at build time by $PROGRAM_NAME"
822 )
823 }
824 }
825 }
826
827 private fun describeApiLevel(level: Int): String {
828 return "$level (Android ${SdkVersionInfo.getVersionString(level)}, ${SdkVersionInfo.getCodeName(level)})"
829 }
830 }
831
getClassVersionnull832 fun ApiLookup.getClassVersion(cls: PsiClass): Int {
833 val owner = cls.qualifiedName ?: return -1
834 return getClassVersion(owner)
835 }
836
837 val defaultEvaluator = DefaultJavaEvaluator(null, null)
838
ApiLookupnull839 fun ApiLookup.getMethodVersion(method: PsiMethod): Int {
840 val containingClass = method.containingClass ?: return -1
841 val owner = containingClass.qualifiedName ?: return -1
842 val desc = defaultEvaluator.getMethodDescription(method, false, false)
843 return getMethodVersion(owner, if (method.isConstructor) "<init>" else method.name, desc)
844 }
845
ApiLookupnull846 fun ApiLookup.getFieldVersion(field: PsiField): Int {
847 val containingClass = field.containingClass ?: return -1
848 val owner = containingClass.qualifiedName ?: return -1
849 return getFieldVersion(owner, field.name)
850 }
851
ApiLookupnull852 fun ApiLookup.getClassDeprecatedIn(cls: PsiClass): Int {
853 val owner = cls.qualifiedName ?: return -1
854 return getClassDeprecatedIn(owner)
855 }
856
ApiLookupnull857 fun ApiLookup.getMethodDeprecatedIn(method: PsiMethod): Int {
858 val containingClass = method.containingClass ?: return -1
859 val owner = containingClass.qualifiedName ?: return -1
860 val desc = defaultEvaluator.getMethodDescription(method, false, false)
861 return getMethodDeprecatedIn(owner, method.name, desc)
862 }
863
ApiLookupnull864 fun ApiLookup.getFieldDeprecatedIn(field: PsiField): Int {
865 val containingClass = field.containingClass ?: return -1
866 val owner = containingClass.qualifiedName ?: return -1
867 return getFieldDeprecatedIn(owner, field.name)
868 }
869