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