blob: c2dee1689801b75eff0bf33b4cc60d88d3ef433a [file] [log] [blame]
/*
* Copyright (C) 2017 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.stub
import com.android.tools.metalava.model.AnnotationTarget
import com.android.tools.metalava.model.BaseItemVisitor
import com.android.tools.metalava.model.ClassItem
import com.android.tools.metalava.model.ConstructorItem
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.ModifierList
import com.android.tools.metalava.model.PrimitiveTypeItem
import com.android.tools.metalava.model.TypeParameterList
import com.android.tools.metalava.model.VariableTypeItem
import com.android.tools.metalava.options
import java.io.PrintWriter
import java.util.function.Predicate
internal class JavaStubWriter(
private val writer: PrintWriter,
private val filterEmit: Predicate<Item>,
private val filterReference: Predicate<Item>,
private val generateAnnotations: Boolean = false,
private val preFiltered: Boolean = true,
private val docStubs: Boolean
) : BaseItemVisitor() {
private val annotationTarget =
if (docStubs) AnnotationTarget.DOC_STUBS_FILE else AnnotationTarget.SDK_STUBS_FILE
override fun visitClass(cls: ClassItem) {
if (cls.isTopLevelClass()) {
val qualifiedName = cls.containingPackage().qualifiedName()
if (qualifiedName.isNotBlank()) {
writer.println("package $qualifiedName;")
writer.println()
}
@Suppress("DEPRECATION")
if (options.includeDocumentationInStubs) {
// All the classes referenced in the stubs are fully qualified, so no imports are
// needed. However, in some cases for javadoc, replacement with fully qualified name
// fails, and thus we need to include imports for the stubs to compile.
cls.getSourceFile()?.getImports(filterReference)?.let {
for (item in it) {
if (item.isMember) {
writer.println("import static ${item.pattern};")
} else {
writer.println("import ${item.pattern};")
}
}
writer.println()
}
}
}
appendDocumentation(cls, writer, docStubs)
// "ALL" doesn't do it; compiler still warns unless you actually explicitly list "unchecked"
writer.println("@SuppressWarnings({\"unchecked\", \"deprecation\", \"all\"})")
// Need to filter out abstract from the modifiers list and turn it
// into a concrete method to make the stub compile
val removeAbstract = cls.modifiers.isAbstract() && (cls.isEnum() || cls.isAnnotationType())
appendModifiers(cls, removeAbstract)
when {
cls.isAnnotationType() -> writer.print("@interface")
cls.isInterface() -> writer.print("interface")
cls.isEnum() -> writer.print("enum")
else -> writer.print("class")
}
writer.print(" ")
writer.print(cls.simpleName())
generateTypeParameterList(typeList = cls.typeParameterList(), addSpace = false)
generateSuperClassDeclaration(cls)
generateInterfaceList(cls)
writer.print(" {\n")
if (cls.isEnum()) {
var first = true
// Enums should preserve the original source order, not alphabetical etc. sort
for (field in cls.filteredFields(filterReference, true).sortedBy { it.sortingRank }) {
if (field.isEnumConstant()) {
if (first) {
first = false
} else {
writer.write(",\n")
}
appendDocumentation(field, writer, docStubs)
// Can't just appendModifiers(field, true, true): enum constants
// don't take modifier lists, only annotations
ModifierList.writeAnnotations(
item = field,
target = annotationTarget,
runtimeAnnotationsOnly = !generateAnnotations,
includeDeprecated = true,
writer = writer,
separateLines = true,
list = field.modifiers,
skipNullnessAnnotations = false,
omitCommonPackages = false
)
writer.write(field.name())
}
}
writer.println(";")
}
generateMissingConstructors(cls)
}
override fun afterVisitClass(cls: ClassItem) {
writer.print("}\n\n")
}
private fun appendModifiers(
item: Item,
removeAbstract: Boolean = false,
removeFinal: Boolean = false,
addPublic: Boolean = false
) {
appendModifiers(item, item.modifiers, removeAbstract, removeFinal, addPublic)
}
private fun appendModifiers(
item: Item,
modifiers: ModifierList,
removeAbstract: Boolean,
removeFinal: Boolean = false,
addPublic: Boolean = false
) {
val separateLines = item is ClassItem || item is MethodItem
ModifierList.write(
writer,
modifiers,
item,
target = annotationTarget,
includeDeprecated = true,
runtimeAnnotationsOnly = !generateAnnotations,
removeAbstract = removeAbstract,
removeFinal = removeFinal,
addPublic = addPublic,
separateLines = separateLines
)
}
private fun generateSuperClassDeclaration(cls: ClassItem) {
if (cls.isEnum() || cls.isAnnotationType()) {
// No extends statement for enums and annotations; it's implied by the "enum" and
// "@interface" keywords
return
}
val superClass =
if (preFiltered) cls.superClassType() else cls.filteredSuperClassType(filterReference)
if (superClass != null && !superClass.isJavaLangObject()) {
val qualifiedName = superClass.toTypeString()
writer.print(" extends ")
if (qualifiedName.contains("<")) {
// TODO: I need to push this into the model at filter-time such that clients don't
// need
// to remember to do this!!
val s = superClass.asClass()
if (s != null) {
val map = cls.mapTypeVariables(s)
val replaced = superClass.convertTypeString(map)
writer.print(replaced)
return
}
}
writer.print(qualifiedName)
}
}
private fun generateInterfaceList(cls: ClassItem) {
if (cls.isAnnotationType()) {
// No extends statement for annotations; it's implied by the "@interface" keyword
return
}
val interfaces =
if (preFiltered) cls.interfaceTypes().asSequence()
else cls.filteredInterfaceTypes(filterReference).asSequence()
if (interfaces.any()) {
if (cls.isInterface() && cls.superClassType() != null) writer.print(", ")
else writer.print(" implements")
interfaces.forEachIndexed { index, type ->
if (index > 0) {
writer.print(",")
}
writer.print(" ")
writer.print(type.toTypeString())
}
}
}
private fun generateTypeParameterList(typeList: TypeParameterList, addSpace: Boolean) {
// TODO: Do I need to map type variables?
val typeListString = typeList.toString()
if (typeListString.isNotEmpty()) {
writer.print(typeListString)
if (addSpace) {
writer.print(' ')
}
}
}
override fun visitConstructor(constructor: ConstructorItem) {
writeConstructor(constructor, constructor.superConstructor)
}
private fun writeConstructor(constructor: MethodItem, superConstructor: MethodItem?) {
writer.println()
appendDocumentation(constructor, writer, docStubs)
appendModifiers(constructor, false)
generateTypeParameterList(typeList = constructor.typeParameterList(), addSpace = true)
writer.print(constructor.containingClass().simpleName())
generateParameterList(constructor)
generateThrowsList(constructor)
writer.print(" { ")
writeConstructorBody(constructor, superConstructor)
writer.println(" }")
}
private fun writeConstructorBody(constructor: MethodItem, superConstructor: MethodItem?) {
// Find any constructor in parent that we can compile against
superConstructor?.let { it ->
val parameters = it.parameters()
if (parameters.isNotEmpty()) {
val includeCasts =
it.containingClass().constructors().filter { filterReference.test(it) }.size > 1
writer.print("super(")
parameters.forEachIndexed { index, parameter ->
if (index > 0) {
writer.write(", ")
}
val type = parameter.type()
if (type !is PrimitiveTypeItem) {
if (includeCasts) {
// Types with varargs can't appear as varargs when used as an argument
val typeString = type.toErasedTypeString(it).replace("...", "[]")
writer.write("(")
if (type is VariableTypeItem) {
// It's a type parameter: see if we should map the type back to the
// concrete
// type in this class
val map =
constructor
.containingClass()
.mapTypeVariables(it.containingClass())
val cast = map[type.toTypeString(context = it)] ?: typeString
writer.write(cast)
} else {
writer.write(typeString)
}
writer.write(")")
}
writer.write("null")
} else {
// Add cast for things like shorts and bytes
val typeString = type.toTypeString(context = it)
if (
typeString != "boolean" && typeString != "int" && typeString != "long"
) {
writer.write("(")
writer.write(typeString)
writer.write(")")
}
writer.write(type.defaultValueString())
}
}
writer.print("); ")
}
}
writeThrowStub()
}
private fun generateMissingConstructors(cls: ClassItem) {
val clsStubConstructor = cls.stubConstructor
val constructors = cls.filteredConstructors(filterEmit)
// If the default stub constructor is not publicly visible then it won't be output during
// the normal visiting
// so visit it specially to ensure that it is output.
if (clsStubConstructor != null && !constructors.contains(clsStubConstructor)) {
visitConstructor(clsStubConstructor)
return
}
}
override fun visitMethod(method: MethodItem) {
writeMethod(method.containingClass(), method)
}
private fun writeMethod(containingClass: ClassItem, method: MethodItem) {
val modifiers = method.modifiers
val isEnum = containingClass.isEnum()
val isAnnotation = containingClass.isAnnotationType()
if (method.isEnumSyntheticMethod()) {
// Skip the values() and valueOf(String) methods in enums: these are added by
// the compiler for enums anyway, but was part of the doclava1 signature files
// so inserted in compat mode.
return
}
writer.println()
appendDocumentation(method, writer, docStubs)
// Need to filter out abstract from the modifiers list and turn it
// into a concrete method to make the stub compile
val removeAbstract = modifiers.isAbstract() && (isEnum || isAnnotation)
appendModifiers(method, modifiers, removeAbstract, false)
generateTypeParameterList(typeList = method.typeParameterList(), addSpace = true)
val returnType = method.returnType()
writer.print(
returnType.toTypeString(
outerAnnotations = false,
innerAnnotations = generateAnnotations,
filter = filterReference
)
)
writer.print(' ')
writer.print(method.name())
generateParameterList(method)
generateThrowsList(method)
if (isAnnotation) {
val default = method.defaultValue()
if (default.isNotEmpty()) {
writer.print(" default ")
writer.print(default)
}
}
if (
modifiers.isAbstract() && !removeAbstract && !isEnum ||
isAnnotation ||
modifiers.isNative()
) {
writer.println(";")
} else {
writer.print(" { ")
writeThrowStub()
writer.println(" }")
}
}
override fun visitField(field: FieldItem) {
// Handled earlier in visitClass
if (field.isEnumConstant()) {
return
}
writer.println()
appendDocumentation(field, writer, docStubs)
appendModifiers(field, removeAbstract = false, removeFinal = false)
writer.print(
field
.type()
.toTypeString(
outerAnnotations = false,
innerAnnotations = generateAnnotations,
filter = filterReference
)
)
writer.print(' ')
writer.print(field.name())
val needsInitialization =
field.modifiers.isFinal() &&
field.initialValue(true) == null &&
field.containingClass().isClass()
field.writeValueWithSemicolon(
writer,
allowDefaultValue = !needsInitialization,
requireInitialValue = !needsInitialization
)
writer.print("\n")
if (needsInitialization) {
if (field.modifiers.isStatic()) {
writer.print("static ")
}
writer.print("{ ${field.name()} = ${field.type().defaultValueString()}; }\n")
}
}
private fun writeThrowStub() {
writer.write("throw new RuntimeException(\"Stub!\");")
}
private fun generateParameterList(method: MethodItem) {
writer.print("(")
method.parameters().asSequence().forEachIndexed { i, parameter ->
if (i > 0) {
writer.print(", ")
}
appendModifiers(parameter, false)
writer.print(
parameter
.type()
.toTypeString(
outerAnnotations = false,
innerAnnotations = generateAnnotations,
filter = filterReference
)
)
writer.print(' ')
val name = parameter.publicName() ?: parameter.name()
writer.print(name)
}
writer.print(")")
}
private fun generateThrowsList(method: MethodItem) {
// Note that throws types are already sorted internally to help comparison matching
val throws =
if (preFiltered) {
method.throwsTypes().asSequence()
} else {
method.filteredThrowsTypes(filterReference).asSequence()
}
if (throws.any()) {
writer.print(" throws ")
throws.sortedWith(ClassItem.fullNameComparator).forEachIndexed { i, type ->
if (i > 0) {
writer.print(", ")
}
// TODO: Shouldn't declare raw types here!
writer.print(type.qualifiedName())
}
}
}
}