• 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.tools.lint.annotations.Extractor
20 import com.android.tools.metalava.model.ANDROIDX_ANNOTATION_PREFIX
21 import com.android.tools.metalava.model.ANDROID_ANNOTATION_PREFIX
22 import com.android.tools.metalava.model.ANNOTATION_ATTR_VALUE
23 import com.android.tools.metalava.model.AnnotationItem
24 import com.android.tools.metalava.model.AnnotationRetention
25 import com.android.tools.metalava.model.AnnotationTarget
26 import com.android.tools.metalava.model.CallableItem
27 import com.android.tools.metalava.model.ClassItem
28 import com.android.tools.metalava.model.Codebase
29 import com.android.tools.metalava.model.FieldItem
30 import com.android.tools.metalava.model.Item
31 import com.android.tools.metalava.model.JAVA_LANG_PREFIX
32 import com.android.tools.metalava.model.MemberItem
33 import com.android.tools.metalava.model.MethodItem
34 import com.android.tools.metalava.model.PackageItem
35 import com.android.tools.metalava.model.ParameterItem
36 import com.android.tools.metalava.model.findAnnotation
37 import com.android.tools.metalava.model.psi.CodePrinter
38 import com.android.tools.metalava.model.psi.report
39 import com.android.tools.metalava.model.psi.uAnnotation
40 import com.android.tools.metalava.model.visitors.ApiVisitor
41 import com.android.tools.metalava.reporter.Issues
42 import com.android.tools.metalava.reporter.Reporter
43 import com.google.common.xml.XmlEscapers
44 import com.intellij.psi.PsiAnnotation
45 import com.intellij.psi.PsiNameValuePair
46 import java.io.BufferedOutputStream
47 import java.io.File
48 import java.io.FileOutputStream
49 import java.io.PrintWriter
50 import java.io.StringWriter
51 import java.util.jar.JarEntry
52 import java.util.jar.JarOutputStream
53 import kotlin.text.Charsets.UTF_8
54 import org.jetbrains.uast.UAnnotation
55 import org.jetbrains.uast.UCallExpression
56 import org.jetbrains.uast.UExpression
57 import org.jetbrains.uast.UastEmptyExpression
58 import org.jetbrains.uast.UastFacade
59 import org.jetbrains.uast.toUElement
60 
61 // Like the tools/base Extractor class, but limited to our own (mapped) AnnotationItems,
62 // and only those with source retention (and in particular right now that just means the
63 // typedef annotations.)
64 class ExtractAnnotations(
65     private val codebase: Codebase,
66     private val reporter: Reporter,
67     private val outputFile: File,
68 ) :
69     ApiVisitor(
70         apiPredicateConfig = @Suppress("DEPRECATION") options.apiPredicateConfig,
71     ) {
72     // Used linked hash map for order such that we always emit parameters after their surrounding
73     // method etc
74     private val packageToAnnotationPairs =
75         LinkedHashMap<PackageItem, MutableList<Pair<Item, AnnotationItem>>>()
76 
77     private val fieldNamePrinter =
78         CodePrinter(
79             codebase = codebase,
80             reporter = reporter,
81             filterReference = filterReference,
82             inlineFieldValues = false,
83             skipUnknown = true,
84         )
85 
86     private val fieldValuePrinter =
87         CodePrinter(
88             codebase = codebase,
89             reporter = reporter,
90             filterReference = filterReference,
91             inlineFieldValues = true,
92             skipUnknown = true,
93         )
94 
95     private val classToAnnotationHolder = mutableMapOf<String, AnnotationItem>()
96 
97     fun extractAnnotations() {
98         codebase.accept(this)
99 
100         // Write external annotations
101         FileOutputStream(outputFile).use { fileOutputStream ->
102             JarOutputStream(BufferedOutputStream(fileOutputStream)).use { zos ->
103                 val sortedPackages =
104                     packageToAnnotationPairs.keys
105                         .asSequence()
106                         .sortedBy { it.qualifiedName() }
107                         .toList()
108 
109                 for (pkg in sortedPackages) {
110                     // Note: Using / rather than File.separator: jar lib requires it
111                     val name = pkg.qualifiedName().replace('.', '/') + "/annotations.xml"
112 
113                     val outEntry = JarEntry(name)
114                     outEntry.time = 0
115                     zos.putNextEntry(outEntry)
116 
117                     val pairs = packageToAnnotationPairs[pkg] ?: continue
118 
119                     // Ensure stable output
120                     if (pairs.size > 1) {
121                         pairs.sortBy { it.first.getExternalAnnotationSignature() }
122                     }
123 
124                     StringPrintWriter.create().use { writer ->
125                         writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>")
126 
127                         var open = false
128                         var previousSignature: String? = null
129                         for ((item, annotation) in pairs) {
130                             val signature = item.getExternalAnnotationSignature()
131                             if (signature != previousSignature) {
132                                 if (open) {
133                                     writer.print("  </item>")
134                                     writer.println()
135                                 }
136                                 writer.print("  <item name=\"")
137                                 writer.print(signature)
138                                 writer.println("\">")
139                                 open = true
140                             }
141                             previousSignature = signature
142 
143                             writeAnnotation(writer, item, annotation)
144                         }
145                         if (open) {
146                             writer.print("  </item>")
147                             writer.println()
148                         }
149                         writer.println("</root>\n")
150                         writer.close()
151                         val bytes = writer.contents.toByteArray(UTF_8)
152                         zos.write(bytes)
153                         zos.closeEntry()
154                     }
155                 }
156             }
157         }
158     }
159 
160     private fun addItem(item: Item, annotation: AnnotationItem) {
161         val pkg =
162             when (item) {
163                 is ClassItem -> item.containingPackage()
164                 is MemberItem -> item.containingClass().containingPackage()
165                 is ParameterItem -> item.containingCallable().containingClass().containingPackage()
166                 else -> return
167             }
168 
169         val list =
170             packageToAnnotationPairs[pkg]
171                 ?: run {
172                     val new = mutableListOf<Pair<Item, AnnotationItem>>()
173                     packageToAnnotationPairs[pkg] = new
174                     new
175                 }
176         list.add(Pair(item, annotation))
177     }
178 
179     override fun visitClass(cls: ClassItem) {
180         checkItem(cls)
181     }
182 
183     override fun visitField(field: FieldItem) {
184         checkItem(field)
185     }
186 
187     override fun visitCallable(callable: CallableItem) {
188         checkItem(callable)
189     }
190 
191     override fun visitParameter(parameter: ParameterItem) {
192         checkItem(parameter)
193     }
194 
195     /** For a given item, extract the relevant annotations for that item */
196     private fun checkItem(item: Item) {
197         for (annotation in item.modifiers.annotations()) {
198             val qualifiedName = annotation.qualifiedName
199             if (
200                 qualifiedName.startsWith(JAVA_LANG_PREFIX) ||
201                     qualifiedName.startsWith(ANDROIDX_ANNOTATION_PREFIX) ||
202                     qualifiedName.startsWith(ANDROID_ANNOTATION_PREFIX)
203             ) {
204                 if (annotation.isTypeDefAnnotation()) {
205                     // Imported typedef
206                     addItem(item, annotation)
207                 } else if (
208                     annotation.targets.contains(AnnotationTarget.EXTERNAL_ANNOTATIONS_FILE)
209                 ) {
210                     addItem(item, annotation)
211                 }
212 
213                 continue
214             } else if (
215                 qualifiedName.startsWith(ORG_JETBRAINS_ANNOTATIONS_PREFIX) ||
216                     qualifiedName.startsWith(ORG_INTELLIJ_LANG_ANNOTATIONS_PREFIX)
217             ) {
218                 // Externally merged metadata, like @Contract and @Language
219                 addItem(item, annotation)
220                 continue
221             }
222 
223             val typeDefClass = annotation.resolve() ?: continue
224             val className = typeDefClass.qualifiedName()
225             if (typeDefClass.isAnnotationType()) {
226                 val cached = classToAnnotationHolder[className]
227                 if (cached != null) {
228                     addItem(item, cached)
229                     continue
230                 }
231 
232                 val typeDefAnnotation =
233                     typeDefClass.modifiers.findAnnotation(AnnotationItem::isTypeDefAnnotation)
234                 if (typeDefAnnotation != null) {
235                     // Make sure it has the right retention
236                     if (typeDefClass.getRetention() != AnnotationRetention.SOURCE) {
237                         reporter.report(
238                             Issues.ANNOTATION_EXTRACTION,
239                             typeDefClass,
240                             "This typedef annotation class should have @Retention(RetentionPolicy.SOURCE)"
241                         )
242                     }
243 
244                     if (filterEmit.test(typeDefClass)) {
245                         reporter.report(
246                             Issues.ANNOTATION_EXTRACTION,
247                             typeDefClass,
248                             "This typedef annotation class should be marked @hide or should not be marked public"
249                         )
250                     }
251 
252                     classToAnnotationHolder[className] = typeDefAnnotation
253                     addItem(item, typeDefAnnotation)
254 
255                     if (
256                         item is MethodItem &&
257                             !reporter.isSuppressed(Issues.RETURNING_UNEXPECTED_CONSTANT)
258                     ) {
259                         item.body.verifyReturnedConstants(typeDefAnnotation, typeDefClass)
260                     }
261                 }
262             }
263         }
264     }
265 
266     /**
267      * A writer which stores all its contents into a string and has the ability to mark a certain
268      * freeze point and then reset back to it
269      */
270     private class StringPrintWriter constructor(private val stringWriter: StringWriter) :
271         PrintWriter(stringWriter) {
272         private var mark: Int = 0
273 
274         val contents: String
275             get() = stringWriter.toString()
276 
277         fun mark() {
278             flush()
279             mark = stringWriter.buffer.length
280         }
281 
282         fun reset() {
283             stringWriter.buffer.setLength(mark)
284         }
285 
286         override fun toString(): String {
287             return contents
288         }
289 
290         companion object {
291             fun create(): StringPrintWriter {
292                 return StringPrintWriter(StringWriter(1000))
293             }
294         }
295     }
296 
297     private fun escapeXml(unescaped: String): String {
298         return XmlEscapers.xmlAttributeEscaper().escape(unescaped)
299     }
300 
301     private fun Item.getExternalAnnotationSignature(): String? {
302         when (this) {
303             is PackageItem -> {
304                 return escapeXml(qualifiedName())
305             }
306             is ClassItem -> {
307                 return escapeXml(qualifiedName())
308             }
309             is CallableItem -> {
310                 val sb = StringBuilder(100)
311                 sb.append(escapeXml(containingClass().qualifiedName()))
312                 sb.append(' ')
313 
314                 if (isConstructor()) {
315                     sb.append(escapeXml(containingClass().simpleName()))
316                 } else {
317                     sb.append(escapeXml(returnType().toTypeString()))
318                     sb.append(' ')
319                     sb.append(escapeXml(name()))
320                 }
321 
322                 sb.append('(')
323 
324                 // The signature must match *exactly* the formatting used by IDEA,
325                 // since it looks up external annotations in a map by this key.
326                 // Therefore, it is vital that the parameter list uses exactly one
327                 // space after each comma between parameters, and *no* spaces between
328                 // generics variables, e.g. foo(Map<A,B>, int)
329                 var i = 0
330                 val parameterList = parameters()
331                 val n = parameterList.size
332                 while (i < n) {
333                     if (i > 0) {
334                         sb.append(',').append(' ')
335                     }
336                     val type =
337                         parameterList[i]
338                             .type()
339                             .toTypeString()
340                             .replace(" ", "")
341                             .replace("?extends", "? extends ")
342                             .replace("?super", "? super ")
343                     sb.append(escapeXml(type))
344                     i++
345                 }
346                 sb.append(')')
347                 return sb.toString()
348             }
349             is FieldItem -> {
350                 return escapeXml(containingClass().qualifiedName()) + " " + name()
351             }
352             is ParameterItem -> {
353                 return containingCallable().getExternalAnnotationSignature() +
354                     " " +
355                     this.parameterIndex
356             }
357         }
358 
359         return null
360     }
361 
362     private fun writeAnnotation(
363         writer: StringPrintWriter,
364         item: Item,
365         annotationItem: AnnotationItem
366     ) {
367         val uAnnotation = annotationItem.uAnnotation ?: return
368         val qualifiedName = annotationItem.qualifiedName
369 
370         writer.mark()
371         writer.print("    <annotation name=\"")
372         writer.print(qualifiedName)
373 
374         var attributes =
375             // Ensure consistent ordering.
376             uAnnotation.attributeValues.sortedWith(
377                 compareBy(
378                     // Ensure that the value attribute is written first
379                     { (it.name ?: ANNOTATION_ATTR_VALUE) != ANNOTATION_ATTR_VALUE },
380                     { it.name }
381                 )
382             )
383 
384         if (attributes.isEmpty()) {
385             writer.print("\"/>")
386             writer.println()
387             return
388         }
389 
390         writer.print("\">")
391         writer.println()
392 
393         if (attributes.size == 1 && Extractor.REQUIRES_PERMISSION.isPrefix(qualifiedName, true)) {
394             val expression = attributes[0].expression
395             if (expression is UAnnotation) {
396                 // The external annotations format does not allow for nested/complex annotations.
397                 // However, these special annotations (@RequiresPermission.Read,
398                 // @RequiresPermission.Write, etc.) are known to only be simple containers with a
399                 // single permission child, so instead we "inline" the content:
400                 //  @Read(@RequiresPermission(allOf={P1,P2},conditional=true)
401                 //     =>
402                 //      @RequiresPermission.Read(allOf({P1,P2},conditional=true)
403                 // That's setting attributes that don't actually exist on the container permission,
404                 // but we'll counteract that on the read-annotations side.
405                 val annotation = expression as UAnnotation
406                 attributes = annotation.attributeValues
407             } else if (expression is UCallExpression) {
408                 val nestedPsi = expression.sourcePsi as? PsiAnnotation
409                 val annotation =
410                     nestedPsi?.let {
411                         UastFacade.convertElement(it, expression, UAnnotation::class.java)
412                     } as? UAnnotation
413                 annotation?.attributeValues?.let { attributes = it }
414             } else if (
415                 expression is UastEmptyExpression && attributes[0].sourcePsi is PsiNameValuePair
416             ) {
417                 val memberValue = (attributes[0].sourcePsi as PsiNameValuePair).value
418                 if (memberValue is PsiAnnotation) {
419                     val annotation = memberValue.toUElement(UAnnotation::class.java)
420                     annotation?.attributeValues?.let { attributes = it }
421                 }
422             }
423         }
424 
425         val inlineConstants = isInlinedConstant(annotationItem)
426         var empty = true
427         for (pair in attributes) {
428             val expression = pair.expression
429             val value = attributeString(expression, inlineConstants) ?: continue
430             empty = false
431             var name = pair.name
432             if (name == null) {
433                 name = ANNOTATION_ATTR_VALUE // default name
434             }
435 
436             // Platform typedef annotations declare prefix/suffix attributes for historical reasons,
437             // and they are no longer necessary; they should also not be part of the extracted
438             // metadata.
439             if (("prefix" == name || "suffix" == name) && annotationItem.isTypeDefAnnotation()) {
440                 reporter.report(
441                     Issues.SUPERFLUOUS_PREFIX,
442                     item,
443                     "Superfluous $name attribute on typedef"
444                 )
445                 continue
446             }
447 
448             // The value could contain fully qualified references to enum values that are in the
449             // android.annotation package. If so, then replace them with references in the
450             // androidx.annotation package.
451             val normalizedValue =
452                 value.replace(ANDROID_ANNOTATION_PREFIX, ANDROIDX_ANNOTATION_PREFIX)
453 
454             writer.print("      <val name=\"")
455             writer.print(name)
456             writer.print("\" val=\"")
457             writer.print(escapeXml(normalizedValue))
458             writer.println("\" />")
459         }
460 
461         if (empty && attributes.isNotEmpty()) {
462             // All items were filtered out: don't write the annotation at all
463             writer.reset()
464             return
465         }
466 
467         writer.println("    </annotation>")
468     }
469 
470     private fun attributeString(value: UExpression?, inlineConstants: Boolean): String? {
471         val printer =
472             if (inlineConstants) {
473                 fieldValuePrinter
474             } else {
475                 fieldNamePrinter
476             }
477 
478         return printer.toSourceString(value)
479     }
480 
481     private fun isInlinedConstant(annotationItem: AnnotationItem): Boolean {
482         return annotationItem.isTypeDefAnnotation()
483     }
484 }
485