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