diff --git a/build.gradle b/build.gradle index eedd62afb7b259591a580a5bcf9b253cd010713c..24126e6f4a228338fc234db051e8b681cad16439 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { - ext.gradle_version = '3.2.1' - ext.studio_version = '26.2.1' + ext.gradle_version = '3.4.0-beta01' + ext.studio_version = '26.4.0-beta01' ext.kotlin_version = '1.3.11' repositories { google() diff --git a/src/main/java/com/android/tools/metalava/ComparisonVisitor.kt b/src/main/java/com/android/tools/metalava/ComparisonVisitor.kt index 77d3dc070ae397093bd82e6eb0c4775abc2afde7..6b703c8007b9ab84c790e5bc7c8a462ba2e80bd1 100644 --- a/src/main/java/com/android/tools/metalava/ComparisonVisitor.kt +++ b/src/main/java/com/android/tools/metalava/ComparisonVisitor.kt @@ -333,7 +333,17 @@ class CodebaseComparator { val simpleType2 = parameter2.type().toCanonicalType() delta = simpleType1.compareTo(simpleType2) if (delta != 0) { - break + // Special case: Kotlin coroutines + if (simpleType1.startsWith("kotlin.coroutines.") && simpleType2.startsWith("kotlin.coroutines.")) { + val t1 = simpleType1.removePrefix("kotlin.coroutines.").removePrefix("experimental.") + val t2 = simpleType2.removePrefix("kotlin.coroutines.").removePrefix("experimental.") + delta = t1.compareTo(t2) + if (delta != 0) { + break + } + } else { + break + } } } } diff --git a/src/main/java/com/android/tools/metalava/CompatibilityCheck.kt b/src/main/java/com/android/tools/metalava/CompatibilityCheck.kt index 78aebe16ff64d43041b399eaee7c5cb14bc0392d..94ac90b4b451b459a0448bc71b2f1c9fa652304c 100644 --- a/src/main/java/com/android/tools/metalava/CompatibilityCheck.kt +++ b/src/main/java/com/android/tools/metalava/CompatibilityCheck.kt @@ -510,7 +510,7 @@ class CompatibilityCheck( } } - if (new.modifiers.isInline() && new.isKotlin()) { + if (new.modifiers.isInline()) { val oldTypes = old.typeParameterList().typeParameters() val newTypes = new.typeParameterList().typeParameters() for (i in 0 until oldTypes.size) { diff --git a/src/main/java/com/android/tools/metalava/Constants.kt b/src/main/java/com/android/tools/metalava/Constants.kt index 4e0a3c7318296344175a75d0a49690a90e8b5f50..88cd3a701cfca56d1e553267e506bc29efaf91a4 100644 --- a/src/main/java/com/android/tools/metalava/Constants.kt +++ b/src/main/java/com/android/tools/metalava/Constants.kt @@ -32,6 +32,9 @@ const val ANDROID_SYSTEM_API = "android.annotation.SystemApi" const val ANDROID_REQUIRES_PERMISSION = "android.annotation.RequiresPermission" const val RECENTLY_NULLABLE = "androidx.annotation.RecentlyNullable" const val RECENTLY_NONNULL = "androidx.annotation.RecentlyNonNull" +const val ANDROIDX_VISIBLE_FOR_TESTING = "androidx.annotation.VisibleForTesting" +const val ANDROID_SUPPORT_VISIBLE_FOR_TESTING = "android.support.annotation.VisibleForTesting" +const val ATTR_OTHERWISE = "otherwise" const val ENV_VAR_METALAVA_TESTS_RUNNING = "METALAVA_TESTS_RUNNING" const val ENV_VAR_METALAVA_DUMP_ARGV = "METALAVA_DUMP_ARGV" diff --git a/src/main/java/com/android/tools/metalava/DocAnalyzer.kt b/src/main/java/com/android/tools/metalava/DocAnalyzer.kt index 44eed3b410b3c0c72b61d468d2f5bfad8846b55b..b831a5b39dccb1e9c8117f4f7ac00ed08ae60f0c 100644 --- a/src/main/java/com/android/tools/metalava/DocAnalyzer.kt +++ b/src/main/java/com/android/tools/metalava/DocAnalyzer.kt @@ -224,7 +224,8 @@ class DocAnalyzer( } private fun handleKotlinDeprecation(annotation: AnnotationItem, item: Item) { - val text = annotation.findAttribute(ATTR_VALUE)?.value?.value()?.toString() ?: return + val text = (annotation.findAttribute("message") ?: annotation.findAttribute(ATTR_VALUE)) + ?.value?.value()?.toString() ?: return if (text.isBlank() || item.documentation.contains(text)) { return } diff --git a/src/main/java/com/android/tools/metalava/ExtractAnnotations.kt b/src/main/java/com/android/tools/metalava/ExtractAnnotations.kt index d69b1af6c7974392cbc0372f807f5d4f85e86034..9076f31d2d5fc73dd4b0bd037650b7fd9c6bc68b 100644 --- a/src/main/java/com/android/tools/metalava/ExtractAnnotations.kt +++ b/src/main/java/com/android/tools/metalava/ExtractAnnotations.kt @@ -34,6 +34,7 @@ import com.android.tools.metalava.model.psi.CodePrinter import com.android.tools.metalava.model.psi.PsiAnnotationItem import com.android.tools.metalava.model.psi.PsiClassItem import com.android.tools.metalava.model.psi.PsiMethodItem +import com.android.tools.metalava.model.psi.UAnnotationItem import com.android.tools.metalava.model.visitors.ApiVisitor import com.google.common.xml.XmlEscapers import com.intellij.psi.JavaRecursiveElementVisitor @@ -234,26 +235,33 @@ class ExtractAnnotations( ) } - if (typeDefAnnotation is PsiAnnotationItem && typeDefClass is PsiClassItem) { - val result = AnnotationHolder( - typeDefClass, typeDefAnnotation, - annotationLookup.findRealAnnotation( - typeDefAnnotation.psiAnnotation, - typeDefClass.psiClass, - null + val result = + if (typeDefAnnotation is PsiAnnotationItem && typeDefClass is PsiClassItem) { + AnnotationHolder( + typeDefClass, typeDefAnnotation, + annotationLookup.findRealAnnotation( + typeDefAnnotation.psiAnnotation, + typeDefClass.psiClass, + null + ) ) - ) - classToAnnotationHolder[className] = result - addItem(item, result) - - if (item is PsiMethodItem && result.uAnnotation != null && - !reporter.isSuppressed(Errors.RETURNING_UNEXPECTED_CONSTANT) - ) { - verifyReturnedConstants(item, result.uAnnotation, result, className) + } else if (typeDefAnnotation is UAnnotationItem && typeDefClass is PsiClassItem) { + AnnotationHolder( + typeDefClass, typeDefAnnotation, typeDefAnnotation.uAnnotation + ) + } else { + continue } - continue + classToAnnotationHolder[className] = result + addItem(item, result) + + if (item is PsiMethodItem && result.uAnnotation != null && + !reporter.isSuppressed(Errors.RETURNING_UNEXPECTED_CONSTANT) + ) { + verifyReturnedConstants(item, result.uAnnotation, result, className) } + continue } } } @@ -469,11 +477,12 @@ class ExtractAnnotations( ) { val annotationItem = annotationHolder.annotationItem val uAnnotation = annotationHolder.uAnnotation - ?: if (annotationItem is PsiAnnotationItem) { - // Imported annotation - JavaUAnnotation.wrap(annotationItem.psiAnnotation) - } else { - return + ?: when (annotationItem) { + is UAnnotationItem -> annotationItem.uAnnotation + is PsiAnnotationItem -> + // Imported annotation + JavaUAnnotation.wrap(annotationItem.psiAnnotation) + else -> return } val qualifiedName = annotationItem.qualifiedName() diff --git a/src/main/java/com/android/tools/metalava/KotlinInteropChecks.kt b/src/main/java/com/android/tools/metalava/KotlinInteropChecks.kt index a904af59aa4c320d1c3fcbc719203bbc40cf4a78..278d1c7fc4b92671307817852735d12a61b596cc 100644 --- a/src/main/java/com/android/tools/metalava/KotlinInteropChecks.kt +++ b/src/main/java/com/android/tools/metalava/KotlinInteropChecks.kt @@ -95,10 +95,8 @@ class KotlinInteropChecks { if (checked) { val annotation = method.modifiers.findAnnotation("kotlin.jvm.Throws") if (annotation != null) { - val attribute = - annotation.findAttribute("exceptionClasses") ?: annotation.findAttribute("value") - ?: annotation.attributes().firstOrNull() - if (attribute != null) { + // There can be multiple values + for (attribute in annotation.attributes()) { for (v in attribute.leafValues()) { val source = v.toSource() if (source.endsWith(exception.simpleName() + "::class")) { diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiAnnotationItem.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiAnnotationItem.kt index e8c6acb218495dce993a589c699c5ce1f56e57ab..decece4fe4962c4faa5e7d662f457fcc74dfc47f 100644 --- a/src/main/java/com/android/tools/metalava/model/psi/PsiAnnotationItem.kt +++ b/src/main/java/com/android/tools/metalava/model/psi/PsiAnnotationItem.kt @@ -284,7 +284,7 @@ class PsiAnnotationSingleAttributeValue( override val value: Any? get() { if (psiValue is PsiLiteral) { - return psiValue.value + return psiValue.value ?: psiValue.text.removeSurrounding("\"") } val value = ConstantEvaluator.evaluate(null, psiValue) @@ -292,7 +292,7 @@ class PsiAnnotationSingleAttributeValue( return value } - return psiValue.text + return psiValue.text ?: psiValue.text.removeSurrounding("\"") } override fun value(): Any? = value diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiClassItem.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiClassItem.kt index 564e15cbe5afe41f8da6339ce06fae61df1624a4..4a5ba9d34aae13be80225f69e26930fca0e3a72e 100644 --- a/src/main/java/com/android/tools/metalava/model/psi/PsiClassItem.kt +++ b/src/main/java/com/android/tools/metalava/model/psi/PsiClassItem.kt @@ -420,6 +420,8 @@ open class PsiClassItem( ) } + val isKotlin = isKotlin(psiClass) + val constructors: MutableList<PsiConstructorItem> = ArrayList(5) for (psiMethod in psiMethods) { if (psiMethod.isPrivate() || psiMethod.isPackagePrivate()) { @@ -468,7 +470,7 @@ open class PsiClassItem( item.fields = fields item.properties = emptyList() - if (isKotlin(psiClass)) { + if (isKotlin) { // Try to initialize the Kotlin properties val properties = mutableListOf<PsiPropertyItem>() for (method in psiMethods) { 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 f856c9193d17f9ef6d826628676a3ed7fb3862b9..c337332dc2deaad874e5b85312cb79afa7b338e8 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 @@ -357,9 +357,16 @@ open class PsiMethodItem( // methods with super methods also consider this method non-final.) modifiers.setFinal(false) } - val parameters = psiMethod.parameterList.parameters.mapIndexed { index, parameter -> - PsiParameterItem.create(codebase, parameter, index) - } + val parameters = + if (psiMethod is UMethod) { + psiMethod.uastParameters.mapIndexed { index, parameter -> + PsiParameterItem.create(codebase, parameter, index) + } + } else { + psiMethod.parameterList.parameters.mapIndexed { index, parameter -> + PsiParameterItem.create(codebase, parameter, index) + } + } val returnType = codebase.getType(psiMethod.returnType!!) val method = PsiMethodItem( codebase = codebase, diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiModifierItem.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiModifierItem.kt index 3a112957cc7003ee2398ed9cc220c71c5335c18b..5ac50e57ac5301a42c3dd906a12c47b24cab7422 100644 --- a/src/main/java/com/android/tools/metalava/model/psi/PsiModifierItem.kt +++ b/src/main/java/com/android/tools/metalava/model/psi/PsiModifierItem.kt @@ -16,6 +16,9 @@ package com.android.tools.metalava.model.psi +import com.android.tools.metalava.ANDROIDX_VISIBLE_FOR_TESTING +import com.android.tools.metalava.ANDROID_SUPPORT_VISIBLE_FOR_TESTING +import com.android.tools.metalava.ATTR_OTHERWISE import com.android.tools.metalava.model.AnnotationItem import com.android.tools.metalava.model.Codebase import com.android.tools.metalava.model.DefaultModifierList @@ -24,12 +27,18 @@ import com.android.tools.metalava.model.MutableModifierList import com.intellij.psi.PsiDocCommentOwner import com.intellij.psi.PsiMethod import com.intellij.psi.PsiModifier +import com.intellij.psi.PsiModifierList import com.intellij.psi.PsiModifierListOwner import com.intellij.psi.PsiReferenceExpression +import com.intellij.psi.PsiPrimitiveType import org.jetbrains.kotlin.asJava.elements.KtLightModifierList +import org.jetbrains.kotlin.asJava.elements.KtLightNullabilityAnnotation import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.uast.UAnnotated import org.jetbrains.uast.UMethod +import org.jetbrains.uast.UVariable +import org.jetbrains.uast.kotlin.KotlinNullabilityUAnnotation class PsiModifierItem( codebase: Codebase, @@ -38,8 +47,12 @@ class PsiModifierItem( ) : DefaultModifierList(codebase, flags, annotations), ModifierList, MutableModifierList { companion object { fun create(codebase: PsiBasedCodebase, element: PsiModifierListOwner, documentation: String?): PsiModifierItem { - val modifiers = create(codebase, element) - + val modifiers = + if (element is UAnnotated) { + create(codebase, element, element) + } else { + create(codebase, element) + } if (documentation?.contains("@deprecated") == true || // Check for @Deprecated annotation ((element as? PsiDocCommentOwner)?.isDeprecated == true) @@ -50,9 +63,7 @@ class PsiModifierItem( return modifiers } - private fun create(codebase: PsiBasedCodebase, element: PsiModifierListOwner): PsiModifierItem { - val modifierList = element.modifierList ?: return PsiModifierItem(codebase) - + private fun computeFlag(element: PsiModifierListOwner, modifierList: PsiModifierList): Int { var flags = 0 if (modifierList.hasModifierProperty(PsiModifier.PUBLIC)) { flags = flags or PUBLIC @@ -139,6 +150,14 @@ class PsiModifierItem( } } + return flags + } + + private fun create(codebase: PsiBasedCodebase, element: PsiModifierListOwner): PsiModifierItem { + val modifierList = element.modifierList ?: return PsiModifierItem(codebase) + + var flags = computeFlag(element, modifierList) + val psiAnnotations = modifierList.annotations return if (psiAnnotations.isEmpty()) { PsiModifierItem(codebase, flags) @@ -146,22 +165,16 @@ class PsiModifierItem( val annotations: MutableList<AnnotationItem> = psiAnnotations.map { val qualifiedName = it.qualifiedName - // TODO: com.android.internal.annotations.VisibleForTesting? - if (qualifiedName == "androidx.annotation.VisibleForTesting" || - qualifiedName == "android.support.annotation.VisibleForTesting") { - val otherwise = it.findAttributeValue("otherwise") + // Consider also supporting com.android.internal.annotations.VisibleForTesting? + if (qualifiedName == ANDROIDX_VISIBLE_FOR_TESTING || + qualifiedName == ANDROID_SUPPORT_VISIBLE_FOR_TESTING) { + val otherwise = it.findAttributeValue(ATTR_OTHERWISE) val ref = when { otherwise is PsiReferenceExpression -> otherwise.referenceName ?: "" otherwise != null -> otherwise.text else -> "" } - if (ref.endsWith("PROTECTED")) { - flags = (flags and PUBLIC.inv() and PRIVATE.inv() and INTERNAL.inv()) or PROTECTED - } else if (ref.endsWith("PACKAGE_PRIVATE")) { - flags = (flags and PUBLIC.inv() and PRIVATE.inv() and INTERNAL.inv() and PROTECTED.inv()) - } else if (ref.endsWith("PRIVATE") || ref.endsWith("NONE")) { - flags = (flags and PUBLIC.inv() and PROTECTED.inv() and INTERNAL.inv()) or PRIVATE - } + flags = getVisibilityFlag(ref, flags) } PsiAnnotationItem.create(codebase, it, qualifiedName) @@ -170,6 +183,74 @@ class PsiModifierItem( } } + private fun create( + codebase: PsiBasedCodebase, + element: PsiModifierListOwner, + annotated: UAnnotated + ): PsiModifierItem { + val modifierList = element.modifierList ?: return PsiModifierItem(codebase) + var flags = computeFlag(element, modifierList) + val uAnnotations = annotated.annotations + + return if (uAnnotations.isEmpty()) { + val psiAnnotations = modifierList.annotations + if (!psiAnnotations.isEmpty()) { + val annotations: MutableList<AnnotationItem> = + psiAnnotations.map { PsiAnnotationItem.create(codebase, it) }.toMutableList() + PsiModifierItem(codebase, flags, annotations) + } else { + PsiModifierItem(codebase, flags) + } + } else { + val isPrimitiveVariable = element is UVariable && element.type is PsiPrimitiveType + + val annotations: MutableList<AnnotationItem> = uAnnotations + // Uast sometimes puts nullability annotations on primitives!? + .filter { !isPrimitiveVariable || it !is KotlinNullabilityUAnnotation } + .map { + + val qualifiedName = it.qualifiedName + if (qualifiedName == ANDROIDX_VISIBLE_FOR_TESTING || + qualifiedName == ANDROID_SUPPORT_VISIBLE_FOR_TESTING) { + val otherwise = it.findAttributeValue(ATTR_OTHERWISE) + val ref = when { + otherwise is PsiReferenceExpression -> otherwise.referenceName ?: "" + otherwise != null -> otherwise.asSourceString() + else -> "" + } + flags = getVisibilityFlag(ref, flags) + } + + UAnnotationItem.create(codebase, it, qualifiedName) + }.toMutableList() + + if (!isPrimitiveVariable) { + val psiAnnotations = modifierList.annotations + if (psiAnnotations.isNotEmpty() && annotations.none { it.isNullnessAnnotation() }) { + val ktNullAnnotation = psiAnnotations.firstOrNull { it is KtLightNullabilityAnnotation } + ktNullAnnotation?.let { + annotations.add(PsiAnnotationItem.create(codebase, it)) + } + } + } + + PsiModifierItem(codebase, flags, annotations) + } + } + + /** Modifies the modifier flags based on the VisibleForTesting otherwise constants */ + private fun getVisibilityFlag(ref: String, flags: Int): Int { + return if (ref.endsWith("PROTECTED")) { + (flags and PUBLIC.inv() and PRIVATE.inv() and INTERNAL.inv()) or PROTECTED + } else if (ref.endsWith("PACKAGE_PRIVATE")) { + (flags and PUBLIC.inv() and PRIVATE.inv() and INTERNAL.inv() and PROTECTED.inv()) + } else if (ref.endsWith("PRIVATE") || ref.endsWith("NONE")) { + (flags and PUBLIC.inv() and PROTECTED.inv() and INTERNAL.inv()) or PRIVATE + } else { + flags + } + } + fun create(codebase: PsiBasedCodebase, original: PsiModifierItem): PsiModifierItem { val originalAnnotations = original.annotations ?: return PsiModifierItem(codebase, original.flags) val copy: MutableList<AnnotationItem> = ArrayList(originalAnnotations.size) diff --git a/src/main/java/com/android/tools/metalava/model/psi/UAnnotationItem.kt b/src/main/java/com/android/tools/metalava/model/psi/UAnnotationItem.kt new file mode 100644 index 0000000000000000000000000000000000000000..0d0266e6db7d36b9c8f4278cb6e35322502bd11a --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/psi/UAnnotationItem.kt @@ -0,0 +1,326 @@ +/* + * 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.model.psi + +import com.android.SdkConstants +import com.android.tools.lint.detector.api.ConstantEvaluator +import com.android.tools.metalava.model.AnnotationArrayAttributeValue +import com.android.tools.metalava.model.AnnotationAttribute +import com.android.tools.metalava.model.AnnotationAttributeValue +import com.android.tools.metalava.model.AnnotationItem +import com.android.tools.metalava.model.AnnotationSingleAttributeValue +import com.android.tools.metalava.model.AnnotationTarget +import com.android.tools.metalava.model.ClassItem +import com.android.tools.metalava.model.DefaultAnnotationItem +import com.android.tools.metalava.model.Item +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiExpression +import com.intellij.psi.PsiField +import com.intellij.psi.PsiLiteral +import com.intellij.psi.PsiMethod +import com.intellij.psi.impl.JavaConstantExpressionEvaluator +import org.jetbrains.kotlin.asJava.elements.KtLightNullabilityAnnotation +import org.jetbrains.uast.UAnnotation +import org.jetbrains.uast.UBinaryExpression +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.UReferenceExpression +import org.jetbrains.uast.util.isArrayInitializer + +class UAnnotationItem private constructor( + override val codebase: PsiBasedCodebase, + val uAnnotation: UAnnotation, + private val originalName: String? +) : DefaultAnnotationItem(codebase) { + private val qualifiedName = AnnotationItem.mapName(codebase, originalName) + + private var attributes: List<AnnotationAttribute>? = null + + override fun originalName(): String? = originalName + + override fun toString(): String = toSource() + + override fun toSource(target: AnnotationTarget): String { + val sb = StringBuilder(60) + appendAnnotation(codebase, sb, uAnnotation, originalName, target) + return sb.toString() + } + + override fun resolve(): ClassItem? { + return codebase.findClass(originalName ?: return null) + } + + override fun isNonNull(): Boolean { + if (uAnnotation.javaPsi is KtLightNullabilityAnnotation && + originalName == "" + ) { + // Hack/workaround: some UAST annotation nodes do not provide qualified name :=( + return true + } + return super.isNonNull() + } + + override fun qualifiedName() = qualifiedName + + override fun attributes(): List<AnnotationAttribute> { + if (attributes == null) { + val uAttributes = uAnnotation.attributeValues + attributes = if (uAttributes.isEmpty()) { + emptyList() + } else { + val list = mutableListOf<AnnotationAttribute>() + for (parameter in uAttributes) { + list.add( + UAnnotationAttribute( + codebase, + parameter.name ?: SdkConstants.ATTR_VALUE, parameter.expression + ) + ) + } + list + } + } + + return attributes!! + } + + companion object { + fun create(codebase: PsiBasedCodebase, uAnnotation: UAnnotation, qualifiedName: String? = uAnnotation.qualifiedName): UAnnotationItem { + return UAnnotationItem(codebase, uAnnotation, qualifiedName) + } + + fun create(codebase: PsiBasedCodebase, original: UAnnotationItem): UAnnotationItem { + return UAnnotationItem(codebase, original.uAnnotation, original.originalName) + } + + private fun appendAnnotation( + codebase: PsiBasedCodebase, + sb: StringBuilder, + uAnnotation: UAnnotation, + originalName: String?, + target: AnnotationTarget + ) { + val qualifiedName = AnnotationItem.mapName(codebase, originalName, null, target) ?: return + + val attributes = uAnnotation.attributeValues + if (attributes.isEmpty()) { + sb.append("@$qualifiedName") + return + } + + sb.append("@") + sb.append(qualifiedName) + sb.append("(") + if (attributes.size == 1 && (attributes[0].name == null || attributes[0].name == SdkConstants.ATTR_VALUE)) { + // Special case: omit "value" if it's the only attribute + appendValue(codebase, sb, attributes[0].expression, target) + } else { + var first = true + for (attribute in attributes) { + if (first) { + first = false + } else { + sb.append(", ") + } + sb.append(attribute.name ?: SdkConstants.ATTR_VALUE) + sb.append('=') + appendValue(codebase, sb, attribute.expression, target) + } + } + sb.append(")") + } + + private fun appendValue( + codebase: PsiBasedCodebase, + sb: StringBuilder, + value: UExpression?, + target: AnnotationTarget + ) { + // Compute annotation string -- we don't just use value.text here + // because that may not use fully qualified names, e.g. the source may say + // @RequiresPermission(Manifest.permission.ACCESS_COARSE_LOCATION) + // and we want to compute + // @android.support.annotation.RequiresPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) + when (value) { + null -> sb.append("null") + is ULiteralExpression -> sb.append(CodePrinter.constantToSource(value.value)) + is UReferenceExpression -> { + val resolved = value.resolve() + when (resolved) { + is PsiField -> { + val containing = resolved.containingClass + if (containing != null) { + // If it's a field reference, see if it looks like the field is hidden; if + // so, inline the value + val cls = codebase.findOrCreateClass(containing) + val initializer = resolved.initializer + if (initializer != null) { + val fieldItem = cls.findField(resolved.name) + if (fieldItem == null || fieldItem.isHiddenOrRemoved()) { + // Use the literal value instead + val source = getConstantSource(initializer) + if (source != null) { + sb.append(source) + return + } + } + } + containing.qualifiedName?.let { + sb.append(it).append('.') + } + } + + sb.append(resolved.name) + } + is PsiClass -> resolved.qualifiedName?.let { sb.append(it) } + else -> { + sb.append(value.sourcePsi?.text ?: value.asSourceString()) + } + } + } + is UBinaryExpression -> { + appendValue(codebase, sb, value.leftOperand, target) + sb.append(' ') + sb.append(value.operator.text) + sb.append(' ') + appendValue(codebase, sb, value.rightOperand, target) + } + is UCallExpression -> { + if (value.isArrayInitializer()) { + sb.append('{') + var first = true + for (initializer in value.valueArguments) { + if (first) { + first = false + } else { + sb.append(", ") + } + appendValue(codebase, sb, initializer, target) + } + sb.append('}') + } else { + println("todo") + } + } + is UAnnotation -> { + appendAnnotation(codebase, sb, value, value.qualifiedName, target) + } + else -> { + val source = getConstantSource(value) + if (source != null) { + sb.append(source) + return + } + sb.append(value.sourcePsi?.text ?: value.asSourceString()) + } + } + } + + private fun getConstantSource(value: UExpression): String? { + val constant = value.evaluate() + return CodePrinter.constantToExpression(constant) + } + + private fun getConstantSource(value: PsiExpression): String? { + val constant = JavaConstantExpressionEvaluator.computeConstantExpression(value, false) + return CodePrinter.constantToExpression(constant) + } + } +} + +class UAnnotationAttribute( + codebase: PsiBasedCodebase, + override val name: String, + psiValue: UExpression +) : AnnotationAttribute { + override val value: AnnotationAttributeValue = UAnnotationValue.create( + codebase, psiValue + ) +} + +abstract class UAnnotationValue : AnnotationAttributeValue { + companion object { + fun create(codebase: PsiBasedCodebase, value: UExpression): UAnnotationValue { + return if (value.isArrayInitializer()) { + UAnnotationArrayAttributeValue(codebase, value as UCallExpression) + } else { + UAnnotationSingleAttributeValue(codebase, value) + } + } + } + + override fun toString(): String = toSource() +} + +class UAnnotationSingleAttributeValue( + private val codebase: PsiBasedCodebase, + private val psiValue: UExpression +) : UAnnotationValue(), AnnotationSingleAttributeValue { + override val valueSource: String = getText(psiValue) + override val value: Any? + get() { + if (psiValue is ULiteralExpression) { + val value = psiValue.value + if (value != null) { + return value + } else if (psiValue.isNull) { + return null + } + } + if (psiValue is PsiLiteral) { + return psiValue.value ?: getText(psiValue).removeSurrounding("\"") + } + + val value = ConstantEvaluator.evaluate(null, psiValue) + if (value != null) { + return value + } + + return getText(psiValue).removeSurrounding("\"") + } + + override fun value(): Any? = value + + override fun toSource(): String = getText(psiValue) + + override fun resolve(): Item? { + if (psiValue is UReferenceExpression) { + val resolved = psiValue.resolve() + when (resolved) { + is PsiField -> return codebase.findField(resolved) + is PsiClass -> return codebase.findOrCreateClass(resolved) + is PsiMethod -> return codebase.findMethod(resolved) + } + } + return null + } +} + +class UAnnotationArrayAttributeValue(codebase: PsiBasedCodebase, private val value: UCallExpression) : + UAnnotationValue(), AnnotationArrayAttributeValue { + override val values = value.valueArguments.map { + create(codebase, it) + }.toList() + + override fun toSource(): String = getText(value) +} + +private fun getText(element: UElement): String { + return element.sourcePsi?.text ?: element.asSourceString() +} \ No newline at end of file diff --git a/src/main/resources/version.properties b/src/main/resources/version.properties index 233e17ea97e1c8f6cf1cc9d0fa73c97a81797f0f..0e3b7f676ee6a72ba7c8b5120079798fdd601608 100644 --- a/src/main/resources/version.properties +++ b/src/main/resources/version.properties @@ -2,4 +2,4 @@ # Version definition # This file is read by gradle build scripts, but also packaged with metalava # as a resource for the Version classes to read. -metalavaVersion=1.2.5 +metalavaVersion=1.2.6 diff --git a/src/test/java/com/android/tools/metalava/ApiFileTest.kt b/src/test/java/com/android/tools/metalava/ApiFileTest.kt index 962f9c6147d277003c36d1497da9ec5ebd586d57..61bf8012fcb7cc1d1e15d5daa652a19f981df815 100644 --- a/src/test/java/com/android/tools/metalava/ApiFileTest.kt +++ b/src/test/java/com/android/tools/metalava/ApiFileTest.kt @@ -285,7 +285,7 @@ class ApiFileTest : DriverTest() { package androidx.core.util { public final class TestKt { ctor public TestKt(); - method public static inline <K, V> android.util.LruCache<K,V> lruCache(int maxSize, kotlin.jvm.functions.Function2<? super K,? super V,java.lang.Integer> sizeOf = { _, _ -> 1 }, kotlin.jvm.functions.Function1<? super K,? extends V> create = { (V)null }, kotlin.jvm.functions.Function4<? super java.lang.Boolean,? super K,? super V,? super V,kotlin.Unit> onEntryRemoved = { _, _, _, _ -> }); + method public static inline <K, V> android.util.LruCache<K,V> lruCache(int maxSize, kotlin.jvm.functions.Function2<? super K,? super V,java.lang.Integer> sizeOf = { _, _ -> 1 }, kotlin.jvm.functions.Function1<? super K,? extends V> create = { (java.lang.Object)null }, kotlin.jvm.functions.Function4<? super java.lang.Boolean,? super K,? super V,? super V,kotlin.Unit> onEntryRemoved = { _, _, _, _ -> }); } } """, @@ -457,10 +457,10 @@ class ApiFileTest : DriverTest() { package test.pkg { public final class TestKt { ctor public TestKt(); - method public static inline <T> void a(T t); - method public static inline <reified T> void b(T t); - method public static inline <reified T> void e(T t); - method public static inline <reified T> void f(T, T t); + method public static inline <T> void a(@Nullable T t); + method public static inline <reified T> void b(@Nullable T t); + method public static inline <reified T> void e(@Nullable T t); + method public static inline <reified T> void f(@Nullable T, @Nullable T t); } } """, @@ -484,7 +484,7 @@ class ApiFileTest : DriverTest() { package test.pkg { public final class TestKt { ctor public TestKt(); - method public static suspend inline Object hello(kotlin.coroutines.experimental.Continuation<? super kotlin.Unit> p); + method public static suspend inline Object hello(@NonNull kotlin.coroutines.Continuation<? super kotlin.Unit> p); } } """, @@ -3212,8 +3212,8 @@ class ApiFileTest : DriverTest() { import androidx.annotation.VisibleForTesting; @SuppressWarnings({"ClassNameDiffersFromFileName", "WeakerAccess"}) - public class ProductionCode { - private ProductionCode() { } + public class ProductionCodeJava { + private ProductionCodeJava() { } @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) public void shouldBeProtected() { @@ -3242,7 +3242,7 @@ class ApiFileTest : DriverTest() { package test.pkg import androidx.annotation.VisibleForTesting - open class ProductionCode2 private constructor() { + open class ProductionCodeKotlin private constructor() { @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) fun shouldBeProtected() { @@ -3270,10 +3270,10 @@ class ApiFileTest : DriverTest() { ), api = """ package test.pkg { - public class ProductionCode { + public class ProductionCodeJava { method protected void shouldBeProtected(); } - public class ProductionCode2 { + public class ProductionCodeKotlin { method protected final void shouldBeProtected(); } } @@ -3331,4 +3331,4 @@ class ApiFileTest : DriverTest() { ) ) } -} \ No newline at end of file +} diff --git a/src/test/java/com/android/tools/metalava/ApiLintTest.kt b/src/test/java/com/android/tools/metalava/ApiLintTest.kt index 87d9a7459a03e2eb0d1af1a4b7b30478c95d0ec6..bcb59a86a46f4a0494325f089c1f6e4f5b878295 100644 --- a/src/test/java/com/android/tools/metalava/ApiLintTest.kt +++ b/src/test/java/com/android/tools/metalava/ApiLintTest.kt @@ -1456,7 +1456,7 @@ class ApiLintTest : DriverTest() { import java.io.InputStream; public class CheckFiles { - public MyClass(Context context, File file) { + public CheckFiles(Context context, File file) { } public void ok(int i, File file) { } public void ok(int i, InputStream stream) { } @@ -1487,7 +1487,7 @@ class ApiLintTest : DriverTest() { import java.io.InputStream; public class CheckFiles { - public MyClass(Context context, File file) { + public CheckFiles(Context context, File file) { } public void ok(int i, File file) { } public void ok(int i, InputStream stream) { } diff --git a/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt b/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt index a457fcdb4b4c38e9b8cee67883fe2ae6864c4782..819bd2bea2d771830ba4975a22cae9f19422a38c 100644 --- a/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt +++ b/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt @@ -284,6 +284,32 @@ CompatibilityCheckTest : DriverTest() { ) } + @Test + fun `Kotlin Coroutines`() { + check( + warnings = "", + compatibilityMode = false, + inputKotlinStyleNulls = true, + outputKotlinStyleNulls = true, + checkCompatibilityApi = """ + package test.pkg { + public final class TestKt { + ctor public TestKt(); + method public static suspend inline java.lang.Object hello(kotlin.coroutines.experimental.Continuation<? super kotlin.Unit>); + } + } + """, + signatureSource = """ + package test.pkg { + public final class TestKt { + ctor public TestKt(); + method public static suspend inline Object hello(@NonNull kotlin.coroutines.Continuation<? super kotlin.Unit> p); + } + } + """ + ) + } + @Test fun `Add flag new methods but not overrides from platform`() { check( diff --git a/src/test/java/com/android/tools/metalava/DriverTest.kt b/src/test/java/com/android/tools/metalava/DriverTest.kt index 780c0674a2b8a9334991a267d73c9f8f8a5a27f9..5d521dcf039a92ac2ef8c972651917b6c2ba3387 100644 --- a/src/test/java/com/android/tools/metalava/DriverTest.kt +++ b/src/test/java/com/android/tools/metalava/DriverTest.kt @@ -131,6 +131,9 @@ abstract class DriverTest { } } + // This is here to make sure we don't have any unexpected random println's + // in the source that are left behind after debugging and ends up polluting + // the production output class OutputForbiddenWriter(private val stream: String) : ByteArrayOutputStream() { override fun write(b: ByteArray?, off: Int, len: Int) { fail("Unexpected write directly to $stream") diff --git a/src/test/java/com/android/tools/metalava/KotlinInteropChecksTest.kt b/src/test/java/com/android/tools/metalava/KotlinInteropChecksTest.kt index f3efebca9faa75b147450c21d21cc6ffb2fde66e..037100f1d1383ce8d2791a571c9a7fa8208ab99d 100644 --- a/src/test/java/com/android/tools/metalava/KotlinInteropChecksTest.kt +++ b/src/test/java/com/android/tools/metalava/KotlinInteropChecksTest.kt @@ -91,6 +91,7 @@ class KotlinInteropChecksTest : DriverTest() { extraArguments = arrayOf(ARG_CHECK_KOTLIN_INTEROP), warnings = """ src/test/pkg/Foo.kt:7: warning: Companion object constants like INTEGER_ONE should be marked @JvmField for Java interoperability; see https://android.github.io/kotlin-guides/interop.html#companion-constants [MissingJvmstatic] + src/test/pkg/Foo.kt:10: warning: Companion object constants like WRONG2 should be using @JvmField, not @JvmStatic; see https://android.github.io/kotlin-guides/interop.html#companion-constants [MissingJvmstatic] src/test/pkg/Foo.kt:13: warning: Companion object methods like missing should be marked @JvmStatic for Java interoperability; see https://android.github.io/kotlin-guides/interop.html#companion-functions [MissingJvmstatic] """, sourceFiles = *arrayOf( @@ -105,7 +106,7 @@ class KotlinInteropChecksTest : DriverTest() { const val INTEGER_ONE = 1 var BIG_INTEGER_ONE = BigInteger.ONE @JvmStatic val WRONG = 2 // not yet flagged - @JvmStatic @JvmField val WRONG2 = 2 // not yet flagged + @JvmStatic @JvmField val WRONG2 = 2 @JvmField val ok3 = 3 fun missing() { }