Merge "41753: Lint API Check: Consult source files for constants"
diff --git a/lint/cli/src/test/java/com/android/tools/lint/MainTest.java b/lint/cli/src/test/java/com/android/tools/lint/MainTest.java
index ba31ed8..5a48a47 100644
--- a/lint/cli/src/test/java/com/android/tools/lint/MainTest.java
+++ b/lint/cli/src/test/java/com/android/tools/lint/MainTest.java
@@ -142,6 +142,14 @@
"Similarly, you can use tools:targetApi=\"11\" in an XML file to indicate that\n" +
"the element will only be inflated in an adequate context.\n" +
"\n" +
+ "Lint will also flag certain constants, such as static final integers, which\n" +
+ "were introduced in later versions. These will actually be copied into the\n" +
+ "class files rather than being referenced, which means that the value is\n" +
+ "available even when running on older devices. In some cases that's fine, and\n" +
+ "in other cases it can result in a runtime crash or incorrect behavior. It\n" +
+ "depends on the context, so consider the code carefully and device whether it's\n" +
+ "safe and can be suppressed or whether the code needs to be guarded.\n" +
+ "\n" +
"\n",
// Expected error
diff --git a/lint/cli/src/test/java/com/android/tools/lint/checks/ApiDetectorTest.java b/lint/cli/src/test/java/com/android/tools/lint/checks/ApiDetectorTest.java
index e42280f..1bcbfaa 100644
--- a/lint/cli/src/test/java/com/android/tools/lint/checks/ApiDetectorTest.java
+++ b/lint/cli/src/test/java/com/android/tools/lint/checks/ApiDetectorTest.java
@@ -138,14 +138,14 @@
" ~~~~~~~~~~~~~~~~~~~\n" +
"src/foo/bar/ApiCallTest.java:33: Error: Field requires API level 11 (current min is 1): dalvik.bytecode.OpcodeInfo#MAXIMUM_VALUE [NewApi]\n" +
" int field = OpcodeInfo.MAXIMUM_VALUE; // API 11\n" +
- " ~~~~~~~~~~~~~\n" +
+ " ~~~~~~~~~~~~~~~~~~~~~~~~\n" +
"src/foo/bar/ApiCallTest.java:38: Error: Field requires API level 14 (current min is 1): android.app.ApplicationErrorReport#batteryInfo [NewApi]\n" +
" BatteryInfo batteryInfo = getReport().batteryInfo;\n" +
" ~~~~~~~~~~~\n" +
// Note: the above error range is wrong; should be pointing to the second
"src/foo/bar/ApiCallTest.java:41: Error: Field requires API level 11 (current min is 1): android.graphics.PorterDuff.Mode#OVERLAY [NewApi]\n" +
" Mode mode = PorterDuff.Mode.OVERLAY; // API 11\n" +
- " ~~~~~~~\n" +
+ " ~~~~~~~~~~~~~~~~~~~~~~~\n" +
"7 errors, 0 warnings\n",
lintProject(
@@ -172,13 +172,13 @@
" ~~~~~~~~~~~~~~~~~~~\n" +
"src/foo/bar/ApiCallTest.java:33: Error: Field requires API level 11 (current min is 2): dalvik.bytecode.OpcodeInfo#MAXIMUM_VALUE [NewApi]\n" +
" int field = OpcodeInfo.MAXIMUM_VALUE; // API 11\n" +
- " ~~~~~~~~~~~~~\n" +
+ " ~~~~~~~~~~~~~~~~~~~~~~~~\n" +
"src/foo/bar/ApiCallTest.java:38: Error: Field requires API level 14 (current min is 2): android.app.ApplicationErrorReport#batteryInfo [NewApi]\n" +
" BatteryInfo batteryInfo = getReport().batteryInfo;\n" +
" ~~~~~~~~~~~\n" +
"src/foo/bar/ApiCallTest.java:41: Error: Field requires API level 11 (current min is 2): android.graphics.PorterDuff.Mode#OVERLAY [NewApi]\n" +
" Mode mode = PorterDuff.Mode.OVERLAY; // API 11\n" +
- " ~~~~~~~\n" +
+ " ~~~~~~~~~~~~~~~~~~~~~~~\n" +
"7 errors, 0 warnings\n",
lintProject(
@@ -202,13 +202,13 @@
" ~~~~~~~~~~~~~~~~~~~\n" +
"src/foo/bar/ApiCallTest.java:33: Error: Field requires API level 11 (current min is 4): dalvik.bytecode.OpcodeInfo#MAXIMUM_VALUE [NewApi]\n" +
" int field = OpcodeInfo.MAXIMUM_VALUE; // API 11\n" +
- " ~~~~~~~~~~~~~\n" +
+ " ~~~~~~~~~~~~~~~~~~~~~~~~\n" +
"src/foo/bar/ApiCallTest.java:38: Error: Field requires API level 14 (current min is 4): android.app.ApplicationErrorReport#batteryInfo [NewApi]\n" +
" BatteryInfo batteryInfo = getReport().batteryInfo;\n" +
" ~~~~~~~~~~~\n" +
"src/foo/bar/ApiCallTest.java:41: Error: Field requires API level 11 (current min is 4): android.graphics.PorterDuff.Mode#OVERLAY [NewApi]\n" +
" Mode mode = PorterDuff.Mode.OVERLAY; // API 11\n" +
- " ~~~~~~~\n" +
+ " ~~~~~~~~~~~~~~~~~~~~~~~\n" +
"6 errors, 0 warnings\n",
lintProject(
@@ -229,13 +229,13 @@
" ~~~~~~~~~~~~~~~~~~~\n" +
"src/foo/bar/ApiCallTest.java:33: Error: Field requires API level 11 (current min is 10): dalvik.bytecode.OpcodeInfo#MAXIMUM_VALUE [NewApi]\n" +
" int field = OpcodeInfo.MAXIMUM_VALUE; // API 11\n" +
- " ~~~~~~~~~~~~~\n" +
+ " ~~~~~~~~~~~~~~~~~~~~~~~~\n" +
"src/foo/bar/ApiCallTest.java:38: Error: Field requires API level 14 (current min is 10): android.app.ApplicationErrorReport#batteryInfo [NewApi]\n" +
" BatteryInfo batteryInfo = getReport().batteryInfo;\n" +
" ~~~~~~~~~~~\n" +
"src/foo/bar/ApiCallTest.java:41: Error: Field requires API level 11 (current min is 10): android.graphics.PorterDuff.Mode#OVERLAY [NewApi]\n" +
" Mode mode = PorterDuff.Mode.OVERLAY; // API 11\n" +
- " ~~~~~~~\n" +
+ " ~~~~~~~~~~~~~~~~~~~~~~~\n" +
"5 errors, 0 warnings\n",
lintProject(
@@ -366,13 +366,13 @@
" ~~~~~~~~~~~~~~~~~~~\n" +
"src/foo/bar/SuppressTest1.java:89: Error: Field requires API level 11 (current min is 1): dalvik.bytecode.OpcodeInfo#MAXIMUM_VALUE [NewApi]\n" +
" int field = OpcodeInfo.MAXIMUM_VALUE; // API 11\n" +
- " ~~~~~~~~~~~~~\n" +
+ " ~~~~~~~~~~~~~~~~~~~~~~~~\n" +
"src/foo/bar/SuppressTest1.java:94: Error: Field requires API level 14 (current min is 1): android.app.ApplicationErrorReport#batteryInfo [NewApi]\n" +
" BatteryInfo batteryInfo = getReport().batteryInfo;\n" +
" ~~~~~~~~~~~\n" +
"src/foo/bar/SuppressTest1.java:97: Error: Field requires API level 11 (current min is 1): android.graphics.PorterDuff.Mode#OVERLAY [NewApi]\n" +
" Mode mode = PorterDuff.Mode.OVERLAY; // API 11\n" +
- " ~~~~~~~\n" +
+ " ~~~~~~~~~~~~~~~~~~~~~~~\n" +
// Note: These annotations are within the methods, not ON the methods, so they have
// no effect (because they don't end up in the bytecode)
@@ -716,4 +716,74 @@
"apicheck/ApiCallTest12.class.data=>bin/classes/test/pkg/ApiCallTest12.class"
));
}
+
+ public void testJavaConstants() throws Exception {
+ assertEquals(""
+ + "src/test/pkg/ApiSourceCheck.java:5: Error: Field requires API level 11 (current min is 1): android.view.View#MEASURED_STATE_MASK [NewApi]\n"
+ + "import static android.view.View.MEASURED_STATE_MASK;\n"
+ + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"
+ + "src/test/pkg/ApiSourceCheck.java:30: Error: Field requires API level 11 (current min is 1): android.widget.ZoomControls#MEASURED_STATE_MASK [NewApi]\n"
+ + " int x = MEASURED_STATE_MASK;\n"
+ + " ~~~~~~~~~~~~~~~~~~~\n"
+ + "src/test/pkg/ApiSourceCheck.java:33: Error: Field requires API level 11 (current min is 1): android.view.View#MEASURED_STATE_MASK [NewApi]\n"
+ + " int y = android.view.View.MEASURED_STATE_MASK;\n"
+ + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"
+ + "src/test/pkg/ApiSourceCheck.java:36: Error: Field requires API level 11 (current min is 1): android.view.View#MEASURED_STATE_MASK [NewApi]\n"
+ + " int z = View.MEASURED_STATE_MASK;\n"
+ + " ~~~~~~~~~~~~~~~~~~~~~~~~\n"
+ + "src/test/pkg/ApiSourceCheck.java:37: Error: Field requires API level 14 (current min is 1): android.view.View#FIND_VIEWS_WITH_TEXT [NewApi]\n"
+ + " int find2 = View.FIND_VIEWS_WITH_TEXT; // requires API 14\n"
+ + " ~~~~~~~~~~~~~~~~~~~~~~~~~\n"
+ + "src/test/pkg/ApiSourceCheck.java:40: Error: Field requires API level 12 (current min is 1): android.app.ActivityManager#MOVE_TASK_NO_USER_ACTION [NewApi]\n"
+ + " int w = ActivityManager.MOVE_TASK_NO_USER_ACTION;\n"
+ + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"
+ + "src/test/pkg/ApiSourceCheck.java:41: Error: Field requires API level 14 (current min is 1): android.widget.ZoomButton#FIND_VIEWS_WITH_CONTENT_DESCRIPTION [NewApi]\n"
+ + " int find1 = ZoomButton.FIND_VIEWS_WITH_CONTENT_DESCRIPTION; // requires\n"
+ + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"
+ + "src/test/pkg/ApiSourceCheck.java:44: Error: Field requires API level 9 (current min is 1): android.widget.ZoomControls#OVER_SCROLL_ALWAYS [NewApi]\n"
+ + " int overScroll = OVER_SCROLL_ALWAYS; // requires API 9\n"
+ + " ~~~~~~~~~~~~~~~~~~\n"
+ + "src/test/pkg/ApiSourceCheck.java:47: Error: Field requires API level 16 (current min is 1): android.widget.ZoomControls#IMPORTANT_FOR_ACCESSIBILITY_AUTO [NewApi]\n"
+ + " int auto = IMPORTANT_FOR_ACCESSIBILITY_AUTO; // requires API 16\n"
+ + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"
+ + "src/test/pkg/ApiSourceCheck.java:51: Error: Field requires API level 14 (current min is 1): android.widget.ZoomButton#ROTATION_X [NewApi]\n"
+ + " Object rotationX = ZoomButton.ROTATION_X; // Requires API 14\n"
+ + " ~~~~~~~~~~~~~~~~~~~~~\n"
+ + "src/test/pkg/ApiSourceCheck.java:54: Error: Field requires API level 11 (current min is 1): android.view.View#MEASURED_STATE_MASK [NewApi]\n"
+ + " return (child.getMeasuredWidth() & View.MEASURED_STATE_MASK)\n"
+ + " ~~~~~~~~~~~~~~~~~~~~~~~~\n"
+ + "src/test/pkg/ApiSourceCheck.java:55: Error: Field requires API level 11 (current min is 1): android.view.View#MEASURED_HEIGHT_STATE_SHIFT [NewApi]\n"
+ + " | ((child.getMeasuredHeight() >> View.MEASURED_HEIGHT_STATE_SHIFT) & (View.MEASURED_STATE_MASK >> View.MEASURED_HEIGHT_STATE_SHIFT));\n"
+ + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"
+ + "src/test/pkg/ApiSourceCheck.java:55: Error: Field requires API level 11 (current min is 1): android.view.View#MEASURED_HEIGHT_STATE_SHIFT [NewApi]\n"
+ + " | ((child.getMeasuredHeight() >> View.MEASURED_HEIGHT_STATE_SHIFT) & (View.MEASURED_STATE_MASK >> View.MEASURED_HEIGHT_STATE_SHIFT));\n"
+ + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"
+ + "src/test/pkg/ApiSourceCheck.java:55: Error: Field requires API level 11 (current min is 1): android.view.View#MEASURED_STATE_MASK [NewApi]\n"
+ + " | ((child.getMeasuredHeight() >> View.MEASURED_HEIGHT_STATE_SHIFT) & (View.MEASURED_STATE_MASK >> View.MEASURED_HEIGHT_STATE_SHIFT));\n"
+ + " ~~~~~~~~~~~~~~~~~~~~~~~~\n"
+ + "src/test/pkg/ApiSourceCheck.java:90: Error: Field requires API level 8 (current min is 1): android.R.id#custom [NewApi]\n"
+ + " int custom = android.R.id.custom; // API 8\n"
+ + " ~~~~~~~~~~~~~~~~~~~\n"
+ + "src/test/pkg/ApiSourceCheck.java:94: Error: Field requires API level 13 (current min is 1): android.Manifest.permission#SET_POINTER_SPEED [NewApi]\n"
+ + " String setPointerSpeed = permission.SET_POINTER_SPEED;\n"
+ + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"
+ + "src/test/pkg/ApiSourceCheck.java:95: Error: Field requires API level 13 (current min is 1): android.Manifest.permission#SET_POINTER_SPEED [NewApi]\n"
+ + " String setPointerSpeed2 = Manifest.permission.SET_POINTER_SPEED;\n"
+ + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"
+ + "src/test/pkg/ApiSourceCheck.java:120: Error: Field requires API level 11 (current min is 1): android.view.View#MEASURED_STATE_MASK [NewApi]\n"
+ + " int y = View.MEASURED_STATE_MASK; // Not OK\n"
+ + " ~~~~~~~~~~~~~~~~~~~~~~~~\n"
+ + "src/test/pkg/ApiSourceCheck.java:121: Error: Field requires API level 11 (current min is 1): android.view.View#MEASURED_STATE_MASK [NewApi]\n"
+ + " testBenignUsages(View.MEASURED_STATE_MASK); // Not OK\n"
+ + " ~~~~~~~~~~~~~~~~~~~~~~~~\n"
+ + "19 errors, 0 warnings\n",
+
+ lintProject(
+ "apicheck/classpath=>.classpath",
+ "apicheck/minsdk1.xml=>AndroidManifest.xml",
+ "project.properties1=>project.properties",
+ "apicheck/ApiSourceCheck.java.txt=>src/test/pkg/ApiSourceCheck.java",
+ "apicheck/ApiSourceCheck.class.data=>bin/classes/test/pkg/ApiSourceCheck.class"
+ ));
+ }
}
diff --git a/lint/cli/src/test/java/com/android/tools/lint/checks/data/apicheck/ApiSourceCheck.class.data b/lint/cli/src/test/java/com/android/tools/lint/checks/data/apicheck/ApiSourceCheck.class.data
new file mode 100644
index 0000000..726b8a5
--- /dev/null
+++ b/lint/cli/src/test/java/com/android/tools/lint/checks/data/apicheck/ApiSourceCheck.class.data
Binary files differ
diff --git a/lint/cli/src/test/java/com/android/tools/lint/checks/data/apicheck/ApiSourceCheck.java.txt b/lint/cli/src/test/java/com/android/tools/lint/checks/data/apicheck/ApiSourceCheck.java.txt
new file mode 100644
index 0000000..429b388
--- /dev/null
+++ b/lint/cli/src/test/java/com/android/tools/lint/checks/data/apicheck/ApiSourceCheck.java.txt
@@ -0,0 +1,123 @@
+package test.pkg;
+
+import android.util.Property;
+import android.view.View;
+import static android.view.View.MEASURED_STATE_MASK;
+import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
+import android.view.*;
+import android.annotation.*;
+import android.app.*;
+import android.widget.*;
+import static android.widget.ZoomControls.*;
+import android.Manifest.permission;
+import android.Manifest;
+
+/** Various tests for source-level checks */
+final class ApiSourceCheck extends LinearLayout {
+ public ApiSourceCheck(android.content.Context context) {
+ super(context);
+ }
+
+ /**
+ * Return only the state bits of {@link #getMeasuredWidthAndState()} and
+ * {@link #getMeasuredHeightAndState()}, combined into one integer. The
+ * width component is in the regular bits {@link #MEASURED_STATE_MASK} and
+ * the height component is at the shifted bits
+ * {@link #MEASURED_HEIGHT_STATE_SHIFT}>>{@link #MEASURED_STATE_MASK}.
+ */
+ public static int m1(View child) {
+ // from static import of field
+ int x = MEASURED_STATE_MASK;
+
+ // fully qualified name field access
+ int y = android.view.View.MEASURED_STATE_MASK;
+
+ // from explicitly imported class
+ int z = View.MEASURED_STATE_MASK;
+ int find2 = View.FIND_VIEWS_WITH_TEXT; // requires API 14
+
+ // from wildcard import of package
+ int w = ActivityManager.MOVE_TASK_NO_USER_ACTION;
+ int find1 = ZoomButton.FIND_VIEWS_WITH_CONTENT_DESCRIPTION; // requires
+ // API 14
+ // from static wildcard import
+ int overScroll = OVER_SCROLL_ALWAYS; // requires API 9
+
+ // Inherited field from ancestor class (View)
+ int auto = IMPORTANT_FOR_ACCESSIBILITY_AUTO; // requires API 16
+
+ // object field reference: ensure that we don't get two errors
+ // (one from source scan, the other from class scan)
+ Object rotationX = ZoomButton.ROTATION_X; // Requires API 14
+
+ // different type of expression than variable declaration
+ return (child.getMeasuredWidth() & View.MEASURED_STATE_MASK)
+ | ((child.getMeasuredHeight() >> View.MEASURED_HEIGHT_STATE_SHIFT) & (View.MEASURED_STATE_MASK >> View.MEASURED_HEIGHT_STATE_SHIFT));
+ }
+
+ @SuppressLint("NewApi")
+ private void testSuppress1() {
+ // Checks suppress on surrounding method
+ int w = ActivityManager.MOVE_TASK_NO_USER_ACTION;
+ }
+
+ private void testSuppress2() {
+ // Checks suppress on surrounding declaration statement
+ @SuppressLint("NewApi")
+ int w, z = ActivityManager.MOVE_TASK_NO_USER_ACTION;
+ }
+
+ @TargetApi(17)
+ private void testTargetApi1() {
+ // Checks @TargetApi on surrounding method
+ int w, z = ActivityManager.MOVE_TASK_NO_USER_ACTION;
+ }
+
+ @TargetApi(android.os.Build.VERSION_CODES.JELLY_BEAN_MR1)
+ private void testTargetApi2() {
+ // Checks @TargetApi with codename
+ int w, z = ActivityManager.MOVE_TASK_NO_USER_ACTION;
+ }
+
+ @TargetApi(JELLY_BEAN_MR1)
+ private void testTargetApi3() {
+ // Checks @TargetApi with codename
+ int w, z = ActivityManager.MOVE_TASK_NO_USER_ACTION;
+ }
+
+ private void checkOtherFields() {
+ // Look at fields that aren't capitalized
+ int custom = android.R.id.custom; // API 8
+ }
+
+ private void innerclass() {
+ String setPointerSpeed = permission.SET_POINTER_SPEED;
+ String setPointerSpeed2 = Manifest.permission.SET_POINTER_SPEED;
+ }
+
+ private void test() {
+ // Make sure that local variable references which look like fields,
+ // even imported ones, aren't taken as invalid references
+ int OVER_SCROLL_ALWAYS = 1, IMPORTANT_FOR_ACCESSIBILITY_AUTO = 2;
+ int x = OVER_SCROLL_ALWAYS;
+ int y = IMPORTANT_FOR_ACCESSIBILITY_AUTO;
+ findViewById(IMPORTANT_FOR_ACCESSIBILITY_AUTO); // yes, nonsensical
+ }
+
+ private void testBenignUsages(int x) {
+ // Certain types of usages (such as switch/case constants) are okay
+ switch (x) {
+ case View.MEASURED_STATE_MASK: { // OK
+ break;
+ }
+ }
+ if (x == View.MEASURED_STATE_MASK) { // OK
+ }
+ if (false || x == View.MEASURED_STATE_MASK) { // OK
+ }
+ if (x >= View.MEASURED_STATE_MASK) { // OK
+ }
+ int y = View.MEASURED_STATE_MASK; // Not OK
+ testBenignUsages(View.MEASURED_STATE_MASK); // Not OK
+ }
+}
diff --git a/lint/cli/src/test/java/com/android/tools/lint/checks/data/src/test/pkg/WrongAnnotation.java.txt b/lint/cli/src/test/java/com/android/tools/lint/checks/data/src/test/pkg/WrongAnnotation.java.txt
index 6fef833..45743ce 100644
--- a/lint/cli/src/test/java/com/android/tools/lint/checks/data/src/test/pkg/WrongAnnotation.java.txt
+++ b/lint/cli/src/test/java/com/android/tools/lint/checks/data/src/test/pkg/WrongAnnotation.java.txt
@@ -28,4 +28,9 @@
@SuppressLint("NewApi")
int localvar = 5;
}
+
+ private static void test() {
+ @SuppressLint("NewApi") // Invalid
+ int a = View.MEASURED_STATE_MASK;
+ }
}
diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/JavaVisitor.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/JavaVisitor.java
index b74693a..81a0339 100644
--- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/JavaVisitor.java
+++ b/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/JavaVisitor.java
@@ -284,6 +284,9 @@
private class DispatchVisitor extends AstVisitor {
@Override
public void endVisit(Node node) {
+ for (VisitingDetector v : mAllDetectors) {
+ v.getVisitor().endVisit(node);
+ }
}
@Override
diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/ClassContext.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/ClassContext.java
index 800e969..161f088 100644
--- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/ClassContext.java
+++ b/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/ClassContext.java
@@ -29,6 +29,7 @@
import com.android.tools.lint.detector.api.Location.SearchDirection;
import com.android.tools.lint.detector.api.Location.SearchHints;
import com.google.common.annotations.Beta;
+import com.google.common.base.Splitter;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
@@ -658,10 +659,13 @@
*/
@NonNull
public static String getInternalName(@NonNull String fqcn) {
- String[] parts = fqcn.split("\\."); //$NON-NLS-1$
+ if (fqcn.indexOf('.') == -1) {
+ return fqcn;
+ }
+
StringBuilder sb = new StringBuilder(fqcn.length());
String prev = null;
- for (String part : parts) {
+ for (String part : Splitter.on('.').split(fqcn)) {
if (prev != null) {
if (Character.isUpperCase(prev.charAt(0))) {
sb.append('$');
diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/AnnotationDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/AnnotationDetector.java
index 2d2a2ef..f091269 100644
--- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/AnnotationDetector.java
+++ b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/AnnotationDetector.java
@@ -47,10 +47,12 @@
import lombok.ast.MethodDeclaration;
import lombok.ast.Modifiers;
import lombok.ast.Node;
+import lombok.ast.Select;
import lombok.ast.StrictListAccessor;
import lombok.ast.StringLiteral;
import lombok.ast.TypeBody;
import lombok.ast.VariableDefinition;
+import lombok.ast.VariableDefinitionEntry;
/**
* Checks annotations to make sure they are valid
@@ -157,7 +159,11 @@
private boolean checkId(Annotation node, String id) {
IssueRegistry registry = mContext.getDriver().getRegistry();
Issue issue = registry.getIssue(id);
- if (issue != null && !issue.getScope().contains(Scope.JAVA_FILE)) {
+ // Special-case the ApiDetector issue, since it does both source file analysis
+ // only on field references, and class file analysis on the rest, so we allow
+ // annotations outside of methods only on fields
+ if (issue != null && !issue.getScope().contains(Scope.JAVA_FILE)
+ || issue == ApiDetector.UNSUPPORTED) {
// Ensure that this isn't a field
Node parent = node.getParent();
while (parent != null) {
@@ -167,6 +173,15 @@
break;
} else if (parent instanceof TypeBody) { // It's a field
return true;
+ } else if (issue == ApiDetector.UNSUPPORTED
+ && parent instanceof VariableDefinition) {
+ VariableDefinition definition = (VariableDefinition) parent;
+ for (VariableDefinitionEntry entry : definition.astVariables()) {
+ Expression initializer = entry.astInitializer();
+ if (initializer instanceof Select) {
+ return true;
+ }
+ }
}
parent = parent.getParent();
if (parent == null) {
diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ApiDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ApiDetector.java
index d2a7844..2c0d773 100644
--- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ApiDetector.java
+++ b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ApiDetector.java
@@ -21,9 +21,12 @@
import static com.android.SdkConstants.ATTR_CLASS;
import static com.android.SdkConstants.ATTR_TARGET_API;
import static com.android.SdkConstants.CONSTRUCTOR_NAME;
+import static com.android.SdkConstants.R_CLASS;
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.lint.detector.api.ClassContext.getFqcn;
+import static com.android.tools.lint.detector.api.ClassContext.getInternalName;
import static com.android.tools.lint.detector.api.LintUtils.getNextInstruction;
import static com.android.tools.lint.detector.api.Location.SearchDirection.BACKWARD;
import static com.android.tools.lint.detector.api.Location.SearchDirection.FORWARD;
@@ -31,21 +34,28 @@
import com.android.SdkConstants;
import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
import com.android.resources.ResourceFolderType;
import com.android.tools.lint.client.api.LintDriver;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.ClassContext;
import com.android.tools.lint.detector.api.Context;
+import com.android.tools.lint.detector.api.DefaultPosition;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Issue;
+import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Location.SearchHints;
+import com.android.tools.lint.detector.api.Position;
import com.android.tools.lint.detector.api.ResourceXmlDetector;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.detector.api.XmlContext;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
@@ -65,15 +75,47 @@
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
+import java.util.Iterator;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import lombok.ast.Annotation;
+import lombok.ast.AnnotationElement;
+import lombok.ast.AnnotationValue;
+import lombok.ast.AstVisitor;
+import lombok.ast.BinaryExpression;
+import lombok.ast.Case;
+import lombok.ast.ClassDeclaration;
+import lombok.ast.ConstructorDeclaration;
+import lombok.ast.ConstructorInvocation;
+import lombok.ast.Expression;
+import lombok.ast.ForwardingAstVisitor;
+import lombok.ast.If;
+import lombok.ast.ImportDeclaration;
+import lombok.ast.IntegralLiteral;
+import lombok.ast.MethodDeclaration;
+import lombok.ast.MethodInvocation;
+import lombok.ast.Modifiers;
+import lombok.ast.Select;
+import lombok.ast.StrictListAccessor;
+import lombok.ast.StringLiteral;
+import lombok.ast.SuperConstructorInvocation;
+import lombok.ast.Switch;
+import lombok.ast.TypeReference;
+import lombok.ast.VariableDefinition;
+import lombok.ast.VariableDefinitionEntry;
+import lombok.ast.VariableReference;
/**
* Looks for usages of APIs that are not supported in all the versions targeted
* by this application (according to its minimum API requirement in the manifest).
*/
-public class ApiDetector extends ResourceXmlDetector implements Detector.ClassScanner {
+public class ApiDetector extends ResourceXmlDetector
+ implements Detector.ClassScanner, Detector.JavaScanner {
/**
* 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
@@ -105,14 +147,24 @@
"file's minimum SDK as the required API level.\n" +
"\n" +
"Similarly, you can use tools:targetApi=\"11\" in an XML file to indicate that " +
- "the element will only be inflated in an adequate context.",
+ "the element will only be inflated in an adequate context.\n" +
+ "\n" +
+ "Lint will also flag certain constants, such as static final integers, " +
+ "which were introduced in later versions. These will actually be copied " +
+ "into the class files rather than being referenced, which means that " +
+ "the value is available even when running on older devices. In some " +
+ "cases that's fine, and in other cases it can result in a runtime " +
+ "crash or incorrect behavior. It depends on the context, so consider " +
+ "the code carefully and device whether it's safe and can be suppressed " +
+ "or whether the code needs to be guarded.",
Category.CORRECTNESS,
6,
Severity.ERROR,
ApiDetector.class,
- EnumSet.of(Scope.CLASS_FILE, Scope.RESOURCE_FILE, Scope.MANIFEST))
+ EnumSet.of(Scope.CLASS_FILE, Scope.RESOURCE_FILE, Scope.MANIFEST, Scope.JAVA_FILE))
.addAnalysisScope(Scope.RESOURCE_FILE_SCOPE)
- .addAnalysisScope(Scope.CLASS_FILE_SCOPE);
+ .addAnalysisScope(Scope.CLASS_FILE_SCOPE)
+ .addAnalysisScope(Scope.JAVA_FILE_SCOPE);
/** Accessing an unsupported API */
public static final Issue OVERRIDE = Issue.create("Override", //$NON-NLS-1$
@@ -146,6 +198,7 @@
private ApiLookup mApiDatabase;
private int mMinApi = -1;
+ private Set<String> mWarnedFields;
/** Constructs a new API check */
public ApiDetector() {
@@ -547,11 +600,13 @@
continue;
}
String fqcn = ClassContext.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());
+ if (mWarnedFields == null || !mWarnedFields.contains(fqcn)) {
+ 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;
@@ -845,7 +900,7 @@
}
}
- for (int api = 1; api < SdkConstants.HIGHEST_KNOWN_API; api++) {
+ for (int api = 1; api <= SdkConstants.HIGHEST_KNOWN_API; api++) {
String code = LintUtils.getBuildCode(api);
if (code != null && code.equalsIgnoreCase(targetApi)) {
return api;
@@ -893,4 +948,490 @@
hints);
context.report(UNSUPPORTED, method, node, location, message, null);
}
+
+ // ---- Implements JavaScanner ----
+
+ @Nullable
+ @Override
+ public AstVisitor createJavaVisitor(@NonNull JavaContext context) {
+ return new ApiVisitor(context);
+ }
+
+ @Nullable
+ @Override
+ public List<Class<? extends lombok.ast.Node>> getApplicableNodeTypes() {
+ List<Class<? extends lombok.ast.Node>> types =
+ new ArrayList<Class<? extends lombok.ast.Node>>(2);
+ types.add(ImportDeclaration.class);
+ types.add(Select.class);
+ types.add(MethodDeclaration.class);
+ types.add(ConstructorDeclaration.class);
+ types.add(VariableDefinitionEntry.class);
+ types.add(VariableReference.class);
+ return types;
+ }
+
+ private final class ApiVisitor extends ForwardingAstVisitor {
+ private JavaContext mContext;
+ private Map<String, String> mClassToImport = Maps.newHashMap();
+ private List<String> mStarImports;
+ private Set<String> mLocalVars;
+ private lombok.ast.Node mCurrentMethod;
+ private Set<String> mFields;
+ private List<String> mStaticStarImports;
+
+ private ApiVisitor(JavaContext context) {
+ mContext = context;
+ }
+
+ @Override
+ public boolean visitImportDeclaration(ImportDeclaration node) {
+ if (node.astStarImport()) {
+ // Similarly, if you're inheriting from a constants class, figure out
+ // how that works... :=(
+ String fqcn = node.asFullyQualifiedName();
+ int strip = fqcn.lastIndexOf('*');
+ if (strip != -1) {
+ strip = fqcn.lastIndexOf('.', strip);
+ if (strip != -1) {
+ String pkgName = getInternalName(fqcn.substring(0, strip));
+ if (ApiLookup.isRelevantOwner(pkgName)) {
+ if (node.astStaticImport()) {
+ if (mStaticStarImports == null) {
+ mStaticStarImports = Lists.newArrayList();
+ }
+ mStaticStarImports.add(pkgName);
+ } else {
+ if (mStarImports == null) {
+ mStarImports = Lists.newArrayList();
+ }
+ mStarImports.add(pkgName);
+ }
+ }
+ }
+ }
+ } else if (node.astStaticImport()) {
+ String fqcn = node.asFullyQualifiedName();
+ String fieldName = getInternalName(fqcn);
+ int index = fieldName.lastIndexOf('$');
+ if (index != -1) {
+ String owner = fieldName.substring(0, index);
+ String name = fieldName.substring(index + 1);
+ checkField(node, name, owner);
+ }
+ } else {
+ // Store in map -- if it's "one of ours"
+ // Use override detector's map for that purpose
+ String fqcn = node.asFullyQualifiedName();
+
+ int last = fqcn.lastIndexOf('.');
+ if (last != -1) {
+ String className = fqcn.substring(last + 1);
+ mClassToImport.put(className, fqcn);
+ }
+ }
+
+ return super.visitImportDeclaration(node);
+ }
+
+ @Override
+ public boolean visitSelect(Select node) {
+ boolean result = super.visitSelect(node);
+
+ if (node.getParent() instanceof Select) {
+ // We only want to look at the leaf expressions; e.g. if you have
+ // "foo.bar.baz" we only care about the select foo.bar.baz, not foo.bar
+ return result;
+ }
+
+ // See if this corresponds to a field reference. We assume it's a field if
+ // it's a select (x.y) and either the identifier y is capitalized (e.g.
+ // foo.VIEW_MASK) or if it's a member of an R class (R.id.foo).
+ String name = node.astIdentifier().astValue();
+ boolean isField = Character.isUpperCase(name.charAt(0));
+ if (!isField) {
+ // See if there's an R class
+ Select current = node;
+ while (current != null) {
+ Expression operand = current.astOperand();
+ if (operand instanceof Select) {
+ current = (Select) operand;
+ if (R_CLASS.equals(current.astIdentifier().astValue())) {
+ isField = true;
+ break;
+ }
+ } else if (operand instanceof VariableReference) {
+ VariableReference reference = (VariableReference) operand;
+ if (R_CLASS.equals(reference.astIdentifier().astValue())) {
+ isField = true;
+ }
+ break;
+ } else {
+ break;
+ }
+ }
+ }
+
+ if (isField) {
+ Expression operand = node.astOperand();
+ if (operand.getClass() == Select.class) {
+ // Possibly a fully qualified name in place
+ String cls = operand.toString();
+
+ // See if it's an imported class with an inner class
+ // (e.g. Manifest.permission.FIELD)
+ if (Character.isUpperCase(cls.charAt(0))) {
+ int firstDot = cls.indexOf('.');
+ if (firstDot != -1) {
+ String base = cls.substring(0, firstDot);
+ String fqcn = mClassToImport.get(base);
+ if (fqcn != null) {
+ // Yes imported
+ String owner = getInternalName(fqcn + cls.substring(firstDot));
+ checkField(node, name, owner);
+ return result;
+ }
+
+ // Might be a star import: have to iterate and check here
+ if (mStarImports != null) {
+ for (String packagePrefix : mStarImports) {
+ String owner = getInternalName(packagePrefix + '/' + cls);
+ if (checkField(node, name, owner)) {
+ mClassToImport.put(name, owner);
+ return result;
+ }
+ }
+ }
+ }
+ }
+
+ // See if it's a fully qualified reference in place
+ String owner = getInternalName(cls);
+ checkField(node, name, owner);
+ return result;
+ } else if (operand.getClass() == VariableReference.class) {
+ String className = ((VariableReference) operand).astIdentifier().astValue();
+ // Not a FQCN that we care about: look in imports
+ String fqcn = mClassToImport.get(className);
+ if (fqcn != null) {
+ // Yes imported
+ String owner = getInternalName(fqcn);
+ checkField(node, name, owner);
+ return result;
+ }
+
+ if (Character.isUpperCase(className.charAt(0))) {
+ // Might be a star import: have to iterate and check here
+ if (mStarImports != null) {
+ for (String packagePrefix : mStarImports) {
+ String owner = getInternalName(packagePrefix) + '/' + className;
+ if (checkField(node, name, owner)) {
+ mClassToImport.put(name, owner);
+ return result;
+ }
+ }
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public boolean visitVariableReference(VariableReference node) {
+ boolean result = super.visitVariableReference(node);
+
+ if (node.getParent() != null) {
+ lombok.ast.Node parent = node.getParent();
+ Class<? extends lombok.ast.Node> parentClass = parent.getClass();
+ if (parentClass == Select.class
+ || parentClass == Switch.class // look up on the switch expression type
+ || parentClass == Case.class
+ || parentClass == ConstructorInvocation.class
+ || parentClass == SuperConstructorInvocation.class
+ || parentClass == AnnotationElement.class) {
+ return result;
+ }
+
+ if (parent instanceof MethodInvocation &&
+ ((MethodInvocation) parent).astOperand() == node) {
+ return result;
+ } else if (parent instanceof BinaryExpression) {
+ BinaryExpression expression = (BinaryExpression) parent;
+ if (expression.astLeft() == node) {
+ return result;
+ }
+ }
+ }
+
+ String name = node.astIdentifier().astValue();
+ if (Character.isUpperCase(name.charAt(0))
+ && (mLocalVars == null || !mLocalVars.contains(name))
+ && (mFields == null || !mFields.contains(name))) {
+ // Potential field reference: check it
+ if (mStaticStarImports != null) {
+ for (String owner : mStaticStarImports) {
+ if (checkField(node, name, owner)) {
+ break;
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+
+ @Override
+ public boolean visitVariableDefinitionEntry(VariableDefinitionEntry node) {
+ if (mCurrentMethod != null) {
+ if (mLocalVars == null) {
+ mLocalVars = Sets.newHashSet();
+ }
+ mLocalVars.add(node.astName().astValue());
+ } else {
+ if (mFields == null) {
+ mFields = Sets.newHashSet();
+ }
+ mFields.add(node.astName().astValue());
+ }
+ return super.visitVariableDefinitionEntry(node);
+ }
+
+ @Override
+ public boolean visitMethodDeclaration(MethodDeclaration node) {
+ mLocalVars = null;
+ mCurrentMethod = node;
+ return super.visitMethodDeclaration(node);
+ }
+
+ @Override
+ public boolean visitConstructorDeclaration(ConstructorDeclaration node) {
+ mLocalVars = null;
+ mCurrentMethod = node;
+ return super.visitConstructorDeclaration(node);
+ }
+
+ @Override
+ public void endVisit(lombok.ast.Node node) {
+ if (node == mCurrentMethod) {
+ mCurrentMethod = null;
+ }
+ super.endVisit(node);
+ }
+
+ /**
+ * 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 lombok.ast.Node node,
+ @NonNull String name,
+ @NonNull String owner) {
+ int api = mApiDatabase.getFieldVersion(owner, name);
+ if (api != -1) {
+ int minSdk = getMinSdk(mContext);
+ if (api > minSdk
+ && api > getLocalMinSdk(node)) {
+ if (isBenignConstantUsage(node, name, owner)) {
+ return true;
+ }
+
+ Location location = mContext.getLocation(node);
+ String fqcn = getFqcn(owner) + '#' + name;
+
+ if (node instanceof ImportDeclaration) {
+ // Replace import statement location range with just
+ // the identifier part
+ ImportDeclaration d = (ImportDeclaration) node;
+ int startOffset = d.astParts().first().getPosition().getStart();
+ Position start = location.getStart();
+ int startColumn = start.getColumn();
+ int startLine = start.getLine();
+ start = new DefaultPosition(startLine,
+ startColumn + startOffset - start.getOffset(), startOffset);
+ int fqcnLength = fqcn.length();
+ Position end = new DefaultPosition(startLine,
+ start.getColumn() + fqcnLength,
+ start.getOffset() + fqcnLength);
+ location = Location.create(location.getFile(), start, end);
+ }
+
+ String message = String.format(
+ "Field requires API level %1$d (current min is %2$d): %3$s",
+ api, minSdk, fqcn);
+ mContext.report(UNSUPPORTED, node, location, message, null);
+
+ // Record this field as already reported such that when we scan the
+ // class files later, we don't report the same error again.
+ // (This happens when a field isn't a final primitive value which
+ // gets copied into the .class file)
+ if (mWarnedFields == null) {
+ mWarnedFields = Sets.newHashSet();
+ }
+ mWarnedFields.add(fqcn);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks whether the given instruction is a benign usage of a constant defined
+ * in a later version of Android than the application's {@code minSdkVersion}.
+ *
+ * @param node the instruction to check
+ * @return true if the given usage is safe on older versions than the introduction
+ * level of the constant
+ */
+ public boolean isBenignConstantUsage(
+ @NonNull lombok.ast.Node node,
+ @NonNull String name,
+ @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
+ return true;
+ }
+ if (owner.equals("android/view/ViewGroup$LayoutParams") //$NON-NLS-1$
+ && name.equals("MATCH_PARENT")) { //$NON-NLS-1$
+ return true;
+ }
+
+ // It's okay to reference the constant as a case constant (since that
+ // code path won't be taken) or in a condition of an if statement
+ lombok.ast.Node curr = node.getParent();
+ boolean usedInExpression = false;
+ while (curr != null) {
+ Class<? extends lombok.ast.Node> nodeType = curr.getClass();
+ if (nodeType == Case.class) {
+ return true;
+ } else if (nodeType == BinaryExpression.class) {
+ usedInExpression = true;
+ } else if (nodeType == If.class) {
+ return usedInExpression;
+ }
+ curr = curr.getParent();
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the minimum SDK to use according to the given AST node, or null
+ * if no {@code TargetApi} annotations were found
+ *
+ * @return the API level to use for this node, or -1
+ */
+ public int getLocalMinSdk(@Nullable lombok.ast.Node scope) {
+ while (scope != null) {
+ Class<? extends lombok.ast.Node> type = scope.getClass();
+ // The Lombok AST uses a flat hierarchy of node type implementation classes
+ // so no need to do instanceof stuff here.
+ if (type == VariableDefinition.class) {
+ // Variable
+ VariableDefinition declaration = (VariableDefinition) scope;
+ int targetApi = getLocalMinSdk(declaration.astModifiers());
+ if (targetApi != -1) {
+ return targetApi;
+ }
+ } else if (type == MethodDeclaration.class) {
+ // Method
+ // Look for annotations on the method
+ MethodDeclaration declaration = (MethodDeclaration) scope;
+ int targetApi = getLocalMinSdk(declaration.astModifiers());
+ if (targetApi != -1) {
+ return targetApi;
+ }
+ } else if (type == ConstructorDeclaration.class) {
+ // Constructor
+ // Look for annotations on the method
+ ConstructorDeclaration declaration = (ConstructorDeclaration) scope;
+ int targetApi = getLocalMinSdk(declaration.astModifiers());
+ if (targetApi != -1) {
+ return targetApi;
+ }
+ } else if (type == ClassDeclaration.class) {
+ // Class
+ ClassDeclaration declaration = (ClassDeclaration) scope;
+ int targetApi = getLocalMinSdk(declaration.astModifiers());
+ if (targetApi != -1) {
+ return targetApi;
+ }
+ }
+
+ scope = scope.getParent();
+ }
+
+ return -1;
+ }
+
+ /**
+ * Returns true if the given AST modifier has a suppress annotation for the
+ * given issue (which can be null to check for the "all" annotation)
+ *
+ * @param modifiers the modifier to check
+ * @return true if the issue or all issues should be suppressed for this
+ * modifier
+ */
+ private int getLocalMinSdk(@Nullable Modifiers modifiers) {
+ if (modifiers == null) {
+ return -1;
+ }
+ StrictListAccessor<Annotation, Modifiers> annotations = modifiers.astAnnotations();
+ if (annotations == null) {
+ return -1;
+ }
+
+ Iterator<Annotation> iterator = annotations.iterator();
+ while (iterator.hasNext()) {
+ Annotation annotation = iterator.next();
+ TypeReference t = annotation.astAnnotationTypeReference();
+ String typeName = t.getTypeName();
+ if (typeName.endsWith(TARGET_API)) {
+ StrictListAccessor<AnnotationElement, Annotation> values =
+ annotation.astElements();
+ if (values != null) {
+ Iterator<AnnotationElement> valueIterator = values.iterator();
+ while (valueIterator.hasNext()) {
+ AnnotationElement element = valueIterator.next();
+ AnnotationValue valueNode = element.astValue();
+ if (valueNode == null) {
+ continue;
+ }
+ if (valueNode instanceof IntegralLiteral) {
+ IntegralLiteral literal = (IntegralLiteral) valueNode;
+ return literal.astIntValue();
+ } else if (valueNode instanceof StringLiteral) {
+ String value = ((StringLiteral) valueNode).astValue();
+ return codeNameToApi(value);
+ } else if (valueNode instanceof Select) {
+ Select select = (Select) valueNode;
+ String codename = select.astIdentifier().astValue();
+ return codeNameToApi(codename);
+ } else if (valueNode instanceof VariableReference) {
+ VariableReference reference = (VariableReference) valueNode;
+ String codename = reference.astIdentifier().astValue();
+ return codeNameToApi(codename);
+ }
+ }
+ }
+ }
+ }
+
+ return -1;
+ }
+ }
+
+ private static int codeNameToApi(String codename) {
+ for (int api = 1; api <= SdkConstants.HIGHEST_KNOWN_API; api++) {
+ String code = LintUtils.getBuildCode(api);
+ if (code != null && code.equalsIgnoreCase(codename)) {
+ return api;
+ }
+ }
+
+ return -1;
+ }
}
diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ApiLookup.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ApiLookup.java
index a056822..6a4de6e 100644
--- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ApiLookup.java
+++ b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ApiLookup.java
@@ -16,6 +16,7 @@
package com.android.tools.lint.checks;
+import static com.android.SdkConstants.ANDROID_PKG;
import static com.android.SdkConstants.DOT_XML;
import com.android.annotations.NonNull;
@@ -341,7 +342,7 @@
System.out.println("\nRead API database in " + (end - start)
+ " milliseconds.");
System.out.println("Size of data table: " + mData.length + " bytes ("
- + Integer.toString(mData.length/1024) + "k)\n");
+ + Integer.toString(mData.length / 1024) + "k)\n");
}
}
@@ -372,6 +373,11 @@
javaPackageSet.add(pkg);
}
+ if (!isRelevantOwner(className)) {
+ System.out.println("Warning: The isRelevantOwner method does not pass "
+ + className);
+ }
+
Set<String> allMethods = apiClass.getAllMethods(info);
Set<String> allFields = apiClass.getAllFields(info);
@@ -755,6 +761,44 @@
}
/**
+ * Returns true if the given owner (in VM format) is relevant to the database.
+ * This allows quick filtering out of owners that won't return any data
+ * for the various {@code #getFieldVersion} etc methods.
+ *
+ * @param owner the owner to look up
+ * @return true if the owner might be relevant to the API database
+ */
+ public static boolean isRelevantOwner(@NonNull String owner) {
+ if (owner.startsWith("java")) { //$NON-NLS-1$ // includes javax/
+ return true;
+ }
+ if (owner.startsWith(ANDROID_PKG)) {
+ if (owner.startsWith("/support/", 7)) { //$NON-NLS-1$
+ return false;
+ }
+ return true;
+ } else if (owner.startsWith("org/")) { //$NON-NLS-1$
+ if (owner.startsWith("xml", 4) //$NON-NLS-1$
+ || owner.startsWith("w3c/", 4) //$NON-NLS-1$
+ || owner.startsWith("json/", 4) //$NON-NLS-1$
+ || owner.startsWith("apache/", 4)) { //$NON-NLS-1$
+ return true;
+ }
+ } else if (owner.startsWith("com/")) { //$NON-NLS-1$
+ if (owner.startsWith("google/", 4) //$NON-NLS-1$
+ || owner.startsWith("android/", 4)) { //$NON-NLS-1$
+ return true;
+ }
+ } else if (owner.startsWith("junit") //$NON-NLS-1$
+ || owner.startsWith("dalvik")) { //$NON-NLS-1$
+ return true;
+ }
+
+ return false;
+ }
+
+
+ /**
* Returns true if the given owner (in VM format) is a valid Java package supported
* in any version of Android.
*