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