/*
* 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.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";
@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, null);
}
}
@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;
}
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, null);
}
}
@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, null);
}
}
}
@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, null);
}
for (PsiParameter parameter : statement.getCatchBlockParameters()) {
PsiTypeElement typeElement = parameter.getTypeElement();
if (typeElement != null) {
PsiType type = typeElement.getType();
if (type instanceof PsiClassReferenceType) {
PsiClassReferenceType referenceType = (PsiClassReferenceType)type;
PsiClass resolved = referenceType.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;
PsiReference reference = referenceType.getReference();
location = IntellijLintUtils.getLocation(myContext.file, reference.getElement());
String fqcn = referenceType.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, null);
}
}
}
}
}
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;
}
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);
while (cls != null) {
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;
}
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, null);
}
}
}
private static boolean isWithinVersionCheckConditional(PsiElement element, int api) {
PsiElement current = element.getParent();
PsiElement prev = current;
while (current != null) {
if (current instanceof PsiIfStatement) {
PsiIfStatement ifStatement = (PsiIfStatement)current;
PsiExpression condition = ifStatement.getCondition();
if (condition != prev && condition instanceof PsiBinaryExpression) {
PsiBinaryExpression binary = (PsiBinaryExpression)condition;
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();
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 = prev == ifStatement.getThenBranch();
boolean fromElse = 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 (current instanceof PsiMethod || current instanceof PsiFile) {
return false;
}
prev = current;
current = current.getParent();
}
return false;
}
}