blob: d3d89a92f90c8d0fc0f122043c860d8089aa0136 [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.metalava
import com.android.SdkConstants
import com.android.tools.metalava.model.ANDROIDX_NONNULL
import com.android.tools.metalava.model.ANDROIDX_NULLABLE
import com.android.tools.metalava.model.ClassItem
import com.android.tools.metalava.model.Codebase
import com.android.tools.metalava.model.FieldItem
import com.android.tools.metalava.model.Item
import com.android.tools.metalava.model.MethodItem
import com.android.tools.metalava.model.PackageItem
import com.android.tools.metalava.model.SUPPORT_TYPE_USE_ANNOTATIONS
import com.android.tools.metalava.model.psi.PsiEnvironmentManager
import com.android.tools.metalava.model.visitors.ApiVisitor
import com.google.common.io.ByteStreams
import java.io.File
import java.io.IOException
import java.util.function.Predicate
import java.util.zip.ZipFile
import org.objectweb.asm.ClassReader
import org.objectweb.asm.Opcodes
import org.objectweb.asm.Type
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.FieldNode
import org.objectweb.asm.tree.MethodNode
/**
* In an Android source tree, rewrite the signature files in prebuilts/sdk by reading what's
* actually there in the android.jar files.
*/
class ConvertJarsToSignatureFiles {
fun convertJars(psiEnvironmentManager: PsiEnvironmentManager, root: File) {
var api = 1
while (true) {
val apiJar =
File(
root,
if (api <= 3) "prebuilts/tools/common/api-versions/android-$api/android.jar"
else "prebuilts/sdk/$api/public/android.jar"
)
if (!apiJar.isFile) {
break
}
val signatureFile = "prebuilts/sdk/$api/public/api/android.txt"
val oldApiFile = File(root, "prebuilts/sdk/$api/public/api/android.txt")
val newApiFile =
// Place new-style signature files in separate files?
// File(root, "prebuilts/sdk/$api/public/api/android.${if (options.compatOutput)
// "txt" else "v2.txt"}")
File(root, "prebuilts/sdk/$api/public/api/android.txt")
progress("Writing signature files $signatureFile for $apiJar")
// Treat android.jar file as not filtered since they contain misc stuff that shouldn't
// be
// there: package private super classes etc.
val jarCodebase =
loadFromJarFile(
PsiSourceParser(psiEnvironmentManager, reporter),
apiJar,
preFiltered = false,
DefaultAnnotationManager()
)
val apiEmit = ApiType.PUBLIC_API.getEmitFilter()
val apiReference = ApiType.PUBLIC_API.getReferenceFilter()
if (api >= 28) {
// As of API 28 we'll put nullness annotations into the jar but some of them
// may be @RecentlyNullable/@RecentlyNonNull. Translate these back into
// normal @Nullable/@NonNull
jarCodebase.accept(
object : ApiVisitor() {
override fun visitItem(item: Item) {
unmarkRecent(item)
super.visitItem(item)
}
private fun unmarkRecent(new: Item) {
val annotation = NullnessMigration.findNullnessAnnotation(new) ?: return
// Nullness information change: Add migration annotation
val annotationClass =
if (annotation.isNullable()) ANDROIDX_NULLABLE else ANDROIDX_NONNULL
val modifiers = new.mutableModifiers()
modifiers.removeAnnotation(annotation)
modifiers.addAnnotation(
new.codebase.createAnnotation(
"@$annotationClass",
new,
)
)
}
}
)
assert(!SUPPORT_TYPE_USE_ANNOTATIONS) {
"We'll need to rewrite type annotations here too"
}
}
// Sadly the old signature files have some APIs recorded as deprecated which
// are not in fact deprecated in the jar files. Try to pull this back in.
val oldRemovedFile = File(root, "prebuilts/sdk/$api/public/api/removed.txt")
if (oldRemovedFile.isFile) {
val oldCodebase = SignatureFileLoader.load(oldRemovedFile)
val visitor =
object : ComparisonVisitor() {
override fun compare(old: MethodItem, new: MethodItem) {
new.removed = true
progress("Removed $old")
}
override fun compare(old: FieldItem, new: FieldItem) {
new.removed = true
progress("Removed $old")
}
}
CodebaseComparator().compare(visitor, oldCodebase, jarCodebase, null)
}
// Read deprecated attributes. Seem to be missing from code model;
// try to read via ASM instead since it must clearly be there.
markDeprecated(jarCodebase, apiJar, apiJar.path)
// ASM doesn't seem to pick up everything that's actually there according to
// javap. So as another fallback, read from the existing signature files:
if (oldApiFile.isFile) {
val oldCodebase = SignatureFileLoader.load(oldApiFile)
val visitor =
object : ComparisonVisitor() {
override fun compare(old: Item, new: Item) {
if (old.deprecated && !new.deprecated && old !is PackageItem) {
new.deprecated = true
progress(
"Recorded deprecation from previous signature file for $old"
)
}
}
}
CodebaseComparator().compare(visitor, oldCodebase, jarCodebase, null)
}
createReportFile(jarCodebase, newApiFile, "API") { printWriter ->
SignatureWriter(printWriter, apiEmit, apiReference, jarCodebase.preFiltered)
}
// Delete older redundant .xml files
val xmlFile = File(newApiFile.parentFile, "android.xml")
if (xmlFile.isFile) {
xmlFile.delete()
}
api++
}
}
private fun markDeprecated(codebase: Codebase, file: File, path: String) {
when {
file.name.endsWith(SdkConstants.DOT_JAR) ->
try {
ZipFile(file).use { jar ->
val enumeration = jar.entries()
while (enumeration.hasMoreElements()) {
val entry = enumeration.nextElement()
if (entry.name.endsWith(SdkConstants.DOT_CLASS)) {
try {
jar.getInputStream(entry).use { `is` ->
val bytes = ByteStreams.toByteArray(`is`)
markDeprecated(codebase, bytes, path + ":" + entry.name)
}
} catch (e: Exception) {
options.stdout.println(
"Could not read jar file entry ${entry.name} from $file: $e"
)
}
}
}
}
} catch (e: IOException) {
options.stdout.println("Could not read jar file contents from $file: $e")
}
file.isDirectory -> {
val listFiles = file.listFiles()
listFiles?.forEach { markDeprecated(codebase, it, it.path) }
}
file.path.endsWith(SdkConstants.DOT_CLASS) -> {
val bytes = file.readBytes()
markDeprecated(codebase, bytes, file.path)
}
else -> options.stdout.println("Ignoring entry $file")
}
}
private fun markDeprecated(codebase: Codebase, bytes: ByteArray, path: String) {
val reader: ClassReader
val classNode: ClassNode
try {
// TODO: We don't actually need to build a DOM.
reader = ClassReader(bytes)
classNode = ClassNode()
reader.accept(classNode, 0)
} catch (t: Throwable) {
options.stderr.println("Error processing $path: broken class file?")
return
}
if ((classNode.access and Opcodes.ACC_DEPRECATED) != 0) {
val item = codebase.findClass(classNode, MATCH_ALL)
if (item != null && !item.deprecated) {
item.deprecated = true
progress("Turned deprecation on for $item")
}
}
val methodList = classNode.methods
for (f in methodList) {
val methodNode = f as MethodNode
if ((methodNode.access and Opcodes.ACC_DEPRECATED) == 0) {
continue
}
val item = codebase.findMethod(classNode, methodNode, MATCH_ALL)
if (item != null && !item.deprecated) {
item.deprecated = true
progress("Turned deprecation on for $item")
}
}
val fieldList = classNode.fields
for (f in fieldList) {
val fieldNode = f as FieldNode
if ((fieldNode.access and Opcodes.ACC_DEPRECATED) == 0) {
continue
}
val item = codebase.findField(classNode, fieldNode, MATCH_ALL)
if (item != null && !item.deprecated) {
item.deprecated = true
progress("Turned deprecation on for $item")
}
}
}
companion object {
val MATCH_ALL: Predicate<Item> = Predicate { true }
}
}
/** Finds the given class by JVM owner */
private fun Codebase.findClassByOwner(owner: String, apiFilter: Predicate<Item>): ClassItem? {
val className = owner.replace('/', '.').replace('$', '.')
val cls = findClass(className)
return if (cls != null && apiFilter.test(cls)) {
cls
} else {
null
}
}
private fun Codebase.findClass(node: ClassNode, apiFilter: Predicate<Item>): ClassItem? {
return findClassByOwner(node.name, apiFilter)
}
private fun Codebase.findMethod(
classNode: ClassNode,
node: MethodNode,
apiFilter: Predicate<Item>
): MethodItem? {
val cls = findClass(classNode, apiFilter) ?: return null
val types = Type.getArgumentTypes(node.desc)
val parameters =
if (types.isNotEmpty()) {
val sb = StringBuilder()
for (type in types) {
if (sb.isNotEmpty()) {
sb.append(", ")
}
sb.append(type.className.replace('/', '.').replace('$', '.'))
}
sb.toString()
} else {
""
}
val methodName = if (node.name == "<init>") cls.simpleName() else node.name
val method = cls.findMethod(methodName, parameters)
return if (method != null && apiFilter.test(method)) {
method
} else {
null
}
}
private fun Codebase.findField(
classNode: ClassNode,
node: FieldNode,
apiFilter: Predicate<Item>
): FieldItem? {
val cls = findClass(classNode, apiFilter) ?: return null
val field = cls.findField(node.name + 2)
return if (field != null && apiFilter.test(field)) {
field
} else {
null
}
}