diff --git a/idea/testData/android/lint/apiCheck.kt b/idea/testData/android/lint/apiCheck.kt
index 17420c25ac8..f62882d52a6 100644
--- a/idea/testData/android/lint/apiCheck.kt
+++ b/idea/testData/android/lint/apiCheck.kt
@@ -4,6 +4,7 @@
// INSPECTION_CLASS3: org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintOverrideInspection
import android.animation.RectEvaluator
+import android.annotation.SuppressLint
import android.annotation.TargetApi
import org.w3c.dom.DOMError
import org.w3c.dom.DOMErrorHandler
@@ -14,6 +15,7 @@ import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
import android.app.Activity
import android.app.ApplicationErrorReport
+import android.graphics.drawable.VectorDrawable
import android.graphics.Path
import android.graphics.PorterDuff
import android.graphics.Rect
@@ -22,7 +24,7 @@ import android.widget.*
import dalvik.bytecode.OpcodeInfo
import android.os.Build.VERSION
-import android.os.Build.VERSION.SDK_INT
+import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES
import android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH
import android.os.Build.VERSION_CODES.JELLY_BEAN
@@ -30,7 +32,7 @@ import android.os.Bundle
import android.system.ErrnoException
import android.widget.TextView
-@Suppress("SENSELESS_COMPARISON", "UNUSED_EXPRESSION", "UsePropertyAccessSyntax", "UNUSED_VARIABLE", "unused", "UNUSED_PARAMETER", "DEPRECATION")
+@Suppress("SENSELESS_COMPARISON", "UNUSED_EXPRESSION", "UsePropertyAccessSyntax", "UNUSED_VARIABLE", "unused", "UNUSED_PARAMETER", "DEPRECATION", "USELESS_CAST")
class ApiCallTest: Activity() {
fun method(chronometer: Chronometer, locator: DOMLocator) {
@@ -39,8 +41,7 @@ class ApiCallTest: Activity() {
// Ok
Bundle().getInt("")
- // Ok, this constant is inlined
- View.SYSTEM_UI_FLAG_FULLSCREEN
+ View.SYSTEM_UI_FLAG_FULLSCREEN
// Virtual call
getActionBar() // API 11
@@ -48,7 +49,7 @@ class ApiCallTest: Activity() {
// Class references (no call or field access)
val error: DOMError? = null // API 8
- val clz = DOMErrorHandler::class // API 8
+ val clz = DOMErrorHandler::class // API 8
// Method call
chronometer.onChronometerTickListener // API 3
@@ -56,21 +57,21 @@ class ApiCallTest: Activity() {
// Inherited method call (from TextView
chronometer.setTextIsSelectable(true) // API 11
- GridLayout::class
+ GridLayout::class
// Field access
- val field = OpcodeInfo.MAXIMUM_VALUE // API 11
+ val field = OpcodeInfo.MAXIMUM_VALUE // API 11
val fillParent = LayoutParams.FILL_PARENT // API 1
// This is a final int, which means it gets inlined
val matchParent = LayoutParams.MATCH_PARENT // API 8
// Field access: non final
- val batteryInfo = report!!.batteryInfo
+ val batteryInfo = report!!.batteryInfo
// Enum access
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP) {
- val mode = PorterDuff.Mode.OVERLAY // API 11
+ val mode = PorterDuff.Mode.OVERLAY // API 11
}
}
@@ -115,12 +116,6 @@ class ApiCallTest: Activity() {
}
fun test(priority: Boolean, layout: ViewGroup) {
- if (layout is LinearLayout) {}
- layout as? LinearLayout
-
- if (layout is GridLayout) {}
- layout as? GridLayout
-
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
GridLayout(null).getOrientation(); // Not flagged
} else {
@@ -250,14 +245,22 @@ class ApiCallTest: Activity() {
//ERROR
}
- if (SDK_INT >= JELLY_BEAN && textView.isSuggestionsEnabled()) {
+ if (SDK_INT >= JELLY_BEAN && textView.isSuggestionsEnabled()) {
//NO ERROR
}
- if (SDK_INT >= JELLY_BEAN && textView.isSuggestionsEnabled) {
+ if (SDK_INT >= JELLY_BEAN && textView.isSuggestionsEnabled) {
//NO ERROR
}
+ if (SDK_INT >= JELLY_BEAN && (textView.text != "" || textView.isSuggestionsEnabled)) {
+ //NO ERROR
+ }
+
+ if (SDK_INT < JELLY_BEAN && (textView.text != "" || textView.isSuggestionsEnabled)) {
+ //ERROR
+ }
+
if (SDK_INT < JELLY_BEAN && textView.isSuggestionsEnabled()) {
//ERROR
}
@@ -265,10 +268,108 @@ class ApiCallTest: Activity() {
if (SDK_INT < JELLY_BEAN && textView.isSuggestionsEnabled) {
//ERROR
}
+
+ if (SDK_INT < JELLY_BEAN || textView.isSuggestionsEnabled) {
+ //NO ERROR
+ }
+
+ if (SDK_INT > JELLY_BEAN || textView.isSuggestionsEnabled) {
+ //ERROR
+ }
+
+
+ // getActionBar() API 11
+ if (SDK_INT <= 10 || getActionBar() == null) {
+ //NO ERROR
+ }
+
+ if (SDK_INT < 10 || getActionBar() == null) {
+ //ERROR
+ }
+
+ if (SDK_INT < 11 || getActionBar() == null) {
+ //NO ERROR
+ }
+
+ if (SDK_INT != 11 || getActionBar() == null) {
+ //NO ERROR
+ }
+
+ if (SDK_INT != 12 || getActionBar() == null) {
+ //ERROR
+ }
+
+ if (SDK_INT <= 11 || getActionBar() == null) {
+ //NO ERROR
+ }
+
+ if (SDK_INT < 12 || getActionBar() == null) {
+ //NO ERROR
+ }
+
+ if (SDK_INT <= 12 || getActionBar() == null) {
+ //NO ERROR
+ }
+
+ if (SDK_INT < 9 || getActionBar() == null) {
+ //ERROR
+ }
+
+ if (SDK_INT <= 9 || getActionBar() == null) {
+ //ERROR
+ }
+ }
+
+ fun testReturn() {
+ if (SDK_INT < 11) {
+ return
+ }
+
+ // No Error
+ val actionBar = getActionBar()
+ }
+
+ fun testThrow() {
+ if (SDK_INT < 11) {
+ throw IllegalStateException()
+ }
+
+ // No Error
+ val actionBar = getActionBar()
+ }
+
+ fun testError() {
+ if (SDK_INT < 11) {
+ error("Api")
+ }
+
+ // No Error
+ val actionBar = getActionBar()
+ }
+
+ fun testWithoutAnnotation(textView: TextView) {
+ if (textView.isSuggestionsEnabled()) {
+
+ }
+
+ if (textView.isSuggestionsEnabled) {
+
+ }
}
@TargetApi(JELLY_BEAN)
- fun testWithAnnotation(textView: TextView) {
+ fun testWithTargetApiAnnotation(textView: TextView) {
+ if (textView.isSuggestionsEnabled()) {
+ //NO ERROR, annotation
+ }
+
+ if (textView.isSuggestionsEnabled) {
+ //NO ERROR, annotation
+ }
+ }
+
+ @SuppressLint("NewApi")
+ fun testWithSuppressLintAnnotation(textView: TextView) {
if (textView.isSuggestionsEnabled()) {
//NO ERROR, annotation
}
@@ -291,6 +392,59 @@ class ApiCallTest: Activity() {
Path().addOval(0f, 0f, 0f, 0f, Path.Direction.CW)
}
+ // KT-14737 False error with short-circuit evaluation
+ fun testShortCircuitEvaluation() {
+ getDrawable(0) // error here as expected
+ if(Build.VERSION.SDK_INT >= 23
+ && null == getDrawable(0)) // error here should not occur
+ {
+ getDrawable(0) // no error here as expected
+ }
+ }
+
+ // KT-1482 Kotlin Lint: "Calling new methods on older versions" does not report call on receiver in extension function
+ private fun Bundle.caseE1a() { getBinder("") }
+
+ private fun Bundle.caseE1c() { this.getBinder("") }
+
+ private fun caseE1b(bundle: Bundle) { bundle.getBinder("") }
+
+ // KT-12023 Kotlin Lint: Cast doesn't trigger minSdk error
+ fun testCast(layout: ViewGroup) {
+ if (layout is LinearLayout) {} // OK API 1
+ layout as? LinearLayout // OK API 1
+ layout as LinearLayout // OK API 1
+
+ if (layout !is GridLayout) {} // TODO: should be error, fixed in uast > 1.0.8
+ layout as? GridLayout // TODO: should be error, fixed in uast > 1.0.8
+ layout as GridLayout // TODO: should be error, fixed in uast > 1.0.8
+
+ val grid = layout as? GridLayout // TODO: should be error, fixed in uast > 1.0.8
+ val linear = layout as LinearLayout // OK API 1
+ }
+
+ // TODO: should be error
+ class ErrorVectorDravable : VectorDrawable()
+
+ @TargetApi(21)
+ class MyVectorDravable : VectorDrawable()
+
+ fun testTypes() {
+ GridLayout(this)
+ val c = VectorDrawable::class.java
+ }
+
+ fun testCallWithApiAnnotation(textView: TextView) {
+ MyVectorDravable()
+ testWithTargetApiAnnotation(textView)
+ }
+
+ companion object : Activity() {
+ fun test() {
+ getDrawable(0)
+ }
+ }
+
// Return type
internal // API 14
val gridLayout: GridLayout?
@@ -298,4 +452,10 @@ class ApiCallTest: Activity() {
private val report: ApplicationErrorReport?
get() = null
+}
+
+object O: Activity() {
+ fun test() {
+ getDrawable(0)
+ }
}
\ No newline at end of file
diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiDetector.java
index 7dd9020126e..95e0dd32c60 100644
--- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiDetector.java
+++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiDetector.java
@@ -16,42 +16,6 @@
package com.android.tools.klint.checks;
-import static com.android.SdkConstants.ANDROID_PREFIX;
-import static com.android.SdkConstants.ANDROID_THEME_PREFIX;
-import static com.android.SdkConstants.ANDROID_URI;
-import static com.android.SdkConstants.ATTR_CLASS;
-import static com.android.SdkConstants.ATTR_FULL_BACKUP_CONTENT;
-import static com.android.SdkConstants.ATTR_ID;
-import static com.android.SdkConstants.ATTR_LABEL_FOR;
-import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
-import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
-import static com.android.SdkConstants.ATTR_NAME;
-import static com.android.SdkConstants.ATTR_PADDING_START;
-import static com.android.SdkConstants.ATTR_PARENT;
-import static com.android.SdkConstants.ATTR_TARGET_API;
-import static com.android.SdkConstants.ATTR_TEXT_IS_SELECTABLE;
-import static com.android.SdkConstants.ATTR_VALUE;
-import static com.android.SdkConstants.BUTTON;
-import static com.android.SdkConstants.CHECK_BOX;
-import static com.android.SdkConstants.CLASS_CONSTRUCTOR;
-import static com.android.SdkConstants.CONSTRUCTOR_NAME;
-import static com.android.SdkConstants.FQCN_TARGET_API;
-import static com.android.SdkConstants.PREFIX_ANDROID;
-import static com.android.SdkConstants.SUPPORT_ANNOTATIONS_PREFIX;
-import static com.android.SdkConstants.SWITCH;
-import static com.android.SdkConstants.TAG;
-import static com.android.SdkConstants.TAG_ITEM;
-import static com.android.SdkConstants.TAG_STYLE;
-import static com.android.SdkConstants.TARGET_API;
-import static com.android.SdkConstants.TOOLS_URI;
-import static com.android.SdkConstants.VIEW_TAG;
-import static com.android.tools.klint.detector.api.ClassContext.getFqcn;
-import static com.android.tools.klint.detector.api.LintUtils.getNextInstruction;
-import static com.android.tools.klint.detector.api.Location.SearchDirection.BACKWARD;
-import static com.android.tools.klint.detector.api.Location.SearchDirection.FORWARD;
-import static com.android.tools.klint.detector.api.Location.SearchDirection.NEAREST;
-import static com.android.utils.SdkUtils.getResourceFieldName;
-
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
@@ -64,84 +28,34 @@ import com.android.sdklib.AndroidVersion;
import com.android.sdklib.BuildToolInfo;
import com.android.sdklib.SdkVersionInfo;
import com.android.sdklib.repositoryv2.AndroidSdkHandler;
-import com.android.tools.klint.client.api.*;
-import com.android.tools.klint.detector.api.Category;
-import com.android.tools.klint.detector.api.ClassContext;
-import com.android.tools.klint.detector.api.Context;
-import com.android.tools.klint.detector.api.DefaultPosition;
-import com.android.tools.klint.detector.api.Detector;
+import com.android.tools.klint.client.api.IssueRegistry;
+import com.android.tools.klint.client.api.JavaEvaluator;
+import com.android.tools.klint.client.api.LintDriver;
+import com.android.tools.klint.client.api.UastLintUtils;
+import com.android.tools.klint.detector.api.*;
import com.android.tools.klint.detector.api.Detector.ClassScanner;
-import com.android.tools.klint.detector.api.Implementation;
-import com.android.tools.klint.detector.api.Issue;
-import com.android.tools.klint.detector.api.JavaContext;
-import com.android.tools.klint.detector.api.LintUtils;
-import com.android.tools.klint.detector.api.Location;
-import com.android.tools.klint.detector.api.Location.SearchHints;
-import com.android.tools.klint.detector.api.Position;
-import com.android.tools.klint.detector.api.ResourceXmlDetector;
-import com.android.tools.klint.detector.api.Scope;
-import com.android.tools.klint.detector.api.Severity;
-import com.android.tools.klint.detector.api.TextFormat;
-import com.android.tools.klint.detector.api.XmlContext;
-import com.intellij.psi.PsiAnnotation;
-import com.intellij.psi.PsiAnnotationMemberValue;
-import com.intellij.psi.PsiAnnotationParameterList;
-import com.intellij.psi.PsiArrayInitializerMemberValue;
-import com.intellij.psi.PsiClass;
-import com.intellij.psi.PsiClassType;
-import com.intellij.psi.PsiElement;
-import com.intellij.psi.PsiExpression;
-import com.intellij.psi.PsiField;
-import com.intellij.psi.PsiFile;
-import com.intellij.psi.PsiLiteral;
-import com.intellij.psi.PsiMethod;
-import com.intellij.psi.PsiModifier;
-import com.intellij.psi.PsiModifierList;
-import com.intellij.psi.PsiModifierListOwner;
-import com.intellij.psi.PsiNameValuePair;
-import com.intellij.psi.PsiParameter;
-import com.intellij.psi.PsiParameterList;
-import com.intellij.psi.PsiPrimitiveType;
-import com.intellij.psi.PsiReferenceExpression;
-import com.intellij.psi.PsiType;
-
+import com.intellij.psi.*;
+import com.intellij.psi.util.MethodSignatureUtil;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.jetbrains.android.inspections.klint.IntellijLintUtils;
+import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.*;
import org.jetbrains.uast.expressions.UReferenceExpression;
import org.jetbrains.uast.expressions.UTypeReferenceExpression;
-import org.jetbrains.uast.java.JavaUAnnotation;
import org.jetbrains.uast.util.UastExpressionUtils;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
import org.jetbrains.uast.visitor.UastVisitor;
-import org.jetbrains.org.objectweb.asm.Opcodes;
-import org.jetbrains.org.objectweb.asm.Type;
-import org.jetbrains.org.objectweb.asm.tree.AbstractInsnNode;
-import org.jetbrains.org.objectweb.asm.tree.AnnotationNode;
-import org.jetbrains.org.objectweb.asm.tree.ClassNode;
-import org.jetbrains.org.objectweb.asm.tree.FieldInsnNode;
-import org.jetbrains.org.objectweb.asm.tree.FieldNode;
-import org.jetbrains.org.objectweb.asm.tree.InsnList;
-import org.jetbrains.org.objectweb.asm.tree.IntInsnNode;
-import org.jetbrains.org.objectweb.asm.tree.JumpInsnNode;
-import org.jetbrains.org.objectweb.asm.tree.LdcInsnNode;
-import org.jetbrains.org.objectweb.asm.tree.LocalVariableNode;
-import org.jetbrains.org.objectweb.asm.tree.LookupSwitchInsnNode;
-import org.jetbrains.org.objectweb.asm.tree.MethodInsnNode;
-import org.jetbrains.org.objectweb.asm.tree.MethodNode;
-import org.jetbrains.org.objectweb.asm.tree.TryCatchBlockNode;
-import org.jetbrains.org.objectweb.asm.tree.analysis.AnalyzerException;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.File;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.EnumSet;
-import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+import java.util.*;
+
+import static com.android.SdkConstants.*;
+import static com.android.tools.klint.detector.api.ClassContext.getFqcn;
+import static com.android.utils.SdkUtils.getResourceFieldName;
/**
* Looks for usages of APIs that are not supported in all the versions targeted
@@ -150,22 +64,10 @@ import java.util.regex.Pattern;
public class ApiDetector extends ResourceXmlDetector
implements ClassScanner, Detector.UastScanner {
- /**
- * Whether we flag variable, field, parameter and return type declarations of a type
- * not yet available. It appears Dalvik is very forgiving and doesn't try to preload
- * classes until actually needed, so there is no need to flag these, and in fact,
- * patterns used for supporting new and old versions sometimes declares these methods
- * and only conditionally end up actually accessing methods and fields, so only check
- * method and field accesses.
- */
- private static final boolean CHECK_DECLARATIONS = false;
-
private static final String ATTR_WIDTH = "width";
private static final String ATTR_HEIGHT = "height";
private static final String ATTR_SUPPORTS_RTL = "supportsRtl";
- private static final boolean AOSP_BUILD = System.getenv("ANDROID_BUILD_TOP") != null; //$NON-NLS-1$
-
public static final String REQUIRES_API_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "RequiresApi"; //$NON-NLS-1$
/** Accessing an unsupported API */
@@ -289,23 +191,16 @@ public class ApiDetector extends ResourceXmlDetector
ApiDetector.class,
Scope.RESOURCE_FILE_SCOPE));
- private static final String TARGET_API_VMSIG = '/' + TARGET_API + ';';
- private static final String REQ_API_VMSIG = "/RequiresApi;";
- private static final String SWITCH_TABLE_PREFIX = "$SWITCH_TABLE$"; //$NON-NLS-1$
- private static final String ORDINAL_METHOD = "ordinal"; //$NON-NLS-1$
- public static final String ENUM_SWITCH_PREFIX = "$SwitchMap$"; //$NON-NLS-1$
-
private static final String TAG_RIPPLE = "ripple";
private static final String TAG_VECTOR = "vector";
private static final String TAG_ANIMATED_VECTOR = "animated-vector";
private static final String TAG_ANIMATED_SELECTOR = "animated-selector";
private static final String SDK_INT = "SDK_INT";
- private static final String ANDROID_OS_BUILD_VERSION = "android/os/Build$VERSION";
- private static final String REFLECTIVE_OPERATION_EXCEPTION
- = "java.lang.ReflectiveOperationException";
+ private static final String REFLECTIVE_OPERATION_EXCEPTION = "java.lang.ReflectiveOperationException";
+ public static final String ERROR = "error";
- protected ApiLookup mApiDatabase;
+ private ApiLookup mApiDatabase;
private boolean mWarnedMissingDb;
private int mMinApi = -1;
@@ -625,7 +520,7 @@ public class ApiDetector extends ResourceXmlDetector
* and developers would not typically need to be aware of to try to implement workarounds
* on older platforms.
*/
- public static boolean isBenignUnusedAttribute(@NonNull String name) {
+ private static boolean isBenignUnusedAttribute(@NonNull String name) {
return ATTR_LABEL_FOR.equals(name)
|| ATTR_TEXT_IS_SELECTABLE.equals(name)
|| "textAlignment".equals(name)
@@ -811,702 +706,6 @@ public class ApiDetector extends ResourceXmlDetector
return mMinApi;
}
- // ---- Implements ClassScanner ----
-
- @SuppressWarnings("rawtypes") // ASM API
- @Override
- public void checkClass(@NonNull final ClassContext context, @NonNull ClassNode classNode) {
- if (mApiDatabase == null) {
- return;
- }
-
- if (AOSP_BUILD && classNode.name.startsWith("android/support/")) { //$NON-NLS-1$
- return;
- }
-
- // Requires util package (add prebuilts/tools/common/asm-tools/asm-debug-all-4.0.jar)
- //classNode.accept(new TraceClassVisitor(new PrintWriter(System.out)));
-
- int classMinSdk = getClassMinSdk(context, classNode);
- if (classMinSdk == -1) {
- classMinSdk = getMinSdk(context);
- }
-
- List methodList = classNode.methods;
- if (methodList.isEmpty()) {
- return;
- }
-
- boolean checkCalls = context.isEnabled(UNSUPPORTED)
- || context.isEnabled(INLINED);
- boolean checkMethods = context.isEnabled(OVERRIDE)
- && context.getMainProject().getBuildSdk() >= 1;
- String frameworkParent = null;
- if (checkMethods) {
- LintDriver driver = context.getDriver();
- String owner = classNode.superName;
- while (owner != null) {
- // For virtual dispatch, walk up the inheritance chain checking
- // each inherited method
- if ((owner.startsWith("android/") //$NON-NLS-1$
- && !owner.startsWith("android/support/")) //$NON-NLS-1$
- || owner.startsWith("java/") //$NON-NLS-1$
- || owner.startsWith("javax/")) { //$NON-NLS-1$
- frameworkParent = owner;
- break;
- }
- owner = driver.getSuperClass(owner);
- }
- if (frameworkParent == null) {
- checkMethods = false;
- }
- }
-
- if (checkCalls) { // Check implements/extends
- if (classNode.superName != null) {
- String signature = classNode.superName;
- checkExtendsClass(context, classNode, classMinSdk, signature);
- }
- if (classNode.interfaces != null) {
- @SuppressWarnings("unchecked") // ASM API
- List interfaceList = classNode.interfaces;
- for (String signature : interfaceList) {
- checkExtendsClass(context, classNode, classMinSdk, signature);
- }
- }
- }
-
- for (Object m : methodList) {
- MethodNode method = (MethodNode) m;
-
- int minSdk = getLocalMinSdk(method.invisibleAnnotations);
- if (minSdk == -1) {
- minSdk = classMinSdk;
- }
-
- InsnList nodes = method.instructions;
-
- if (checkMethods && Character.isJavaIdentifierStart(method.name.charAt(0))) {
- int buildSdk = context.getMainProject().getBuildSdk();
- String name = method.name;
- assert frameworkParent != null;
- int api = mApiDatabase.getCallVersion(frameworkParent, name, method.desc);
- if (api > buildSdk && buildSdk != -1) {
- // TODO: Don't complain if it's annotated with @Override; that means
- // somehow the build target isn't correct.
- String fqcn;
- String owner = classNode.name;
- if (CONSTRUCTOR_NAME.equals(name)) {
- fqcn = "new " + getFqcn(owner); //$NON-NLS-1$
- } else {
- fqcn = getFqcn(owner) + '#' + 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);
-
- Location location = context.getLocation(method, classNode);
- context.report(OVERRIDE, method, null, location, message);
- }
- }
-
- if (!checkCalls) {
- continue;
- }
-
- if (CHECK_DECLARATIONS) {
- // Check types in parameter list and types of local variables
- List localVariables = method.localVariables;
- if (localVariables != null) {
- for (Object v : localVariables) {
- LocalVariableNode var = (LocalVariableNode) v;
- String desc = var.desc;
- if (desc.charAt(0) == 'L') {
- // "Lpackage/Class;" ⇒ "package/Bar"
- String className = desc.substring(1, desc.length() - 1);
- int api = mApiDatabase.getClassVersion(className);
- if (api > minSdk) {
- String fqcn = getFqcn(className);
- String message = String.format(
- "Class requires API level %1$d (current min is %2$d): `%3$s`",
- api, minSdk, fqcn);
- report(context, message, var.start, method,
- className.substring(className.lastIndexOf('/') + 1), null,
- SearchHints.create(NEAREST).matchJavaSymbol());
- }
- }
- }
- }
-
- // Check return type
- // The parameter types are already handled as local variables so we can skip
- // right to the return type.
- // Check types in parameter list
- String signature = method.desc;
- if (signature != null) {
- int args = signature.indexOf(')');
- if (args != -1 && signature.charAt(args + 1) == 'L') {
- String type = signature.substring(args + 2, signature.length() - 1);
- int api = mApiDatabase.getClassVersion(type);
- if (api > minSdk) {
- String fqcn = getFqcn(type);
- String message = String.format(
- "Class requires API level %1$d (current min is %2$d): `%3$s`",
- api, minSdk, fqcn);
- AbstractInsnNode first = nodes.size() > 0 ? nodes.get(0) : null;
- report(context, message, first, method, method.name, null,
- SearchHints.create(BACKWARD).matchJavaSymbol());
- }
- }
- }
- }
-
- for (int i = 0, n = nodes.size(); i < n; i++) {
- AbstractInsnNode instruction = nodes.get(i);
- int type = instruction.getType();
- if (type == AbstractInsnNode.METHOD_INSN) {
- MethodInsnNode node = (MethodInsnNode) instruction;
- String name = node.name;
- String owner = node.owner;
- String desc = node.desc;
-
- // No need to check methods in this local class; we know they
- // won't be an API match
- if (node.getOpcode() == Opcodes.INVOKEVIRTUAL
- && owner.equals(classNode.name)) {
- owner = classNode.superName;
- }
-
- boolean checkingSuperClass = false;
- while (owner != null) {
- int api = mApiDatabase.getCallVersion(owner, name, desc);
- if (api > minSdk) {
- if (method.name.startsWith(SWITCH_TABLE_PREFIX)) {
- // We're in a compiler-generated method to generate an
- // array indexed by enum ordinal values to enum values. The enum
- // itself must be requiring a higher API number than is
- // currently used, but the call site for the switch statement
- // will also be referencing it, so no need to report these
- // calls.
- break;
- }
-
- if (!checkingSuperClass
- && node.getOpcode() == Opcodes.INVOKEVIRTUAL
- && methodDefinedLocally(classNode, name, desc)) {
- break;
- }
-
- String fqcn;
- if (CONSTRUCTOR_NAME.equals(name)) {
- fqcn = "new " + getFqcn(owner); //$NON-NLS-1$
- } else {
- fqcn = getFqcn(owner) + '#' + name;
- }
- String message = String.format(
- "Call requires API level %1$d (current min is %2$d): `%3$s`",
- api, minSdk, fqcn);
-
- if (name.equals(ORDINAL_METHOD)
- && instruction.getNext() != null
- && instruction.getNext().getNext() != null
- && instruction.getNext().getOpcode() == Opcodes.IALOAD
- && instruction.getNext().getNext().getOpcode()
- == Opcodes.TABLESWITCH) {
- message = String.format(
- "Enum for switch requires API level %1$d " +
- "(current min is %2$d): `%3$s`",
- api, minSdk, getFqcn(owner));
- }
-
- // 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 (instruction.getOpcode() == Opcodes.INVOKESPECIAL &&
- name.equals(method.name) && desc.equals(method.desc) &&
- // 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
- !name.equals(CONSTRUCTOR_NAME)) {
- break;
- }
-
- if (isWithinSdkConditional(context, classNode, method, instruction,
- api)) {
- break;
- }
-
- if (api == 19
- && owner.equals("java/lang/ReflectiveOperationException")
- && !method.tryCatchBlocks.isEmpty()) {
- boolean direct = false;
- for (Object o : method.tryCatchBlocks) {
- if (((TryCatchBlockNode)o).type.equals("java/lang/ReflectiveOperationException")) {
- direct = true;
- break;
- }
- }
- if (!direct) {
- 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);
- }
- }
-
- if (api == 24
- && "java.util.concurrent.ConcurrentHashMap.KeySetView#iterator".equals(fqcn)) {
- message += ". The `keySet()` method in `ConcurrentHashMap` "
- + "changed in a backwards incompatible way in Java 8; "
- + "to work around this issue, add an explicit cast to "
- + "`(Map)` before the `keySet()` call.";
- }
-
- report(context, message, node, method, name, null,
- SearchHints.create(FORWARD).matchJavaSymbol());
- break;
- }
-
- // For virtual dispatch, walk up the inheritance chain checking
- // each inherited method
- if (owner.startsWith("android/") //$NON-NLS-1$
- || owner.startsWith("javax/")) { //$NON-NLS-1$
- // The API map has already inlined all inherited methods
- // so no need to keep checking up the chain
- // -- unless it's the support library which is also in
- // the android/ namespace:
- if (owner.startsWith("android/support/") && api == -1) { //$NON-NLS-1$
- owner = context.getDriver().getSuperClass(owner);
- } else {
- owner = null;
- }
- } else if (owner.startsWith("java/")) { //$NON-NLS-1$
- if (owner.equals("java/text/SimpleDateFormat")) {
- checkSimpleDateFormat(context, method, node, minSdk);
- }
- // Already inlined; see comment above
- owner = null;
- } else if (node.getOpcode() == Opcodes.INVOKEVIRTUAL) {
- owner = context.getDriver().getSuperClass(owner);
- } else if (node.getOpcode() == Opcodes.INVOKESTATIC && api == -1) {
- // Inherit through static classes as well
- owner = context.getDriver().getSuperClass(owner);
- } else {
- owner = null;
- }
-
- checkingSuperClass = true;
- }
- } else if (type == AbstractInsnNode.FIELD_INSN) {
- FieldInsnNode node = (FieldInsnNode) instruction;
- String name = node.name;
- String owner = node.owner;
- int api = mApiDatabase.getFieldVersion(owner, name);
- if (api > minSdk) {
- if (method.name.startsWith(SWITCH_TABLE_PREFIX)) {
- checkSwitchBlock(context, classNode, node, method, name, owner,
- api, minSdk);
- continue;
- }
-
- if (isSkippedEnumSwitch(context, classNode, method, node, owner, api)) {
- continue;
- }
-
- if (isWithinSdkConditional(context, classNode, method, instruction, api)) {
- continue;
- }
-
- String fqcn = getFqcn(owner) + '#' + name;
- String message = String.format(
- "Field requires API level %1$d (current min is %2$d): `%3$s`",
- api, minSdk, fqcn);
- report(context, message, node, method, name, null,
- SearchHints.create(FORWARD).matchJavaSymbol());
- }
- } else if (type == AbstractInsnNode.LDC_INSN) {
- LdcInsnNode node = (LdcInsnNode) instruction;
- if (node.cst instanceof Type) {
- Type t = (Type) node.cst;
- String className = t.getInternalName();
-
- int api = mApiDatabase.getClassVersion(className);
- if (api > minSdk) {
- String fqcn = getFqcn(className);
- String message = String.format(
- "Class requires API level %1$d (current min is %2$d): `%3$s`",
- api, minSdk, fqcn);
- report(context, message, node, method,
- className.substring(className.lastIndexOf('/') + 1), null,
- SearchHints.create(FORWARD).matchJavaSymbol());
- }
- }
- }
- }
- }
- }
-
- private void checkExtendsClass(ClassContext context, ClassNode classNode, int classMinSdk,
- String signature) {
- int api = mApiDatabase.getClassVersion(signature);
- if (api > classMinSdk) {
- String fqcn = getFqcn(signature);
- String message = String.format(
- "Class requires API level %1$d (current min is %2$d): `%3$s`",
- api, classMinSdk, fqcn);
-
- String name = signature.substring(signature.lastIndexOf('/') + 1);
- name = name.substring(name.lastIndexOf('$') + 1);
- SearchHints hints = SearchHints.create(BACKWARD).matchJavaSymbol();
- int lineNumber = ClassContext.findLineNumber(classNode);
- Location location = context.getLocationForLine(lineNumber, name, null,
- hints);
- context.report(UNSUPPORTED, location, message);
- }
- }
-
- private static void checkSimpleDateFormat(ClassContext context, MethodNode method,
- MethodInsnNode node, int minSdk) {
- if (minSdk >= 9) {
- // Already OK
- return;
- }
- if (node.name.equals(CONSTRUCTOR_NAME) && !node.desc.equals("()V")) { //$NON-NLS-1$
- // Check first argument
- AbstractInsnNode prev = LintUtils.getPrevInstruction(node);
- if (prev != null && !node.desc.equals("(Ljava/lang/String;)V")) { //$NON-NLS-1$
- prev = LintUtils.getPrevInstruction(prev);
- }
- if (prev != null && prev.getOpcode() == Opcodes.LDC) {
- LdcInsnNode ldc = (LdcInsnNode) prev;
- Object cst = ldc.cst;
- if (cst instanceof String) {
- String pattern = (String) cst;
- boolean isEscaped = false;
- for (int i = 0; i < pattern.length(); i++) {
- char c = pattern.charAt(i);
- if (c == '\'') {
- isEscaped = !isEscaped;
- } else if (!isEscaped && (c == 'L' || c == 'c')) {
- String message = String.format(
- "The pattern character '%1$c' requires API level 9 (current " +
- "min is %2$d) : \"`%3$s`\"", c, minSdk, pattern);
- report(context, message, node, method, pattern, null,
- SearchHints.create(FORWARD));
- return;
- }
- }
- }
- }
- }
- }
-
- @SuppressWarnings("rawtypes") // ASM API
- private static boolean methodDefinedLocally(ClassNode classNode, String name, String desc) {
- List methodList = classNode.methods;
- for (Object m : methodList) {
- MethodNode method = (MethodNode) m;
- if (name.equals(method.name) && desc.equals(method.desc)) {
- return true;
- }
- }
-
- return false;
- }
-
- @SuppressWarnings("rawtypes") // ASM API
- private static void checkSwitchBlock(ClassContext context, ClassNode classNode,
- FieldInsnNode field, MethodNode method, String name, String owner, int api,
- int minSdk) {
- // Switch statements on enums are tricky. The compiler will generate a method
- // which returns an array of the enum constants, indexed by their ordinal() values.
- // However, we only want to complain if the code is actually referencing one of
- // the non-available enum fields.
- //
- // For the android.graphics.PorterDuff.Mode enum for example, the first few items
- // in the array are populated like this:
- //
- // L0
- // ALOAD 0
- // GETSTATIC android/graphics/PorterDuff$Mode.ADD : Landroid/graphics/PorterDuff$Mode;
- // INVOKEVIRTUAL android/graphics/PorterDuff$Mode.ordinal ()I
- // ICONST_1
- // IASTORE
- // L1
- // GOTO L3
- // L2
- // FRAME FULL [[I] [java/lang/NoSuchFieldError]
- // POP
- // L3
- // FRAME SAME
- // ALOAD 0
- // GETSTATIC android/graphics/PorterDuff$Mode.CLEAR : Landroid/graphics/PorterDuff$Mode;
- // INVOKEVIRTUAL android/graphics/PorterDuff$Mode.ordinal ()I
- // ICONST_2
- // IASTORE
- // ...
- // So if we for example find that the "ADD" field isn't accessible, since it requires
- // API 11, we need to
- // (1) First find out what its ordinal number is. We can look at the following
- // instructions to discover this; it's the "ICONST_1" and "IASTORE" instructions.
- // (After ICONST_5 it moves on to BIPUSH 6, BIPUSH 7, etc.)
- // (2) Find the corresponding *usage* of this switch method. For the above enum,
- // the switch ordinal lookup method will be called
- // "$SWITCH_TABLE$android$graphics$PorterDuff$Mode" with desc "()[I".
- // This means we will be looking for an invocation in some other method which looks
- // like this:
- // INVOKESTATIC (current class).$SWITCH_TABLE$android$graphics$PorterDuff$Mode ()[I
- // (obviously, it can be invoked more than once)
- // Note that it can be used more than once in this class and all sites should be
- // checked!
- // (3) Look up the corresponding table switch, which should look something like this:
- // INVOKESTATIC (current class).$SWITCH_TABLE$android$graphics$PorterDuff$Mode ()[I
- // ALOAD 0
- // INVOKEVIRTUAL android/graphics/PorterDuff$Mode.ordinal ()I
- // IALOAD
- // LOOKUPSWITCH
- // 2: L1
- // 11: L2
- // default: L3
- // Here we need to see if the LOOKUPSWITCH instruction is referencing our target
- // case. Above we were looking for the "ADD" case which had ordinal 1. Since this
- // isn't explicitly referenced, we can ignore this field reference.
- AbstractInsnNode next = field.getNext();
- if (next == null || next.getOpcode() != Opcodes.INVOKEVIRTUAL) {
- return;
- }
- next = next.getNext();
- if (next == null) {
- return;
- }
- int ordinal;
- switch (next.getOpcode()) {
- case Opcodes.ICONST_0: ordinal = 0; break;
- case Opcodes.ICONST_1: ordinal = 1; break;
- case Opcodes.ICONST_2: ordinal = 2; break;
- case Opcodes.ICONST_3: ordinal = 3; break;
- case Opcodes.ICONST_4: ordinal = 4; break;
- case Opcodes.ICONST_5: ordinal = 5; break;
- case Opcodes.BIPUSH: {
- IntInsnNode iin = (IntInsnNode) next;
- ordinal = iin.operand;
- break;
- }
- default:
- return;
- }
-
- // Find usages of this call site
- List methodList = classNode.methods;
- for (Object m : methodList) {
- InsnList nodes = ((MethodNode) m).instructions;
- for (int i = 0, n = nodes.size(); i < n; i++) {
- AbstractInsnNode instruction = nodes.get(i);
- if (instruction.getOpcode() != Opcodes.INVOKESTATIC){
- continue;
- }
- MethodInsnNode node = (MethodInsnNode) instruction;
- if (node.name.equals(method.name)
- && node.desc.equals(method.desc)
- && node.owner.equals(classNode.name)) {
- // Find lookup switch
- AbstractInsnNode target = getNextInstruction(node);
- while (target != null) {
- if (target.getOpcode() == Opcodes.LOOKUPSWITCH) {
- LookupSwitchInsnNode lookup = (LookupSwitchInsnNode) target;
- @SuppressWarnings("unchecked") // ASM API
- List keys = lookup.keys;
- if (keys != null && keys.contains(ordinal)) {
- String fqcn = getFqcn(owner) + '#' + name;
- String message = String.format(
- "Enum value requires API level %1$d " +
- "(current min is %2$d): `%3$s`",
- api, minSdk, fqcn);
- report(context, message, lookup, (MethodNode) m, name, null,
- SearchHints.create(FORWARD).matchJavaSymbol());
-
- // Break out of the inner target search only; the switch
- // statement could be used in other places in this class as
- // well and we want to report all problematic usages.
- break;
- }
- }
- target = getNextInstruction(target);
- }
- }
- }
- }
- }
-
- private static boolean isEnumSwitchInitializer(ClassNode classNode) {
- @SuppressWarnings("rawtypes") // ASM API
- List fieldList = classNode.fields;
- for (Object f : fieldList) {
- FieldNode field = (FieldNode) f;
- if (field.name.startsWith(ENUM_SWITCH_PREFIX)) {
- return true;
- }
- }
- return false;
- }
-
- private static MethodNode findEnumSwitchUsage(ClassNode classNode, String owner) {
- String target = ENUM_SWITCH_PREFIX + owner.replace('/', '$');
- @SuppressWarnings("rawtypes") // ASM API
- List methodList = classNode.methods;
- for (Object f : methodList) {
- MethodNode method = (MethodNode) f;
- InsnList nodes = method.instructions;
- for (int i = 0, n = nodes.size(); i < n; i++) {
- AbstractInsnNode instruction = nodes.get(i);
- if (instruction.getOpcode() == Opcodes.GETSTATIC) {
- FieldInsnNode field = (FieldInsnNode) instruction;
- if (field.name.equals(target)) {
- return method;
- }
- }
- }
- }
- return null;
- }
-
- private static boolean isSkippedEnumSwitch(ClassContext context, ClassNode classNode,
- MethodNode method, FieldInsnNode node, String owner, int api) {
- // Enum-style switches are handled in a different way: it generates
- // an innerclass where the class initializer creates a mapping from
- // the ordinals to the corresponding values.
- // Here we need to check to see if the call site which *used* the
- // table switch had a suppress node on it (or up that node's parent
- // chain
- AbstractInsnNode next = getNextInstruction(node);
- if (next != null && next.getOpcode() == Opcodes.INVOKEVIRTUAL
- && CLASS_CONSTRUCTOR.equals(method.name)
- && ORDINAL_METHOD.equals(((MethodInsnNode) next).name)
- && classNode.outerClass != null
- && isEnumSwitchInitializer(classNode)) {
- LintDriver driver = context.getDriver();
- ClassNode outer = driver.getOuterClassNode(classNode);
- if (outer != null) {
- MethodNode switchUser = findEnumSwitchUsage(outer, owner);
- if (switchUser != null) {
- // Is the API check suppressed at the call site?
- if (driver.isSuppressed(UNSUPPORTED, outer, switchUser,
- null)) {
- return true;
- }
- // Is there a @TargetAPI annotation on the method or
- // class referencing this switch map class?
- if (getLocalMinSdk(switchUser.invisibleAnnotations) >= api
- || getLocalMinSdk(outer.invisibleAnnotations) >= api) {
- return true;
- }
- }
- }
- }
- return false;
- }
-
- /**
- * Return the {@code @TargetApi} level to use for the given {@code classNode};
- * this will be the {@code @TargetApi} annotation on the class, or any outer
- * methods (for anonymous inner classes) or outer classes (for inner classes)
- * of the given class.
- */
- private static int getClassMinSdk(ClassContext context, ClassNode classNode) {
- int classMinSdk = getLocalMinSdk(classNode.invisibleAnnotations);
- if (classMinSdk != -1) {
- return classMinSdk;
- }
-
- LintDriver driver = context.getDriver();
- while (classNode != null) {
- ClassNode prev = classNode;
- classNode = driver.getOuterClassNode(classNode);
- if (classNode != null) {
- // TODO: Should this be "curr" instead?
- if (prev.outerMethod != null) {
- @SuppressWarnings("rawtypes") // ASM API
- List methods = classNode.methods;
- for (Object m : methods) {
- MethodNode method = (MethodNode) m;
- if (method.name.equals(prev.outerMethod)
- && method.desc.equals(prev.outerMethodDesc)) {
- // Found the outer method for this anonymous class; check method
- // annotations on it, then continue up the class hierarchy
- int methodMinSdk = getLocalMinSdk(method.invisibleAnnotations);
- if (methodMinSdk != -1) {
- return methodMinSdk;
- }
-
- break;
- }
- }
- }
-
- classMinSdk = getLocalMinSdk(classNode.invisibleAnnotations);
- if (classMinSdk != -1) {
- return classMinSdk;
- }
- }
- }
-
- return -1;
- }
-
- /**
- * Returns the minimum SDK to use according to the given annotation list, or
- * -1 if no annotation was found.
- *
- * @param annotations a list of annotation nodes from ASM
- * @return the API level to use for this node, or -1
- */
- @SuppressWarnings({"unchecked", "rawtypes"})
- private static int getLocalMinSdk(List annotations) {
- if (annotations != null) {
- for (AnnotationNode annotation : (List)annotations) {
- String desc = annotation.desc;
- if (desc.endsWith(TARGET_API_VMSIG)) {
- if (annotation.values != null) {
- for (int i = 0, n = annotation.values.size(); i < n; i += 2) {
- String key = (String) annotation.values.get(i);
- if (key.equals(ATTR_VALUE)) { //$NON-NLS-1$
- Object value = annotation.values.get(i + 1);
- if (value instanceof Integer) {
- return (Integer) value;
- }
- }
- }
- }
- } else if (desc.endsWith(REQ_API_VMSIG)) {
- if (annotation.values != null) {
- for (int i = 0, n = annotation.values.size(); i < n; i += 2) {
- String key = (String) annotation.values.get(i);
- if (key.equals(ATTR_VALUE) || key.equals("api")) {
- Object value = annotation.values.get(i + 1);
- if (value instanceof Integer) {
- int api = (Integer) value;
- if (api > 1) {
- return api;
- }
- }
- }
- }
- }
- }
- }
- }
-
- return -1;
- }
-
/**
* Returns the minimum SDK to use in the given element context, or -1 if no
* {@code tools:targetApi} attribute was found.
@@ -1566,46 +765,15 @@ public class ApiDetector extends ResourceXmlDetector
return false;
}
- private static void report(final ClassContext context, String message, AbstractInsnNode node,
- MethodNode method, String patternStart, String patternEnd, SearchHints hints) {
- int lineNumber = node != null ? ClassContext.findLineNumber(node) : -1;
-
- // If looking for a constructor, the string we'll see in the source is not the
- // method name () but the class name
- if (patternStart != null && patternStart.equals(CONSTRUCTOR_NAME)
- && node instanceof MethodInsnNode) {
- if (hints != null) {
- hints = hints.matchConstructor();
- }
- patternStart = ((MethodInsnNode) node).owner;
- }
-
- if (patternStart != null) {
- int index = patternStart.lastIndexOf('$');
- if (index != -1) {
- patternStart = patternStart.substring(index + 1);
- }
- index = patternStart.lastIndexOf('/');
- if (index != -1) {
- patternStart = patternStart.substring(index + 1);
- }
- }
-
- Location location = context.getLocationForLine(lineNumber, patternStart, patternEnd,
- hints);
- context.report(UNSUPPORTED, method, node, location, message);
- }
-
// ---- Implements UastScanner ----
-
@Nullable
@Override
public UastVisitor createUastVisitor(@NonNull JavaContext context) {
if (mApiDatabase == null) {
return new AbstractUastVisitor() {
@Override
- public boolean visitElement(UElement element) {
+ public boolean visitElement(@NotNull UElement element) {
// No-op. Workaround for super currently calling
// ProgressIndicatorProvider.checkCanceled();
return super.visitElement(element);
@@ -1627,6 +795,8 @@ public class ApiDetector extends ResourceXmlDetector
types.add(UBinaryExpression.class);
types.add(UCallExpression.class);
types.add(UClass.class);
+ types.add(UTypeReferenceExpression.class);
+ types.add(UClassLiteralExpression.class);
types.add(UMethod.class);
return types;
}
@@ -1641,10 +811,11 @@ public class ApiDetector extends ResourceXmlDetector
* @return true if the given usage is safe on older versions than the introduction
* level of the constant
*/
- public static boolean isBenignConstantUsage(
+ private static boolean isBenignConstantUsage(
@Nullable UElement node,
@NonNull String name,
- @NonNull String owner) {
+ @NonNull String owner
+ ) {
if (owner.equals("android/os/Build$VERSION_CODES")) { //$NON-NLS-1$
// These constants are required for compilation, not execution
// and valid code checks it even on older platforms
@@ -1712,7 +883,19 @@ public class ApiDetector extends ResourceXmlDetector
}
@Override
- public boolean visitImportStatement(UImportStatement statement) {
+ public boolean visitTypeReferenceExpression(@NotNull UTypeReferenceExpression node) {
+ checkType(node.getType(), node);
+ return super.visitTypeReferenceExpression(node);
+ }
+
+ @Override
+ public boolean visitClassLiteralExpression(@NotNull UClassLiteralExpression node) {
+ checkType(node.getType(), node);
+ return super.visitClassLiteralExpression(node);
+ }
+
+ @Override
+ public boolean visitImportStatement(@NotNull UImportStatement statement) {
if (!statement.isOnDemand()) {
PsiElement resolved = statement.resolve();
if (resolved instanceof PsiField) {
@@ -1724,7 +907,7 @@ public class ApiDetector extends ResourceXmlDetector
}
@Override
- public boolean visitSimpleNameReferenceExpression(USimpleNameReferenceExpression node) {
+ public boolean visitSimpleNameReferenceExpression(@NotNull USimpleNameReferenceExpression node) {
PsiElement resolved = node.resolve();
if (resolved instanceof PsiField) {
checkField(node, (PsiField)resolved);
@@ -1734,11 +917,8 @@ public class ApiDetector extends ResourceXmlDetector
}
@Override
- public boolean visitBinaryExpressionWithType(UBinaryExpressionWithType node) {
- if (UastExpressionUtils.isTypeCast(node)) {
- visitTypeCastExpression(node);
- }
-
+ public boolean visitBinaryExpressionWithType(@NotNull UBinaryExpressionWithType node) {
+ visitTypeCastExpression(node);
return super.visitBinaryExpressionWithType(node);
}
@@ -1784,7 +964,7 @@ public class ApiDetector extends ResourceXmlDetector
return;
}
- if (isSuppressed(api, node, minSdk, mContext)) {
+ if (isSuppressed(api, node, minSdk, mContext, UNSUPPORTED)) {
return;
}
@@ -1796,14 +976,14 @@ public class ApiDetector extends ResourceXmlDetector
}
@Override
- public boolean visitMethod(UMethod method) {
+ public boolean visitMethod(@NotNull UMethod method) {
// API check for default methods
if (method.getModifierList().hasExplicitModifier(PsiModifier.DEFAULT)) {
int api = 24; // minSdk for default methods
int minSdk = getMinSdk(mContext);
- if (!isSuppressed(api, method, minSdk, mContext)) {
- Location location = mContext.getLocation((PsiElement) method);
+ if (!isSuppressed(api, method, minSdk, mContext, UNSUPPORTED)) {
+ Location location = mContext.getLocation(method);
String message = String.format("Default method requires API level %1$d "
+ "(current min is %2$d)", api, minSdk);
mContext.reportUast(UNSUPPORTED, method, location, message);
@@ -1814,7 +994,7 @@ public class ApiDetector extends ResourceXmlDetector
}
@Override
- public boolean visitClass(UClass aClass) {
+ public boolean visitClass(@NotNull UClass aClass) {
// Check for repeatable annotations
if (aClass.isAnnotationType()) {
PsiModifierList modifierList = aClass.getModifierList();
@@ -1824,7 +1004,7 @@ public class ApiDetector extends ResourceXmlDetector
if ("java.lang.annotation.Repeatable".equals(name)) {
int api = 24; // minSdk for repeatable annotations
int minSdk = getMinSdk(mContext);
- if (!isSuppressed(api, aClass, minSdk, mContext)) {
+ if (!isSuppressed(api, aClass, minSdk, mContext, UNSUPPORTED)) {
Location location = mContext.getLocation(annotation);
String message = String.format("Repeatable annotation requires "
+ "API level %1$d (current min is %2$d)", api, minSdk);
@@ -1874,12 +1054,23 @@ public class ApiDetector extends ResourceXmlDetector
}
@Override
- public boolean visitCallExpression(UCallExpression expression) {
+ public boolean visitCallExpression(@NotNull UCallExpression expression) {
+ checkMethodCallExpression(expression);
+ return super.visitCallExpression(expression);
+ }
+
+ private void checkMethodCallExpression(@NotNull UCallExpression expression) {
PsiMethod method = expression.resolve();
if (method != null) {
+ PsiClass containingClass = method.getContainingClass();
+ if (containingClass == null) {
+ return;
+ }
+
PsiParameterList parameterList = method.getParameterList();
if (parameterList.getParametersCount() > 0) {
PsiParameter[] parameters = parameterList.getParameters();
+
List arguments = expression.getValueArguments();
for (int i = 0; i < parameters.length; i++) {
PsiType parameterType = parameters[i].getType();
@@ -1891,76 +1082,186 @@ public class ApiDetector extends ResourceXmlDetector
}
UExpression argument = arguments.get(i);
PsiType argumentType = argument.getExpressionType();
- if (argumentType == null || parameterType.equals(argumentType)
- || !(argumentType instanceof PsiClassType)) {
+ if (argumentType == null || parameterType.equals(argumentType) || !(argumentType instanceof PsiClassType)) {
continue;
}
- checkCast(argument, (PsiClassType) argumentType,
- (PsiClassType) parameterType);
+ checkCast(argument, (PsiClassType)argumentType, (PsiClassType)parameterType);
}
}
}
- PsiModifierList modifierList = method.getModifierList();
- List annotations = JavaUAnnotation.wrap(modifierList.getAnnotations());
- if (!checkRequiresApi(expression, method, annotations)) {
- PsiClass containingClass = method.getContainingClass();
- if (containingClass != null) {
- modifierList = containingClass.getModifierList();
- if (modifierList != null) {
- checkRequiresApi(expression, method, annotations);
- }
+ JavaEvaluator evaluator = mContext.getEvaluator();
+ String fqcn = containingClass.getQualifiedName();
+ String owner = evaluator.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;
+ }
+
+ boolean hasApiAnnotation = false;
+ int api = mApiDatabase.getCallVersion(owner, name, desc);
+ if (api == -1) {
+ api = getTargetApi(method.getModifierList());
+ if (api == -1 && method.isConstructor()) {
+ api = getTargetApi(method.getContainingClass().getModifierList());
+ }
+
+ if (api == -1) {
+ return;
+ } else {
+ hasApiAnnotation = true;
}
}
- }
- return super.visitCallExpression(expression);
- }
+ int minSdk = getMinSdk(mContext);
+ if (api <= minSdk) {
+ return;
+ }
- // Look for @RequiresApi in modifier lists
- private boolean checkRequiresApi(
- UCallExpression expression,
- PsiMethod method,
- List annotations) {
- for (UAnnotation annotation : annotations) {
- if (REQUIRES_API_ANNOTATION.equals(annotation.getQualifiedName())) {
- int api = (int) SupportAnnotationDetector.getLongAttribute(annotation,
- ATTR_VALUE, -1);
- if (api <= 1) {
- // @RequiresApi has two aliasing attributes: api and value
- api = (int) SupportAnnotationDetector.getLongAttribute(annotation,
- "api", -1);
- }
- int minSdk = getMinSdk(mContext);
- if (api > minSdk) {
- int target = getTargetApi(expression);
- if (target == -1 || api > target) {
- if (isWithinVersionCheckConditional(expression, api, mContext)) {
- return true;
- }
- if (isPrecededByVersionCheckExit(expression, api, mContext)) {
- return true;
+ // 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 (!hasApiAnnotation && UastExpressionUtils.isMethodCall(expression)) {
+ UExpression qualifier = expression.getReceiver();
+ if (qualifier != null && !(qualifier instanceof UThisExpression) && !(qualifier instanceof USuperExpression)) {
+ PsiType type = qualifier.getExpressionType();
+ if (type != null && type instanceof PsiClassType) {
+ String expressionOwner = evaluator.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 {
+ // For example, for Bundle#getString(String,String) the API level is 12, whereas for
+ // BaseBundle#getString(String,String) the API level is 21. If the code specified a Bundle instead of
+ // a BaseBundle, reported the Bundle level in the error message instead.
+ if (specificApi < api) {
+ api = specificApi;
+ fqcn = expressionOwner.replace('/', '.');
+ }
+ api = Math.min(specificApi, api);
+ }
}
+ }
+ } else {
+ // Unqualified call; need to search in our super hierarchy
+ PsiClass cls = null;
+ PsiType receiverType = expression.getReceiverType();
+ if (receiverType instanceof PsiClassType) {
+ cls = ((PsiClassType) receiverType).resolve();
+ }
- Location location;
- location = mContext.getUastLocation(expression);
- String fqcn = method.getName();
- String message = String.format(
- "Call requires API level %1$d (current min is %2$d): `%3$s`",
- api, minSdk, fqcn);
- mContext.report(UNSUPPORTED, location, message);
+ 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
+ boolean found = false;
+ PsiClassType anonymousBaseType = ((PsiAnonymousClass)cls).getBaseClassType();
+ PsiClass anonymousBase = anonymousBaseType.resolve();
+ if (anonymousBase != null && anonymousBase.isInheritor(containingClass, true)) {
+ cls = anonymousBase;
+ found = true;
+ } else {
+ PsiClass surroundingBaseType = PsiTreeUtil.getParentOfType(cls, PsiClass.class, true);
+ if (surroundingBaseType != null && surroundingBaseType.isInheritor(containingClass, true)) {
+ cls = surroundingBaseType;
+ found = true;
+ }
+ }
+ if (!found) {
+ break;
+ }
+ }
+ String expressionOwner = evaluator.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 {
+ if (specificApi < api) {
+ api = specificApi;
+ fqcn = expressionOwner.replace('/', '.');
+ }
+ api = Math.min(specificApi, api);
+ break;
+ }
+ cls = cls.getSuperClass();
}
}
-
- return true;
}
- }
- return false;
+ if (isSuppressed(api, expression, minSdk, mContext, UNSUPPORTED)) {
+ 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 (UastExpressionUtils.isMethodCall(expression)) {
+ if (expression.getReceiver() instanceof USuperExpression) {
+ PsiMethod containingMethod = UastUtils.getContainingMethod(expression);
+ 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;
+ }
+ }
+ }
+
+ UElement locationNode = expression.getMethodIdentifier();
+ if (locationNode == null) {
+ locationNode = expression;
+ }
+ Location location = mContext.getUastLocation(locationNode);
+ String message = String.format("Call requires API level %1$d (current min is %2$d): %3$s", api, minSdk,
+ fqcn + '#' + method.getName());
+
+ mContext.report(UNSUPPORTED, location, message);
+ }
}
@Override
- public boolean visitVariable(UVariable node) {
+ public boolean visitVariable(@NotNull UVariable node) {
if (node instanceof ULocalVariable) {
visitLocalVariable((ULocalVariable) node);
}
@@ -1991,7 +1292,7 @@ public class ApiDetector extends ResourceXmlDetector
}
@Override
- public boolean visitBinaryExpression(UBinaryExpression node) {
+ public boolean visitBinaryExpression(@NotNull UBinaryExpression node) {
if (UastExpressionUtils.isAssignment(node)) {
visitAssignmentExpression(node);
}
@@ -2088,16 +1389,51 @@ public class ApiDetector extends ResourceXmlDetector
}
}
+ private void checkType(PsiType type, UElement element) {
+ if (!(type instanceof PsiClassType)) {
+ return;
+ }
+
+ PsiClass resolved = ((PsiClassType) type).resolve();
+ if (resolved == null) {
+ return;
+ }
+
+ String signature = mContext.getEvaluator().getInternalName(resolved);
+ int api = mApiDatabase.getClassVersion(signature);
+ if (api == -1) {
+ return;
+ }
+
+ int minSdk = getMinSdk(mContext);
+ if (api <= minSdk) {
+ return;
+ }
+
+ if (isSuppressed(api, element, minSdk, mContext, UNSUPPORTED)) {
+ return;
+ }
+
+ Location location = mContext.getUastLocation(element);
+ String fqcn = resolved.getQualifiedName();
+ String message = String.format("Class requires API level %1$d (current min is %2$d): %3$s",
+ api, minSdk, fqcn);
+ mContext.report(UNSUPPORTED, location, message);
+ }
+
/**
* Checks a Java source field reference. Returns true if the field is known
* regardless of whether it's an invalid field or not
*/
private boolean checkField(@NonNull UElement node, @NonNull PsiField field) {
PsiType type = field.getType();
- // Only look for compile time constants. See JLS 15.28 and JLS 13.4.9.
- if (!(type instanceof PsiPrimitiveType) && !LintUtils.isString(type)) {
- return false;
+ Issue issue;
+ if ((type instanceof PsiPrimitiveType) || LintUtils.isString(type)) {
+ issue = INLINED;
+ } else {
+ issue = UNSUPPORTED;
}
+
String name = field.getName();
PsiClass containingClass = field.getContainingClass();
if (containingClass == null || name == null) {
@@ -2146,7 +1482,7 @@ public class ApiDetector extends ResourceXmlDetector
api, minSdk, fqcn);
Location location = mContext.getUastLocation(node);
- mContext.report(INLINED, node, location, message);
+ mContext.report(issue, node, location, message);
}
return true;
@@ -2156,25 +1492,22 @@ public class ApiDetector extends ResourceXmlDetector
}
}
- private static boolean isSuppressed(
- int api, UElement element, int minSdk, JavaContext context) {
+ private static boolean isSuppressed(int api, UElement element, int minSdk, JavaContext context, Issue issue) {
if (api <= minSdk) {
return true;
}
- //if (mySeenTargetApi) {
+
int target = getTargetApi(element);
if (target != -1) {
if (api <= target) {
return true;
}
}
- //}
- // TODO: This MUST BE RESTORED
- // if (context.getDriver().isSuppressed(UNSUPPORTED, element))
- // if (/*mySeenSuppress &&*/
- // (IntellijLintUtils.isSuppressed(element, myFile, UNSUPPORTED) || IntellijLintUtils.isSuppressed(element, myFile, INLINED))) {
- // return true;
- // }
+
+ LintDriver driver = context.getDriver();
+ if(driver.isSuppressed(context, issue, element)) {
+ return true;
+ }
if (isWithinVersionCheckConditional(element, api, context)) {
return true;
@@ -2186,7 +1519,7 @@ public class ApiDetector extends ResourceXmlDetector
return false;
}
- public static int getTargetApi(@Nullable UElement scope) {
+ private static int getTargetApi(@Nullable UElement scope) {
while (scope != null) {
if (scope instanceof PsiModifierListOwner) {
PsiModifierList modifierList = ((PsiModifierListOwner) scope).getModifierList();
@@ -2263,7 +1596,7 @@ public class ApiDetector extends ResourceXmlDetector
return -1;
}
- public static int codeNameToApi(@NonNull String text) {
+ private static int codeNameToApi(@NonNull String text) {
int dotIndex = text.lastIndexOf('.');
if (dotIndex != -1) {
text = text.substring(dotIndex + 1);
@@ -2272,182 +1605,6 @@ public class ApiDetector extends ResourceXmlDetector
return SdkVersionInfo.getApiByBuildCode(text, true);
}
- public static int getRequiredVersion(@NonNull Issue issue, @NonNull String errorMessage,
- @NonNull TextFormat format) {
- errorMessage = format.toText(errorMessage);
-
- if (issue == UNSUPPORTED || issue == INLINED) {
- Pattern pattern = Pattern.compile("\\s(\\d+)\\s"); //$NON-NLS-1$
- Matcher matcher = pattern.matcher(errorMessage);
- if (matcher.find()) {
- return Integer.parseInt(matcher.group(1));
- }
- }
-
- return -1;
- }
-
- private 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 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;
- }
-
- /**
- * 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);
- }
- }
-
private static class VersionCheckWithExitFinder extends AbstractUastVisitor {
private final UExpression mExpression;
@@ -2468,7 +1625,7 @@ public class ApiDetector extends ResourceXmlDetector
}
@Override
- public boolean visitElement(UElement node) {
+ public boolean visitElement(@NotNull UElement node) {
if (mDone) {
return true;
}
@@ -2481,7 +1638,7 @@ public class ApiDetector extends ResourceXmlDetector
}
@Override
- public boolean visitIfExpression(UIfExpression ifStatement) {
+ public boolean visitIfExpression(@NotNull UIfExpression ifStatement) {
if (mDone) {
return true;
@@ -2521,8 +1678,11 @@ public class ApiDetector extends ResourceXmlDetector
}
}
- protected static boolean isPrecededByVersionCheckExit(UElement element, int api,
- JavaContext context) {
+ private static boolean isPrecededByVersionCheckExit(
+ UElement element,
+ int api,
+ JavaContext context
+ ) {
//noinspection unchecked
UExpression currentExpression = UastUtils.getParentOfType(element, UExpression.class,
true, UMethod.class, UClass.class);
@@ -2545,18 +1705,23 @@ public class ApiDetector extends ResourceXmlDetector
return false;
}
- private static boolean isUnconditionalReturn(UExpression statement) {
- if (statement instanceof UBlockExpression) {
- List expressions = ((UBlockExpression) statement).getExpressions();
- if (expressions.size() == 1 && expressions.get(0) instanceof UReturnExpression) {
- return true;
- }
+ private static boolean isUnconditionalReturn(UExpression expression) {
+ if (expression instanceof UBlockExpression) {
+ List expressions = ((UBlockExpression) expression).getExpressions();
+ return !expressions.isEmpty() && (isUnconditionalReturn(expressions.get(expressions.size() - 1)));
}
- return statement instanceof UReturnExpression;
+
+ return expression instanceof UReturnExpression ||
+ expression instanceof UThrowExpression ||
+ (expression instanceof UCallExpression &&
+ ERROR.equals(((UCallExpression)expression).getMethodName()));
}
- public static boolean isWithinVersionCheckConditional(
- UElement element, int api, JavaContext context) {
+ private static boolean isWithinVersionCheckConditional(
+ UElement element,
+ int api,
+ JavaContext context
+ ) {
UElement current = element.getContainingElement();
UElement prev = element;
while (current != null) {
@@ -2566,6 +1731,10 @@ public class ApiDetector extends ResourceXmlDetector
if (isConditional != null) {
return isConditional;
}
+ } else if (current instanceof UBinaryExpression) {
+ if (isAndedWithConditional(current, api, prev) || isOredWithConditional(current, api, prev)) {
+ return true;
+ }
} else if (current instanceof UMethod || current instanceof UFile) {
return false;
}
@@ -2620,7 +1789,8 @@ public class ApiDetector extends ResourceXmlDetector
}
@Nullable
- private static Boolean isVersionCheckConditional(int api,
+ private static Boolean isVersionCheckConditional(
+ int api,
@Nullable UElement prev,
@Nullable UIfExpression ifStatement,
@NonNull UBinaryExpression binary) {
@@ -2687,8 +1857,7 @@ public class ApiDetector extends ResourceXmlDetector
return null;
}
-
- public static Location getCatchParametersLocation(JavaContext context, UCatchClause catchClause) {
+ private static Location getCatchParametersLocation(JavaContext context, UCatchClause catchClause) {
List types = catchClause.getTypeReferences();
if (types.isEmpty()) {
return Location.NONE;
@@ -2711,7 +1880,7 @@ public class ApiDetector extends ResourceXmlDetector
return Location.create(file, start, end);
}
- public static boolean isMultiCatchReflectiveOperationException(UCatchClause catchClause) {
+ private static boolean isMultiCatchReflectiveOperationException(UCatchClause catchClause) {
List types = catchClause.getTypes();
if (types.size() < 2) {
return false;
@@ -2726,31 +1895,17 @@ public class ApiDetector extends ResourceXmlDetector
return true;
}
- private static boolean isAndedWithConditional(UElement element, int api, @Nullable UElement before) {
+ private static boolean isAndedWithConditional(UElement element, int api, @Nullable UElement target) {
if (element instanceof UBinaryExpression) {
UBinaryExpression inner = (UBinaryExpression) element;
if (inner.getOperator() == UastBinaryOperator.LOGICAL_AND) {
- return isAndedWithConditional(inner.getLeftOperand(), api, before) ||
- inner.getRightOperand() != before && isAndedWithConditional(inner.getRightOperand(), api, before);
- } else if (inner.getLeftOperand() instanceof UReferenceExpression &&
+ return isAndedWithConditional(inner.getLeftOperand(), api, target) ||
+ inner.getRightOperand() != target && isAndedWithConditional(inner.getRightOperand(), api, target);
+ } else if (inner.getLeftOperand() instanceof UReferenceExpression &&
SDK_INT.equals(((UReferenceExpression)inner.getLeftOperand()).getResolvedName())) {
- int level = -1;
UastOperator tokenType = inner.getOperator();
UExpression right = inner.getRightOperand();
- 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).intValue();
- }
- }
+ int level = getApiLevel(right);
if (level != -1) {
if (tokenType == UastBinaryOperator.GREATER_OR_EQUALS) {
// if (SDK_INT >= ICE_CREAM_SANDWICH &&
@@ -2772,6 +1927,55 @@ public class ApiDetector extends ResourceXmlDetector
return false;
}
+ private static boolean isOredWithConditional(UElement element, int api, @Nullable UElement target) {
+ if (element instanceof UBinaryExpression) {
+ UBinaryExpression inner = (UBinaryExpression) element;
+ if (inner.getOperator() == UastBinaryOperator.LOGICAL_OR) {
+ return isOredWithConditional(inner.getLeftOperand(), api, target) ||
+ inner.getRightOperand() != target && isOredWithConditional(inner.getRightOperand(), api, target);
+ } else if (inner.getLeftOperand() instanceof UReferenceExpression &&
+ SDK_INT.equals(((UReferenceExpression)inner.getLeftOperand()).getResolvedName())) {
+ UastOperator tokenType = inner.getOperator();
+ UExpression right = inner.getRightOperand();
+ int level = getApiLevel(right);
+ if (level != -1) {
+ if (tokenType == UastBinaryOperator.LESS_OR_EQUALS) {
+ // if (SDK_INT <= ICE_CREAM_SANDWICH ||
+ return level >= api - 1;
+ }
+ else if (tokenType == UastBinaryOperator.LESS) {
+ // if (SDK_INT < ICE_CREAM_SANDWICH) ||
+ return level >= api;
+ }
+ else if (tokenType == UastBinaryOperator.NOT_EQUALS) {
+ // if (SDK_INT < ICE_CREAM_SANDWICH) ||
+ return level == api;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private static int getApiLevel(UElement apiLevelElement) {
+ if (apiLevelElement instanceof UReferenceExpression) {
+ UReferenceExpression ref2 = (UReferenceExpression)apiLevelElement;
+ String codeName = ref2.getResolvedName();
+ if (codeName == null) {
+ return -1;
+ }
+ return SdkVersionInfo.getApiByBuildCode(codeName, true);
+ } else if (apiLevelElement instanceof ULiteralExpression) {
+ ULiteralExpression lit = (ULiteralExpression)apiLevelElement;
+ Object value = lit.getValue();
+ if (value instanceof Integer) {
+ return ((Integer)value).intValue();
+ }
+ }
+ return -1;
+ }
+
private static boolean isSubclassOfReflectiveOperationException(PsiType type) {
for (PsiType t : type.getSuperTypes()) {
if (REFLECTIVE_OPERATION_EXCEPTION.equals(t.getCanonicalText())) {
diff --git a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijApiDetector.java b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijApiDetector.java
deleted file mode 100644
index 266b2a6a5a9..00000000000
--- a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijApiDetector.java
+++ /dev/null
@@ -1,885 +0,0 @@
-/*
- * 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.klint;
-
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
-import com.android.tools.klint.checks.ApiDetector;
-import com.android.tools.klint.checks.ApiLookup;
-import com.android.tools.klint.client.api.UastLintUtils;
-import com.android.tools.klint.detector.api.*;
-import com.intellij.psi.*;
-import com.intellij.psi.impl.source.PsiClassReferenceType;
-import com.intellij.psi.util.MethodSignatureUtil;
-import com.intellij.psi.util.PsiTreeUtil;
-import org.jetbrains.annotations.NonNls;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.uast.*;
-import org.jetbrains.uast.expressions.UTypeReferenceExpression;
-import org.jetbrains.uast.util.UastExpressionUtils;
-import org.jetbrains.uast.visitor.AbstractUastVisitor;
-import org.jetbrains.uast.visitor.UastVisitor;
-
-import java.util.Collections;
-import java.util.EnumSet;
-import java.util.List;
-
-import static org.jetbrains.android.inspections.klint.IntellijLintUtils.SUPPRESS_LINT_FQCN;
-import static org.jetbrains.android.inspections.klint.IntellijLintUtils.SUPPRESS_WARNINGS_FQCN;
-
-/**
- * Intellij-specific version of the {@link ApiDetector} which uses the PSI structure
- * to check accesses
- *
- * TODO:
- *
- * - Port this part from the bytecode based check:
- * if (owner.equals("java/text/SimpleDateFormat")) {
- * checkSimpleDateFormat(context, method, node, minSdk);
- * }
- *
- * - Compare to the bytecode based results
- *
- */
-public class IntellijApiDetector extends ApiDetector {
- @SuppressWarnings("unchecked")
- public 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";
-
- @Override
- public List> getApplicableUastTypes() {
- return Collections.>singletonList(UFile.class);
- }
-
- // TODO: Reuse the parent ApiVisitor to share more code between these two!
- @Override
- public UastVisitor createUastVisitor(@NonNull final JavaContext context) {
- return new AbstractUastVisitor() {
- @Override
- public boolean visitFile(@NotNull UFile file) {
- List classes = file.getClasses();
- if (!classes.isEmpty()) {
- // TODO: This is weird; I should just perform the per class checks as part of visitClass!!
- file.accept(new ApiCheckVisitor(context, classes.get(0), file));
- }
- return super.visitFile(file);
- }
- };
- }
-
- private static int getTargetApi(@NonNull UElement e, @NonNull UFile file) {
- UElement element = e;
- // Search upwards for target api annotations
- while (element != null && element != file) {
- if (element instanceof UAnnotated) {
- UAnnotated owner = (UAnnotated)element;
- UAnnotation annotation = owner.findAnnotation(TARGET_API_FQCN);
- if (annotation == null) {
- annotation = owner.findAnnotation(REQUIRES_API_ANNOTATION);
- }
- if (annotation != null) {
- for (UNamedExpression pair : annotation.getAttributeValues()) {
- UExpression v = pair.getExpression();
-
- if (v instanceof ULiteralExpression) {
- ULiteralExpression literal = (ULiteralExpression)v;
- Object value = literal.getValue();
- if (value instanceof Integer) {
- return (Integer) value;
- } else if (value instanceof String) {
- return codeNameToApi((String) value);
- }
- } else if (UastExpressionUtils.isArrayInitializer(v)) {
- UCallExpression mv = (UCallExpression)v;
- for (UExpression mmv : mv.getValueArguments()) {
- if (mmv instanceof ULiteralExpression) {
- ULiteralExpression literal = (ULiteralExpression)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 UResolvable) {
- PsiElement resolved = ((UResolvable) v).resolve();
- if (resolved != null) {
- String fqcn = UastLintUtils.getQualifiedName(resolved);
- return codeNameToApi(fqcn);
- } else {
- return codeNameToApi(v.asRenderString());
- }
- }
- }
- }
- }
- element = element.getContainingElement();
- }
-
- return -1;
- }
-
- private class ApiCheckVisitor extends AbstractUastVisitor {
- private final JavaContext myContext;
- private boolean mySeenSuppress;
- private boolean mySeenTargetApi;
- private final UClass myClass;
- private final UFile myFile;
- private final boolean myCheckAccess;
- private boolean myCheckOverride;
- private String myFrameworkParent;
-
- public ApiCheckVisitor(JavaContext context, UClass clz, UFile file) {
- myContext = context;
- myClass = clz;
- myFile = file;
-
- myCheckAccess = context.isEnabled(UNSUPPORTED) || context.isEnabled(INLINED);
- myCheckOverride = context.isEnabled(OVERRIDE)
- && context.getMainProject().getBuildSdk() >= 1;
- int depth = 0;
- if (myCheckOverride) {
- myFrameworkParent = null;
- UClass superClass = myClass.getUastSuperClass();
- 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.getUastSuperClass();
- depth++;
- if (depth == 500) {
- // Shouldn't happen in practice; this prevents the IDE from
- // hanging if the user has accidentally typed in an incorrect
- // super class which creates a cycle.
- break;
- }
- }
- if (myFrameworkParent == null) {
- myCheckOverride = false;
- }
- }
- }
-
- @Override
- public boolean visitAnnotation(@NotNull UAnnotation annotation) {
- String fqcn = annotation.getQualifiedName();
- if (TARGET_API_FQCN.equals(fqcn) || REQUIRES_API_ANNOTATION.equals(fqcn)) {
- mySeenTargetApi = true;
- }
- else if (SUPPRESS_LINT_FQCN.equals(fqcn) || SUPPRESS_WARNINGS_FQCN.equals(fqcn)) {
- mySeenSuppress = true;
- }
-
- return super.visitAnnotation(annotation);
- }
-
- @Override
- public boolean visitMethod(@NotNull UMethod method) {
-
- // API check for default methods
- if (method.getModifierList().hasExplicitModifier(PsiModifier.DEFAULT)) {
- int api = 24; // minSdk for default methods
- int minSdk = getMinSdk(myContext);
-
- if (!isSuppressed(api, method, minSdk)) {
- Location location = IntellijLintUtils.getUastLocation(myContext.file, method);
- String message = String.format("Default method requires API level %1$d (current min is %2$d)", api, minSdk);
- myContext.report(UNSUPPORTED, location, message);
- }
- }
-
- if (!myCheckOverride) {
- return super.visitMethod(method);
- }
-
- 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 super.visitMethod(method);
- }
- int api = mApiDatabase.getCallVersion(myFrameworkParent, name, desc);
- if (api > buildSdk && buildSdk != -1) {
- if (mySeenSuppress &&
- IntellijLintUtils.isSuppressed(method, myFile.getPsi(), OVERRIDE)) {
- return super.visitMethod(method);
- }
-
- // 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);
- }
-
- return super.visitMethod(method);
- }
-
- @Override
- public boolean visitClass(@NotNull UClass aClass) {
- if (!myCheckAccess) {
- return super.visitClass(aClass);
- }
-
- if (aClass.isAnnotationType()) {
- PsiModifierList modifierList = aClass.getModifierList();
- if (modifierList != null) {
- for (PsiAnnotation annotation : modifierList.getAnnotations()) {
- String name = annotation.getQualifiedName();
- if ("java.lang.annotation.Repeatable".equals(name)) {
- int api = 24; // minSdk for repeatable annotations
- int minSdk = getMinSdk(myContext);
- if (!isSuppressed(api, aClass, minSdk)) {
- Location location = IntellijLintUtils.getLocation(myContext.file, annotation);
- String message = String.format("Repeatable annotation requires API level %1$d (current min is %2$d)", api, minSdk);
- myContext.report(UNSUPPORTED, location, message);
- }
- }
- }
- }
- }
-
- 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.getPsi(), UNSUPPORTED)) {
- continue;
- }
-
- Location location;
- if (type instanceof PsiClassReferenceType) {
- PsiReference reference = ((PsiClassReferenceType)type).getReference();
- PsiElement element = reference.getElement();
- if (isWithinVersionCheckConditional(aClass, api, myContext)) {
- continue;
- }
- if (isPrecededByVersionCheckExit(aClass, api, myContext)) {
- continue;
- }
- location = IntellijLintUtils.getLocation(myContext.file, element);
- } else {
- location = IntellijLintUtils.getLocation(myContext.file, aClass); //TODO to super type reference
- }
- 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);
- }
-
- return super.visitClass(aClass);
- }
-
- @Override
- public boolean visitSimpleNameReferenceExpression(@NotNull USimpleNameReferenceExpression expression) {
- if (!myCheckAccess) {
- return super.visitSimpleNameReferenceExpression(expression);
- }
-
- PsiElement resolved = expression.resolve();
- if (resolved != null) {
- if (resolved instanceof PsiField) {
- PsiField field = (PsiField)resolved;
- PsiClass containingClass = field.getContainingClass();
- if (containingClass == null) {
- return super.visitSimpleNameReferenceExpression(expression);
- }
- String owner = IntellijLintUtils.getInternalName(containingClass);
- if (owner == null) {
- return super.visitSimpleNameReferenceExpression(expression); // Couldn't resolve type
- }
- String name = field.getName();
- if (name == null) {
- return super.visitSimpleNameReferenceExpression(expression);
- }
-
- int api = mApiDatabase.getFieldVersion(owner, name);
- if (api == -1) {
- return super.visitSimpleNameReferenceExpression(expression);
- }
- int minSdk = getMinSdk(myContext);
- if (isSuppressed(api, expression, minSdk)) {
- return super.visitSimpleNameReferenceExpression(expression);
- }
-
- Location location = IntellijLintUtils.getUastLocation(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 (PsiType.INT.equals(type) || PsiType.CHAR.equals(type) || PsiType.BOOLEAN.equals(type)
- || PsiType.DOUBLE.equals(type) || PsiType.FLOAT.equals(type) || PsiType.BYTE.equals(type)
- || type.equalsToText(CommonClassNames.JAVA_LANG_STRING)) {
- issue = INLINED;
-
- // Some usages of inlined constants are okay:
- if (isBenignConstantUsage(expression, name, owner)) {
- return super.visitSimpleNameReferenceExpression(expression);
- }
- }
-
- myContext.report(issue, location, message);
- }
- }
-
- return super.visitSimpleNameReferenceExpression(expression);
- }
-
- @Override
- public boolean visitTryExpression(@NotNull UTryExpression statement) {
- if (statement.isResources()) {
- int api = 19; // minSdk for try with resources
- int minSdk = getMinSdk(myContext);
-
- if (isSuppressed(api, statement, minSdk)) {
- return super.visitTryExpression(statement);
- }
- Location location = IntellijLintUtils.getUastLocation(myContext.file, statement.getTryClause());
- 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 (UCatchClause catchClause : statement.getCatchClauses()) {
-
- // Special case reflective operation exception which can be implicitly used
- // with multi-catches: see issue 153406
- int minSdk = getMinSdk(myContext);
- if(minSdk < 19 && isMultiCatchReflectiveOperationException(catchClause)) {
- String message = String.format("Multi-catch with these reflection exceptions requires API level 19 (current min is %d) " +
- "because they get compiled to the common but new super type `ReflectiveOperationException`. " +
- "As a workaround either create individual catch statements, or catch `Exception`.",
- minSdk);
-
- myContext.report(UNSUPPORTED, getCatchParametersLocation(myContext, catchClause), message);
- continue;
- }
-
- for (UTypeReferenceExpression typeReference : catchClause.getTypeReferences()) {
- checkCatchTypeElement(statement, typeReference, typeReference.getType());
- }
- }
-
- return super.visitTryExpression(statement);
- }
-
- private void checkCatchTypeElement(@NonNull UTryExpression statement,
- @NonNull UTypeReferenceExpression typeReference,
- @Nullable PsiType type) {
- PsiClass resolved = null;
- if (type instanceof PsiClassType) {
- resolved = ((PsiClassType) type).resolve();
- }
- if (resolved != null) {
- String signature = myContext.getEvaluator().getInternalName(resolved);
- int api = mApiDatabase.getClassVersion(signature);
- if (api == -1) {
- return;
- }
- int minSdk = getMinSdk(myContext);
- if (api <= minSdk) {
- return;
- }
- int target = getTargetApi(statement);
- if (target != -1 && api <= target) {
- return;
- }
-
- Location location = myContext.getUastLocation(typeReference);
- String fqcn = resolved.getQualifiedName();
- 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);
- }
- }
-
- private boolean isSuppressed(int api, UElement 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, myContext)) {
- return true;
- }
- if (isPrecededByVersionCheckExit(element, api, myContext)) {
- return true;
- }
-
- return false;
- }
-
- @Override
- public boolean visitBinaryExpressionWithType(@NotNull UBinaryExpressionWithType expression) {
- if (myCheckAccess) {
- UExpression operand = expression.getOperand();
- PsiType operandType = operand.getExpressionType();
- PsiType castType = expression.getType();
- if (castType.equals(operandType)) {
- return super.visitBinaryExpressionWithType(expression);
- }
- if (!(operandType instanceof PsiClassType)) {
- return super.visitBinaryExpressionWithType(expression);
- }
- if (!(castType instanceof PsiClassType)) {
- return super.visitBinaryExpressionWithType(expression);
- }
- PsiClassType classType = (PsiClassType)operandType;
- PsiClassType interfaceType = (PsiClassType)castType;
- checkCast(expression, classType, interfaceType);
- }
-
- return super.visitBinaryExpressionWithType(expression);
- }
-
- private void checkCast(@NotNull UElement node, @NotNull PsiClassType classType, @NotNull PsiClassType interfaceType) {
- if (classType.equals(interfaceType)) {
- return;
- }
- String classTypeInternal = IntellijLintUtils.getInternalName(classType);
- String interfaceTypeInternal = IntellijLintUtils.getInternalName(interfaceType);
- if (classTypeInternal == null || interfaceTypeInternal == null || "java/lang/Object".equals(interfaceTypeInternal)) {
- return; // Couldn't resolve type
- }
-
- int api = mApiDatabase.getValidCastVersion(classTypeInternal, interfaceTypeInternal);
- if (api == -1) {
- return;
- }
-
- int minSdk = getMinSdk(myContext);
- if (api <= minSdk) {
- return;
- }
-
- if (isSuppressed(api, node, minSdk)) {
- return;
- }
-
- Location location = IntellijLintUtils.getUastLocation(myContext.file, node);
- String message = String.format("Cast from %1$s to %2$s requires API level %3$d (current min is %4$d)",
- classType.getClassName(), interfaceType.getClassName(), api, minSdk);
- myContext.report(UNSUPPORTED, location, message);
- }
-
- @Override
- public boolean visitVariable(@NotNull UVariable variable) {
- if (variable instanceof ULocalVariable) {
- if (!myCheckAccess) {
- return super.visitVariable(variable);
- }
-
- UExpression initializer = variable.getUastInitializer();
- if (initializer == null) {
- return super.visitVariable(variable);
- }
-
- PsiType initializerType = initializer.getExpressionType();
- if (initializerType == null || !(initializerType instanceof PsiClassType)) {
- return super.visitVariable(variable);
- }
-
- PsiType interfaceType = variable.getType();
- if (initializerType.equals(interfaceType)) {
- return super.visitVariable(variable);
- }
-
- if (!(interfaceType instanceof PsiClassType)) {
- return super.visitVariable(variable);
- }
-
- checkCast(initializer, (PsiClassType)initializerType, (PsiClassType)interfaceType);
- }
-
- return super.visitVariable(variable);
- }
-
- @Override
- public boolean visitBinaryExpression(@NotNull UBinaryExpression expression) {
- if (expression.getOperator() instanceof UastBinaryOperator.AssignOperator) {
- if (!myCheckAccess) {
- return super.visitBinaryExpression(expression);
- }
-
- UExpression rExpression = expression.getRightOperand();
- PsiType rhsType = rExpression.getExpressionType();
- if (rhsType == null || !(rhsType instanceof PsiClassType)) {
- return super.visitBinaryExpression(expression);
- }
-
- PsiType interfaceType = expression.getLeftOperand().getExpressionType();
- if (rhsType.equals(interfaceType)) {
- return super.visitBinaryExpression(expression);
- }
-
- if (!(interfaceType instanceof PsiClassType)) {
- return super.visitBinaryExpression(expression);
- }
-
- checkCast(rExpression, (PsiClassType)rhsType, (PsiClassType)interfaceType);
- }
-
- return super.visitBinaryExpression(expression);
- }
-
- @Override
- public boolean visitForEachExpression(@NotNull UForEachExpression statement) {
- // The for each method will implicitly call iterator() on the
- // Iterable that is used in the for each loop; make sure that
- // the API level for that
- if (!myCheckAccess) {
- return super.visitForEachExpression(statement);
- }
-
- UExpression value = statement.getIteratedValue();
- PsiType type = value.getExpressionType();
-
- if (type instanceof PsiClassType) {
- String expressionOwner = IntellijLintUtils.getInternalName((PsiClassType)type);
- if (expressionOwner != null) {
- int api = mApiDatabase.getClassVersion(expressionOwner);
- if (api == -1) {
- return super.visitForEachExpression(statement);
- }
- int minSdk = getMinSdk(myContext);
- if (api <= minSdk) {
- return super.visitForEachExpression(statement);
- }
- if (mySeenTargetApi) {
- int target = getTargetApi(statement, myFile);
- if (target != -1) {
- if (api <= target) {
- return super.visitForEachExpression(statement);
- }
- }
- }
- if (mySeenSuppress && IntellijLintUtils.isSuppressed(statement, myFile, UNSUPPORTED)) {
- return super.visitForEachExpression(statement);
- }
-
- if (isWithinVersionCheckConditional(statement, api, myContext)) {
- return super.visitForEachExpression(statement);
- }
- if (isPrecededByVersionCheckExit(statement, api, myContext)) {
- return super.visitForEachExpression(statement);
- }
-
- Location location = IntellijLintUtils.getUastLocation(myContext.file, value);
- String message = String.format("The type of the for loop iterated value is %1$s, which requires API level %2$d" +
- " (current min is %3$d)", type.getCanonicalText(), api, minSdk);
-
- // Add specific check ConcurrentHashMap#keySet and add workaround text.
- // This was an unfortunate incompatible API change in Open JDK 8, which is
- // not an issue for the Android SDK but is relevant if you're using a
- // Java library.
- if (value instanceof PsiMethodCallExpression) {
- PsiMethodCallExpression valueCall = (PsiMethodCallExpression)value;
- if ("keySet".equals(valueCall.getMethodExpression().getReferenceName())) {
- PsiMethod keySet = valueCall.resolveMethod();
- if (keySet != null && keySet.getContainingClass() != null &&
- "java.util.concurrent.ConcurrentHashMap".equals(keySet.getContainingClass().getQualifiedName())) {
- message += "; to work around this, add an explicit cast to (Map) before the `keySet` call.";
- }
- }
- }
- myContext.report(UNSUPPORTED, location, message);
- }
- }
-
- return super.visitForEachExpression(statement);
- }
-
- @Override
- public boolean visitCallExpression(@NotNull UCallExpression node) {
- checkMethodCallExpression(node);
- return super.visitCallExpression(node);
- }
-
- private void checkMethodCallExpression(@NotNull UCallExpression expression) {
- super.visitCallExpression(expression);
-
- if (!myCheckAccess) {
- return;
- }
-
- PsiMethod method = expression.resolve();
- if (method != null) {
- PsiClass containingClass = method.getContainingClass();
- if (containingClass == null) {
- return;
- }
-
- PsiParameterList parameterList = method.getParameterList();
- if (parameterList.getParametersCount() > 0) {
- PsiParameter[] parameters = parameterList.getParameters();
-
- List arguments = expression.getValueArguments();
- for (int i = 0; i < parameters.length; i++) {
- PsiType parameterType = parameters[i].getType();
- if (parameterType instanceof PsiClassType) {
- if (i >= arguments.size()) {
- // We can end up with more arguments than parameters when
- // there is a varargs call.
- break;
- }
- UExpression argument = arguments.get(i);
- PsiType argumentType = argument.getExpressionType();
- if (argumentType == null || parameterType.equals(argumentType) || !(argumentType instanceof PsiClassType)) {
- continue;
- }
- checkCast(argument, (PsiClassType)argumentType, (PsiClassType)parameterType);
- }
- }
- }
-
- 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 (UastExpressionUtils.isMethodCall(expression)) {
- UExpression qualifier = expression.getReceiver();
- if (qualifier != null && !(qualifier instanceof UThisExpression) && !(qualifier instanceof USuperExpression)) {
- PsiType type = qualifier.getExpressionType();
- 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 {
- // For example, for Bundle#getString(String,String) the API level is 12, whereas for
- // BaseBundle#getString(String,String) the API level is 21. If the code specified a Bundle instead of
- // a BaseBundle, reported the Bundle level in the error message instead.
- if (specificApi < api) {
- api = specificApi;
- fqcn = expressionOwner.replace('/', '.');
- }
- api = Math.min(specificApi, api);
- }
- }
- }
- } else {
- // Unqualified call; need to search in our super hierarchy
- PsiClass cls = UastUtils.getContainingClass(expression);
-
- //noinspection ConstantConditions
- if (qualifier instanceof UThisExpression || qualifier instanceof USuperExpression) {
- UInstanceExpression pte = (UInstanceExpression) qualifier;
- PsiElement resolved = pte.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
- boolean found = false;
- PsiClassType anonymousBaseType = ((PsiAnonymousClass)cls).getBaseClassType();
- PsiClass anonymousBase = anonymousBaseType.resolve();
- if (anonymousBase != null && anonymousBase.isInheritor(containingClass, true)) {
- cls = anonymousBase;
- found = true;
- } else {
- PsiClass surroundingBaseType = PsiTreeUtil.getParentOfType(cls, PsiClass.class, true);
- if (surroundingBaseType != null && surroundingBaseType.isInheritor(containingClass, true)) {
- cls = surroundingBaseType;
- found = true;
- }
- }
- if (!found) {
- 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 {
- if (specificApi < api) {
- api = specificApi;
- fqcn = expressionOwner.replace('/', '.');
- }
- api = Math.min(specificApi, api);
- 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 (UastExpressionUtils.isMethodCall(expression)) {
- if (expression.getReceiver() instanceof USuperExpression) {
- PsiMethod containingMethod = UastUtils.getContainingMethod(expression);
- 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;
- }
- }
- }
-
- UElement locationNode = expression.getMethodIdentifier();
- if (locationNode == null) {
- locationNode = expression;
- }
- Location location = IntellijLintUtils.getUastLocation(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);
- }
- }
- }
-}
diff --git a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintIssueRegistry.java b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintIssueRegistry.java
index c3ae8d19029..2a47da66fb0 100644
--- a/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintIssueRegistry.java
+++ b/plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintIssueRegistry.java
@@ -69,7 +69,7 @@ public class IntellijLintIssueRegistry extends BuiltinIssueRegistry {
EnumSet scope = implementation.getScope();
Class extends Detector> detectorClass = implementation.getDetectorClass();
if (detectorClass == ApiDetector.class) {
- issue.setImplementation(IntellijApiDetector.IMPLEMENTATION);
+ //issue.setImplementation(IntellijApiDetector.IMPLEMENTATION);
} else if (detectorClass == ViewTypeDetector.class) {
issue.setImplementation(IntellijViewTypeDetector.IMPLEMENTATION);
}