Merge changes Ibcd0a287,I4a8b5d75,I78936db3 am: 9708180ac0 am: d267cfb1aa am: e8dfa0ca95
Original change: https://android-review.googlesource.com/c/platform/tools/metalava/+/2526700
Change-Id: I5a40fc11ef0dfe20f42f9b1841131d48fc3d71ce
Signed-off-by: Automerger Merge Worker <[email protected]>
diff --git a/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt b/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt
index f15bd34..8c7ecc2 100644
--- a/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt
+++ b/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt
@@ -856,7 +856,7 @@
if (!method.isConstructor()) {
checkTypeReferencesHidden(
method,
- method.returnType()!!
+ method.returnType()
) // returnType is nullable only for constructors
}
@@ -880,7 +880,7 @@
method.mutableModifiers().removeAnnotation(it)
// Have to also clear the annotation out of the return type itself, if it's a type
// use annotation
- method.returnType()?.scrubAnnotations()
+ method.returnType().scrubAnnotations()
}
}
}
@@ -987,7 +987,7 @@
}
val returnType = m.returnType()
- if (!m.deprecated && !cl.deprecated && returnType != null && returnType.asClass()?.deprecated == true) {
+ if (!m.deprecated && !cl.deprecated && returnType.asClass()?.deprecated == true) {
reporter.report(
Issues.REFERENCES_DEPRECATED, m,
"Return type of deprecated type $returnType in ${cl.qualifiedName()}.${m.name()}(): this method should also be deprecated"
@@ -996,7 +996,7 @@
var hiddenClass = findHiddenClasses(returnType, stubImportPackages)
if (hiddenClass != null && !hiddenClass.isFromClassPath()) {
- if (hiddenClass.qualifiedName() == returnType?.asClass()?.qualifiedName()) {
+ if (hiddenClass.qualifiedName() == returnType.asClass()?.qualifiedName()) {
// Return type is hidden
reporter.report(
Issues.UNAVAILABLE_SYMBOL, m,
@@ -1043,7 +1043,7 @@
}
val t = m.returnType()
- if (t != null && !t.primitive && !m.deprecated && !cl.deprecated && t.asClass()?.deprecated == true) {
+ if (!t.primitive && !m.deprecated && !cl.deprecated && t.asClass()?.deprecated == true) {
reporter.report(
Issues.REFERENCES_DEPRECATED, m,
"Returning deprecated type $t from ${cl.qualifiedName()}.${m.name()}(): this method should also be deprecated"
@@ -1234,7 +1234,7 @@
cantStripThis(thrown, filter, notStrippable, stubImportPackages, method, "as exception")
}
val returnType = method.returnType()
- if (returnType != null && !returnType.primitive) {
+ if (!returnType.primitive) {
val returnTypeClass = returnType.asClass()
if (returnTypeClass != null) {
cantStripThis(returnTypeClass, filter, notStrippable, stubImportPackages, method, "as return type")
diff --git a/src/main/java/com/android/tools/metalava/ApiLint.kt b/src/main/java/com/android/tools/metalava/ApiLint.kt
index 06c963e..3d27d1e 100644
--- a/src/main/java/com/android/tools/metalava/ApiLint.kt
+++ b/src/main/java/com/android/tools/metalava/ApiLint.kt
@@ -241,11 +241,9 @@
override fun visitMethod(method: MethodItem) {
checkMethod(method, filterReference)
val returnType = method.returnType()
- if (returnType != null) {
- checkType(returnType, method)
- checkNullableCollections(returnType, method)
- checkMethodSuffixListenableFutureReturn(returnType, method)
- }
+ checkType(returnType, method)
+ checkNullableCollections(returnType, method)
+ checkMethodSuffixListenableFutureReturn(returnType, method)
for (parameter in method.parameters()) {
checkType(parameter.type(), parameter)
}
@@ -577,7 +575,7 @@
method.parameters().size == 1 &&
method.name().startsWith("on") &&
!method.parameters().first().type().primitive &&
- method.returnType()?.toTypeString() == Void.TYPE.name
+ method.returnType().toTypeString() == Void.TYPE.name
if (!methods.all(::isSingleParamCallbackMethod)) return
@@ -915,7 +913,7 @@
}
private fun checkIntentBuilder(method: MethodItem) {
- if (method.returnType()?.toTypeString() == "android.content.Intent") {
+ if (method.returnType().toTypeString() == "android.content.Intent") {
val name = method.name()
if (name.startsWith("create") && name.endsWith("Intent")) {
return
@@ -1052,27 +1050,25 @@
)
} else if (name.startsWith("set") || name.startsWith("add") || name.startsWith("clear")) {
val returnType = method.returnType()
- if (returnType != null) {
- val returnsClassType = if (
- returnType is PsiTypeItem && clsType is PsiTypeItem
- ) {
- clsType.isAssignableFromWithoutUnboxing(returnType)
- } else {
- // fallback to a limited text based check
- val returnTypeBounds = returnType
- .asTypeParameter(context = method)
- ?.typeBounds()?.map {
- it.toTypeString()
- } ?: emptyList()
- returnTypeBounds.contains(clsType.toTypeString()) || returnType == clsType
- }
- if (!returnsClassType) {
- report(
- SETTER_RETURNS_THIS, method,
- "Methods must return the builder object (return type " +
- "$clsType instead of $returnType): ${method.describe()}"
- )
- }
+ val returnsClassType = if (
+ returnType is PsiTypeItem && clsType is PsiTypeItem
+ ) {
+ clsType.isAssignableFromWithoutUnboxing(returnType)
+ } else {
+ // fallback to a limited text based check
+ val returnTypeBounds = returnType
+ .asTypeParameter(context = method)
+ ?.typeBounds()?.map {
+ it.toTypeString()
+ } ?: emptyList()
+ returnTypeBounds.contains(clsType.toTypeString()) || returnType == clsType
+ }
+ if (!returnsClassType) {
+ report(
+ SETTER_RETURNS_THIS, method,
+ "Methods must return the builder object (return type " +
+ "$clsType instead of $returnType): ${method.describe()}"
+ )
}
if (method.modifiers.isNullable()) {
@@ -1245,16 +1241,14 @@
for (method in methodsAndConstructors) {
val returnType = method.returnType()
- if (returnType != null) { // not a constructor
- val returnTypeRank = getTypeRank(returnType)
- if (returnTypeRank != -1 && returnTypeRank < classRank) {
- report(
- PACKAGE_LAYERING, cls,
- "Method return type `${returnType.toTypeString()}` violates package layering: nothing in `$classPackage` should depend on `${getTypePackage(
- returnType
- )}`"
- )
- }
+ val returnTypeRank = getTypeRank(returnType)
+ if (returnTypeRank != -1 && returnTypeRank < classRank) {
+ report(
+ PACKAGE_LAYERING, cls,
+ "Method return type `${returnType.toTypeString()}` violates package layering: nothing in `$classPackage` should depend on `${getTypePackage(
+ returnType
+ )}`"
+ )
}
for (parameter in method.parameters()) {
@@ -1303,7 +1297,7 @@
}
fun isGetter(method: MethodItem): Boolean {
- val returnType = method.returnType() ?: return false
+ val returnType = method.returnType()
return method.parameters().isEmpty() && returnType.primitive && returnType.toTypeString() == "boolean"
}
@@ -1544,7 +1538,7 @@
)
}
for (method in methods) {
- if (method.returnType()?.asClass() == cls) {
+ if (method.returnType().asClass() == cls) {
report(
MANAGER_LOOKUP, method,
"Managers must always be obtained from Context (`${method.name()}`)"
@@ -1597,7 +1591,7 @@
is MethodItem -> {
// For methods requiresNullnessInfo and hasNullnessInfo considers both parameters and return,
// only warn about non-annotated returns here as parameters will get visited individually.
- if (item.isConstructor() || item.returnType()?.primitive == true) return
+ if (item.isConstructor() || item.returnType().primitive) return
if (item.modifiers.hasNullnessInfo()) return
"method `${item.name()}` return"
}
@@ -1634,9 +1628,8 @@
private fun anySuperMethodIsNonNull(method: MethodItem): Boolean {
return method.superMethods().any { superMethod ->
- superMethod.modifiers.isNonNull() &&
- // Disable check for generics
- superMethod.returnType()?.isTypeParameter() != true
+ // Disable check for generics
+ superMethod.modifiers.isNonNull() && !superMethod.returnType().isTypeParameter()
}
}
@@ -1656,9 +1649,8 @@
private fun anySuperMethodLacksNullnessInfo(method: MethodItem): Boolean {
return method.superMethods().any { superMethod ->
- !superMethod.hasNullnessInfo() &&
- // Disable check for generics
- superMethod.returnType()?.isTypeParameter() != true
+ // Disable check for generics
+ !superMethod.hasNullnessInfo() && !superMethod.returnType().isTypeParameter()
}
}
@@ -2124,7 +2116,7 @@
return
}
for (method in methods) {
- val returnType = method.returnType() ?: continue
+ val returnType = method.returnType()
if (returnType.primitive) {
return
}
@@ -2164,7 +2156,7 @@
}
private fun checkUnits(method: MethodItem) {
- val returnType = method.returnType() ?: return
+ val returnType = method.returnType()
var type = returnType.toTypeString()
val name = method.name()
if (type == "int" || type == "long" || type == "short") {
@@ -2254,7 +2246,7 @@
}
// https://kotlinlang.org/docs/reference/operator-overloading.html#increments-and-decrements
"inc", "dec" -> {
- if (method.parameters().isEmpty() && method.returnType()?.toTypeString() != "void") {
+ if (method.parameters().isEmpty() && method.returnType().toTypeString() != "void") {
flagKotlinOperator(
method, "Method can be invoked as a pre/postfix inc/decrement operator from Kotlin: `$name`"
)
@@ -2272,7 +2264,7 @@
if (methods.any {
it.name() == assignName &&
it.parameters().size == 1 &&
- it.returnType()?.toTypeString() == "void"
+ it.returnType().toTypeString() == "void"
}
) {
report(
@@ -2283,7 +2275,7 @@
}
// https://kotlinlang.org/docs/reference/operator-overloading.html#in
"contains" -> {
- if (method.parameters().size == 1 && method.returnType()?.toTypeString() == "boolean") {
+ if (method.parameters().size == 1 && method.returnType().toTypeString() == "boolean") {
flagKotlinOperator(
method, "Method can be invoked as a \"in\" operator from Kotlin: `$name`"
)
@@ -2315,7 +2307,7 @@
}
// https://kotlinlang.org/docs/reference/operator-overloading.html#assignments
"plusAssign", "minusAssign", "timesAssign", "divAssign", "remAssign", "modAssign" -> {
- if (method.parameters().size == 1 && method.returnType()?.toTypeString() == "void") {
+ if (method.parameters().size == 1 && method.returnType().toTypeString() == "void") {
flagKotlinOperator(
method, "Method can be invoked as a compound assignment operator from Kotlin: `$name`"
)
diff --git a/src/main/java/com/android/tools/metalava/CompatibilityCheck.kt b/src/main/java/com/android/tools/metalava/CompatibilityCheck.kt
index abf2c7f..89d3645 100644
--- a/src/main/java/com/android/tools/metalava/CompatibilityCheck.kt
+++ b/src/main/java/com/android/tools/metalava/CompatibilityCheck.kt
@@ -354,7 +354,7 @@
val oldReturnType = old.returnType()
val newReturnType = new.returnType()
- if (!new.isConstructor() && oldReturnType != null && newReturnType != null) {
+ if (!new.isConstructor()) {
val oldTypeParameter = oldReturnType.asTypeParameter(old)
val newTypeParameter = newReturnType.asTypeParameter(new)
var compatible = true
diff --git a/src/main/java/com/android/tools/metalava/DexApiWriter.kt b/src/main/java/com/android/tools/metalava/DexApiWriter.kt
index 1142b2e..c2e844e 100644
--- a/src/main/java/com/android/tools/metalava/DexApiWriter.kt
+++ b/src/main/java/com/android/tools/metalava/DexApiWriter.kt
@@ -59,7 +59,7 @@
writer.print("V")
} else {
val returnType = method.returnType()
- writer.print(returnType?.internalName(method) ?: "V")
+ writer.print(returnType.internalName(method))
}
writer.print("\n")
}
diff --git a/src/main/java/com/android/tools/metalava/ExtractAnnotations.kt b/src/main/java/com/android/tools/metalava/ExtractAnnotations.kt
index 2ad39c7..4dd3b36 100644
--- a/src/main/java/com/android/tools/metalava/ExtractAnnotations.kt
+++ b/src/main/java/com/android/tools/metalava/ExtractAnnotations.kt
@@ -455,8 +455,8 @@
if (isConstructor()) {
sb.append(escapeXml(containingClass().simpleName()))
- } else if (returnType() != null) {
- sb.append(escapeXml(returnType()!!.toTypeString()))
+ } else {
+ sb.append(escapeXml(returnType().toTypeString()))
sb.append(' ')
sb.append(escapeXml(name()))
}
diff --git a/src/main/java/com/android/tools/metalava/JDiffXmlWriter.kt b/src/main/java/com/android/tools/metalava/JDiffXmlWriter.kt
index cb6a114..607b21c 100644
--- a/src/main/java/com/android/tools/metalava/JDiffXmlWriter.kt
+++ b/src/main/java/com/android/tools/metalava/JDiffXmlWriter.kt
@@ -207,7 +207,7 @@
writer.print("<method name=\"")
writer.print(method.name())
- method.returnType()?.let {
+ method.returnType().let {
writer.print("\"\n return=\"")
writer.print(XmlUtils.toXmlAttributeValue(formatType(it)))
}
diff --git a/src/main/java/com/android/tools/metalava/NullnessMigration.kt b/src/main/java/com/android/tools/metalava/NullnessMigration.kt
index 77c9a83..3cccb11 100644
--- a/src/main/java/com/android/tools/metalava/NullnessMigration.kt
+++ b/src/main/java/com/android/tools/metalava/NullnessMigration.kt
@@ -50,8 +50,8 @@
override fun compare(old: MethodItem, new: MethodItem) {
@Suppress("ConstantConditionIf")
if (SUPPORT_TYPE_USE_ANNOTATIONS) {
- val newType = new.returnType() ?: return
- val oldType = old.returnType() ?: return
+ val newType = new.returnType()
+ val oldType = old.returnType()
checkType(oldType, newType)
}
}
diff --git a/src/main/java/com/android/tools/metalava/model/Item.kt b/src/main/java/com/android/tools/metalava/model/Item.kt
index f309fe4..462403c 100644
--- a/src/main/java/com/android/tools/metalava/model/Item.kt
+++ b/src/main/java/com/android/tools/metalava/model/Item.kt
@@ -299,7 +299,7 @@
}
builder.append(' ')
if (includeReturnValue && !item.isConstructor()) {
- builder.append(item.returnType()?.toSimpleType())
+ builder.append(item.returnType().toSimpleType())
builder.append(' ')
}
appendMethodSignature(builder, item, includeParameterNames, includeParameterTypes)
diff --git a/src/main/java/com/android/tools/metalava/model/MethodItem.kt b/src/main/java/com/android/tools/metalava/model/MethodItem.kt
index e4eb38e..fce301a 100644
--- a/src/main/java/com/android/tools/metalava/model/MethodItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/MethodItem.kt
@@ -32,8 +32,8 @@
/** Whether this method is a constructor */
fun isConstructor(): Boolean
- /** The type of this field, or null for constructors */
- fun returnType(): TypeItem?
+ /** The type of this field. Returns the containing class for constructors */
+ fun returnType(): TypeItem
/** The list of parameters */
fun parameters(): List<ParameterItem>
@@ -71,7 +71,7 @@
}
sb.append(")")
- sb.append(if (voidConstructorTypes && isConstructor()) "V" else returnType()?.internalName() ?: "V")
+ sb.append(if (voidConstructorTypes && isConstructor()) "V" else returnType().internalName())
return sb.toString()
}
@@ -214,9 +214,7 @@
if (!isConstructor()) {
val type = returnType()
- if (type != null) { // always true when not a constructor
- visitor.visitType(type, this)
- }
+ visitor.visitType(type, this)
}
for (parameter in parameters()) {
@@ -229,9 +227,7 @@
if (!isConstructor()) {
val type = returnType()
- if (type != null) {
- visitor.visitType(type, this)
- }
+ visitor.visitType(type, this)
}
}
@@ -369,7 +365,7 @@
return when {
modifiers.hasJvmSyntheticAnnotation() -> false
isConstructor() -> false
- (returnType()?.primitive != true) -> true
+ (!returnType().primitive) -> true
parameters().any { !it.type().primitive } -> true
else -> false
}
@@ -380,7 +376,7 @@
return true
}
- if (!isConstructor() && returnType()?.primitive != true) {
+ if (!isConstructor() && !returnType().primitive) {
if (!modifiers.hasNullnessInfo()) {
return false
}
@@ -479,16 +475,14 @@
}
val returnType = returnType()
- if (returnType != null) {
- val returnTypeClass = returnType.asClass()
- if (returnTypeClass != null && !filterReference.test(returnTypeClass)) {
- return true
- }
- if (returnType.hasTypeArguments()) {
- for (argument in returnType.typeArgumentClasses()) {
- if (!filterReference.test(argument)) {
- return true
- }
+ val returnTypeClass = returnType.asClass()
+ if (returnTypeClass != null && !filterReference.test(returnTypeClass)) {
+ return true
+ }
+ if (returnType.hasTypeArguments()) {
+ for (argument in returnType.typeArgumentClasses()) {
+ if (!filterReference.test(argument)) {
+ return true
}
}
}
diff --git a/src/main/java/com/android/tools/metalava/model/TypeItem.kt b/src/main/java/com/android/tools/metalava/model/TypeItem.kt
index 8f93e52..960b471 100644
--- a/src/main/java/com/android/tools/metalava/model/TypeItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/TypeItem.kt
@@ -95,6 +95,15 @@
return s
}
+ /**
+ * Returns the element type if the type is an array or contains a vararg.
+ * If the element is not an array or does not contain a vararg,
+ * returns the original type string.
+ */
+ fun toElementType(): String {
+ return toErasedTypeString().replace("...", "").replace("[]", "")
+ }
+
val primitive: Boolean
fun typeArgumentClasses(): List<ClassItem>
@@ -140,6 +149,53 @@
fun hasTypeArguments(): Boolean = toTypeString().contains("<")
/**
+ * If the item has type arguments, return a list of type arguments.
+ * If simplified is true, returns the simplified forms of the type arguments.
+ * e.g. when type arguments are <K, V extends some.arbitrary.Class>, [K, V] will be returned.
+ * If the item does not have any type arguments, return an empty list.
+ */
+ fun typeArguments(simplified: Boolean = false): List<String> {
+ if (!hasTypeArguments()) {
+ return emptyList()
+ }
+ val typeString = toTypeString()
+ val bracketRemovedTypeString = toTypeString().indexOf('<')
+ .let { typeString.substring(it + 1, typeString.length - 1) }
+ val typeArguments = mutableListOf<String>()
+ var builder = StringBuilder()
+ var balance = 0
+ var idx = 0
+ while (idx < bracketRemovedTypeString.length) {
+ when (val s = bracketRemovedTypeString[idx]) {
+ ',' -> {
+ if (balance == 0) {
+ typeArguments.add(builder.toString())
+ builder = StringBuilder()
+ } else {
+ builder.append(s)
+ }
+ }
+ '<' -> {
+ balance += 1
+ builder.append(s)
+ }
+ '>' -> {
+ balance -= 1
+ builder.append(s)
+ }
+ else -> builder.append(s)
+ }
+ idx += 1
+ }
+ typeArguments.add(builder.toString())
+
+ if (simplified) {
+ return typeArguments.map { it.substringBefore(" extends ").trim() }
+ }
+ return typeArguments.map { it.trim() }
+ }
+
+ /**
* If this type is a type parameter, then return the corresponding [TypeParameterItem].
* The optional [context] provides the method or class where this type parameter
* appears, and can be used for example to resolve the bounds for a type variable
diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiMethodItem.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiMethodItem.kt
index 167173f..52ce6b1 100644
--- a/src/main/java/com/android/tools/metalava/model/psi/PsiMethodItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/psi/PsiMethodItem.kt
@@ -110,7 +110,7 @@
override fun isImplicitConstructor(): Boolean = false
- override fun returnType(): TypeItem? = returnType
+ override fun returnType(): TypeItem = returnType
override fun parameters(): List<PsiParameterItem> = parameters
@@ -316,7 +316,7 @@
}
val returnType = method.returnType()
- sb.append(returnType?.convertTypeString(replacementMap))
+ sb.append(returnType.convertTypeString(replacementMap))
sb.append(' ')
sb.append(method.name())
diff --git a/src/main/java/com/android/tools/metalava/model/text/TextClassItem.kt b/src/main/java/com/android/tools/metalava/model/text/TextClassItem.kt
index 98a3257..cae85c0 100644
--- a/src/main/java/com/android/tools/metalava/model/text/TextClassItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/text/TextClassItem.kt
@@ -324,5 +324,105 @@
return qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1)
}
+
+ private fun hasEqualTypeVar(
+ type1: TypeItem,
+ class1: ClassItem,
+ type2: TypeItem,
+ class2: ClassItem
+ ): Boolean {
+
+ // Given a type and its containing class,
+ // find the interface types that contains the type.
+ // For instance, for a method that looks like:
+ // class SomeClass implements InterfaceA<some.return.Type>, InterfaceB<some.return.Type>
+ // Type foo()
+ // this function will return [InterfaceA, InterfaceB] when Type and SomeClass
+ // are passed as inputs.
+ val typeContainingInterfaces = {
+ t: TypeItem, cl: ClassItem ->
+ val interfaceTypes = cl.interfaceTypes()
+ .plus(cl.toType())
+ .plus(cl.superClassType())
+ .filterNotNull()
+ interfaceTypes.filter {
+ val typeArgs = it.typeArguments(simplified = true)
+ t.toString() in typeArgs ||
+ t.toElementType() in typeArgs ||
+ t.asClass()?.superClass()?.qualifiedName() in typeArgs
+ }
+ }
+
+ val typeContainingInterfaces1 = typeContainingInterfaces(type1, class1)
+ val typeContainingInterfaces2 = typeContainingInterfaces(type2, class2)
+
+ if (typeContainingInterfaces1.isEmpty() || typeContainingInterfaces2.isEmpty()) {
+ return false
+ }
+
+ val interfaceTypesAreCovariant = {
+ t1: TypeItem, t2: TypeItem ->
+ t1.toErasedTypeString() == t2.toErasedTypeString() ||
+ t1.asClass()?.superClass()?.qualifiedName() == t2.asClass()?.qualifiedName() ||
+ t2.asClass()?.superClass()?.qualifiedName() == t1.asClass()?.qualifiedName()
+ }
+
+ // Check if the return type containing interfaces of the two methods have an intersection.
+ return typeContainingInterfaces1.any {
+ typeInterface1 ->
+ typeContainingInterfaces2.any {
+ typeInterface2 ->
+ interfaceTypesAreCovariant(typeInterface1, typeInterface2)
+ }
+ }
+ }
+
+ private fun hasEqualTypeBounds(method1: MethodItem, method2: MethodItem): Boolean {
+ val typeInTypeParams = {
+ t: TypeItem, m: MethodItem ->
+ t in m.typeParameterList().typeParameters().map { it.toType() }
+ }
+
+ val getTypeBounds = {
+ t: TypeItem, m: MethodItem ->
+ m.typeParameterList().typeParameters().single { it.toType() == t }.typeBounds().toSet()
+ }
+
+ val returnType1 = method1.returnType()
+ val returnType2 = method2.returnType()
+
+ // The following two methods are considered equal:
+ // method public <A extends some.package.SomeClass> A foo (Class<A>);
+ // method public <T extends some.package.SomeClass> T foo (Class<T>);
+ // This can be verified by converting return types to bounds ([some.package.SomeClass])
+ // and compare equivalence.
+ return typeInTypeParams(returnType1, method1) && typeInTypeParams(returnType2, method2) &&
+ getTypeBounds(returnType1, method1) == getTypeBounds(returnType2, method2)
+ }
+
+ /**
+ * Compares two [MethodItem]s and determines if the two methods have equal return types.
+ * The two methods' return types are considered equal even if the two are not identical,
+ * but are compatible in compiler level. For instance, return types in a same hierarchy tree
+ * are considered equal.
+ *
+ * @param method1 first [MethodItem] to compare the return type
+ * @param method2 second [MethodItem] to compare the return type
+ * @return a [Boolean] value representing if the two methods' return types are equal
+ */
+ fun hasEqualReturnType(method1: MethodItem, method2: MethodItem): Boolean {
+ val returnType1 = method1.returnType()
+ val returnType2 = method2.returnType()
+ val class1 = method1.containingClass()
+ val class2 = method2.containingClass()
+
+ if (returnType1 == returnType2) return true
+
+ if (hasEqualTypeVar(returnType1, class1, returnType2, class2)) return true
+
+ if (hasEqualTypeBounds(method1, method2)) return true
+
+ return false
+ }
}
}
diff --git a/src/main/java/com/android/tools/metalava/model/text/TextConstructorItem.kt b/src/main/java/com/android/tools/metalava/model/text/TextConstructorItem.kt
index 63382cd..4577801 100644
--- a/src/main/java/com/android/tools/metalava/model/text/TextConstructorItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/text/TextConstructorItem.kt
@@ -24,7 +24,7 @@
name: String,
containingClass: TextClassItem,
modifiers: TextModifiers,
- returnType: TextTypeItem?,
+ returnType: TextTypeItem,
position: SourcePositionInfo
) : TextMethodItem(codebase, name, containingClass, modifiers, returnType, position),
ConstructorItem {
diff --git a/src/main/java/com/android/tools/metalava/model/text/TextMethodItem.kt b/src/main/java/com/android/tools/metalava/model/text/TextMethodItem.kt
index 9dba79a..a010667 100644
--- a/src/main/java/com/android/tools/metalava/model/text/TextMethodItem.kt
+++ b/src/main/java/com/android/tools/metalava/model/text/TextMethodItem.kt
@@ -31,7 +31,7 @@
name: String,
containingClass: TextClassItem,
modifiers: TextModifiers,
- private val returnType: TextTypeItem?,
+ private val returnType: TextTypeItem,
position: SourcePositionInfo
) : TextMemberItem(
codebase, name, containingClass, position,
@@ -79,7 +79,7 @@
override fun isConstructor(): Boolean = false
- override fun returnType(): TypeItem? = returnType
+ override fun returnType(): TypeItem = returnType
override fun superMethods(): List<MethodItem> {
if (isConstructor()) {
diff --git a/src/main/java/com/android/tools/metalava/stub/JavaStubWriter.kt b/src/main/java/com/android/tools/metalava/stub/JavaStubWriter.kt
index b757078..41e77ed 100644
--- a/src/main/java/com/android/tools/metalava/stub/JavaStubWriter.kt
+++ b/src/main/java/com/android/tools/metalava/stub/JavaStubWriter.kt
@@ -353,7 +353,7 @@
val returnType = method.returnType()
writer.print(
- returnType?.toTypeString(
+ returnType.toTypeString(
outerAnnotations = false,
innerAnnotations = generateAnnotations,
filter = filterReference
diff --git a/src/test/java/com/android/tools/metalava/model/psi/PsiMethodItemTest.kt b/src/test/java/com/android/tools/metalava/model/psi/PsiMethodItemTest.kt
index 390643f..6a99f85 100644
--- a/src/test/java/com/android/tools/metalava/model/psi/PsiMethodItemTest.kt
+++ b/src/test/java/com/android/tools/metalava/model/psi/PsiMethodItemTest.kt
@@ -16,8 +16,10 @@
package com.android.tools.metalava.model.psi
+import com.android.tools.metalava.java
import com.android.tools.metalava.kotlin
import org.junit.Test
+import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertSame
@@ -48,4 +50,29 @@
assertNull(component1.property)
}
}
+
+ @Test
+ fun `method return type is non-null`() {
+ val codebase = java(
+ """
+ public class Foo {
+ public Foo() {}
+ public void bar() {}
+ }
+ """
+ )
+ testCodebase(codebase) { c ->
+ val ctorItem = c.assertClass("Foo").findMethod("Foo", "")
+ val ctorReturnType = ctorItem!!.returnType()
+
+ val methodItem = c.assertClass("Foo").findMethod("bar", "")
+ val methodReturnType = methodItem!!.returnType()
+
+ assertNotNull(ctorReturnType)
+ assertEquals("Foo", ctorReturnType.toString(), "Return type of the constructor item must be the containing class.")
+
+ assertNotNull(methodReturnType)
+ assertEquals("void", methodReturnType.toString(), "Return type of an method item should match the expected value.")
+ }
+ }
}
diff --git a/src/test/java/com/android/tools/metalava/model/text/TextClassItemTest.kt b/src/test/java/com/android/tools/metalava/model/text/TextClassItemTest.kt
new file mode 100644
index 0000000..e2f5c1a
--- /dev/null
+++ b/src/test/java/com/android/tools/metalava/model/text/TextClassItemTest.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2023 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.model.text
+
+import org.junit.Test
+import kotlin.test.assertTrue
+
+class TextClassItemTest {
+ @Test
+ fun `test hasEqualReturnType() when return types are derived from interface type variables`() {
+ val codebase = ApiFile.parseApi(
+ "test",
+ """
+ package java.lang {
+ public final class Float extends java.lang.Number implements java.lang.Comparable<java.lang.Float> {
+ }
+ }
+ package java.time {
+ public final class LocalDateTime implements java.time.chrono.ChronoLocalDateTime<java.time.LocalDate> java.io.Serializable java.time.temporal.Temporal java.time.temporal.TemporalAdjuster {
+ method public java.time.LocalDate toLocalDate();
+ }
+ }
+ package java.time.chrono {
+ public interface ChronoLocalDateTime<D extends java.time.chrono.ChronoLocalDate> extends java.time.temporal.Temporal java.lang.Comparable<java.time.chrono.ChronoLocalDateTime<?>> java.time.temporal.TemporalAdjuster {
+ method public D toLocalDate();
+ }
+ }
+ package android.animation {
+ public interface TypeEvaluator<T> {
+ method public T evaluate(float, T, T);
+ }
+ public class FloatEvaluator implements android.animation.TypeEvaluator<java.lang.Number> {
+ method public Float evaluate(float, Number, Number);
+ }
+ }
+ package android.widget {
+ public abstract class AdapterView<T extends android.widget.Adapter> extends android.view.ViewGroup {
+ method public abstract T getAdapter();
+ }
+ public abstract class AbsListView extends android.widget.AdapterView<android.widget.ListAdapter> implements android.widget.Filter.FilterListener android.text.TextWatcher android.view.ViewTreeObserver.OnGlobalLayoutListener android.view.ViewTreeObserver.OnTouchModeChangeListener {
+ }
+ public interface ListAdapter extends android.widget.Adapter {
+ }
+ @android.widget.RemoteViews.RemoteView public class ListView extends android.widget.AbsListView {
+ method public android.widget.ListAdapter getAdapter();
+ }
+ }
+ package android.content {
+ public abstract class AsyncTaskLoader<D> extends android.content.Loader<D> {
+ method public abstract D loadInBackground();
+ }
+ public class CursorLoader extends android.content.AsyncTaskLoader<android.database.Cursor> {
+ method public android.database.Cursor loadInBackground();
+ }
+ }
+ package android.database {
+ public final class CursorJoiner implements java.lang.Iterable<android.database.CursorJoiner.Result> java.util.Iterator<android.database.CursorJoiner.Result> {
+ method public android.database.CursorJoiner.Result next();
+ }
+ }
+ package java.util {
+ public interface Iterator<E> {
+ method public E next();
+ }
+ }
+ package java.lang.invoke {
+ public final class MethodType implements java.io.Serializable java.lang.invoke.TypeDescriptor.OfMethod<java.lang.Class<?>,java.lang.invoke.MethodType> {
+ method public java.lang.invoke.MethodType changeParameterType(int, Class<?>);
+ method public Class<?>[] parameterArray();
+ }
+ public static interface TypeDescriptor.OfMethod<F extends java.lang.invoke.TypeDescriptor.OfField<F>, M extends java.lang.invoke.TypeDescriptor.OfMethod<F, M>> extends java.lang.invoke.TypeDescriptor {
+ method public M changeParameterType(int, F);
+ method public F[] parameterArray();
+ }
+ }
+ """.trimIndent(),
+ false
+ )
+
+ val toLocalDate1 = codebase.getOrCreateClass("java.time.LocalDateTime").findMethod("toLocalDate", "")!!
+ val toLocalDate2 = codebase.getOrCreateClass("java.time.chrono.ChronoLocalDateTime").findMethod("toLocalDate", "")!!
+ val evaluate1 = codebase.getOrCreateClass("android.animation.TypeEvaluator<T>").findMethod("evaluate", "float, T, T")!!
+ val evaluate2 = codebase.getOrCreateClass("android.animation.FloatEvaluator").findMethod("evaluate", "float, java.lang.Number, java.lang.Number")!!
+ val loadInBackground1 = codebase.getOrCreateClass("android.content.AsyncTaskLoader<D>").findMethod("loadInBackground", "")!!
+ val loadInBackground2 = codebase.getOrCreateClass("android.content.CursorLoader").findMethod("loadInBackground", "")!!
+ val next1 = codebase.getOrCreateClass("android.database.CursorJoiner").findMethod("next", "")!!
+ val next2 = codebase.getOrCreateClass("java.util.Iterator<E>").findMethod("next", "")!!
+ val changeParameterType1 = codebase.getOrCreateClass("java.lang.invoke.MethodType").findMethod("changeParameterType", "int, java.lang.Class")!!
+ val changeParameterType2 = codebase.getOrCreateClass("java.lang.invoke.TypeDescriptor.OfMethod").findMethod("changeParameterType", "int, java.lang.invoke.TypeDescriptor.OfField")!!
+
+ assertTrue(TextClassItem.hasEqualReturnType(toLocalDate1, toLocalDate2))
+ assertTrue(TextClassItem.hasEqualReturnType(evaluate1, evaluate2))
+ assertTrue(TextClassItem.hasEqualReturnType(loadInBackground1, loadInBackground2))
+ assertTrue(TextClassItem.hasEqualReturnType(next1, next2))
+ assertTrue(TextClassItem.hasEqualReturnType(changeParameterType1, changeParameterType2))
+ }
+
+ @Test
+ fun `test hasEqualReturnType() with equal bounds return types`() {
+ val codebase = ApiFile.parseApi(
+ "test",
+ """
+ package java.lang {
+ public final class Class<T> implements java.lang.reflect.AnnotatedElement {
+ method @Nullable public <A extends java.lang.annotation.Annotation> A getAnnotation(@NonNull Class<A>);
+ }
+ public interface AnnotatedElement {
+ method @Nullable public <T extends java.lang.annotation.Annotation> T getAnnotation(@NonNull Class<T>);
+ }
+ }
+ """.trimIndent(),
+ false
+ )
+
+ val getAnnotation1 = codebase.getOrCreateClass("java.lang.Class<T>").findMethod("getAnnotation", "java.lang.Class")!!
+ val getAnnotation2 = codebase.getOrCreateClass("java.lang.AnnotatedElement").findMethod("getAnnotation", "java.lang.Class")!!
+
+ assertTrue(TextClassItem.hasEqualReturnType(getAnnotation1, getAnnotation2))
+ }
+}
diff --git a/src/test/java/com/android/tools/metalava/model/text/TextMethodItemTest.kt b/src/test/java/com/android/tools/metalava/model/text/TextMethodItemTest.kt
new file mode 100644
index 0000000..ab9db49
--- /dev/null
+++ b/src/test/java/com/android/tools/metalava/model/text/TextMethodItemTest.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2023 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.model.text
+
+import org.junit.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+
+class TextMethodItemTest {
+ @Test
+ fun `text method item return type is non-null`() {
+ val codebase = ApiFile.parseApi(
+ "test",
+ """
+ package test.pkg {
+ public class Foo {
+ ctor public Foo();
+ method public void bar();
+ }
+ }
+ """.trimIndent(),
+ false
+ )
+
+ val cls = codebase.findClass("test.pkg.Foo")!!
+ val ctorItem = cls.findMethod("Foo", "")!!
+ val methodItem = cls.findMethod("bar", "")!!
+
+ assertNotNull(ctorItem.returnType())
+ assertEquals("test.pkg.Foo", ctorItem.returnType().toString(), "Return type of the constructor item must be the containing class.")
+ assertNotNull(methodItem.returnType())
+ assertEquals("void", methodItem.returnType().toString(), "Return type of an method item should match the expected value.")
+ }
+}