blob: d0f4365917eb2c4f19e32b924dd9089862faeca3 [file] [log] [blame]
/*
* Copyright (C) 2013 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 org.jetbrains.android.inspections.lint;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.sdklib.SdkVersionInfo;
import com.android.tools.lint.checks.ApiDetector;
import com.android.tools.lint.checks.ApiLookup;
import com.android.tools.lint.detector.api.*;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.PsiClassReferenceType;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.MethodSignatureUtil;
import com.intellij.psi.util.PsiTreeUtil;
import lombok.ast.AstVisitor;
import lombok.ast.CompilationUnit;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.Node;
import org.jetbrains.annotations.NonNls;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import static org.jetbrains.android.inspections.lint.IntellijLintUtils.SUPPRESS_LINT_FQCN;
import static org.jetbrains.android.inspections.lint.IntellijLintUtils.SUPPRESS_WARNINGS_FQCN;
/**
* Intellij-specific version of the {@link ApiDetector} which uses the PSI structure
* to check accesses
* <p>
* TODO:
* <ul>
* <li>Compare to the bytecode based results</li>
* </ul>
*/
public class IntellijApiDetector extends ApiDetector {
@SuppressWarnings("unchecked")
static final Implementation IMPLEMENTATION = new Implementation(
IntellijApiDetector.class,
EnumSet.of(Scope.RESOURCE_FILE, Scope.MANIFEST, Scope.JAVA_FILE),
Scope.MANIFEST_SCOPE,
Scope.RESOURCE_FILE_SCOPE,
Scope.JAVA_FILE_SCOPE
);
@NonNls
private static final String TARGET_API_FQCN = "android.annotation.TargetApi";
private static final String SDK_INT = "SDK_INT";
@Nullable
@Override
public List<Class<? extends Node>> getApplicableNodeTypes() {
return Collections.<Class<? extends Node>>singletonList(CompilationUnit.class);
}
@Nullable
@Override
public AstVisitor createJavaVisitor(@NonNull final JavaContext context) {
return new ForwardingAstVisitor() {
@Override
public boolean visitCompilationUnit(CompilationUnit node) {
check(context);
return true;
}
};
}
private void check(final JavaContext context) {
if (mApiDatabase == null) {
return;
}
ApplicationManager.getApplication().runReadAction(new Runnable() {
@Override
public void run() {
final PsiFile psiFile = IntellijLintUtils.getPsiFile(context);
if (!(psiFile instanceof PsiJavaFile)) {
return;
}
PsiJavaFile javaFile = (PsiJavaFile)psiFile;
for (PsiClass clz : javaFile.getClasses()) {
PsiElementVisitor visitor = new ApiCheckVisitor(context, clz, psiFile);
javaFile.accept(visitor);
}
}
});
}
private static int getTargetApi(@NonNull PsiElement e, @NonNull PsiElement file) {
PsiElement element = e;
// Search upwards for target api annotations
while (element != null && element != file) { // otherwise it will keep going into directories!
if (element instanceof PsiModifierListOwner) {
PsiModifierListOwner owner = (PsiModifierListOwner)element;
PsiModifierList modifierList = owner.getModifierList();
PsiAnnotation annotation = null;
if (modifierList != null) {
annotation = modifierList.findAnnotation(TARGET_API_FQCN);
}
if (annotation != null) {
for (PsiNameValuePair pair : annotation.getParameterList().getAttributes()) {
PsiAnnotationMemberValue v = pair.getValue();
if (v instanceof PsiLiteral) {
PsiLiteral literal = (PsiLiteral)v;
Object value = literal.getValue();
if (value instanceof Integer) {
return (Integer) value;
} else if (value instanceof String) {
return codeNameToApi((String) value);
}
} else if (v instanceof PsiArrayInitializerMemberValue) {
PsiArrayInitializerMemberValue mv = (PsiArrayInitializerMemberValue)v;
for (PsiAnnotationMemberValue mmv : mv.getInitializers()) {
if (mmv instanceof PsiLiteral) {
PsiLiteral literal = (PsiLiteral)mmv;
Object value = literal.getValue();
if (value instanceof Integer) {
return (Integer) value;
} else if (value instanceof String) {
return codeNameToApi((String) value);
}
}
}
} else if (v instanceof PsiExpression) {
if (v instanceof PsiReferenceExpression) {
String fqcn = ((PsiReferenceExpression)v).getQualifiedName();
return codeNameToApi(fqcn);
} else {
return codeNameToApi(v.getText());
}
}
}
}
}
element = element.getParent();
}
return -1;
}
private static int codeNameToApi(String text) {
int dotIndex = text.lastIndexOf('.');
if (dotIndex != -1) {
text = text.substring(dotIndex + 1);
}
return SdkVersionInfo.getApiByBuildCode(text, true);
}
private class ApiCheckVisitor extends JavaRecursiveElementVisitor {
private final Context myContext;
private boolean mySeenSuppress;
private boolean mySeenTargetApi;
private final PsiClass myClass;
private final PsiFile myFile;
private final boolean myCheckAccess;
private boolean myCheckOverride;
private String myFrameworkParent;
public ApiCheckVisitor(Context context, PsiClass clz, PsiFile file) {
myContext = context;
myClass = clz;
myFile = file;
myCheckAccess = context.isEnabled(UNSUPPORTED) || context.isEnabled(INLINED);
myCheckOverride = context.isEnabled(OVERRIDE)
&& context.getMainProject().getBuildSdk() >= 1;
if (myCheckOverride) {
myFrameworkParent = null;
PsiClass superClass = myClass.getSuperClass();
while (superClass != null) {
String fqcn = superClass.getQualifiedName();
if (fqcn == null) {
myCheckOverride = false;
} else if (fqcn.startsWith("android.") //$NON-NLS-1$
|| fqcn.startsWith("java.") //$NON-NLS-1$
|| fqcn.startsWith("javax.")) { //$NON-NLS-1$
if (!fqcn.equals(CommonClassNames.JAVA_LANG_OBJECT)) {
myFrameworkParent = ClassContext.getInternalName(fqcn);
}
break;
}
superClass = superClass.getSuperClass();
}
if (myFrameworkParent == null) {
myCheckOverride = false;
}
}
}
@Override
public void visitAnnotation(PsiAnnotation annotation) {
super.visitAnnotation(annotation);
String fqcn = annotation.getQualifiedName();
if (TARGET_API_FQCN.equals(fqcn)) {
mySeenTargetApi = true;
}
else if (SUPPRESS_LINT_FQCN.equals(fqcn) || SUPPRESS_WARNINGS_FQCN.equals(fqcn)) {
mySeenSuppress = true;
}
}
@Override
public void visitMethod(PsiMethod method) {
super.visitMethod(method);
if (!myCheckOverride) {
return;
}
int buildSdk = myContext.getMainProject().getBuildSdk();
String name = method.getName();
assert myFrameworkParent != null;
String desc = IntellijLintUtils.getInternalDescription(method, false, false);
if (desc == null) {
// Couldn't compute description of method for some reason; probably
// failure to resolve parameter types
return;
}
int api = mApiDatabase.getCallVersion(myFrameworkParent, name, desc);
if (api > buildSdk && buildSdk != -1) {
if (mySeenSuppress &&
IntellijLintUtils.isSuppressed(method, myFile, OVERRIDE)) {
return;
}
// TODO: Don't complain if it's annotated with @Override; that means
// somehow the build target isn't correct.
String fqcn;
PsiClass containingClass = method.getContainingClass();
if (containingClass != null) {
String className = containingClass.getName();
String fullClassName = containingClass.getQualifiedName();
if (fullClassName != null) {
className = fullClassName;
}
fqcn = className + '#' + name;
} else {
fqcn = name;
}
String message = String.format(
"This method is not overriding anything with the current build " +
"target, but will in API level %1$d (current target is %2$d): %3$s",
api, buildSdk, fqcn);
PsiElement locationNode = method.getNameIdentifier();
if (locationNode == null) {
locationNode = method;
}
Location location = IntellijLintUtils.getLocation(myContext.file, locationNode);
myContext.report(OVERRIDE, location, message);
}
}
@Override
public void visitClass(PsiClass aClass) {
super.visitClass(aClass);
if (!myCheckAccess) {
return;
}
for (PsiClassType type : aClass.getSuperTypes()) {
String signature = IntellijLintUtils.getInternalName(type);
if (signature == null) {
continue;
}
int api = mApiDatabase.getClassVersion(signature);
if (api == -1) {
continue;
}
int minSdk = getMinSdk(myContext);
if (api <= minSdk) {
continue;
}
if (mySeenTargetApi) {
int target = getTargetApi(aClass, myFile);
if (target != -1) {
if (api <= target) {
continue;
}
}
}
if (mySeenSuppress && IntellijLintUtils.isSuppressed(aClass, myFile, UNSUPPORTED)) {
continue;
}
Location location;
if (type instanceof PsiClassReferenceType) {
PsiReference reference = ((PsiClassReferenceType)type).getReference();
PsiElement element = reference.getElement();
if (isWithinVersionCheckConditional(element, api)) {
continue;
}
if (isPrecededByVersionCheckExit(element, api)) {
continue;
}
location = IntellijLintUtils.getLocation(myContext.file, element);
} else {
location = IntellijLintUtils.getLocation(myContext.file, aClass);
}
String fqcn = type.getClassName();
String message = String.format("Class requires API level %1$d (current min is %2$d): %3$s", api, minSdk, fqcn);
myContext.report(UNSUPPORTED, location, message);
}
}
@Override
public void visitReferenceExpression(PsiReferenceExpression expression) {
super.visitReferenceExpression(expression);
if (!myCheckAccess) {
return;
}
PsiReference reference = expression.getReference();
if (reference == null) {
return;
}
PsiElement resolved = reference.resolve();
if (resolved != null) {
if (resolved instanceof PsiField) {
PsiField field = (PsiField)resolved;
PsiClass containingClass = field.getContainingClass();
if (containingClass == null) {
return;
}
String owner = IntellijLintUtils.getInternalName(containingClass);
if (owner == null) {
return; // Couldn't resolve type
}
String name = field.getName();
int api = mApiDatabase.getFieldVersion(owner, name);
if (api == -1) {
return;
}
int minSdk = getMinSdk(myContext);
if (isSuppressed(api, expression, minSdk)) {
return;
}
Location location = IntellijLintUtils.getLocation(myContext.file, expression);
String fqcn = containingClass.getQualifiedName();
String message = String.format(
"Field requires API level %1$d (current min is %2$d): %3$s",
api, minSdk, fqcn + '#' + name);
Issue issue = UNSUPPORTED;
// When accessing primitive types or Strings, the values get copied into
// the class files (e.g. get inlined) which has a separate issue type:
// INLINED.
PsiType type = field.getType();
if (type == PsiType.INT || type == PsiType.CHAR || type == PsiType.BOOLEAN
|| type == PsiType.DOUBLE || type == PsiType.FLOAT || type == PsiType.BYTE
|| type.equalsToText(CommonClassNames.JAVA_LANG_STRING)) {
issue = INLINED;
// Some usages of inlined constants are okay:
if (isBenignConstantUsage(expression, name, owner)) {
return;
}
}
myContext.report(issue, location, message);
}
}
}
@Override
public void visitTryStatement(PsiTryStatement statement) {
super.visitTryStatement(statement);
PsiResourceList resourceList = statement.getResourceList();
if (resourceList != null) {
int api = 19; // minSdk for try with resources
int minSdk = getMinSdk(myContext);
if (isSuppressed(api, statement, minSdk)) {
return;
}
Location location = IntellijLintUtils.getLocation(myContext.file, resourceList);
String message = String.format("Try-with-resources requires API level %1$d (current min is %2$d)", api, minSdk);
myContext.report(UNSUPPORTED, location, message);
}
for (PsiParameter parameter : statement.getCatchBlockParameters()) {
PsiTypeElement typeElement = parameter.getTypeElement();
if (typeElement != null) {
PsiType type = typeElement.getType();
PsiClass resolved = null;
PsiElement reference = parameter;
if (type instanceof PsiDisjunctionType) {
type = ((PsiDisjunctionType)type).getLeastUpperBound();
if (type instanceof PsiClassType) {
resolved = ((PsiClassType)type).resolve();
}
} else if (type instanceof PsiClassReferenceType) {
PsiClassReferenceType referenceType = (PsiClassReferenceType)type;
resolved = referenceType.resolve();
reference = referenceType.getReference().getElement();
} else if (type instanceof PsiClassType) {
resolved = ((PsiClassType)type).resolve();
}
if (resolved != null) {
String signature = IntellijLintUtils.getInternalName(resolved);
if (signature == null) {
continue;
}
int api = mApiDatabase.getClassVersion(signature);
if (api == -1) {
continue;
}
int minSdk = getMinSdk(myContext);
if (api <= minSdk) {
continue;
}
if (mySeenTargetApi) {
int target = getTargetApi(statement, myFile);
if (target != -1) {
if (api <= target) {
continue;
}
}
}
if (mySeenSuppress && IntellijLintUtils.isSuppressed(statement, myFile, UNSUPPORTED)) {
continue;
}
Location location;
location = IntellijLintUtils.getLocation(myContext.file, reference);
String fqcn = resolved.getName();
String message = String.format("Class requires API level %1$d (current min is %2$d): %3$s", api, minSdk, fqcn);
// Special case reflective operation exception which can be implicitly used
// with multi-catches: see issue 153406
if (api == 19 && fqcn.equals("ReflectiveOperationException")) {
message = String.format("Multi-catch with these reflection exceptions requires API level 19 (current min is %2$d) " +
"because they get compiled to the common but new super type `ReflectiveOperationException`. " +
"As a workaround either create individual catch statements, or catch `Exception`.",
api, minSdk);
}
myContext.report(UNSUPPORTED, location, message);
}
}
}
}
private boolean isSuppressed(int api, PsiElement element, int minSdk) {
if (api <= minSdk) {
return true;
}
if (mySeenTargetApi) {
int target = getTargetApi(element, myFile);
if (target != -1) {
if (api <= target) {
return true;
}
}
}
if (mySeenSuppress &&
(IntellijLintUtils.isSuppressed(element, myFile, UNSUPPORTED) || IntellijLintUtils.isSuppressed(element, myFile, INLINED))) {
return true;
}
if (isWithinVersionCheckConditional(element, api)) {
return true;
}
if (isPrecededByVersionCheckExit(element, api)) {
return true;
}
return false;
}
public boolean isBenignConstantUsage(
@NonNull PsiElement node,
@NonNull String name,
@NonNull String owner) {
if (ApiDetector.isBenignConstantUsage(null, name, owner)) {
return true;
}
// It's okay to reference the constant as a case constant (since that
// code path won't be taken) or in a condition of an if statement
// or as a case value
PsiElement curr = node.getParent();
while (curr != null) {
if (curr instanceof PsiSwitchLabelStatement) {
PsiSwitchLabelStatement caseStatement = (PsiSwitchLabelStatement)curr;
PsiExpression condition = caseStatement.getCaseValue();
return condition != null && PsiTreeUtil.isAncestor(condition, node, false);
} else if (curr instanceof PsiIfStatement) {
PsiIfStatement ifStatement = (PsiIfStatement)curr;
PsiExpression condition = ifStatement.getCondition();
return condition != null && PsiTreeUtil.isAncestor(condition, node, false);
} else if (curr instanceof PsiConditionalExpression) {
// ?:-statement
PsiConditionalExpression ifStatement = (PsiConditionalExpression)curr;
PsiExpression condition = ifStatement.getCondition();
return PsiTreeUtil.isAncestor(condition, node, false);
}
curr = curr.getParent();
}
return false;
}
@Override
public void visitCallExpression(PsiCallExpression expression) {
super.visitCallExpression(expression);
if (!myCheckAccess) {
return;
}
// TODO: How does this differ from visitMethodCallExpression?
// Inferred super perhaps? No, I think it refers to constructor invocations!
PsiMethod method = expression.resolveMethod();
if (method != null) {
PsiClass containingClass = method.getContainingClass();
if (containingClass == null) {
return;
}
String fqcn = containingClass.getQualifiedName();
String owner = IntellijLintUtils.getInternalName(containingClass);
if (owner == null) {
return; // Couldn't resolve type
}
String name = IntellijLintUtils.getInternalMethodName(method);
String desc = IntellijLintUtils.getInternalDescription(method, false, false);
if (desc == null) {
// Couldn't compute description of method for some reason; probably
// failure to resolve parameter types
return;
}
int api = mApiDatabase.getCallVersion(owner, name, desc);
if (api == -1) {
return;
}
int minSdk = getMinSdk(myContext);
if (api <= minSdk) {
return;
}
// The lint API database contains two optimizations:
// First, all members that were available in API 1 are omitted from the database, since that saves
// about half of the size of the database, and for API check purposes, we don't need to distinguish
// between "doesn't exist" and "available in all versions".
// Second, all inherited members were inlined into each class, so that it doesn't have to do a
// repeated search up the inheritance chain.
//
// Unfortunately, in this custom PSI detector, we look up the real resolved method, which can sometimes
// have a different minimum API.
//
// For example, SQLiteDatabase had a close() method from API 1. Therefore, calling SQLiteDatabase is supported
// in all versions. However, it extends SQLiteClosable, which in API 16 added "implements Closable". In
// this detector, if we have the following code:
// void test(SQLiteDatabase db) { db.close }
// here the call expression will be the close method on type SQLiteClosable. And that will result in an API
// requirement of API 16, since the close method it now resolves to is in API 16.
//
// To work around this, we can now look up the type of the call expression ("db" in the above, but it could
// have been more complicated), and if that's a different type than the type of the method, we look up
// *that* method from lint's database instead. Furthermore, it's possible for that method to return "-1"
// and we can't tell if that means "doesn't exist" or "present in API 1", we then check the package prefix
// to see whether we know it's an API method whose members should all have been inlined.
if (expression instanceof PsiMethodCallExpression) {
PsiExpression qualifier = ((PsiMethodCallExpression)expression).getMethodExpression().getQualifierExpression();
if (qualifier != null && !(qualifier instanceof PsiThisExpression) && !(qualifier instanceof PsiSuperExpression)) {
PsiType type = qualifier.getType();
if (type != null && type instanceof PsiClassType) {
String expressionOwner = IntellijLintUtils.getInternalName((PsiClassType)type);
if (expressionOwner != null && !expressionOwner.equals(owner)) {
int specificApi = mApiDatabase.getCallVersion(expressionOwner, name, desc);
if (specificApi == -1) {
if (ApiLookup.isRelevantOwner(expressionOwner)) {
return;
}
} else if (specificApi <= minSdk) {
return;
}
}
}
} else {
// Unqualified call; need to search in our super hierarchy
PsiClass cls = PsiTreeUtil.getParentOfType(expression, PsiClass.class);
//noinspection ConstantConditions
if (qualifier instanceof PsiThisExpression || qualifier instanceof PsiSuperExpression) {
PsiQualifiedExpression pte = (PsiQualifiedExpression)qualifier;
PsiJavaCodeReferenceElement operand = pte.getQualifier();
if (operand != null) {
PsiElement resolved = operand.resolve();
if (resolved instanceof PsiClass) {
cls = (PsiClass)resolved;
}
}
}
while (cls != null) {
if (cls instanceof PsiAnonymousClass) {
// If it's an unqualified call in an anonymous class, we need to rely on the
// resolve method to find out whether the method is picked up from the anonymous
// class chain or any outer classes
break;
}
String expressionOwner = IntellijLintUtils.getInternalName(cls);
if (expressionOwner == null) {
break;
}
int specificApi = mApiDatabase.getCallVersion(expressionOwner, name, desc);
if (specificApi == -1) {
if (ApiLookup.isRelevantOwner(expressionOwner)) {
return;
}
} else if (specificApi <= minSdk) {
return;
} else {
break;
}
cls = cls.getSuperClass();
}
}
}
if (isSuppressed(api, expression, minSdk)) {
return;
}
// If you're simply calling super.X from method X, even if method X is in a higher API level than the minSdk, we're
// generally safe; that method should only be called by the framework on the right API levels. (There is a danger of
// somebody calling that method locally in other contexts, but this is hopefully unlikely.)
if (expression instanceof PsiMethodCallExpression) {
PsiMethodCallExpression call = (PsiMethodCallExpression)expression;
PsiReferenceExpression methodExpression = call.getMethodExpression();
if (methodExpression.getQualifierExpression() instanceof PsiSuperExpression) {
PsiMethod containingMethod = PsiTreeUtil.getParentOfType(expression, PsiMethod.class, true);
if (containingMethod != null && name.equals(containingMethod.getName())
&& MethodSignatureUtil.areSignaturesEqual(method, containingMethod)
// We specifically exclude constructors from this check, because we do want to flag constructors requiring the
// new API level; it's highly likely that the constructor is called by local code so you should specifically
// investigate this as a developer
&& !method.isConstructor()) {
return;
}
}
}
PsiElement locationNode = IntellijLintUtils.getCallName(expression);
if (locationNode == null) {
locationNode = expression;
}
Location location = IntellijLintUtils.getLocation(myContext.file, locationNode);
String message = String.format("Call requires API level %1$d (current min is %2$d): %3$s", api, minSdk,
fqcn + '#' + method.getName());
myContext.report(UNSUPPORTED, location, message);
}
}
}
private static boolean isPrecededByVersionCheckExit(PsiElement element, int api) {
PsiElement current = PsiTreeUtil.getParentOfType(element, PsiStatement.class);
if (current != null) {
PsiElement prev = getPreviousStatement(current);
if (prev == null) {
//noinspection unchecked
current = PsiTreeUtil.getParentOfType(current, PsiStatement.class, true, PsiMethod.class, PsiClass.class);
} else {
current = prev;
}
}
while (current != null) {
if (current instanceof PsiIfStatement) {
PsiIfStatement ifStatement = (PsiIfStatement)current;
PsiStatement thenBranch = ifStatement.getThenBranch();
PsiStatement elseBranch = ifStatement.getElseBranch();
if (thenBranch != null) {
Boolean level = isVersionCheckConditional(api, thenBranch, ifStatement);
if (level != null) {
// See if the body does an immediate return
if (isUnconditionalReturn(thenBranch)) {
return true;
}
}
}
if (elseBranch != null) {
Boolean level = isVersionCheckConditional(api, elseBranch, ifStatement);
if (level != null) {
if (isUnconditionalReturn(elseBranch)) {
return true;
}
}
}
}
PsiElement prev = getPreviousStatement(current);
if (prev == null) {
//noinspection unchecked
current = PsiTreeUtil.getParentOfType(current, PsiStatement.class, true, PsiMethod.class, PsiClass.class);
if (current == null) {
return false;
}
} else {
current = prev;
}
}
return false;
}
private static boolean isUnconditionalReturn(PsiStatement statement) {
if (statement instanceof PsiBlockStatement) {
PsiBlockStatement blockStatement = (PsiBlockStatement)statement;
PsiCodeBlock block = blockStatement.getCodeBlock();
PsiStatement[] statements = block.getStatements();
if (statements.length == 1 && statements[0] instanceof PsiReturnStatement) {
return true;
}
}
if (statement instanceof PsiReturnStatement) {
return true;
}
return false;
}
@Nullable
public static PsiStatement getPreviousStatement(PsiElement element) {
final PsiElement prevStatement = PsiTreeUtil.skipSiblingsBackward(element, PsiWhiteSpace.class, PsiComment.class);
return prevStatement instanceof PsiStatement ? (PsiStatement)prevStatement : null;
}
private static boolean isWithinVersionCheckConditional(PsiElement element, int api) {
PsiElement current = element.getParent();
PsiElement prev = element;
while (current != null) {
if (current instanceof PsiIfStatement) {
PsiIfStatement ifStatement = (PsiIfStatement)current;
Boolean isConditional = isVersionCheckConditional(api, prev, ifStatement);
if (isConditional != null) {
return isConditional;
}
} else if (current instanceof PsiPolyadicExpression && isAndedWithConditional(current, api, prev)) {
return true;
} else if (current instanceof PsiMethod || current instanceof PsiFile) {
return false;
}
prev = current;
current = current.getParent();
}
return false;
}
@Nullable
private static Boolean isVersionCheckConditional(int api, PsiElement prev, PsiIfStatement ifStatement) {
PsiExpression condition = ifStatement.getCondition();
if (condition != prev && condition instanceof PsiBinaryExpression) {
Boolean isConditional = isVersionCheckConditional(api, prev, ifStatement, (PsiBinaryExpression)condition);
if (isConditional != null) {
return isConditional;
}
} else if (condition instanceof PsiPolyadicExpression) {
PsiPolyadicExpression ppe = (PsiPolyadicExpression)condition;
if (ppe.getOperationTokenType() == JavaTokenType.ANDAND && (prev == ifStatement.getThenBranch())) {
if (isAndedWithConditional(ppe, api, prev)) {
return true;
}
}
} else if (condition instanceof PsiMethodCallExpression) {
PsiMethodCallExpression call = (PsiMethodCallExpression) condition;
PsiMethod method = call.resolveMethod();
if (method != null) {
PsiCodeBlock body = method.getBody();
if (body != null) {
PsiStatement[] statements = body.getStatements();
if (statements.length == 1) {
PsiStatement statement = statements[0];
if (statement instanceof PsiReturnStatement) {
PsiReturnStatement returnStatement = (PsiReturnStatement) statement;
PsiExpression returnValue = returnStatement.getReturnValue();
if (returnValue instanceof PsiBinaryExpression) {
Boolean isConditional = isVersionCheckConditional(api, null, null, (PsiBinaryExpression)returnValue);
if (isConditional != null) {
return isConditional;
}
}
}
}
}
}
}
return null;
}
@Nullable
private static Boolean isVersionCheckConditional(int api,
@Nullable PsiElement prev,
@Nullable PsiIfStatement ifStatement,
@NonNull PsiBinaryExpression binary) {
IElementType tokenType = binary.getOperationTokenType();
if (tokenType == JavaTokenType.GT || tokenType == JavaTokenType.GE ||
tokenType == JavaTokenType.LE || tokenType == JavaTokenType.LT ||
tokenType == JavaTokenType.EQEQ) {
PsiExpression left = binary.getLOperand();
if (left instanceof PsiReferenceExpression) {
PsiReferenceExpression ref = (PsiReferenceExpression)left;
if (SDK_INT.equals(ref.getReferenceName())) {
PsiExpression right = binary.getROperand();
int level = -1;
if (right instanceof PsiReferenceExpression) {
PsiReferenceExpression ref2 = (PsiReferenceExpression)right;
String codeName = ref2.getReferenceName();
if (codeName == null) {
return false;
}
level = SdkVersionInfo.getApiByBuildCode(codeName, true);
} else if (right instanceof PsiLiteralExpression) {
PsiLiteralExpression lit = (PsiLiteralExpression)right;
Object value = lit.getValue();
if (value instanceof Integer) {
level = ((Integer)value).intValue();
}
}
if (level != -1) {
boolean fromThen = ifStatement == null || prev == ifStatement.getThenBranch();
boolean fromElse = ifStatement != null && prev == ifStatement.getElseBranch();
assert fromThen == !fromElse;
if (tokenType == JavaTokenType.GE) {
// if (SDK_INT >= ICE_CREAM_SANDWICH) { <call> } else { ... }
return level >= api && fromThen;
}
else if (tokenType == JavaTokenType.GT) {
// if (SDK_INT > ICE_CREAM_SANDWICH) { <call> } else { ... }
return level >= api - 1 && fromThen;
}
else if (tokenType == JavaTokenType.LE) {
// if (SDK_INT <= ICE_CREAM_SANDWICH) { ... } else { <call> }
return level >= api - 1 && fromElse;
}
else if (tokenType == JavaTokenType.LT) {
// if (SDK_INT < ICE_CREAM_SANDWICH) { ... } else { <call> }
return level >= api && fromElse;
}
else if (tokenType == JavaTokenType.EQEQ) {
// if (SDK_INT == ICE_CREAM_SANDWICH) { <call> } else { }
return level >= api && fromThen;
} else {
assert false : tokenType;
}
}
}
}
} else if (tokenType == JavaTokenType.ANDAND && (ifStatement != null && prev == ifStatement.getThenBranch())) {
if (isAndedWithConditional(ifStatement.getCondition(), api, prev)) {
return true;
}
}
return null;
}
private static boolean isAndedWithConditional(PsiElement element, int api, @Nullable PsiElement before) {
if (element instanceof PsiBinaryExpression) {
PsiBinaryExpression inner = (PsiBinaryExpression)element;
if (inner.getOperationTokenType() == JavaTokenType.ANDAND) {
return isAndedWithConditional(inner.getLOperand(), api, before) ||
inner.getROperand() != before && isAndedWithConditional(inner.getROperand(), api, before);
} else if (inner.getLOperand() instanceof PsiReferenceExpression &&
SDK_INT.equals(((PsiReferenceExpression)inner.getLOperand()).getReferenceName())) {
int level = -1;
IElementType tokenType = inner.getOperationTokenType();
PsiExpression right = inner.getROperand();
if (right instanceof PsiReferenceExpression) {
PsiReferenceExpression ref2 = (PsiReferenceExpression)right;
String codeName = ref2.getReferenceName();
if (codeName == null) {
return false;
}
level = SdkVersionInfo.getApiByBuildCode(codeName, true);
} else if (right instanceof PsiLiteralExpression) {
PsiLiteralExpression lit = (PsiLiteralExpression)right;
Object value = lit.getValue();
if (value instanceof Integer) {
level = ((Integer)value).intValue();
}
}
if (level != -1) {
if (tokenType == JavaTokenType.GE) {
// if (SDK_INT >= ICE_CREAM_SANDWICH && <call>
return level >= api;
}
else if (tokenType == JavaTokenType.GT) {
// if (SDK_INT > ICE_CREAM_SANDWICH) && <call>
return level >= api - 1;
}
else if (tokenType == JavaTokenType.EQEQ) {
// if (SDK_INT == ICE_CREAM_SANDWICH) && <call>
return level >= api;
}
}
}
}
else if (element instanceof PsiPolyadicExpression) {
PsiPolyadicExpression ppe = (PsiPolyadicExpression)element;
if (ppe.getOperationTokenType() == JavaTokenType.ANDAND) {
for (PsiExpression operand : ppe.getOperands()) {
if (operand == before) {
break;
}
else if (isAndedWithConditional(operand, api, before)) {
return true;
}
}
}
}
return false;
}
}