1 /*
<lambda>null2  * Copyright (C) 2018 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.metalava.cli.common.SignatureFileLoader
21 import com.android.tools.metalava.model.ANDROIDX_NONNULL
22 import com.android.tools.metalava.model.ANDROIDX_NULLABLE
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.MethodItem
28 import com.android.tools.metalava.model.PackageItem
29 import com.android.tools.metalava.model.SUPPORT_TYPE_USE_ANNOTATIONS
30 import com.android.tools.metalava.model.text.FileFormat
31 import com.android.tools.metalava.model.text.SignatureFile
32 import com.android.tools.metalava.model.visitors.ApiVisitor
33 import java.io.File
34 import java.io.IOException
35 import java.io.PrintWriter
36 import java.util.function.Predicate
37 import java.util.zip.ZipFile
38 import org.objectweb.asm.ClassReader
39 import org.objectweb.asm.Opcodes
40 import org.objectweb.asm.Type
41 import org.objectweb.asm.tree.ClassNode
42 import org.objectweb.asm.tree.FieldNode
43 import org.objectweb.asm.tree.MethodNode
44 
45 /**
46  * In an Android source tree, rewrite the signature files in prebuilts/sdk by reading what's
47  * actually there in the android.jar files.
48  */
49 class ConvertJarsToSignatureFiles(
50     private val stderr: PrintWriter,
51     private val stdout: PrintWriter,
52     private val progressTracker: ProgressTracker,
53     private val fileFormat: FileFormat,
54 ) {
55     fun convertJars(jarCodebaseLoader: JarCodebaseLoader, root: File) {
56         var api = 1
57         while (true) {
58             val apiJar =
59                 File(
60                     root,
61                     if (api <= 3) "prebuilts/tools/common/api-versions/android-$api/android.jar"
62                     else "prebuilts/sdk/$api/public/android.jar"
63                 )
64             if (!apiJar.isFile) {
65                 break
66             }
67             val signatureFile = "prebuilts/sdk/$api/public/api/android.txt"
68             val oldApiFile = File(root, "prebuilts/sdk/$api/public/api/android.txt")
69             val newApiFile =
70                 // Place new-style signature files in separate files?
71                 File(root, "prebuilts/sdk/$api/public/api/android.txt")
72 
73             progressTracker.progress("Writing signature files $signatureFile for $apiJar")
74 
75             val annotationManager = DefaultAnnotationManager()
76             val signatureFileLoader = SignatureFileLoader(annotationManager = annotationManager)
77 
78             val jarCodebase = jarCodebaseLoader.loadFromJarFile(apiJar)
79             val apiPredicateConfig = ApiPredicate.Config()
80             val apiEmit = ApiType.PUBLIC_API.getEmitFilter(apiPredicateConfig)
81             val apiReference = ApiType.PUBLIC_API.getReferenceFilter(apiPredicateConfig)
82 
83             if (api >= 28) {
84                 // As of API 28 we'll put nullness annotations into the jar but some of them
85                 // may be @RecentlyNullable/@RecentlyNonNull. Translate these back into
86                 // normal @Nullable/@NonNull
87                 jarCodebase.accept(
88                     object :
89                         ApiVisitor(
90                             config = ApiVisitor.Config(),
91                         ) {
92                         override fun visitItem(item: Item) {
93                             unmarkRecent(item)
94                             super.visitItem(item)
95                         }
96 
97                         private fun unmarkRecent(new: Item) {
98                             val annotation = NullnessMigration.findNullnessAnnotation(new) ?: return
99                             // Nullness information change: Add migration annotation
100                             val annotationClass =
101                                 if (annotation.isNullable()) ANDROIDX_NULLABLE else ANDROIDX_NONNULL
102 
103                             val modifiers = new.mutableModifiers()
104                             modifiers.removeAnnotation(annotation)
105 
106                             modifiers.addAnnotation(
107                                 new.codebase.createAnnotation(
108                                     "@$annotationClass",
109                                     new,
110                                 )
111                             )
112                         }
113                     }
114                 )
115                 assert(!SUPPORT_TYPE_USE_ANNOTATIONS) {
116                     "We'll need to rewrite type annotations here too"
117                 }
118             }
119 
120             // Read deprecated attributes. Seem to be missing from code model;
121             // try to read via ASM instead since it must clearly be there.
122             markDeprecated(jarCodebase, apiJar, apiJar.path)
123 
124             // ASM doesn't seem to pick up everything that's actually there according to
125             // javap. So as another fallback, read from the existing signature files:
126             if (oldApiFile.isFile) {
127                 try {
128                     val oldCodebase = signatureFileLoader.load(SignatureFile.fromFile(oldApiFile))
129                     val visitor =
130                         object : ComparisonVisitor() {
131                             override fun compare(old: Item, new: Item) {
132                                 if (old.originallyDeprecated && old !is PackageItem) {
133                                     new.deprecateIfRequired("previous signature file for $old")
134                                 }
135                             }
136                         }
137                     CodebaseComparator(apiVisitorConfig = ApiVisitor.Config())
138                         .compare(visitor, oldCodebase, jarCodebase, null)
139                 } catch (e: Exception) {
140                     throw IllegalStateException("Could not load $oldApiFile: ${e.message}", e)
141                 }
142             }
143 
144             createReportFile(progressTracker, jarCodebase, newApiFile, "API") { printWriter ->
145                 SignatureWriter(
146                     printWriter,
147                     apiEmit,
148                     apiReference,
149                     jarCodebase.preFiltered,
150                     fileFormat = fileFormat,
151                     showUnannotated = false,
152                     apiVisitorConfig = ApiVisitor.Config(),
153                 )
154             }
155 
156             // Delete older redundant .xml files
157             val xmlFile = File(newApiFile.parentFile, "android.xml")
158             if (xmlFile.isFile) {
159                 xmlFile.delete()
160             }
161 
162             api++
163         }
164     }
165 
166     private fun markDeprecated(codebase: Codebase, file: File, path: String) {
167         when {
168             file.name.endsWith(SdkConstants.DOT_JAR) ->
169                 try {
170                     ZipFile(file).use { jar ->
171                         val enumeration = jar.entries()
172                         while (enumeration.hasMoreElements()) {
173                             val entry = enumeration.nextElement()
174                             if (entry.name.endsWith(SdkConstants.DOT_CLASS)) {
175                                 try {
176                                     jar.getInputStream(entry).use { inputStream ->
177                                         val bytes = inputStream.readBytes()
178                                         markDeprecated(codebase, bytes, path + ":" + entry.name)
179                                     }
180                                 } catch (e: Exception) {
181                                     stdout.println(
182                                         "Could not read jar file entry ${entry.name} from $file: $e"
183                                     )
184                                 }
185                             }
186                         }
187                     }
188                 } catch (e: IOException) {
189                     stdout.println("Could not read jar file contents from $file: $e")
190                 }
191             file.isDirectory -> {
192                 val listFiles = file.listFiles()
193                 listFiles?.forEach { markDeprecated(codebase, it, it.path) }
194             }
195             file.path.endsWith(SdkConstants.DOT_CLASS) -> {
196                 val bytes = file.readBytes()
197                 markDeprecated(codebase, bytes, file.path)
198             }
199             else -> stdout.println("Ignoring entry $file")
200         }
201     }
202 
203     private fun markDeprecated(codebase: Codebase, bytes: ByteArray, path: String) {
204         val reader: ClassReader
205         val classNode: ClassNode
206         try {
207             // TODO: We don't actually need to build a DOM.
208             reader = ClassReader(bytes)
209             classNode = ClassNode()
210             reader.accept(classNode, 0)
211         } catch (t: Throwable) {
212             stderr.println("Error processing $path: broken class file?")
213             return
214         }
215 
216         if ((classNode.access and Opcodes.ACC_DEPRECATED) != 0) {
217             val item = codebase.findClass(classNode, MATCH_ALL)
218             item.deprecateIfRequired("byte code for ${classNode.name}")
219         }
220 
221         val methodList = classNode.methods
222         for (f in methodList) {
223             val methodNode = f as MethodNode
224             if ((methodNode.access and Opcodes.ACC_DEPRECATED) == 0) {
225                 continue
226             }
227             val item = codebase.findMethod(classNode, methodNode, MATCH_ALL)
228             item.deprecateIfRequired("byte code for ${methodNode.name}")
229         }
230 
231         val fieldList = classNode.fields
232         for (f in fieldList) {
233             val fieldNode = f as FieldNode
234             if ((fieldNode.access and Opcodes.ACC_DEPRECATED) == 0) {
235                 continue
236             }
237             val item = codebase.findField(classNode, fieldNode, MATCH_ALL)
238             item.deprecateIfRequired("byte code for ${fieldNode.name}")
239         }
240     }
241 
242     /** Mark the [Item] as deprecated if required. */
243     private fun Item?.deprecateIfRequired(source: String) {
244         this ?: return
245         if (!originallyDeprecated) {
246             // Set the deprecated flag in the modifiers which underpins [originallyDeprecated].
247             mutableModifiers().setDeprecated(true)
248             progressTracker.progress("Turned deprecation on for $this from $source")
249         }
250     }
251 
252     companion object {
253         val MATCH_ALL: Predicate<Item> = Predicate { true }
254     }
255 }
256 
257 /** Finds the given class by JVM owner */
Codebasenull258 private fun Codebase.findClassByOwner(owner: String, apiFilter: Predicate<Item>): ClassItem? {
259     val className = owner.replace('/', '.').replace('$', '.')
260     val cls = findClass(className)
261     return if (cls != null && apiFilter.test(cls)) {
262         cls
263     } else {
264         null
265     }
266 }
267 
Codebasenull268 private fun Codebase.findClass(node: ClassNode, apiFilter: Predicate<Item>): ClassItem? {
269     return findClassByOwner(node.name, apiFilter)
270 }
271 
findMethodnull272 private fun Codebase.findMethod(
273     classNode: ClassNode,
274     node: MethodNode,
275     apiFilter: Predicate<Item>
276 ): MethodItem? {
277     val cls = findClass(classNode, apiFilter) ?: return null
278     val types = Type.getArgumentTypes(node.desc)
279     val parameters =
280         if (types.isNotEmpty()) {
281             val sb = StringBuilder()
282             for (type in types) {
283                 if (sb.isNotEmpty()) {
284                     sb.append(", ")
285                 }
286                 sb.append(type.className.replace('/', '.').replace('$', '.'))
287             }
288             sb.toString()
289         } else {
290             ""
291         }
292     val methodName = if (node.name == "<init>") cls.simpleName() else node.name
293     val method = cls.findMethod(methodName, parameters)
294     return if (method != null && apiFilter.test(method)) {
295         method
296     } else {
297         null
298     }
299 }
300 
Codebasenull301 private fun Codebase.findField(
302     classNode: ClassNode,
303     node: FieldNode,
304     apiFilter: Predicate<Item>
305 ): FieldItem? {
306     val cls = findClass(classNode, apiFilter) ?: return null
307     val field = cls.findField(node.name + 2)
308     return if (field != null && apiFilter.test(field)) {
309         field
310     } else {
311         null
312     }
313 }
314