Check superclasses and interfaces in the API Detector
This corresponds to the parallel change to the bytecode
detector: https://android-review.googlesource.com/#/c/52001/
Also adds unit test for the ApiDetector (was incomplete).
Change-Id: Icc3a2fce86ab8c3f4741ef6d489b881273e292ab
diff --git a/android/src/org/jetbrains/android/inspections/lint/IntellijApiDetector.java b/android/src/org/jetbrains/android/inspections/lint/IntellijApiDetector.java
index 11e6947..4070d03 100644
--- a/android/src/org/jetbrains/android/inspections/lint/IntellijApiDetector.java
+++ b/android/src/org/jetbrains/android/inspections/lint/IntellijApiDetector.java
@@ -15,7 +15,6 @@
*/
package org.jetbrains.android.inspections.lint;
-import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.sdk.SdkVersionInfo;
@@ -23,6 +22,7 @@
import com.android.tools.lint.detector.api.*;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.psi.*;
+import com.intellij.psi.impl.source.PsiClassReferenceType;
import com.intellij.psi.util.PsiTreeUtil;
import lombok.ast.AstVisitor;
import lombok.ast.CompilationUnit;
@@ -274,6 +274,53 @@
}
@Override
+ public void visitClass(PsiClass aClass) {
+ super.visitClass(aClass);
+
+ if (!myCheckAccess) {
+ return;
+ }
+
+ for (PsiClassType type : aClass.getSuperTypes()) {
+ String signature = IntellijLintUtils.getInternalName(type);
+ if (signature == null) {
+ continue;
+ }
+
+ int api = mApiDatabase.getClassVersion(signature);
+ if (api == -1) {
+ continue;
+ }
+ int minSdk = getMinSdk(myContext);
+ if (api < minSdk) {
+ continue;
+ }
+ if (mySeenTargetApi) {
+ int target = getTargetApi(aClass, myFile);
+ if (target != -1) {
+ if (api <= target) {
+ continue;
+ }
+ }
+ }
+ if (mySeenSuppress && IntellijLintUtils.isSuppressed(aClass, myFile, UNSUPPORTED)) {
+ continue;
+ }
+
+ Location location;
+ if (type instanceof PsiClassReferenceType) {
+ PsiReference reference = ((PsiClassReferenceType)type).getReference();
+ location = IntellijLintUtils.getLocation(myContext.file, reference.getElement());
+ } else {
+ location = IntellijLintUtils.getLocation(myContext.file, aClass);
+ }
+ String fqcn = type.getClassName();
+ String message = String.format("Class requires API level %1$d (current min is %2$d): %3$s", api, minSdk, fqcn);
+ myContext.report(UNSUPPORTED, location, message, null);
+ }
+ }
+
+ @Override
public void visitReferenceExpression(PsiReferenceExpression expression) {
super.visitReferenceExpression(expression);
@@ -293,7 +340,6 @@
if (containingClass == null) {
return;
}
- String fqcn = containingClass.getQualifiedName();
String owner = IntellijLintUtils.getInternalName(containingClass);
if (owner == null) {
return; // Couldn't resolve type
@@ -323,6 +369,7 @@
}
Location location = IntellijLintUtils.getLocation(myContext.file, expression);
+ String fqcn = containingClass.getQualifiedName();
String message = String.format(
"Field requires API level %1$d (current min is %2$d): %3$s",
api, minSdk, fqcn + '#' + name);
diff --git a/android/src/org/jetbrains/android/inspections/lint/IntellijLintUtils.java b/android/src/org/jetbrains/android/inspections/lint/IntellijLintUtils.java
index 5a1c029..121e569 100644
--- a/android/src/org/jetbrains/android/inspections/lint/IntellijLintUtils.java
+++ b/android/src/org/jetbrains/android/inspections/lint/IntellijLintUtils.java
@@ -187,8 +187,8 @@
/**
- * Computes the internal class name of the given fully qualified class name.
- * For example, it converts foo.bar.Foo.Bar into foo/bar/Foo$Bar
+ * Computes the internal class name of the given class.
+ * For example, for PsiClass foo.bar.Foo.Bar it returns foo/bar/Foo$Bar.
*
* @param psiClass the class to look up the internal name for
* @return the internal class name
@@ -222,6 +222,29 @@
}
/**
+ * Computes the internal class name of the given class type.
+ * For example, for PsiClassType foo.bar.Foo.Bar it returns foo/bar/Foo$Bar.
+ *
+ * @param psiClassType the class type to look up the internal name for
+ * @return the internal class name
+ * @see ClassContext#getInternalName(String)
+ */
+ @Nullable
+ public static String getInternalName(@NonNull PsiClassType psiClassType) {
+ PsiClass resolved = psiClassType.resolve();
+ if (resolved != null) {
+ return getInternalName(resolved);
+ }
+
+ String className = psiClassType.getClassName();
+ if (className != null) {
+ return ClassContext.getInternalName(className);
+ }
+
+ return null;
+ }
+
+ /**
* Computes the internal JVM description of the given method. This is in the same
* format as the ASM desc fields for methods; meaning that a method named foo which for example takes an
* int and a String and returns a void will have description {@code foo(ILjava/lang/String;):V}.
diff --git a/android/testData/apiCheck/Basic.java b/android/testData/apiCheck/Basic.java
new file mode 100644
index 0000000..1a925eb
--- /dev/null
+++ b/android/testData/apiCheck/Basic.java
@@ -0,0 +1,142 @@
+package p1.p2;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.LinearLayout;
+
+import android.view.ViewGroup.LayoutParams;
+import android.app.Activity;
+import android.app.ApplicationErrorReport;
+import android.app.ApplicationErrorReport.BatteryInfo;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuff.Mode;
+import android.widget.Chronometer;
+import android.widget.GridLayout;
+
+import java.io.IOException;
+
+public class Class extends Activity {
+ public void method(Chronometer chronometer) {
+ // Method call
+ chronometer.<error descr="Call requires API level 3 (current min is 1): android.widget.Chronometer#getOnChronometerTickListener">getOnChronometerTickListener</error>(); // API 3
+
+ // Inherited method call (from TextView
+ chronometer.<error descr="Call requires API level 11 (current min is 1): android.widget.TextView#setTextIsSelectable">setTextIsSelectable</error>(true); // API 11
+
+ // Field access
+ int fillParent = LayoutParams.FILL_PARENT; // API 1
+ // This is a final int, which means it gets inlined
+ int matchParent = LayoutParams.MATCH_PARENT; // API 8
+ // Field access: non final
+ BatteryInfo batteryInfo = <error descr="Field requires API level 14 (current min is 1): android.app.ApplicationErrorReport#batteryInfo">getReport().batteryInfo</error>;
+
+ // Enum access
+ Mode mode = <error descr="Field requires API level 11 (current min is 1): android.graphics.PorterDuff.Mode#OVERLAY">PorterDuff.Mode.OVERLAY</error>; // API 11
+ }
+
+ // Return type
+ GridLayout getGridLayout() { // API 14
+ return null;
+ }
+
+ private ApplicationErrorReport getReport() {
+ return null;
+ }
+
+ public static class ApiCallTest10 extends <error descr="Class requires API level 1 (current min is 1): View">View</error> {
+ public ApiCallTest10() {
+ super(null, null, 0);
+ }
+
+ @Override
+ public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ return super.<error descr="Call requires API level 4 (current min is 1): android.view.View#dispatchPopulateAccessibilityEvent">dispatchPopulateAccessibilityEvent</error>(event);
+ }
+
+ @Override
+ public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
+ super.<error descr="Call requires API level 14 (current min is 1): android.view.View#onPopulateAccessibilityEvent">onPopulateAccessibilityEvent</error>(event); // Valid lint warning
+ // Additional override code here:
+ }
+
+ @Override
+ protected boolean dispatchGenericFocusedEvent(MotionEvent event) {
+ return super.<error descr="Call requires API level 14 (current min is 1): android.view.View#dispatchGenericFocusedEvent">dispatchGenericFocusedEvent</error>(event); // Should flag this
+ }
+
+ protected boolean dispatchHoverEvent(int event) {
+ return false;
+ }
+
+ public void test1() {
+ // Should flag this, because the local method has the wrong signature
+ <error descr="Call requires API level 14 (current min is 1): android.view.View#dispatchHoverEvent">dispatchHoverEvent</error>(null);
+
+ // Shouldn't flag this, local method makes it available
+ dispatchGenericFocusedEvent(null);
+ }
+ }
+
+ public static class ApiCallTest11 extends <error descr="Class requires API level 1 (current min is 1): Activity">Activity</error> {
+ public boolean isDestroyed() {
+ return true;
+ }
+
+ @SuppressLint("Override")
+ public void finishAffinity() {
+ }
+
+ private class MyLinear extends <error descr="Class requires API level 1 (current min is 1): LinearLayout">LinearLayout</error> {
+ private Drawable mDividerDrawable;
+
+ public MyLinear(Context context) {
+ super(context);
+ }
+
+ public void setDividerDrawable(Drawable dividerDrawable) {
+ mDividerDrawable = dividerDrawable;
+ }
+ }
+ }
+
+ public static class ApiCallTest5 extends View {
+ public ApiCallTest5(Context context) {
+ super(context);
+ }
+
+ @SuppressWarnings("unused")
+ @Override
+ @TargetApi(2)
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int measuredWidth = View.<error descr="Call requires API level 11 (current min is 1): android.view.View#resolveSizeAndState">resolveSizeAndState</error>(widthMeasureSpec,
+ widthMeasureSpec, 0);
+ int measuredHeight = <error descr="Call requires API level 11 (current min is 1): android.view.View#resolveSizeAndState">resolveSizeAndState</error>(heightMeasureSpec,
+ heightMeasureSpec, 0);
+ View.<error descr="Call requires API level 11 (current min is 1): android.view.View#combineMeasuredStates">combineMeasuredStates</error>(0, 0);
+ ApiCallTest5.<error descr="Call requires API level 11 (current min is 1): android.view.View#combineMeasuredStates">combineMeasuredStates</error>(0, 0);
+ }
+ }
+
+ public static class ApiCallTest6 {
+ public void test(Throwable throwable) {
+ // IOException(Throwable) requires API 9
+ IOException ioException = new IOException(throwable);
+ }
+ }
+
+ @SuppressWarnings("serial")
+ public static class ApiCallTest7 extends IOException {
+ public ApiCallTest7(String message, Throwable cause) {
+ super(message, cause); // API 9
+ }
+
+ public void fun() throws IOException {
+ super.toString(); throw new IOException((Throwable) null); // API 9
+ }
+ }
+}
diff --git a/android/testData/apiCheck/Interfaces1.java b/android/testData/apiCheck/Interfaces1.java
new file mode 100644
index 0000000..947cfed
--- /dev/null
+++ b/android/testData/apiCheck/Interfaces1.java
@@ -0,0 +1,24 @@
+package p1.p2;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.view.View;
+import android.view.View.OnLayoutChangeListener;
+import android.widget.GridLayout;
+
+public class Class extends <error descr="Class requires API level 14 (current min is 1): GridLayout">GridLayout</error> implements
+ <error descr="Class requires API level 11 (current min is 1): OnSystemUiVisibilityChangeListener">View.OnSystemUiVisibilityChangeListener</error>, <error descr="Class requires API level 11 (current min is 1): OnLayoutChangeListener">OnLayoutChangeListener</error> {
+
+ public Class(Context context) {
+ <error descr="Call requires API level 14 (current min is 1): android.widget.GridLayout#GridLayout">super(context)</error>;
+ }
+
+ @Override
+ public void onSystemUiVisibilityChange(int visibility) {
+ }
+
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right,
+ int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ }
+}
diff --git a/android/testData/apiCheck/Interfaces2.java b/android/testData/apiCheck/Interfaces2.java
new file mode 100644
index 0000000..42b39f0
--- /dev/null
+++ b/android/testData/apiCheck/Interfaces2.java
@@ -0,0 +1,23 @@
+package p1.p2;
+
+import android.content.Context;
+import android.view.View;
+import android.view.View.OnLayoutChangeListener;
+import android.widget.GridLayout;
+
+public class Class extends <error descr="Class requires API level 14 (current min is 1): GridLayout">Grid<caret>Layout</error> implements
+ <error descr="Class requires API level 11 (current min is 1): OnSystemUiVisibilityChangeListener">View.OnSystemUiVisibilityChangeListener</error>, <error descr="Class requires API level 11 (current min is 1): OnLayoutChangeListener">OnLayoutChangeListener</error> {
+
+ public Class(Context context) {
+ <error descr="Call requires API level 14 (current min is 1): android.widget.GridLayout#GridLayout">super(context)</error>;
+ }
+
+ @Override
+ public void onSystemUiVisibilityChange(int visibility) {
+ }
+
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right,
+ int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ }
+}
diff --git a/android/testData/apiCheck/Interfaces2_after.java b/android/testData/apiCheck/Interfaces2_after.java
new file mode 100644
index 0000000..ff91ac2
--- /dev/null
+++ b/android/testData/apiCheck/Interfaces2_after.java
@@ -0,0 +1,25 @@
+package p1.p2;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.view.View;
+import android.view.View.OnLayoutChangeListener;
+import android.widget.GridLayout;
+
+@SuppressLint("NewApi")
+public class Class extends GridLayout implements
+ View.OnSystemUiVisibilityChangeListener, OnLayoutChangeListener {
+
+ public Class(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onSystemUiVisibilityChange(int visibility) {
+ }
+
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right,
+ int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ }
+}
diff --git a/android/testSrc/org/jetbrains/android/inspections/lint/AndroidLintInspectionToolProviderTest.java b/android/testSrc/org/jetbrains/android/inspections/lint/AndroidLintInspectionToolProviderTest.java
index 0c956b9..2d17349 100644
--- a/android/testSrc/org/jetbrains/android/inspections/lint/AndroidLintInspectionToolProviderTest.java
+++ b/android/testSrc/org/jetbrains/android/inspections/lint/AndroidLintInspectionToolProviderTest.java
@@ -41,7 +41,6 @@
// testAllLintChecksRegistered(file.getProject());
// at runtime instead and captured the output.
- testAllLintChecksRegistered(myFixture.getProject());
//assertTrue(testAllLintChecksRegistered(myFixture.getProject()));
}
diff --git a/android/testSrc/org/jetbrains/android/inspections/lint/IntellijApiDetectorTest.java b/android/testSrc/org/jetbrains/android/inspections/lint/IntellijApiDetectorTest.java
index d576397..6eb90e7 100644
--- a/android/testSrc/org/jetbrains/android/inspections/lint/IntellijApiDetectorTest.java
+++ b/android/testSrc/org/jetbrains/android/inspections/lint/IntellijApiDetectorTest.java
@@ -16,85 +16,45 @@
package org.jetbrains.android.inspections.lint;
import com.intellij.codeInsight.intention.IntentionAction;
-import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.android.AndroidTestCase;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import static org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider.AndroidLintNewApiInspection;
public class IntellijApiDetectorTest extends AndroidTestCase {
- private static final String BASE_PATH = "intentions/";
+ private static final String BASE_PATH = "apiCheck/";
- public void testApiCheck1() {
- //myFacet.getConfiguration().LIBRARY_PROJECT = true;
- //myFixture.copyFileToProject(BASE_PATH + "MyActivity.java", "src/p1/p2/MyActivity.java");
+ public void testBasic() throws Exception {
AndroidLintNewApiInspection inspection = new AndroidLintNewApiInspection();
- doTest(inspection, true, inspection.getDisplayName());
+ doTest(inspection, null);
}
- //
- //public void testSwitchOnResourceId() {
- // myFacet.getConfiguration().LIBRARY_PROJECT = true;
- // myFixture.copyFileToProject(BASE_PATH + "R.java", "src/p1/p2/R.java");
- // AndroidLintNewApiInspection inspection = new AndroidLintNewApiInspection();
- //
- // doTest(inspection, true, inspection.getDisplayName());
- //}
- //
- ////public void testSwitchOnResourceId() {
- //// myFacet.getConfiguration().LIBRARY_PROJECT = true;
- //// myFixture.copyFileToProject(BASE_PATH + "R.java", "src/p1/p2/R.java");
- //// final AndroidNonConstantResIdsInSwitchInspection inspection = new AndroidNonConstantResIdsInSwitchInspection();
- //// doTest(inspection, true, inspection.getQuickFixName());
- ////}
- //
- //public void testSwitchOnResourceId1() {
- // myFacet.getConfiguration().LIBRARY_PROJECT = false;
- // myFixture.copyFileToProject(BASE_PATH + "R.java", "src/p1/p2/R.java");
- // final AndroidNonConstantResIdsInSwitchInspection inspection = new AndroidNonConstantResIdsInSwitchInspection();
- // doTest(inspection, false, inspection.getQuickFixName());
- //}
- //
- //public void testSwitchOnResourceId2() {
- // myFacet.getConfiguration().LIBRARY_PROJECT = true;
- // myFixture.copyFileToProject(BASE_PATH + "R.java", "src/p1/p2/R.java");
- // final AndroidNonConstantResIdsInSwitchInspection inspection = new AndroidNonConstantResIdsInSwitchInspection();
- // doTest(inspection, false, inspection.getQuickFixName());
- //}
- private void doTest(final LocalInspectionTool inspection, boolean available, String quickFixName) {
+ public void testInterfaces1() throws Exception {
+ AndroidLintNewApiInspection inspection = new AndroidLintNewApiInspection();
+ // TODO: check @TargetApi
+ doTest(inspection, null /* "Add @TargetApi(ICE_CREAM_SANDWICH) Annotation" */);
+ }
+
+ public void testInterfaces2() throws Exception {
+ AndroidLintNewApiInspection inspection = new AndroidLintNewApiInspection();
+ doTest(inspection, "Add @SuppressLint(\"NewApi\") annotation");
+ }
+
+ private void doTest(@NotNull final AndroidLintInspectionBase inspection, @Nullable String quickFixName) throws Exception {
+ createManifest();
myFixture.enableInspections(inspection);
-
- final VirtualFile file = myFixture.copyFileToProject(BASE_PATH + getTestName(false) + ".java", "src/p1/p2/Class.java");
+ VirtualFile file = myFixture.copyFileToProject(BASE_PATH + getTestName(false) + ".java", "src/p1/p2/Class.java");
myFixture.configureFromExistingVirtualFile(file);
+ myFixture.doHighlighting();
myFixture.checkHighlighting(true, false, false);
- final IntentionAction quickFix = myFixture.getAvailableIntention(quickFixName);
- if (available) {
+ if (quickFixName != null) {
+ final IntentionAction quickFix = myFixture.getAvailableIntention(quickFixName);
assertNotNull(quickFix);
myFixture.launchAction(quickFix);
myFixture.checkResultByFile(BASE_PATH + getTestName(false) + "_after.java");
}
- else {
- assertNull(quickFix);
- }
}
-
- private void doTest(final AndroidLintInspectionBase inspection, boolean available, String quickFixName) {
- myFixture.enableInspections(inspection);
-
- final VirtualFile file = myFixture.copyFileToProject(BASE_PATH + getTestName(false) + ".java", "src/p1/p2/Class.java");
- myFixture.configureFromExistingVirtualFile(file);
- myFixture.checkHighlighting(true, false, false);
-
- final IntentionAction quickFix = myFixture.getAvailableIntention(quickFixName);
- if (available) {
- assertNotNull(quickFix);
- myFixture.launchAction(quickFix);
- myFixture.checkResultByFile(BASE_PATH + getTestName(false) + "_after.java");
- }
- else {
- assertNull(quickFix);
- }
- }
-
}