• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2025 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.cli.historical
18 
19 import com.android.SdkConstants
20 import com.android.tools.metalava.CodebaseComparator
21 import com.android.tools.metalava.ComparisonVisitor
22 import com.android.tools.metalava.NullnessMigration
23 import com.android.tools.metalava.ProgressTracker
24 import com.android.tools.metalava.apilevels.ApiVersion
25 import com.android.tools.metalava.apilevels.PatternNode
26 import com.android.tools.metalava.cli.common.DefaultSignatureFileLoader
27 import com.android.tools.metalava.createReportFile
28 import com.android.tools.metalava.jar.JarCodebaseLoader
29 import com.android.tools.metalava.model.ANDROIDX_NONNULL
30 import com.android.tools.metalava.model.ANDROIDX_NULLABLE
31 import com.android.tools.metalava.model.ClassItem
32 import com.android.tools.metalava.model.Codebase
33 import com.android.tools.metalava.model.CodebaseFragment
34 import com.android.tools.metalava.model.DefaultAnnotationItem
35 import com.android.tools.metalava.model.FieldItem
36 import com.android.tools.metalava.model.FilterPredicate
37 import com.android.tools.metalava.model.Item
38 import com.android.tools.metalava.model.JAVA_LANG_DEPRECATED
39 import com.android.tools.metalava.model.MethodItem
40 import com.android.tools.metalava.model.PackageItem
41 import com.android.tools.metalava.model.SUPPORT_TYPE_USE_ANNOTATIONS
42 import com.android.tools.metalava.model.annotation.DefaultAnnotationManager
43 import com.android.tools.metalava.model.api.surface.ApiSurface
44 import com.android.tools.metalava.model.api.surface.ApiSurfaces
45 import com.android.tools.metalava.model.text.FileFormat
46 import com.android.tools.metalava.model.text.SignatureWriter
47 import com.android.tools.metalava.model.text.SnapshotDeltaMaker
48 import com.android.tools.metalava.model.text.createFilteringVisitorForSignatures
49 import com.android.tools.metalava.model.visitors.ApiPredicate
50 import com.android.tools.metalava.model.visitors.ApiType
51 import com.android.tools.metalava.model.visitors.ApiVisitor
52 import com.android.tools.metalava.reporter.BasicReporter
53 import java.io.File
54 import java.io.IOException
55 import java.io.PrintWriter
56 import java.util.zip.ZipFile
57 import org.objectweb.asm.ClassReader
58 import org.objectweb.asm.Opcodes
59 import org.objectweb.asm.Type
60 import org.objectweb.asm.tree.ClassNode
61 import org.objectweb.asm.tree.FieldNode
62 import org.objectweb.asm.tree.MethodNode
63 
64 /**
65  * In an Android source tree, rewrite the signature files in prebuilts/sdk by reading what's
66  * actually there in the android.jar files.
67  */
68 class ConvertJarsToSignatureFiles(
69     private val stderr: PrintWriter,
70     private val stdout: PrintWriter,
71     private val progressTracker: ProgressTracker,
72     private val fileFormat: FileFormat,
73     private val apiVersions: Set<ApiVersion>?,
74     private val apiSurfaces: ApiSurfaces,
75     private val selectedApiSurfaces: List<ApiSurface>,
76     private val jarCodebaseLoader: JarCodebaseLoader,
77     private val root: File
78 ) {
79     private val reporter = BasicReporter(stderr)
80 
81     fun convertJars() {
82         val scanConfig =
83             PatternNode.ScanConfig(
84                 dir = root,
85                 apiVersionFilter = apiVersions?.let { it::contains },
86                 apiSurfaceByName = apiSurfaces.byName,
87             )
88 
89         val historicalApis =
90             HistoricalApiVersionInfo.scan(
91                 reporter,
92                 jarFilePattern = "prebuilts/sdk/{version:major.minor?}/{surface}/android.jar",
93                 signatureFilePattern =
94                     "prebuilts/sdk/{version:major.minor?}/{surface}/api/android.txt",
95                 scanConfig,
96             )
97 
98         for (historicalApi in historicalApis) {
99             // Only convert the files of the selected ApiSurfaces, even if the other surfaces
100             // contribute to this, e.g. if `system` extends `public` and only `system` is selected
101             // then do not convert `public` files.
102             for (selectedApiSurface in selectedApiSurfaces) {
103                 val surfaceInfo = historicalApi.infoBySurface[selectedApiSurface] ?: continue
104                 convertJar(historicalApi.version, surfaceInfo)
105             }
106         }
107     }
108 
109     /**
110      * Convert a single jar for [version] in [SurfaceInfo.jarFile] into its corresponding signature
111      * file, i.e. [SurfaceInfo.signatureFile].
112      */
113     private fun convertJar(version: ApiVersion, surfaceInfo: SurfaceInfo) {
114         val jarFile = surfaceInfo.jarFile
115         val signatureFile = surfaceInfo.signatureFile
116 
117         progressTracker.progress("Writing signature files $signatureFile for $jarFile")
118 
119         val annotationManager = DefaultAnnotationManager()
120         val codebaseConfig =
121             Codebase.Config(
122                 annotationManager = annotationManager,
123                 apiSurfaces = apiSurfaces,
124                 reporter = reporter,
125             )
126         val signatureFileLoader = DefaultSignatureFileLoader(codebaseConfig)
127 
128         val jarCodebase =
129             jarCodebaseLoader.loadFromJarFile(
130                 jarFile,
131                 // Do not freeze codebases after loading as they may need to be modified.
132                 freezeCodebase = false,
133             )
134 
135         if (version.major >= 28) {
136             // As of API 28 we'll put nullness annotations into the jar but some of them may be
137             // @RecentlyNullable/@RecentlyNonNull. Translate these back into normal
138             // @Nullable/@NonNull
139             jarCodebase.accept(
140                 object :
141                     ApiVisitor(
142                         apiPredicateConfig = ApiPredicate.Config(),
143                     ) {
144                     override fun visitItem(item: Item) {
145                         unmarkRecent(item)
146                         super.visitItem(item)
147                     }
148 
149                     private fun unmarkRecent(new: Item) {
150                         val annotation = NullnessMigration.findNullnessAnnotation(new) ?: return
151                         // Nullness information change: Add migration annotation
152                         val annotationClass =
153                             if (annotation.isNullable()) ANDROIDX_NULLABLE else ANDROIDX_NONNULL
154 
155                         val replacementAnnotation =
156                             new.codebase.createAnnotation("@$annotationClass", new)
157                         new.mutateModifiers {
158                             mutateAnnotations {
159                                 remove(annotation)
160                                 replacementAnnotation?.let { add(it) }
161                             }
162                         }
163                     }
164                 }
165             )
166             assert(!SUPPORT_TYPE_USE_ANNOTATIONS) {
167                 "We'll need to rewrite type annotations here too"
168             }
169         }
170 
171         // Read deprecated attributes. Seem to be missing from code model; try to read via ASM
172         // instead since it must clearly be there.
173         markDeprecated(jarCodebase, jarFile, jarFile.path)
174 
175         // ASM doesn't seem to pick up everything that's actually there according to javap. So as
176         // another fallback, read from the existing signature files:
177         try {
178             // Read all the signature files that contribute to this surface to ensure that any
179             // deprecated classes in the extended surface are also deprecated in the extending
180             // surface.
181             val signatureFiles = surfaceInfo.contributingSignatureFiles()
182             val oldCodebase = signatureFileLoader.load(signatureFiles)
183             val visitor =
184                 object : ComparisonVisitor() {
185                     override fun compareItems(old: Item, new: Item) {
186                         if (old.originallyDeprecated && old !is PackageItem) {
187                             new.deprecateIfRequired("previous signature file for $old")
188                         }
189                     }
190                 }
191             CodebaseComparator().compare(visitor, oldCodebase, jarCodebase, null)
192         } catch (e: Exception) {
193             throw IllegalStateException("Could not load existing signature file: ${e.message}", e)
194         }
195 
196         val jarCodebaseFragment =
197             CodebaseFragment.create(
198                 jarCodebase,
199                 { delegate ->
200                     createFilteringVisitorForSignatures(
201                         delegate = delegate,
202                         fileFormat = fileFormat,
203                         apiType = ApiType.PUBLIC_API,
204                         preFiltered = jarCodebase.preFiltered,
205                         showUnannotated = false,
206                         apiPredicateConfig =
207                             ApiPredicate.Config(
208                                 addAdditionalOverrides = fileFormat.addAdditionalOverrides,
209                             ),
210                     )
211                 }
212             )
213 
214         val extendsInfo = surfaceInfo.extends
215         val outputCodebaseFragment =
216             if (extendsInfo == null) jarCodebaseFragment
217             else {
218                 val signatureFiles = extendsInfo.contributingSignatureFiles()
219                 val extendedCodebase = signatureFileLoader.load(signatureFiles)
220 
221                 SnapshotDeltaMaker.createDelta(
222                     base = extendedCodebase,
223                     codebaseFragment = jarCodebaseFragment,
224                 )
225             }
226 
227         createReportFile(progressTracker, outputCodebaseFragment, signatureFile, "API") {
228             printWriter ->
229             SignatureWriter(
230                 writer = printWriter,
231                 fileFormat = fileFormat,
232             )
233         }
234     }
235 
236     private fun markDeprecated(codebase: Codebase, file: File, path: String) {
237         when {
238             file.name.endsWith(SdkConstants.DOT_JAR) ->
239                 try {
240                     ZipFile(file).use { jar ->
241                         val enumeration = jar.entries()
242                         while (enumeration.hasMoreElements()) {
243                             val entry = enumeration.nextElement()
244                             if (entry.name.endsWith(SdkConstants.DOT_CLASS)) {
245                                 try {
246                                     jar.getInputStream(entry).use { inputStream ->
247                                         val bytes = inputStream.readBytes()
248                                         markDeprecated(codebase, bytes, path + ":" + entry.name)
249                                     }
250                                 } catch (e: Exception) {
251                                     stdout.println(
252                                         "Could not read jar file entry ${entry.name} from $file: $e"
253                                     )
254                                 }
255                             }
256                         }
257                     }
258                 } catch (e: IOException) {
259                     stdout.println("Could not read jar file contents from $file: $e")
260                 }
261             file.isDirectory -> {
262                 val listFiles = file.listFiles()
263                 listFiles?.forEach { markDeprecated(codebase, it, it.path) }
264             }
265             file.path.endsWith(SdkConstants.DOT_CLASS) -> {
266                 val bytes = file.readBytes()
267                 markDeprecated(codebase, bytes, file.path)
268             }
269             else -> stdout.println("Ignoring entry $file")
270         }
271     }
272 
273     private fun markDeprecated(codebase: Codebase, bytes: ByteArray, path: String) {
274         val reader: ClassReader
275         val classNode: ClassNode
276         try {
277             // TODO: We don't actually need to build a DOM.
278             reader = ClassReader(bytes)
279             classNode = ClassNode()
280             reader.accept(classNode, 0)
281         } catch (t: Throwable) {
282             stderr.println("Error processing $path: broken class file?")
283             return
284         }
285 
286         if ((classNode.access and Opcodes.ACC_DEPRECATED) != 0) {
287             val item = codebase.findClass(classNode, MATCH_ALL)
288             item.deprecateIfRequired("byte code for ${classNode.name}")
289         }
290 
291         val methodList = classNode.methods
292         for (f in methodList) {
293             val methodNode = f as MethodNode
294             if ((methodNode.access and Opcodes.ACC_DEPRECATED) == 0) {
295                 continue
296             }
297             val item = codebase.findMethod(classNode, methodNode, MATCH_ALL)
298             item.deprecateIfRequired("byte code for ${methodNode.name}")
299         }
300 
301         val fieldList = classNode.fields
302         for (f in fieldList) {
303             val fieldNode = f as FieldNode
304             if ((fieldNode.access and Opcodes.ACC_DEPRECATED) == 0) {
305                 continue
306             }
307             val item = codebase.findField(classNode, fieldNode, MATCH_ALL)
308             item.deprecateIfRequired("byte code for ${fieldNode.name}")
309         }
310     }
311 
312     /** Mark the [Item] as deprecated if required. */
313     private fun Item?.deprecateIfRequired(source: String) {
314         this ?: return
315         if (!originallyDeprecated) {
316             // Set the deprecated flag in the modifiers which underpins [originallyDeprecated].
317             mutateModifiers {
318                 setDeprecated(true)
319                 // Add a Deprecated annotation to be consistent with model providers.
320                 addAnnotation(
321                     DefaultAnnotationItem.create(
322                         codebase,
323                         JAVA_LANG_DEPRECATED,
324                         emptyList(),
325                         context = this@deprecateIfRequired
326                     )
327                 )
328             }
329             progressTracker.progress("Turned deprecation on for $this from $source")
330         }
331     }
332 
333     companion object {
334         val MATCH_ALL: FilterPredicate = FilterPredicate { true }
335     }
336 }
337 
338 /** Finds the given class by JVM owner */
Codebasenull339 private fun Codebase.findClassByOwner(owner: String, apiFilter: FilterPredicate): ClassItem? {
340     val className = owner.replace('/', '.').replace('$', '.')
341     val cls = findClass(className)
342     return if (cls != null && apiFilter.test(cls)) {
343         cls
344     } else {
345         null
346     }
347 }
348 
Codebasenull349 private fun Codebase.findClass(node: ClassNode, apiFilter: FilterPredicate): ClassItem? {
350     return findClassByOwner(node.name, apiFilter)
351 }
352 
findMethodnull353 private fun Codebase.findMethod(
354     classNode: ClassNode,
355     node: MethodNode,
356     apiFilter: FilterPredicate
357 ): MethodItem? {
358     val cls = findClass(classNode, apiFilter) ?: return null
359     val types = Type.getArgumentTypes(node.desc)
360     val parameters =
361         if (types.isNotEmpty()) {
362             val sb = StringBuilder()
363             for (type in types) {
364                 if (sb.isNotEmpty()) {
365                     sb.append(", ")
366                 }
367                 sb.append(type.className.replace('/', '.').replace('$', '.'))
368             }
369             sb.toString()
370         } else {
371             ""
372         }
373     val methodName = if (node.name == "<init>") cls.simpleName() else node.name
374     val method = cls.findMethod(methodName, parameters)
375     return if (method != null && apiFilter.test(method)) {
376         method
377     } else {
378         null
379     }
380 }
381 
Codebasenull382 private fun Codebase.findField(
383     classNode: ClassNode,
384     node: FieldNode,
385     apiFilter: FilterPredicate
386 ): FieldItem? {
387     val cls = findClass(classNode, apiFilter) ?: return null
388     val field = cls.findField(node.name + 2)
389     return if (field != null && apiFilter.test(field)) {
390         field
391     } else {
392         null
393     }
394 }
395