• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tools.metalava
18 
19 import com.android.SdkConstants
20 import com.android.tools.lint.annotations.Extractor
21 import com.android.tools.metalava.model.AnnotationItem
22 import com.android.tools.metalava.model.AnnotationTarget
23 import com.android.tools.metalava.model.ClassItem
24 import com.android.tools.metalava.model.Codebase
25 import com.android.tools.metalava.model.FieldItem
26 import com.android.tools.metalava.model.Item
27 import com.android.tools.metalava.model.MemberItem
28 import com.android.tools.metalava.model.MethodItem
29 import com.android.tools.metalava.model.PackageItem
30 import com.android.tools.metalava.model.ParameterItem
31 import com.android.tools.metalava.model.psi.CodePrinter
32 import com.android.tools.metalava.model.psi.PsiAnnotationItem
33 import com.android.tools.metalava.model.psi.PsiClassItem
34 import com.android.tools.metalava.model.psi.PsiMethodItem
35 import com.android.tools.metalava.model.psi.UAnnotationItem
36 import com.android.tools.metalava.model.visitors.ApiVisitor
37 import com.google.common.xml.XmlEscapers
38 import com.intellij.lang.jvm.annotation.JvmAnnotationConstantValue
39 import com.intellij.lang.jvm.annotation.JvmAnnotationEnumFieldValue
40 import com.intellij.psi.JavaRecursiveElementVisitor
41 import com.intellij.psi.PsiAnnotation
42 import com.intellij.psi.PsiClass
43 import com.intellij.psi.PsiElement
44 import com.intellij.psi.PsiField
45 import com.intellij.psi.PsiModifier
46 import com.intellij.psi.PsiNameValuePair
47 import com.intellij.psi.PsiReferenceExpression
48 import com.intellij.psi.PsiReturnStatement
49 import org.jetbrains.uast.UAnnotation
50 import org.jetbrains.uast.UCallExpression
51 import org.jetbrains.uast.UExpression
52 import org.jetbrains.uast.UReferenceExpression
53 import org.jetbrains.uast.USimpleNameReferenceExpression
54 import org.jetbrains.uast.UastEmptyExpression
55 import org.jetbrains.uast.UastFacade
56 import org.jetbrains.uast.toUElement
57 import java.io.BufferedOutputStream
58 import java.io.File
59 import java.io.FileOutputStream
60 import java.io.PrintWriter
61 import java.io.StringWriter
62 import java.util.jar.JarEntry
63 import java.util.jar.JarOutputStream
64 import kotlin.text.Charsets.UTF_8
65 
66 // Like the tools/base Extractor class, but limited to our own (mapped) AnnotationItems,
67 // and only those with source retention (and in particular right now that just means the
68 // typedef annotations.)
69 class ExtractAnnotations(
70     private val codebase: Codebase,
71     private val outputFile: File
72 ) : ApiVisitor() {
73     // Used linked hash map for order such that we always emit parameters after their surrounding method etc
74     private val packageToAnnotationPairs = LinkedHashMap<PackageItem, MutableList<Pair<Item, AnnotationHolder>>>()
75 
76     private data class AnnotationHolder(
77         val annotationClass: ClassItem?,
78         val annotationItem: AnnotationItem,
79         val uAnnotation: UAnnotation?
80     )
81 
82     private val fieldNamePrinter = CodePrinter(
83         codebase = codebase,
84         filterReference = filterReference,
85         inlineFieldValues = false,
86         skipUnknown = true
87     )
88 
89     private val fieldValuePrinter = CodePrinter(
90         codebase = codebase,
91         filterReference = filterReference,
92         inlineFieldValues = true,
93         skipUnknown = true
94     )
95 
96     private val classToAnnotationHolder = mutableMapOf<String, AnnotationHolder>()
97 
98     fun extractAnnotations() {
99         codebase.accept(this)
100 
101         // Write external annotations
102         FileOutputStream(outputFile).use { fileOutputStream ->
103             JarOutputStream(BufferedOutputStream(fileOutputStream)).use { zos ->
104                 val sortedPackages =
105                     packageToAnnotationPairs.keys.asSequence().sortedBy { it.qualifiedName() }.toList()
106 
107                 for (pkg in sortedPackages) {
108                     // Note: Using / rather than File.separator: jar lib requires it
109                     val name = pkg.qualifiedName().replace('.', '/') + "/annotations.xml"
110 
111                     val outEntry = JarEntry(name)
112                     outEntry.time = 0
113                     zos.putNextEntry(outEntry)
114 
115                     val pairs = packageToAnnotationPairs[pkg] ?: continue
116 
117                     // Ensure stable output
118                     if (pairs.size > 1) {
119                         pairs.sortBy { it.first.getExternalAnnotationSignature() }
120                     }
121 
122                     StringPrintWriter.create().use { writer ->
123                         writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>")
124 
125                         var open = false
126                         var prev: Item? = null
127                         for ((item, annotation) in pairs) {
128                             if (item != prev) {
129                                 if (open) {
130                                     writer.print("  </item>")
131                                     writer.println()
132                                 }
133                                 writer.print("  <item name=\"")
134                                 writer.print(item.getExternalAnnotationSignature())
135                                 writer.println("\">")
136                                 open = true
137                             }
138                             prev = item
139 
140                             writeAnnotation(writer, item, annotation)
141                         }
142                         if (open) {
143                             writer.print("  </item>")
144                             writer.println()
145                         }
146                         writer.println("</root>\n")
147                         writer.close()
148                         val bytes = writer.contents.toByteArray(UTF_8)
149                         zos.write(bytes)
150                         zos.closeEntry()
151                     }
152                 }
153             }
154         }
155     }
156 
157     private fun addItem(item: Item, annotation: AnnotationHolder) {
158         val pkg = when (item) {
159             is MemberItem -> item.containingClass().containingPackage()
160             is ParameterItem -> item.containingMethod().containingClass().containingPackage()
161             else -> return
162         }
163 
164         val list = packageToAnnotationPairs[pkg] ?: run {
165             val new =
166                 mutableListOf<Pair<Item, AnnotationHolder>>()
167             packageToAnnotationPairs[pkg] = new
168             new
169         }
170         list.add(Pair(item, annotation))
171     }
172 
173     override fun visitField(field: FieldItem) {
174         checkItem(field)
175     }
176 
177     override fun visitMethod(method: MethodItem) {
178         checkItem(method)
179     }
180 
181     override fun visitParameter(parameter: ParameterItem) {
182         checkItem(parameter)
183     }
184 
185     /** For a given item, extract the relevant annotations for that item */
186     private fun checkItem(item: Item) {
187         for (annotation in item.modifiers.annotations()) {
188             val qualifiedName = annotation.qualifiedName ?: continue
189             if (qualifiedName.startsWith(JAVA_LANG_PREFIX) ||
190                 qualifiedName.startsWith(ANDROIDX_ANNOTATION_PREFIX) ||
191                 qualifiedName.startsWith(ANDROID_ANNOTATION_PREFIX)
192             ) {
193                 if (annotation.isTypeDefAnnotation()) {
194                     // Imported typedef
195                     addItem(item, AnnotationHolder(null, annotation, null))
196                 } else if (annotation.targets.contains(AnnotationTarget.EXTERNAL_ANNOTATIONS_FILE)) {
197                     addItem(item, AnnotationHolder(null, annotation, null))
198                 }
199 
200                 continue
201             } else if (qualifiedName.startsWith(ORG_JETBRAINS_ANNOTATIONS_PREFIX) ||
202                 qualifiedName.startsWith(ORG_INTELLIJ_LANG_ANNOTATIONS_PREFIX)
203             ) {
204                 // Externally merged metadata, like @Contract and @Language
205                 addItem(item, AnnotationHolder(null, annotation, null))
206                 continue
207             }
208 
209             val typeDefClass = annotation.resolve() ?: continue
210             val className = typeDefClass.qualifiedName()
211             if (typeDefClass.isAnnotationType()) {
212                 val cached = classToAnnotationHolder[className]
213                 if (cached != null) {
214                     addItem(item, cached)
215                     continue
216                 }
217 
218                 val typeDefAnnotation = typeDefClass.modifiers.annotations().firstOrNull {
219                     it.isTypeDefAnnotation()
220                 }
221                 if (typeDefAnnotation != null) {
222                     // Make sure it has the right retention
223                     if (!hasSourceRetention(typeDefClass)) {
224                         reporter.report(
225                             Issues.ANNOTATION_EXTRACTION, typeDefClass,
226                             "This typedef annotation class should have @Retention(RetentionPolicy.SOURCE)"
227                         )
228                     }
229 
230                     if (filterEmit.test(typeDefClass)) {
231                         reporter.report(
232                             Issues.ANNOTATION_EXTRACTION, typeDefClass,
233                             "This typedef annotation class should be marked @hide or should not be marked public"
234                         )
235                     }
236 
237                     val result =
238                         if (typeDefAnnotation is PsiAnnotationItem && typeDefClass is PsiClassItem) {
239                             AnnotationHolder(
240                                 typeDefClass, typeDefAnnotation,
241                                 UastFacade.convertElement(
242                                     typeDefAnnotation.psiAnnotation,
243                                     null,
244                                     UAnnotation::class.java
245                                 ) as UAnnotation
246                             )
247                         } else if (typeDefAnnotation is UAnnotationItem && typeDefClass is PsiClassItem) {
248                             AnnotationHolder(
249                                 typeDefClass, typeDefAnnotation, typeDefAnnotation.uAnnotation
250                             )
251                         } else {
252                             continue
253                         }
254 
255                     classToAnnotationHolder[className] = result
256                     addItem(item, result)
257 
258                     if (item is PsiMethodItem && result.uAnnotation != null &&
259                         !reporter.isSuppressed(Issues.RETURNING_UNEXPECTED_CONSTANT)
260                     ) {
261                         verifyReturnedConstants(item, result.uAnnotation, result, className)
262                     }
263                     continue
264                 }
265             }
266         }
267     }
268 
269     /**
270      * Given a method whose return value is annotated with a typedef, runs checks on the typedef
271      * and flags any returned constants not in the list.
272      */
273     private fun verifyReturnedConstants(
274         item: PsiMethodItem,
275         uAnnotation: UAnnotation,
276         result: AnnotationHolder,
277         className: String
278     ) {
279         val method = item.psiMethod
280         if (method.body != null) {
281             method.body?.accept(object : JavaRecursiveElementVisitor() {
282                 private var constants: List<String>? = null
283 
284                 override fun visitReturnStatement(statement: PsiReturnStatement) {
285                     val value = statement.returnValue
286                     if (value is PsiReferenceExpression) {
287                         val resolved = value.resolve() as? PsiField ?: return
288                         val modifiers = resolved.modifierList ?: return
289                         if (modifiers.hasModifierProperty(PsiModifier.STATIC) &&
290                             modifiers.hasModifierProperty(PsiModifier.FINAL)
291                         ) {
292                             if (resolved.type.arrayDimensions > 0) {
293                                 return
294                             }
295                             val name = resolved.name
296 
297                             // Make sure this is one of the allowed annotations
298                             val names = constants ?: run {
299                                 constants = computeValidConstantNames(uAnnotation)
300                                 constants!!
301                             }
302                             if (names.isNotEmpty() && !names.contains(name)) {
303                                 val expected = names.joinToString { it }
304                                 reporter.report(
305                                     Issues.RETURNING_UNEXPECTED_CONSTANT, value as PsiElement,
306                                     "Returning unexpected constant $name; is @${result.annotationClass?.simpleName()
307                                         ?: className} missing this constant? Expected one of $expected"
308                                 )
309                             }
310                         }
311                     }
312                 }
313             })
314         }
315     }
316 
317     private fun computeValidConstantNames(annotation: UAnnotation): List<String> {
318         val constants = annotation.findAttributeValue(SdkConstants.ATTR_VALUE) ?: return emptyList()
319         if (constants is UCallExpression) {
320             return constants.valueArguments.mapNotNull { (it as? USimpleNameReferenceExpression)?.identifier }.toList()
321         }
322 
323         return emptyList()
324     }
325 
326     private fun hasSourceRetention(annotationClass: ClassItem): Boolean {
327         if (annotationClass is PsiClassItem) {
328             return hasSourceRetention(annotationClass.psiClass)
329         }
330         return false
331     }
332 
333     private fun hasSourceRetention(cls: PsiClass): Boolean {
334         val modifierList = cls.modifierList
335         if (modifierList != null) {
336             for (psiAnnotation in modifierList.annotations) {
337                 val uAnnotation = psiAnnotation.toUElement(UAnnotation::class.java)
338                 val hasSourceRetention =
339                     if (uAnnotation != null) {
340                         hasSourceRetention(uAnnotation)
341                     } else {
342                         // There is a hole in UAST conversion. If so, fall back to using PSI.
343                         hasSourceRetention(psiAnnotation)
344                     }
345                 if (hasSourceRetention) {
346                     return true
347                 }
348             }
349         }
350 
351         return false
352     }
353 
354     private fun hasSourceRetention(annotation: UAnnotation): Boolean {
355         val qualifiedName = annotation.qualifiedName
356         if (isRetention(qualifiedName)) {
357             val attributes = annotation.attributeValues
358             if (attributes.size != 1) {
359                 reporter.report(
360                     Issues.ANNOTATION_EXTRACTION, annotation.sourcePsi,
361                     "Expected exactly one parameter passed to @Retention"
362                 )
363                 return false
364             }
365             val value = attributes[0].expression
366             if (value is UReferenceExpression) {
367                 try {
368                     val element = value.resolve()
369                     if (element is PsiField) {
370                         val field = element as PsiField?
371                         if (SOURCE == field!!.name) {
372                             return true
373                         }
374                     }
375                 } catch (t: Throwable) {
376                     val s = value.asSourceString()
377                     return s.contains(SOURCE)
378                 }
379             }
380         }
381 
382         return false
383     }
384 
385     private fun hasSourceRetention(psiAnnotation: PsiAnnotation): Boolean {
386         val qualifiedName = psiAnnotation.qualifiedName
387         if (isRetention(qualifiedName)) {
388             val attributes = psiAnnotation.parameterList.attributes
389             if (attributes.size != 1) {
390                 reporter.report(
391                     Issues.ANNOTATION_EXTRACTION, psiAnnotation,
392                     "Expected exactly one parameter passed to @Retention"
393                 )
394                 return false
395             }
396             return when (val value = attributes[0].attributeValue) {
397                 is JvmAnnotationEnumFieldValue -> SOURCE == value.fieldName
398                 is JvmAnnotationConstantValue ->
399                     (value.constantValue as? String)?.contains(SOURCE) ?: false
400                 else -> false
401             }
402         }
403 
404         return false
405     }
406 
407     /**
408      * A writer which stores all its contents into a string and has the ability to mark a certain
409      * freeze point and then reset back to it
410      */
411     private class StringPrintWriter constructor(private val stringWriter: StringWriter) :
412         PrintWriter(stringWriter) {
413         private var mark: Int = 0
414 
415         val contents: String get() = stringWriter.toString()
416 
417         fun mark() {
418             flush()
419             mark = stringWriter.buffer.length
420         }
421 
422         fun reset() {
423             stringWriter.buffer.setLength(mark)
424         }
425 
426         override fun toString(): String {
427             return contents
428         }
429 
430         companion object {
431             fun create(): StringPrintWriter {
432                 return StringPrintWriter(StringWriter(1000))
433             }
434         }
435     }
436 
437     private fun escapeXml(unescaped: String): String {
438         return XmlEscapers.xmlAttributeEscaper().escape(unescaped)
439     }
440 
441     private fun Item.getExternalAnnotationSignature(): String? {
442         when (this) {
443             is PackageItem -> {
444                 return escapeXml(qualifiedName())
445             }
446 
447             is ClassItem -> {
448                 return escapeXml(qualifiedName())
449             }
450 
451             is MethodItem -> {
452                 val sb = StringBuilder(100)
453                 sb.append(escapeXml(containingClass().qualifiedName()))
454                 sb.append(' ')
455 
456                 if (isConstructor()) {
457                     sb.append(escapeXml(containingClass().simpleName()))
458                 } else {
459                     sb.append(escapeXml(returnType().toTypeString()))
460                     sb.append(' ')
461                     sb.append(escapeXml(name()))
462                 }
463 
464                 sb.append('(')
465 
466                 // The signature must match *exactly* the formatting used by IDEA,
467                 // since it looks up external annotations in a map by this key.
468                 // Therefore, it is vital that the parameter list uses exactly one
469                 // space after each comma between parameters, and *no* spaces between
470                 // generics variables, e.g. foo(Map<A,B>, int)
471                 var i = 0
472                 val parameterList = parameters()
473                 val n = parameterList.size
474                 while (i < n) {
475                     if (i > 0) {
476                         sb.append(',').append(' ')
477                     }
478                     val type = parameterList[i].type().toTypeString()
479                         .replace(" ", "")
480                         .replace("?extends", "? extends ")
481                         .replace("?super", "? super ")
482                     sb.append(escapeXml(type))
483                     i++
484                 }
485                 sb.append(')')
486                 return sb.toString()
487             }
488 
489             is FieldItem -> {
490                 return escapeXml(containingClass().qualifiedName()) + " " + name()
491             }
492 
493             is ParameterItem -> {
494                 return containingMethod().getExternalAnnotationSignature() + " " + this.parameterIndex
495             }
496         }
497 
498         return null
499     }
500 
501     private fun writeAnnotation(
502         writer: StringPrintWriter,
503         item: Item,
504         annotationHolder: AnnotationHolder
505     ) {
506         val annotationItem = annotationHolder.annotationItem
507         val uAnnotation = annotationHolder.uAnnotation
508             ?: when (annotationItem) {
509                 is UAnnotationItem -> annotationItem.uAnnotation
510                 is PsiAnnotationItem ->
511                     // Imported annotation
512                     annotationItem.psiAnnotation.toUElement(UAnnotation::class.java) ?: return
513                 else -> return
514             }
515         val qualifiedName = annotationItem.qualifiedName
516 
517         writer.mark()
518         writer.print("    <annotation name=\"")
519         writer.print(qualifiedName)
520 
521         var attributes = uAnnotation.attributeValues
522         if (attributes.isEmpty()) {
523             writer.print("\"/>")
524             writer.println()
525             return
526         }
527 
528         writer.print("\">")
529         writer.println()
530 
531         // noinspection PointlessBooleanExpression,ConstantConditions
532         if (sortAnnotations) {
533             // Ensure that the value attribute is written first
534             attributes = attributes.sortedWith(
535                 compareBy(
536                     { (it.name ?: SdkConstants.ATTR_VALUE) != SdkConstants.ATTR_VALUE },
537                     { it.name }
538                 )
539             )
540         }
541 
542         if (attributes.size == 1 && Extractor.REQUIRES_PERMISSION.isPrefix(qualifiedName, true)) {
543             val expression = attributes[0].expression
544             if (expression is UAnnotation) {
545                 // The external annotations format does not allow for nested/complex annotations.
546                 // However, these special annotations (@RequiresPermission.Read,
547                 // @RequiresPermission.Write, etc) are known to only be simple containers with a
548                 // single permission child, so instead we "inline" the content:
549                 //  @Read(@RequiresPermission(allOf={P1,P2},conditional=true)
550                 //     =>
551                 //      @RequiresPermission.Read(allOf({P1,P2},conditional=true)
552                 // That's setting attributes that don't actually exist on the container permission,
553                 // but we'll counteract that on the read-annotations side.
554                 val annotation = expression as UAnnotation
555                 attributes = annotation.attributeValues
556             } else if (expression is UCallExpression) {
557                 val nestedPsi = expression.sourcePsi as? PsiAnnotation
558                 val annotation = nestedPsi?.let {
559                     UastFacade.convertElement(it, expression, UAnnotation::class.java)
560                 } as? UAnnotation
561                 annotation?.attributeValues?.let { attributes = it }
562             } else if (expression is UastEmptyExpression && attributes[0].sourcePsi is PsiNameValuePair) {
563                 val memberValue = (attributes[0].sourcePsi as PsiNameValuePair).value
564                 if (memberValue is PsiAnnotation) {
565                     val annotation = memberValue.toUElement(UAnnotation::class.java)
566                     annotation?.attributeValues?.let { attributes = it }
567                 }
568             }
569         }
570 
571         val inlineConstants = isInlinedConstant(annotationItem)
572         var empty = true
573         for (pair in attributes) {
574             val expression = pair.expression
575             val value = attributeString(expression, inlineConstants) ?: continue
576             empty = false
577             var name = pair.name
578             if (name == null) {
579                 name = SdkConstants.ATTR_VALUE // default name
580             }
581 
582             // Platform typedef annotations now declare a prefix attribute for
583             // documentation generation purposes; this should not be part of the
584             // extracted metadata.
585             if (("prefix" == name || "suffix" == name) && annotationItem.isTypeDefAnnotation()) {
586                 reporter.report(
587                     Issues.SUPERFLUOUS_PREFIX, item,
588                     "Superfluous $name attribute on typedef"
589                 )
590                 continue
591             }
592 
593             writer.print("      <val name=\"")
594             writer.print(name)
595             writer.print("\" val=\"")
596             writer.print(escapeXml(value))
597             writer.println("\" />")
598         }
599 
600         if (empty && attributes.isNotEmpty()) {
601             // All items were filtered out: don't write the annotation at all
602             writer.reset()
603             return
604         }
605 
606         writer.println("    </annotation>")
607     }
608 
609     private fun attributeString(
610         value: UExpression?,
611         inlineConstants: Boolean
612     ): String? {
613         val printer =
614             if (inlineConstants) {
615                 fieldValuePrinter
616             } else {
617                 fieldNamePrinter
618             }
619 
620         return printer.toSourceString(value)
621     }
622 
623     private fun isInlinedConstant(annotationItem: AnnotationItem): Boolean {
624         return annotationItem.isTypeDefAnnotation()
625     }
626 
627     /** Whether to sort annotation attributes (otherwise their declaration order is used)  */
628     private val sortAnnotations: Boolean = true
629 
630     companion object {
631         private const val SOURCE = "SOURCE"
632     }
633 }
634