blob: 2f2f2c2c4c2ce7a08392c62ed0806952659662be [file] [log] [blame]
package com.android.tools.lint.checks;
import static com.android.tools.lint.detector.api.Lint.getMethodName;
import static com.android.tools.lint.detector.api.Lint.getNextInstruction;
import static com.android.tools.lint.detector.api.Lint.skipParentheses;
import static com.android.utils.SdkUtils.endsWithIgnoreCase;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.sdklib.SdkVersionInfo;
import com.android.tools.lint.client.api.JavaEvaluator;
import com.android.tools.lint.detector.api.ClassContext;
import com.android.tools.lint.detector.api.ConstantEvaluator;
import com.android.tools.lint.detector.api.Lint;
import com.google.common.annotations.VisibleForTesting;
import com.intellij.openapi.util.Ref;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
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.PsiModifierList;
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 java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
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.UastContext;
import org.jetbrains.uast.UastPrefixOperator;
import org.jetbrains.uast.UastUtils;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.IntInsnNode;
import org.objectweb.asm.tree.JumpInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.analysis.AnalyzerException;
/**
* Utility methods 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)
*/
public class VersionChecks {
private interface ApiLevelLookup {
int getApiLevel(@NonNull UElement element);
}
public static final String SDK_INT = "SDK_INT";
private static final String ANDROID_OS_BUILD_VERSION = "android/os/Build$VERSION";
/** SDK int method used by the data binding compiler */
private static final String GET_BUILD_SDK_INT = "getBuildSdkInt";
public static int codeNameToApi(@NonNull String text) {
int dotIndex = text.lastIndexOf('.');
if (dotIndex != -1) {
text = text.substring(dotIndex + 1);
}
return SdkVersionInfo.getApiByBuildCode(text, true);
}
public static boolean isWithinSdkConditional(
@NonNull ClassContext context,
@NonNull ClassNode classNode,
@NonNull MethodNode method,
@NonNull AbstractInsnNode call,
int requiredApi) {
assert requiredApi != -1;
if (!containsSimpleSdkCheck(method)) {
return false;
}
try {
// Search in the control graph, from beginning, up to the target call
// node, to see if it's reachable. The call graph is constructed in a
// special way: we include all control flow edges, *except* those that
// are satisfied by a SDK_INT version check (where the operand is a version
// that is at least as high as the one needed for the given call).
//
// If we can reach the call, that means that there is a way this call
// can be reached on some versions, and lint should flag the call/field lookup.
//
//
// Let's say you have code like this:
// if (SDK_INT >= LOLLIPOP) {
// // Call
// return property.hasAdjacentMapping();
// }
// ...
//
// The compiler will turn this into the following byte code:
//
// 0: getstatic #3; //Field android/os/Build$VERSION.SDK_INT:I
// 3: bipush 21
// 5: if_icmple 17
// 8: aload_1
// 9: invokeinterface #4, 1; //InterfaceMethod
// android/view/ViewDebug$ExportedProperty.hasAdjacentMapping:()Z
// 14: ifeq 17
// 17: ... code after if loop
//
// When the call graph is constructed, for an if branch we're called twice; once
// where the target is the next instruction (the one taken if byte code check is false)
// and one to the jump label (the one taken if the byte code condition is true).
//
// Notice how at the byte code level, the logic is reversed: the >= instruction
// is turned into "<" and we jump to the code *after* the if clause; otherwise
// it will just fall through. Therefore, if we take a byte code branch, that means
// that the SDK check was *not* satisfied, and conversely, the target call is reachable
// if we don't take the branch.
//
// Therefore, when we build the call graph, we will add call graph nodes for an
// if check if :
// (1) it is some other comparison than <, <= or !=.
// (2) if the byte code comparison check is *not* satisfied, this means that the the
// SDK check was successful and that the call graph should only include
// the jump edge
// (3) all other edges are added
//
// With a flow control graph like that, we can determine whether a target call
// is guarded by a given SDK check: that will be the case if we cannot reach
// the target call in the call graph
ApiCheckGraph graph = new ApiCheckGraph(requiredApi);
ControlFlowGraph.create(graph, classNode, method);
// Note: To debug unit tests, you may want to for example do
// ControlFlowGraph.Node callNode = graph.getNode(call);
// Set<ControlFlowGraph.Node> highlight = Sets.newHashSet(callNode);
// Files.write(graph.toDot(highlight), new File("/tmp/graph.gv"), Charsets.UTF_8);
// This will generate a graphviz file you can visualize with the "dot" utility
AbstractInsnNode first = method.instructions.get(0);
return !graph.isConnected(first, call);
} catch (AnalyzerException e) {
context.log(e, null);
}
return false;
}
private static boolean containsSimpleSdkCheck(@NonNull MethodNode method) {
// Look for a compiled version of "if (Build.VERSION.SDK_INT op N) {"
InsnList nodes = method.instructions;
for (int i = 0, n = nodes.size(); i < n; i++) {
AbstractInsnNode instruction = nodes.get(i);
if (isSdkVersionLookup(instruction)) {
AbstractInsnNode bipush = getNextInstruction(instruction);
if (bipush != null && bipush.getOpcode() == Opcodes.BIPUSH) {
AbstractInsnNode ifNode = getNextInstruction(bipush);
if (ifNode != null && ifNode.getType() == AbstractInsnNode.JUMP_INSN) {
return true;
}
}
}
}
return false;
}
private static boolean isSdkVersionLookup(@NonNull AbstractInsnNode instruction) {
if (instruction.getOpcode() == Opcodes.GETSTATIC) {
FieldInsnNode fieldNode = (FieldInsnNode) instruction;
return (SDK_INT.equals(fieldNode.name)
&& ANDROID_OS_BUILD_VERSION.equals(fieldNode.owner));
}
return false;
}
public static boolean isPrecededByVersionCheckExit(
@NonNull UElement element, int api, boolean isLowerBound) {
//noinspection unchecked
UExpression currentExpression =
UastUtils.getParentOfType(
element, UExpression.class, true, UMethod.class, UClass.class);
while (currentExpression != null) {
VersionCheckWithExitFinder visitor =
new VersionCheckWithExitFinder(element, api, isLowerBound);
currentExpression.accept(visitor);
if (visitor.found()) {
return true;
}
element = currentExpression;
//noinspection unchecked
currentExpression =
UastUtils.getParentOfType(
currentExpression,
UExpression.class,
true,
UMethod.class,
UClass.class); // TODO: what about lambdas?
}
return false;
}
private static class VersionCheckWithExitFinder extends AbstractUastVisitor {
private final UElement endElement;
private final int api;
private final boolean isLowerBound;
private boolean found = false;
private boolean done = false;
public VersionCheckWithExitFinder(UElement endElement, int api, boolean isLowerBound) {
this.endElement = endElement;
this.api = api;
this.isLowerBound = isLowerBound;
}
@Override
public boolean visitElement(@NonNull UElement node) {
if (done) {
return true;
}
if (node.equals(endElement)) {
done = true;
}
return done;
}
@Override
public boolean visitIfExpression(@NonNull UIfExpression ifStatement) {
if (done) {
return true;
}
UExpression thenBranch = ifStatement.getThenExpression();
UExpression elseBranch = ifStatement.getElseExpression();
if (thenBranch != null) {
Boolean level =
isVersionCheckConditional(
api, isLowerBound, ifStatement.getCondition(), false, null, null);
if (level != null && level) {
// See if the body does an immediate return
if (isUnconditionalReturn(thenBranch)) {
found = true;
done = true;
}
}
}
if (elseBranch != null) {
Boolean level =
isVersionCheckConditional(
api, isLowerBound, ifStatement.getCondition(), true, null, null);
if (level != null && level) {
if (isUnconditionalReturn(elseBranch)) {
found = true;
done = true;
}
}
}
return true;
}
public boolean found() {
return found;
}
}
private static boolean isUnconditionalReturn(UExpression statement) {
if (statement instanceof UBlockExpression) {
List<UExpression> expressions = ((UBlockExpression) statement).getExpressions();
int statements = expressions.size();
if (statements > 0) {
UExpression last = expressions.get(statements - 1);
if (last instanceof UReturnExpression || last instanceof UThrowExpression) {
return true;
} else if (last instanceof UCallExpression) {
UCallExpression call = (UCallExpression) last;
String methodName = getMethodName(call);
// Look for Kotlin runtime library methods that unconditionally exit
if ("error".equals(methodName) || "TODO".equals(methodName)) {
return true;
}
}
}
}
return statement instanceof UReturnExpression;
}
public static boolean isWithinVersionCheckConditional(
@NonNull JavaEvaluator evaluator,
@NonNull UElement element,
int api,
boolean isLowerBound) {
return isWithinVersionCheckConditional(evaluator, element, api, isLowerBound, null);
}
public static boolean isWithinVersionCheckConditional(
@NonNull JavaEvaluator evaluator,
@NonNull UElement element,
int api,
boolean isLowerBound,
@Nullable ApiLevelLookup apiLookup) {
UElement current = skipParentheses(element.getUastParent());
UElement prev = element;
while (current != null) {
if (current instanceof UIfExpression) {
UIfExpression ifStatement = (UIfExpression) current;
UExpression condition = ifStatement.getCondition();
if (prev != condition) {
boolean fromThen = prev.equals(ifStatement.getThenExpression());
Boolean ok =
isVersionCheckConditional(
api, isLowerBound, condition, fromThen, prev, apiLookup);
if (ok != null && ok) {
return true;
}
}
} else if (current instanceof UPolyadicExpression
&& (isAndedWithConditional(current, api, isLowerBound, prev)
|| isOredWithConditional(current, api, isLowerBound, prev))) {
return true;
} else if (current instanceof USwitchClauseExpressionWithBody) {
USwitchClauseExpressionWithBody body = (USwitchClauseExpressionWithBody) current;
for (UExpression condition : body.getCaseValues()) {
Boolean ok =
isVersionCheckConditional(
api, isLowerBound, condition, true, prev, apiLookup);
if (ok != null && ok) {
return true;
}
}
} else if (current instanceof UCallExpression && prev instanceof 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
//
UCallExpression call = (UCallExpression) current;
PsiMethod method = call.resolve();
if (method != null) {
Map<UExpression, PsiParameter> mapping =
evaluator.computeArgumentMapping(call, method);
PsiParameter parameter = mapping.get(prev);
if (parameter != null) {
UastContext context = UastUtils.getUastContext(element);
UMethod uMethod = context.getMethod(method);
Ref<UCallExpression> match = new Ref<>();
String parameterName = parameter.getName();
uMethod.accept(
new AbstractUastVisitor() {
@Override
public boolean visitCallExpression(
@NonNull UCallExpression node) {
String callName = Lint.getMethodName(node);
if (Objects.equals(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);
}
});
UCallExpression lambdaInvocation = match.get();
ApiLevelLookup newApiLookup =
arg -> {
if (arg instanceof UReferenceExpression) {
PsiElement resolved =
((UReferenceExpression) arg).resolve();
if (resolved instanceof PsiParameter) {
PsiParameter parameter1 = (PsiParameter) resolved;
PsiParameterList parameterList =
PsiTreeUtil.getParentOfType(
resolved, PsiParameterList.class);
if (parameterList != null) {
int index =
parameterList.getParameterIndex(parameter1);
List<UExpression> arguments =
call.getValueArguments();
if (index != -1 && index < arguments.size()) {
return getApiLevel(arguments.get(index), null);
}
}
}
}
return -1;
};
if (lambdaInvocation != null
&& isWithinVersionCheckConditional(
evaluator,
lambdaInvocation,
api,
isLowerBound,
newApiLookup)) {
return true;
}
}
}
} else if (current instanceof UMethod) {
UElement parent = current.getUastParent();
if (!(parent instanceof UAnonymousClass)) {
return false;
}
} else if (current instanceof PsiFile) {
return false;
}
prev = current;
current = skipParentheses(current.getUastParent());
}
return false;
}
@Nullable
private static Boolean isVersionCheckConditional(
int api,
boolean isLowerBound,
@NonNull UElement element,
boolean and,
@Nullable UElement prev,
@Nullable ApiLevelLookup apiLookup) {
if (element instanceof UPolyadicExpression) {
if (element instanceof UBinaryExpression) {
UBinaryExpression binary = (UBinaryExpression) element;
Boolean ok = isVersionCheckConditional(api, isLowerBound, and, binary, apiLookup);
if (ok != null) {
return ok;
}
}
UPolyadicExpression expression = (UPolyadicExpression) element;
UastBinaryOperator tokenType = expression.getOperator();
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 instanceof UCallExpression) {
UCallExpression call = (UCallExpression) element;
return isValidVersionCall(api, isLowerBound, and, call);
} else if (element instanceof UReferenceExpression) {
// Constant expression for an SDK version check?
UReferenceExpression refExpression = (UReferenceExpression) element;
PsiElement resolved = refExpression.resolve();
if (resolved instanceof PsiField) {
PsiField field = (PsiField) resolved;
PsiModifierList modifierList = field.getModifierList();
if (modifierList != null && modifierList.hasExplicitModifier(PsiModifier.STATIC)) {
UastContext context = UastUtils.getUastContext(element);
UExpression initializer = context.getInitializerBody(field);
if (initializer != null) {
Boolean ok =
isVersionCheckConditional(
api, isLowerBound, initializer, and, null, null);
if (ok != null) {
return ok;
}
}
}
} else if (resolved instanceof PsiMethod
&& element instanceof UQualifiedReferenceExpression
&& ((UQualifiedReferenceExpression) element).getSelector()
instanceof UCallExpression) {
UCallExpression call =
(UCallExpression) ((UQualifiedReferenceExpression) element).getSelector();
return isValidVersionCall(api, isLowerBound, and, call);
} else if (resolved instanceof PsiMethod
&& element instanceof UQualifiedReferenceExpression
&& ((UQualifiedReferenceExpression) element).getReceiver()
instanceof UReferenceExpression) {
// Method call via Kotlin property syntax
return isValidVersionCall(api, isLowerBound, and, element, (PsiMethod) resolved);
}
} else if (element instanceof UUnaryExpression) {
UUnaryExpression prefixExpression = (UUnaryExpression) element;
if (prefixExpression.getOperator() == UastPrefixOperator.LOGICAL_NOT) {
UExpression operand = prefixExpression.getOperand();
Boolean ok =
isVersionCheckConditional(api, isLowerBound, operand, !and, null, null);
if (ok != null) {
return ok;
}
}
}
return null;
}
@Nullable
private static Boolean isValidVersionCall(
int api, boolean isLowerBound, boolean and, UCallExpression call) {
PsiMethod method = call.resolve();
if (method == null) {
return null;
}
return isValidVersionCall(api, isLowerBound, and, call, method);
}
@Nullable
private static Boolean isValidVersionCall(
int api,
boolean isLowerBound,
boolean and,
@NonNull UElement call,
@NonNull PsiMethod method) {
String name = method.getName();
if (name.startsWith("isAtLeast") && isLowerBound) {
PsiClass containingClass = method.getContainingClass();
if (containingClass != null
// android.support.v4.os.BuildCompat,
// androidx.core.os.BuildCompat
&& "BuildCompat".equals(containingClass.getName())) {
if (name.equals("isAtLeastN")) {
return api <= 24;
} else if (name.equals("isAtLeastNMR1")) {
return api <= 25;
} else if (name.equals("isAtLeastO")) {
return api <= 26;
} else if (name.startsWith("isAtLeastP")) {
return api <= 28;
} else if (name.startsWith("isAtLeastQ")) {
return api <= 29;
} else if (name.startsWith("isAtLeast")
&& name.length() == 10
&& Character.isUpperCase(name.charAt(9))
&& name.charAt(9) > 'Q') {
// Try to guess future API levels before they're announced
return api <= SdkVersionInfo.HIGHEST_KNOWN_API + 1;
}
}
}
int 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)) {
UastContext context = UastUtils.getUastContext(call);
UExpression body = context.getMethodBody(method);
if (body == null) {
return null;
}
List<UExpression> expressions;
if (body instanceof UBlockExpression) {
expressions = ((UBlockExpression) body).getExpressions();
} else {
expressions = Collections.singletonList(body);
}
if (expressions.size() == 1) {
UExpression statement = expressions.get(0);
UExpression returnValue = null;
if (statement instanceof UReturnExpression) {
UReturnExpression returnStatement = (UReturnExpression) statement;
returnValue = returnStatement.getReturnExpression();
} else if (statement != null) {
// Kotlin: may not have an explicit return statement
returnValue = statement;
}
if (returnValue != null) {
List<UExpression> arguments =
call instanceof UCallExpression
? ((UCallExpression) call).getValueArguments()
:
// Property syntax
Collections.emptyList();
if (arguments.isEmpty()) {
if (returnValue instanceof UPolyadicExpression
|| returnValue instanceof UCallExpression
|| returnValue instanceof UQualifiedReferenceExpression) {
Boolean isConditional =
isVersionCheckConditional(
api, isLowerBound, returnValue, and, null, null);
if (isConditional != null) {
return isConditional;
}
}
} else if (arguments.size() == 1) {
// See if we're passing in a value to the version utility method
ApiLevelLookup lookup =
arg -> {
if (arg instanceof UReferenceExpression) {
PsiElement resolved =
((UReferenceExpression) arg).resolve();
if (resolved instanceof PsiParameter) {
PsiParameter parameter = (PsiParameter) resolved;
PsiParameterList parameterList =
PsiTreeUtil.getParentOfType(
resolved, PsiParameterList.class);
if (parameterList != null) {
int index =
parameterList.getParameterIndex(parameter);
if (index != -1 && index < arguments.size()) {
return getApiLevel(arguments.get(index), null);
}
}
}
}
return -1;
};
Boolean ok =
isVersionCheckConditional(
api, isLowerBound, returnValue, and, null, lookup);
if (ok != null) {
return ok;
}
}
}
}
}
return null;
}
private static boolean isSdkInt(@NonNull PsiElement element) {
if (element instanceof PsiReferenceExpression) {
PsiReferenceExpression ref = (PsiReferenceExpression) element;
if (SDK_INT.equals(ref.getReferenceName())) {
return true;
}
PsiElement resolved = ref.resolve();
if (resolved instanceof PsiVariable) {
PsiExpression initializer = ((PsiVariable) resolved).getInitializer();
if (initializer != null) {
return isSdkInt(initializer);
}
}
} else if (element instanceof PsiMethodCallExpression) {
PsiMethodCallExpression callExpression = (PsiMethodCallExpression) element;
if (GET_BUILD_SDK_INT.equals(callExpression.getMethodExpression().getReferenceName())) {
return true;
} // else look inside the body?
}
return false;
}
private static boolean isSdkInt(@NonNull UElement element) {
if (element instanceof UReferenceExpression) {
UReferenceExpression ref = (UReferenceExpression) element;
if (SDK_INT.equals(ref.getResolvedName())) {
return true;
}
PsiElement resolved = ref.resolve();
if (resolved instanceof ULocalVariable) {
UExpression initializer = ((ULocalVariable) resolved).getUastInitializer();
if (initializer != null) {
return isSdkInt(initializer);
}
} else if (resolved instanceof PsiVariable) {
PsiExpression initializer = ((PsiVariable) resolved).getInitializer();
if (initializer != null) {
return isSdkInt(initializer);
}
}
} else if (element instanceof UCallExpression) {
UCallExpression callExpression = (UCallExpression) element;
if (GET_BUILD_SDK_INT.equals(getMethodName(callExpression))) {
return true;
} // else look inside the body?
}
return false;
}
private static final String[] VERSION_METHOD_NAME_PREFIXES = {
"isAtLeast", "isRunning", "is", "runningOn", "running"
};
private static final String[] VERSION_METHOD_NAME_SUFFIXES = {
"OrLater", "OrAbove", "OrHigher", "OrNewer", "Sdk"
};
@VisibleForTesting
static int getMinSdkVersionFromMethodName(String name) {
String prefix = null;
String suffix = null;
for (String p : VERSION_METHOD_NAME_PREFIXES) {
if (name.startsWith(p)) {
prefix = p;
break;
}
}
for (String p : VERSION_METHOD_NAME_SUFFIXES) {
if (endsWithIgnoreCase(name, p)) {
suffix = p;
break;
}
}
if ("isAtLeast".equals(prefix) && suffix == null) {
suffix = "";
}
if (prefix != null && suffix != null) {
String codeName = name.substring(prefix.length(), name.length() - suffix.length());
int version = SdkVersionInfo.getApiByPreviewName(codeName, false);
if (version == -1) {
version = SdkVersionInfo.getApiByBuildCode(codeName, false);
if (version == -1
&& codeName.length() == 1
&& Character.isUpperCase(codeName.charAt(0))) {
// Some future API level
version = SdkVersionInfo.HIGHEST_KNOWN_API + 1;
}
}
return version;
}
return -1;
}
@Nullable
private static Boolean isVersionCheckConditional(
int api,
boolean isLowerBound,
boolean fromThen,
@NonNull UBinaryExpression binary,
@Nullable ApiLevelLookup apiLevelLookup) {
UastBinaryOperator tokenType = binary.getOperator();
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) {
UExpression left = binary.getLeftOperand();
int level;
UExpression right;
if (!isSdkInt(left)) {
right = binary.getRightOperand();
if (isSdkInt(right)) {
fromThen = !fromThen;
level = getApiLevel(left, apiLevelLookup);
} else {
return null;
}
} else {
right = binary.getRightOperand();
level = getApiLevel(right, apiLevelLookup);
}
if (level != -1) {
if (tokenType == UastBinaryOperator.GREATER_OR_EQUALS) {
if (isLowerBound) {
// if (SDK_INT >= ICE_CREAM_SANDWICH) { <call> } else { ... }
return level >= api && fromThen;
} else {
// if (SDK_INT >= ICE_CREAM_SANDWICH) { ... } else { <call> }
return level - 1 <= api && !fromThen;
}
} else if (tokenType == UastBinaryOperator.GREATER) {
if (isLowerBound) {
// if (SDK_INT > ICE_CREAM_SANDWICH) { <call> } else { ... }
return level >= api - 1 && fromThen;
} else {
// if (SDK_INT > ICE_CREAM_SANDWICH) { ... } else { <call> }
return level <= api && !fromThen;
}
} else if (tokenType == UastBinaryOperator.LESS_OR_EQUALS) {
if (isLowerBound) {
// if (SDK_INT <= ICE_CREAM_SANDWICH) { ... } else { <call> }
return level >= api - 1 && !fromThen;
} else {
// if (SDK_INT <= ICE_CREAM_SANDWICH) { <call> } else { ... }
return level <= api && fromThen;
}
} else if (tokenType == UastBinaryOperator.LESS) {
if (isLowerBound) {
// if (SDK_INT < ICE_CREAM_SANDWICH) { ... } else { <call> }
return level >= api && !fromThen;
} else {
// if (SDK_INT < ICE_CREAM_SANDWICH) { <call> } else { ... }
return level - 1 <= api && fromThen;
}
} else if (tokenType == UastBinaryOperator.EQUALS
|| tokenType == UastBinaryOperator.IDENTITY_EQUALS) {
// if (SDK_INT == ICE_CREAM_SANDWICH) { <call> } else { ... }
if (isLowerBound) {
return level >= api && fromThen;
} else {
return 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 static int getApiLevel(
@Nullable UExpression element, @Nullable ApiLevelLookup apiLevelLookup) {
int level = -1;
if (element instanceof UReferenceExpression) {
UReferenceExpression ref2 = (UReferenceExpression) element;
String codeName = ref2.getResolvedName();
if (codeName != null) {
level = SdkVersionInfo.getApiByBuildCode(codeName, false);
}
if (level == -1) {
Object constant = ConstantEvaluator.evaluate(null, element);
if (constant instanceof Number) {
level = ((Number) constant).intValue();
}
}
} else if (element instanceof ULiteralExpression) {
ULiteralExpression lit = (ULiteralExpression) element;
Object value = lit.getValue();
if (value instanceof Integer) {
level = (Integer) value;
}
}
if (level == -1 && apiLevelLookup != null && element != null) {
level = apiLevelLookup.getApiLevel(element);
}
return level;
}
private static boolean isOredWithConditional(
@NonNull UElement element, int api, boolean isLowerBound, @Nullable UElement before) {
if (element instanceof UBinaryExpression) {
UBinaryExpression inner = (UBinaryExpression) element;
if (inner.getOperator() == UastBinaryOperator.LOGICAL_OR) {
UExpression left = inner.getLeftOperand();
if (before != left) {
Boolean ok =
isVersionCheckConditional(api, isLowerBound, left, false, null, null);
if (ok != null) {
return ok;
}
UExpression right = inner.getRightOperand();
ok = isVersionCheckConditional(api, isLowerBound, right, false, null, null);
if (ok != null) {
return ok;
}
}
}
Boolean value = isVersionCheckConditional(api, isLowerBound, false, inner, null);
return value != null && value;
} else if (element instanceof UPolyadicExpression) {
UPolyadicExpression ppe = (UPolyadicExpression) element;
if (ppe.getOperator() == UastBinaryOperator.LOGICAL_OR) {
for (UExpression operand : ppe.getOperands()) {
if (operand.equals(before)) {
break;
} else if (isOredWithConditional(operand, api, isLowerBound, before)) {
return true;
}
}
}
}
return false;
}
private static boolean isAndedWithConditional(
@NonNull UElement element, int api, boolean isLowerBound, @Nullable UElement before) {
if (element instanceof UBinaryExpression) {
UBinaryExpression inner = (UBinaryExpression) element;
if (inner.getOperator() == UastBinaryOperator.LOGICAL_AND) {
UExpression left = inner.getLeftOperand();
if (before != left) {
Boolean ok =
isVersionCheckConditional(api, isLowerBound, left, true, null, null);
if (ok != null) {
return ok;
}
UExpression right = inner.getRightOperand();
ok = isVersionCheckConditional(api, isLowerBound, right, true, null, null);
if (ok != null) {
return ok;
}
}
}
Boolean value = isVersionCheckConditional(api, isLowerBound, true, inner, null);
return value != null && value;
} else if (element instanceof UPolyadicExpression) {
UPolyadicExpression ppe = (UPolyadicExpression) element;
if (ppe.getOperator() == UastBinaryOperator.LOGICAL_AND) {
for (UExpression operand : ppe.getOperands()) {
if (operand.equals(before)) {
break;
} else if (isAndedWithConditional(operand, api, isLowerBound, before)) {
return true;
}
}
}
}
return false;
}
// TODO: Merge with the other isVersionCheckConditional
@Nullable
public static Boolean isVersionCheckConditional(int api, @NonNull UBinaryExpression binary) {
UastBinaryOperator tokenType = binary.getOperator();
if (tokenType == UastBinaryOperator.GREATER
|| tokenType == UastBinaryOperator.GREATER_OR_EQUALS
|| tokenType == UastBinaryOperator.LESS_OR_EQUALS
|| tokenType == UastBinaryOperator.LESS
|| tokenType == UastBinaryOperator.EQUALS
|| tokenType == UastBinaryOperator.IDENTITY_EQUALS) {
UExpression left = binary.getLeftOperand();
if (left instanceof UReferenceExpression) {
UReferenceExpression ref = (UReferenceExpression) left;
if (SDK_INT.equals(ref.getResolvedName())) {
UExpression right = binary.getRightOperand();
int level = -1;
if (right instanceof UReferenceExpression) {
UReferenceExpression ref2 = (UReferenceExpression) right;
String codeName = ref2.getResolvedName();
if (codeName == null) {
return false;
}
level = SdkVersionInfo.getApiByBuildCode(codeName, true);
} else if (right instanceof ULiteralExpression) {
ULiteralExpression lit = (ULiteralExpression) right;
Object value = lit.getValue();
if (value instanceof Integer) {
level = (Integer) 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;
}
/**
* Control flow graph which skips control flow edges that check a given SDK_VERSION requirement
* that is not met by a given call
*/
private static class ApiCheckGraph extends ControlFlowGraph {
private final int mRequiredApi;
public ApiCheckGraph(int requiredApi) {
mRequiredApi = requiredApi;
}
@Override
protected void add(@NonNull AbstractInsnNode from, @NonNull AbstractInsnNode to) {
if (from.getType() == AbstractInsnNode.JUMP_INSN
&& from.getPrevious() != null
&& from.getPrevious().getType() == AbstractInsnNode.INT_INSN) {
IntInsnNode intNode = (IntInsnNode) from.getPrevious();
if (intNode.getPrevious() != null && isSdkVersionLookup(intNode.getPrevious())) {
JumpInsnNode jumpNode = (JumpInsnNode) from;
int api = intNode.operand;
boolean isJumpEdge = to == jumpNode.label;
boolean includeEdge;
switch (from.getOpcode()) {
case Opcodes.IF_ICMPNE:
includeEdge = api < mRequiredApi || isJumpEdge;
break;
case Opcodes.IF_ICMPLE:
includeEdge = api < mRequiredApi - 1 || isJumpEdge;
break;
case Opcodes.IF_ICMPLT:
includeEdge = api < mRequiredApi || isJumpEdge;
break;
case Opcodes.IF_ICMPGE:
includeEdge = api < mRequiredApi || !isJumpEdge;
break;
case Opcodes.IF_ICMPGT:
includeEdge = api < mRequiredApi - 1 || !isJumpEdge;
break;
default:
// unexpected comparison for int API level
includeEdge = true;
}
if (!includeEdge) {
return;
}
}
}
super.add(from, to);
}
}
}