| package com.android.tools.lint.checks |
| |
| import com.android.sdklib.SdkVersionInfo |
| import com.android.tools.lint.client.api.JavaEvaluator |
| import com.android.tools.lint.detector.api.ConstantEvaluator |
| import com.android.tools.lint.detector.api.getMethodName |
| import com.android.tools.lint.detector.api.skipParentheses |
| import com.android.utils.SdkUtils |
| import com.google.common.annotations.VisibleForTesting |
| import com.intellij.codeInsight.AnnotationUtil |
| import com.intellij.openapi.util.Ref |
| import com.intellij.psi.PsiAnnotation |
| import com.intellij.psi.PsiElement |
| import com.intellij.psi.PsiField |
| import com.intellij.psi.PsiFile |
| import com.intellij.psi.PsiMethod |
| import com.intellij.psi.PsiMethodCallExpression |
| import com.intellij.psi.PsiModifier |
| import com.intellij.psi.PsiModifierListOwner |
| import com.intellij.psi.PsiParameter |
| import com.intellij.psi.PsiParameterList |
| import com.intellij.psi.PsiReferenceExpression |
| import com.intellij.psi.PsiVariable |
| import com.intellij.psi.util.PsiTreeUtil |
| import org.jetbrains.uast.UAnonymousClass |
| import org.jetbrains.uast.UBinaryExpression |
| import org.jetbrains.uast.UBlockExpression |
| import org.jetbrains.uast.UCallExpression |
| import org.jetbrains.uast.UClass |
| import org.jetbrains.uast.UElement |
| import org.jetbrains.uast.UExpression |
| import org.jetbrains.uast.UIfExpression |
| import org.jetbrains.uast.ULambdaExpression |
| import org.jetbrains.uast.ULiteralExpression |
| import org.jetbrains.uast.ULocalVariable |
| import org.jetbrains.uast.UMethod |
| import org.jetbrains.uast.UPolyadicExpression |
| import org.jetbrains.uast.UQualifiedReferenceExpression |
| import org.jetbrains.uast.UReferenceExpression |
| import org.jetbrains.uast.UReturnExpression |
| import org.jetbrains.uast.USwitchClauseExpressionWithBody |
| import org.jetbrains.uast.UThrowExpression |
| import org.jetbrains.uast.UUnaryExpression |
| import org.jetbrains.uast.UastBinaryOperator |
| import org.jetbrains.uast.UastFacade |
| import org.jetbrains.uast.UastPrefixOperator |
| import org.jetbrains.uast.getParentOfType |
| import org.jetbrains.uast.isUastChildOf |
| import org.jetbrains.uast.visitor.AbstractUastVisitor |
| |
| private typealias ApiLevelLookup = (UElement) -> Int |
| |
| /** |
| * Helper for checking whether a given element is surrounded (or preceded!) by an API check |
| * using SDK_INT (or other version checking utilities such as BuildCompat#isAtLeastN) |
| */ |
| class VersionChecks(private val evaluator: JavaEvaluator) { |
| companion object { |
| const val SDK_INT = "SDK_INT" |
| const val CHECKS_SDK_INT_AT_LEAST_ANNOTATION = "androidx.annotation.ChecksSdkIntAtLeast" |
| |
| /** SDK int method used by the data binding compiler */ |
| private const val GET_BUILD_SDK_INT = "getBuildSdkInt" |
| |
| @JvmStatic |
| fun codeNameToApi(text: String): Int { |
| val dotIndex = text.lastIndexOf('.') |
| val buildCode = |
| if (dotIndex != -1) { |
| text.substring(dotIndex + 1) |
| } else { |
| text |
| } |
| return SdkVersionInfo.getApiByBuildCode(buildCode, true) |
| } |
| |
| @JvmStatic |
| fun isWithinVersionCheckConditional( |
| evaluator: JavaEvaluator, |
| element: UElement, |
| api: Int, |
| isLowerBound: Boolean |
| ): Boolean { |
| return VersionChecks(evaluator).isWithinVersionCheckConditional( |
| evaluator = evaluator, |
| element = element, |
| api = api, |
| isLowerBound = isLowerBound, |
| apiLookup = null |
| ) |
| } |
| |
| @JvmStatic |
| fun isPrecededByVersionCheckExit( |
| evaluator: JavaEvaluator, |
| element: UElement, |
| api: Int, |
| isLowerBound: Boolean |
| ): Boolean { |
| val check = VersionChecks(evaluator) |
| var prev = element |
| var current: UExpression? = prev.getParentOfType( |
| UExpression::class.java, |
| true, |
| UMethod::class.java, |
| UClass::class.java |
| ) |
| while (current != null) { |
| val visitor = check.VersionCheckWithExitFinder(prev, api, isLowerBound) |
| current.accept(visitor) |
| if (visitor.found()) { |
| return true |
| } |
| prev = current |
| current = current.getParentOfType( |
| UExpression::class.java, |
| true, |
| UMethod::class.java, |
| UClass::class.java |
| ) |
| // TODO: what about lambdas? |
| } |
| return false |
| } |
| |
| // TODO: Merge with the other isVersionCheckConditional |
| @JvmStatic |
| fun isVersionCheckConditional(api: Int, binary: UBinaryExpression): Boolean? { |
| val tokenType = binary.operator |
| if (tokenType === UastBinaryOperator.GREATER || |
| tokenType === UastBinaryOperator.GREATER_OR_EQUALS || |
| tokenType === UastBinaryOperator.LESS_OR_EQUALS || |
| tokenType === UastBinaryOperator.LESS || |
| tokenType === UastBinaryOperator.EQUALS || |
| tokenType === UastBinaryOperator.IDENTITY_EQUALS |
| ) { |
| val left = binary.leftOperand |
| if (left is UReferenceExpression) { |
| if (SDK_INT == left.resolvedName) { |
| val right = binary.rightOperand |
| var level = -1 |
| if (right is UReferenceExpression) { |
| val codeName = right.resolvedName ?: return false |
| level = SdkVersionInfo.getApiByBuildCode(codeName, true) |
| } else if (right is ULiteralExpression) { |
| val value = right.value |
| if (value is Int) { |
| level = value |
| } |
| } |
| if (level != -1) { |
| if (tokenType === UastBinaryOperator.GREATER_OR_EQUALS && level < api) { |
| // SDK_INT >= ICE_CREAM_SANDWICH |
| return true |
| } else if (tokenType === UastBinaryOperator.GREATER && level <= api - 1) { |
| // SDK_INT > ICE_CREAM_SANDWICH |
| return true |
| } else if (tokenType === UastBinaryOperator.LESS_OR_EQUALS && level < api) { |
| return false |
| } else if (tokenType === UastBinaryOperator.LESS && level <= api) { |
| // SDK_INT < ICE_CREAM_SANDWICH |
| return false |
| } else if ((tokenType === UastBinaryOperator.EQUALS || |
| tokenType === UastBinaryOperator.IDENTITY_EQUALS) && |
| level < api |
| ) { |
| return false |
| } |
| } |
| } |
| } |
| } |
| return null |
| } |
| |
| private val VERSION_METHOD_NAME_PREFIXES = arrayOf( |
| "isAtLeast", "isRunning", "is", "runningOn", "running", "has" |
| ) |
| |
| private val VERSION_METHOD_NAME_SUFFIXES = arrayOf( |
| "OrLater", "OrAbove", "OrHigher", "OrNewer", "Sdk" |
| ) |
| |
| @VisibleForTesting |
| fun getMinSdkVersionFromMethodName(name: String): Int { |
| val prefix = |
| VERSION_METHOD_NAME_PREFIXES.firstOrNull { name.startsWith(it) } ?: return -1 |
| val suffix = |
| VERSION_METHOD_NAME_SUFFIXES.firstOrNull { SdkUtils.endsWithIgnoreCase(name, it) } |
| ?: if (prefix != "is") "" else null ?: return -1 |
| val codeName = name.substring(prefix.length, name.length - suffix.length) |
| var version = SdkVersionInfo.getApiByPreviewName(codeName, false) |
| if (version == -1) { |
| version = SdkVersionInfo.getApiByBuildCode(codeName, false) |
| if (version == -1 && codeName.length == 1 && Character.isUpperCase(codeName[0]) |
| ) { |
| // Some future API level |
| version = SdkVersionInfo.HIGHEST_KNOWN_API + 1 |
| } else if (SdkUtils.startsWithIgnoreCase(codeName, "api")) { |
| val length = codeName.length |
| var begin = 3 // "api".length |
| if (begin < length && codeName[begin] == '_') { |
| begin++ |
| } |
| var end = begin |
| while (end < length) { |
| if (!Character.isDigit(codeName[end])) { |
| break |
| } |
| end++ |
| } |
| if (begin < end) { |
| version = Integer.decode(codeName.substring(begin, end)) |
| } |
| } |
| } |
| return version |
| } |
| } |
| |
| private fun isUnconditionalReturn(statement: UExpression): Boolean { |
| if (statement is UBlockExpression) { |
| val expressions = statement.expressions |
| val statements = expressions.size |
| if (statements > 0) { |
| val last = expressions[statements - 1] |
| if (last is UReturnExpression || last is UThrowExpression) { |
| return true |
| } else if (last is UCallExpression) { |
| val methodName = getMethodName(last) |
| // Look for Kotlin runtime library methods that unconditionally exit |
| if ("error" == methodName || "TODO" == methodName) { |
| return true |
| } |
| } |
| } |
| } |
| return statement is UReturnExpression |
| } |
| |
| private fun isWithinVersionCheckConditional( |
| evaluator: JavaEvaluator, |
| element: UElement, |
| api: Int, |
| isLowerBound: Boolean, |
| apiLookup: ApiLevelLookup? |
| ): Boolean { |
| var current = skipParentheses(element.uastParent) |
| var prev = element |
| while (current != null) { |
| if (current is UIfExpression) { |
| val ifStatement = current |
| val condition = ifStatement.condition |
| if (prev !== condition) { |
| val fromThen = prev == ifStatement.thenExpression |
| val ok = isVersionCheckConditional( |
| api = api, |
| isLowerBound = isLowerBound, |
| element = condition, |
| and = fromThen, |
| prev = prev, |
| apiLookup = apiLookup |
| ) |
| if (ok != null && ok) { |
| return true |
| } |
| } |
| } else if (current is UPolyadicExpression && |
| (isAndedWithConditional(current, api, isLowerBound, prev) || |
| isOredWithConditional(current, api, isLowerBound, prev)) |
| ) { |
| return true |
| } else if (current is USwitchClauseExpressionWithBody) { |
| for (condition in current.caseValues) { |
| val ok = isVersionCheckConditional( |
| api = api, |
| isLowerBound = isLowerBound, |
| element = condition, |
| and = true, |
| prev = prev, |
| apiLookup = apiLookup |
| ) |
| if (ok != null && ok) { |
| return true |
| } |
| } |
| } else if (current is UCallExpression && prev is ULambdaExpression) { |
| // If the API violation is in a lambda that is passed to a method, |
| // see if the lambda parameter is invoked inside that method, wrapped within |
| // a suitable version conditional. |
| // |
| // Optionally also see if we're passing in the API level as a parameter |
| // to the function. |
| // |
| // Algorithm: |
| // (1) Figure out which parameter we're mapping the lambda argument to. |
| // (2) Find that parameter invoked within the function |
| // (3) From the invocation see if it's a suitable version conditional |
| // |
| val call = current |
| val method = call.resolve() |
| if (method != null) { |
| val validFromAnnotation = isValidFromAnnotation(api, method, call) |
| if (validFromAnnotation != null) { |
| return validFromAnnotation |
| } |
| |
| val mapping = |
| evaluator.computeArgumentMapping(call, method) |
| val parameter = mapping[prev] |
| if (parameter != null) { |
| val uMethod = UastFacade.convertElementWithParent( |
| method, |
| UMethod::class.java |
| ) as UMethod? ?: return false |
| val match = Ref<UCallExpression>() |
| val parameterName = parameter.name |
| uMethod.accept( |
| object : AbstractUastVisitor() { |
| override fun visitCallExpression( |
| node: UCallExpression |
| ): Boolean { |
| val callName = getMethodName(node) |
| if (callName == parameterName) { |
| // Potentially not correct due to scopes, but these lambda |
| // utility methods tend to be short and for lambda function |
| // calls, resolve on call returns null |
| match.set(node) |
| } |
| return super.visitCallExpression(node) |
| } |
| }) |
| val lambdaInvocation = match.get() |
| val newApiLookup: (UElement) -> Int = { reference -> |
| var apiLevel = -1 |
| if (reference is UReferenceExpression) { |
| val resolved = reference.resolve() |
| if (resolved is PsiParameter) { |
| val parameterList = |
| PsiTreeUtil.getParentOfType( |
| resolved, PsiParameterList::class.java |
| ) |
| if (parameterList != null) { |
| val index = parameterList.getParameterIndex(resolved) |
| val arguments = call.valueArguments |
| if (index != -1 && index < arguments.size) { |
| apiLevel = getApiLevel(arguments[index], null) |
| } |
| } |
| } |
| } |
| apiLevel |
| } |
| if (lambdaInvocation != null && isWithinVersionCheckConditional( |
| evaluator = evaluator, |
| element = lambdaInvocation, |
| api = api, |
| isLowerBound = isLowerBound, |
| apiLookup = newApiLookup |
| ) |
| ) { |
| return true |
| } |
| } |
| } |
| } else if (current is UMethod) { |
| if (current.uastParent !is UAnonymousClass) return false |
| } else if (current is PsiFile) { |
| return false |
| } |
| prev = current |
| current = skipParentheses(current.uastParent) |
| } |
| return false |
| } |
| |
| private fun isVersionCheckConditional( |
| api: Int, |
| isLowerBound: Boolean, |
| element: UElement, |
| and: Boolean, |
| prev: UElement? = null, |
| apiLookup: ApiLevelLookup? = null |
| ): Boolean? { |
| if (element is UPolyadicExpression) { |
| if (element is UBinaryExpression) { |
| val ok = isVersionCheckConditional( |
| api = api, |
| isLowerBound = isLowerBound, |
| fromThen = and, |
| binary = element, |
| apiLevelLookup = apiLookup |
| ) |
| if (ok != null) { |
| return ok |
| } |
| } |
| val tokenType = element.operator |
| if (and && tokenType === UastBinaryOperator.LOGICAL_AND) { |
| if (isAndedWithConditional(element, api, isLowerBound, prev)) { |
| return true |
| } |
| } else if (!and && tokenType === UastBinaryOperator.LOGICAL_OR) { |
| if (isOredWithConditional(element, api, isLowerBound, prev)) { |
| return true |
| } |
| } |
| } else if (element is UCallExpression) { |
| return isValidVersionCall(api, isLowerBound, and, element) |
| } else if (element is UReferenceExpression) { |
| // Constant expression for an SDK version check? |
| val resolved = element.resolve() |
| if (resolved is PsiField) { |
| @Suppress("UnnecessaryVariable") |
| val field = resolved |
| |
| val validFromAnnotation = isValidFromAnnotation(api, field) |
| if (validFromAnnotation != null) { |
| return validFromAnnotation |
| } |
| |
| val modifierList = field.modifierList |
| if (modifierList != null && modifierList.hasExplicitModifier(PsiModifier.STATIC)) { |
| val initializer = UastFacade.getInitializerBody(field) |
| if (initializer != null) { |
| val ok = isVersionCheckConditional( |
| api = api, |
| isLowerBound = isLowerBound, |
| element = initializer, |
| and = and |
| ) |
| if (ok != null) { |
| return ok |
| } |
| } |
| } |
| } else if (resolved is PsiMethod && |
| element is UQualifiedReferenceExpression && |
| element.selector is UCallExpression |
| ) { |
| val call = element.selector as UCallExpression |
| return isValidVersionCall(api, isLowerBound, and, call) |
| } else if (resolved is PsiMethod) { |
| // Method call via Kotlin property syntax |
| return isValidVersionCall( |
| api = api, |
| isLowerBound = isLowerBound, |
| and = and, |
| call = element, |
| method = resolved |
| ) |
| } else if (resolved == null && element is UQualifiedReferenceExpression) { |
| val selector = element.selector |
| if (selector is UCallExpression) { |
| return isValidVersionCall( |
| api = api, |
| isLowerBound = isLowerBound, |
| and = and, |
| call = selector |
| ) |
| } |
| } |
| } else if (element is UUnaryExpression) { |
| if (element.operator === UastPrefixOperator.LOGICAL_NOT) { |
| val operand = element.operand |
| val ok = isVersionCheckConditional( |
| api = api, |
| isLowerBound = isLowerBound, |
| element = operand, |
| and = !and |
| ) |
| if (ok != null) { |
| return ok |
| } |
| } |
| } |
| return null |
| } |
| |
| private fun isValidFromAnnotation( |
| api: Int, |
| owner: PsiModifierListOwner, |
| call: UCallExpression? = null |
| ): Boolean? { |
| val annotation = AnnotationUtil.findAnnotation( |
| owner, true, CHECKS_SDK_INT_AT_LEAST_ANNOTATION |
| ) ?: return null |
| val value = getApiLevel(annotation, owner, call) |
| return if (value != -1) api <= value else null |
| } |
| |
| private fun getApiLevel( |
| annotation: PsiAnnotation, |
| owner: PsiModifierListOwner, |
| call: UCallExpression? |
| ): Int { |
| val api = annotation.getAnnotationIntValue("api") |
| if (api != -1) { |
| return api |
| } |
| val codename = annotation.getAnnotationStringValue("codename") |
| if (codename.isNotEmpty()) { // empty string is the default |
| return SdkVersionInfo.getApiByPreviewName(codename, true) |
| } |
| |
| val parameterIndex = annotation.getAnnotationIntValue("parameter") |
| if (owner is PsiMethod && call != null) { |
| val parameters = owner.parameterList.parameters |
| if (parameterIndex >= 0 && parameterIndex < parameters.size) { |
| val target = parameters[parameterIndex] |
| val mapping = evaluator.computeArgumentMapping(call, owner) |
| for ((key, value1) in mapping) { |
| if (value1 === target || value1.isEquivalentTo(target)) { |
| val v = ConstantEvaluator.evaluate(null, key) |
| return (v as? Number)?.toInt() ?: -1 |
| } |
| } |
| } |
| } |
| |
| return -1 |
| } |
| |
| private fun isValidVersionCall( |
| api: Int, |
| isLowerBound: Boolean, |
| and: Boolean, |
| call: UCallExpression |
| ): Boolean? { |
| val method = call.resolve() |
| if (method == null) { |
| // Fallback when we can't resolve call: Try to guess just based on the method name |
| val identifier = call.methodIdentifier |
| if (identifier != null) { |
| val name = identifier.name |
| val version = getMinSdkVersionFromMethodName(name) |
| if (version != -1 && isLowerBound) { |
| return api <= version |
| } |
| } |
| return null |
| } |
| return isValidVersionCall(api, isLowerBound, and, call, method) |
| } |
| |
| private fun isValidVersionCall( |
| api: Int, |
| isLowerBound: Boolean, |
| and: Boolean, |
| call: UElement, |
| method: PsiMethod |
| ): Boolean? { |
| val validFromAnnotation = isValidFromAnnotation(api, method, call as? UCallExpression) |
| if (validFromAnnotation != null) { |
| return validFromAnnotation |
| } |
| |
| val name = method.name |
| if (name.startsWith("isAtLeast") && isLowerBound) { |
| val containingClass = method.containingClass |
| if (containingClass != null && |
| // android.support.v4.os.BuildCompat, |
| // androidx.core.os.BuildCompat |
| "BuildCompat" == containingClass.name |
| ) { |
| when { |
| name == "isAtLeastN" -> return api <= 24 |
| name == "isAtLeastNMR1" -> return api <= 25 |
| name == "isAtLeastO" -> return api <= 26 |
| name.startsWith("isAtLeastP") -> return api <= 28 |
| name.startsWith("isAtLeastQ") -> return api <= 29 |
| // Try to guess future API levels before they're announced |
| name.startsWith("isAtLeast") && |
| name.length == 10 && Character.isUpperCase(name[9]) |
| && name[9] > 'Q' -> |
| return api <= SdkVersionInfo.HIGHEST_KNOWN_API + 1 |
| } |
| } |
| } |
| val version = getMinSdkVersionFromMethodName(name) |
| if (version != -1 && isLowerBound) { |
| return api <= version |
| } |
| |
| // Unconditional version utility method? If so just attempt to call it |
| if (!method.hasModifierProperty(PsiModifier.ABSTRACT)) { |
| val body = UastFacade.getMethodBody(method) ?: return null |
| val expressions: List<UExpression> |
| expressions = if (body is UBlockExpression) { |
| body.expressions |
| } else { |
| listOf(body) |
| } |
| if (expressions.size == 1) { |
| val statement = expressions[0] |
| val returnValue: UExpression? = if (statement is UReturnExpression) { |
| statement.returnExpression |
| } else { |
| // Kotlin: may not have an explicit return statement |
| statement |
| } |
| if (returnValue != null) { |
| val arguments = |
| if (call is UCallExpression) call.valueArguments else emptyList() |
| if (arguments.isEmpty()) { |
| if (returnValue is UPolyadicExpression || |
| returnValue is UCallExpression || |
| returnValue is UQualifiedReferenceExpression |
| ) { |
| val isConditional = |
| isVersionCheckConditional( |
| api = api, |
| isLowerBound = isLowerBound, |
| element = returnValue, |
| and = and |
| ) |
| if (isConditional != null) { |
| return isConditional |
| } |
| } |
| } else if (arguments.size == 1) { |
| // See if we're passing in a value to the version utility method |
| val lookup: (UElement) -> Int = { reference -> |
| var apiLevel = -1 |
| if (reference is UReferenceExpression) { |
| val resolved = reference.resolve() |
| if (resolved is PsiParameter) { |
| val parameterList = |
| PsiTreeUtil.getParentOfType( |
| resolved, PsiParameterList::class.java |
| ) |
| if (parameterList != null) { |
| val index = parameterList.getParameterIndex(resolved) |
| if (index != -1 && index < arguments.size) { |
| apiLevel = getApiLevel(arguments[index], null) |
| } |
| } |
| } |
| } |
| apiLevel |
| } |
| val ok = isVersionCheckConditional( |
| api = api, |
| isLowerBound = isLowerBound, |
| element = returnValue, |
| and = and, |
| apiLookup = lookup |
| ) |
| if (ok != null) { |
| return ok |
| } |
| } |
| } |
| } |
| } |
| return null |
| } |
| |
| private fun isSdkInt(element: PsiElement): Boolean { |
| if (element is PsiReferenceExpression) { |
| if (SDK_INT == element.referenceName) { |
| return true |
| } |
| val resolved = element.resolve() |
| if (resolved is PsiVariable) { |
| val initializer = resolved.initializer |
| if (initializer != null) { |
| return isSdkInt(initializer) |
| } |
| } |
| } else if (element is PsiMethodCallExpression) { |
| if (GET_BUILD_SDK_INT == element.methodExpression.referenceName) { |
| return true |
| } // else look inside the body? |
| } |
| return false |
| } |
| |
| private fun isSdkInt(element: UElement): Boolean { |
| if (element is UReferenceExpression) { |
| if (SDK_INT == element.resolvedName) { |
| return true |
| } |
| val resolved = element.resolve() |
| if (resolved is ULocalVariable) { |
| val initializer = resolved.uastInitializer |
| if (initializer != null) { |
| return isSdkInt(initializer) |
| } |
| } else if (resolved is PsiVariable) { |
| val initializer = resolved.initializer |
| if (initializer != null) { |
| return isSdkInt(initializer) |
| } |
| } |
| } else if (element is UCallExpression) { |
| if (GET_BUILD_SDK_INT == getMethodName(element)) { |
| return true |
| } // else look inside the body? |
| } |
| return false |
| } |
| |
| private fun isVersionCheckConditional( |
| api: Int, |
| isLowerBound: Boolean, |
| fromThen: Boolean, |
| binary: UBinaryExpression, |
| apiLevelLookup: ApiLevelLookup? = null |
| ): Boolean? { |
| @Suppress("NAME_SHADOWING") |
| var fromThen = fromThen |
| val tokenType = binary.operator |
| if (tokenType === UastBinaryOperator.GREATER || |
| tokenType === UastBinaryOperator.GREATER_OR_EQUALS || |
| tokenType === UastBinaryOperator.LESS_OR_EQUALS || |
| tokenType === UastBinaryOperator.LESS || |
| tokenType === UastBinaryOperator.EQUALS || |
| tokenType === UastBinaryOperator.IDENTITY_EQUALS || |
| tokenType === UastBinaryOperator.NOT_EQUALS || |
| tokenType === UastBinaryOperator.IDENTITY_NOT_EQUALS |
| ) { |
| val left = binary.leftOperand |
| val level: Int |
| val right: UExpression |
| if (!isSdkInt(left)) { |
| right = binary.rightOperand |
| if (isSdkInt(right)) { |
| fromThen = !fromThen |
| level = getApiLevel(left, apiLevelLookup) |
| } else { |
| return null |
| } |
| } else { |
| right = binary.rightOperand |
| level = getApiLevel(right, apiLevelLookup) |
| } |
| if (level != -1) { |
| if (tokenType === UastBinaryOperator.GREATER_OR_EQUALS) { |
| return if (isLowerBound) { |
| // if (SDK_INT >= ICE_CREAM_SANDWICH) { <call> } else { ... } |
| level >= api && fromThen |
| } else { |
| // if (SDK_INT >= ICE_CREAM_SANDWICH) { ... } else { <call> } |
| level - 1 <= api && !fromThen |
| } |
| } else if (tokenType === UastBinaryOperator.GREATER) { |
| return if (isLowerBound) { |
| // if (SDK_INT > ICE_CREAM_SANDWICH) { <call> } else { ... } |
| level >= api - 1 && fromThen |
| } else { |
| // if (SDK_INT > ICE_CREAM_SANDWICH) { ... } else { <call> } |
| level <= api && !fromThen |
| } |
| } else if (tokenType === UastBinaryOperator.LESS_OR_EQUALS) { |
| return if (isLowerBound) { |
| // if (SDK_INT <= ICE_CREAM_SANDWICH) { ... } else { <call> } |
| level >= api - 1 && !fromThen |
| } else { |
| // if (SDK_INT <= ICE_CREAM_SANDWICH) { <call> } else { ... } |
| level <= api && fromThen |
| } |
| } else if (tokenType === UastBinaryOperator.LESS) { |
| return if (isLowerBound) { |
| // if (SDK_INT < ICE_CREAM_SANDWICH) { ... } else { <call> } |
| level >= api && !fromThen |
| } else { |
| // if (SDK_INT < ICE_CREAM_SANDWICH) { <call> } else { ... } |
| level - 1 <= api && fromThen |
| } |
| } else if (tokenType === UastBinaryOperator.EQUALS || |
| tokenType === UastBinaryOperator.IDENTITY_EQUALS |
| ) { |
| // if (SDK_INT == ICE_CREAM_SANDWICH) { <call> } else { ... } |
| return if (isLowerBound) { |
| level >= api && fromThen |
| } else { |
| level <= api && fromThen |
| } |
| } else if (tokenType === UastBinaryOperator.NOT_EQUALS || |
| tokenType === UastBinaryOperator.IDENTITY_NOT_EQUALS |
| ) { |
| // if (SDK_INT != ICE_CREAM_SANDWICH) { ... } else { <call> } |
| return level == api && !fromThen |
| } else { |
| assert(false) { tokenType } |
| } |
| } |
| } |
| return null |
| } |
| |
| private fun getApiLevel( |
| element: UExpression?, |
| apiLevelLookup: ApiLevelLookup? |
| ): Int { |
| var level = -1 |
| if (element is UReferenceExpression) { |
| val codeName = element.resolvedName |
| if (codeName != null) { |
| level = SdkVersionInfo.getApiByBuildCode(codeName, false) |
| } |
| if (level == -1) { |
| val constant = ConstantEvaluator.evaluate(null, element) |
| if (constant is Number) { |
| level = constant.toInt() |
| } |
| } |
| } else if (element is ULiteralExpression) { |
| val value = element.value |
| if (value is Int) { |
| level = value |
| } |
| } |
| if (level == -1 && apiLevelLookup != null && element != null) { |
| level = apiLevelLookup(element) |
| } |
| return level |
| } |
| |
| private fun isOredWithConditional( |
| element: UElement, |
| api: Int, |
| isLowerBound: Boolean, |
| before: UElement? |
| ): Boolean { |
| if (element is UBinaryExpression) { |
| if (element.operator === UastBinaryOperator.LOGICAL_OR) { |
| val left = element.leftOperand |
| if (before !== left) { |
| var ok = isVersionCheckConditional( |
| api = api, |
| isLowerBound = isLowerBound, |
| element = left, |
| and = false |
| ) |
| if (ok != null) { |
| return ok |
| } |
| val right = element.rightOperand |
| ok = isVersionCheckConditional( |
| api = api, |
| isLowerBound = isLowerBound, |
| element = right, |
| and = false |
| ) |
| if (ok != null) { |
| return ok |
| } |
| } |
| } |
| val value = |
| isVersionCheckConditional( |
| api = api, |
| isLowerBound = isLowerBound, |
| fromThen = false, |
| binary = element |
| ) |
| return value != null && value |
| } else if (element is UPolyadicExpression) { |
| if (element.operator === UastBinaryOperator.LOGICAL_OR) { |
| for (operand in element.operands) { |
| if (operand == before) { |
| break |
| } else if (isOredWithConditional( |
| element = operand, |
| api = api, |
| isLowerBound = isLowerBound, |
| before = before |
| ) |
| ) { |
| return true |
| } |
| } |
| } |
| } |
| return false |
| } |
| |
| private fun isAndedWithConditional( |
| element: UElement, |
| api: Int, |
| isLowerBound: Boolean, |
| before: UElement? |
| ): Boolean { |
| if (element is UBinaryExpression) { |
| if (element.operator === UastBinaryOperator.LOGICAL_AND) { |
| val left = element.leftOperand |
| if (before !== left) { |
| var ok = isVersionCheckConditional( |
| api = api, |
| isLowerBound = isLowerBound, |
| element = left, |
| and = true |
| ) |
| if (ok != null) { |
| return ok |
| } |
| val right = element.rightOperand |
| ok = isVersionCheckConditional( |
| api = api, |
| isLowerBound = isLowerBound, |
| element = right, |
| and = true |
| ) |
| if (ok != null) { |
| return ok |
| } |
| } |
| } |
| val value = isVersionCheckConditional( |
| api = api, |
| isLowerBound = isLowerBound, |
| fromThen = true, |
| binary = element |
| ) |
| return value != null && value |
| } else if (element is UPolyadicExpression) { |
| if (element.operator === UastBinaryOperator.LOGICAL_AND) { |
| for (operand in element.operands) { |
| if (operand == before) { |
| break |
| } else if (isAndedWithConditional( |
| operand, |
| api, |
| isLowerBound, |
| before |
| ) |
| ) { |
| return true |
| } |
| } |
| } |
| } |
| return false |
| } |
| |
| private inner class VersionCheckWithExitFinder( |
| private val endElement: UElement, |
| private val api: Int, |
| private val isLowerBound: Boolean |
| ) : AbstractUastVisitor() { |
| private var found = false |
| private var done = false |
| override fun visitElement(node: UElement): Boolean { |
| if (done) { |
| return true |
| } |
| if (node === endElement) { |
| done = true |
| } |
| return done |
| } |
| |
| override fun visitIfExpression(node: UIfExpression): Boolean { |
| super.visitIfExpression(node) |
| if (done) { |
| return true |
| } |
| if (endElement.isUastChildOf(node, true)) { |
| // Even if there is an unconditional exit, endElement will occur before it! |
| done = true |
| return true |
| } |
| val thenBranch = node.thenExpression |
| val elseBranch = node.elseExpression |
| if (thenBranch != null) { |
| val level = isVersionCheckConditional( |
| api = api, |
| isLowerBound = isLowerBound, |
| element = node.condition, |
| and = false |
| ) |
| if (level != null && level) { |
| // See if the body does an immediate return |
| if (isUnconditionalReturn(thenBranch)) { |
| found = true |
| done = true |
| } |
| } |
| } |
| if (elseBranch != null) { |
| val level = isVersionCheckConditional( |
| api = api, |
| isLowerBound = isLowerBound, |
| element = node.condition, |
| and = true |
| ) |
| if (level != null && level) { |
| if (isUnconditionalReturn(elseBranch)) { |
| found = true |
| done = true |
| } |
| } |
| } |
| return true |
| } |
| |
| fun found(): Boolean { |
| return found |
| } |
| } |
| } |
| |
| private fun PsiAnnotation.getAnnotationIntValue( |
| attribute: String, |
| defaultValue: Int = -1 |
| ): Int { |
| val psiValue = findAttributeValue(attribute) ?: return defaultValue |
| val value = ConstantEvaluator.evaluate(null, psiValue) |
| if (value is Number) { |
| return value.toInt() |
| } |
| |
| return defaultValue |
| } |
| |
| private fun PsiAnnotation.getAnnotationStringValue( |
| attribute: String, |
| defaultValue: String = "" |
| ): String { |
| val psiValue = findAttributeValue(attribute) ?: return defaultValue |
| return ConstantEvaluator.evaluateString(null, psiValue, false) |
| ?: defaultValue |
| } |