aboutsummaryrefslogtreecommitdiffstats
path: root/lint
diff options
context:
space:
mode:
authorTor Norbye <tnorbye@google.com>2012-12-26 18:59:39 -0800
committerTor Norbye <tnorbye@google.com>2013-01-02 13:08:46 -0800
commit2d59e6a8e56b928ba22cb4e0315dabbaf8c53fad (patch)
tree4ace57ece47f668e6d3307f24bc98a294b06bddd /lint
parent0083b32f502408b4f3ad5d0be35ac5cf7d5234f7 (diff)
downloadsdk-2d59e6a8e56b928ba22cb4e0315dabbaf8c53fad.zip
sdk-2d59e6a8e56b928ba22cb4e0315dabbaf8c53fad.tar.gz
sdk-2d59e6a8e56b928ba22cb4e0315dabbaf8c53fad.tar.bz2
41753: Lint API Check: Consult source files for constants
Lint currently checks the .class files for field references and method references to APIs that require a version higher than the minSdkVersion in the manifest. However, constants (such as final static integers) will be copied into the .class file, so there is no reference to the original API and its API version, which means lint can't flag these. In some cases, such as referencing LayoutParams.MATCH_PARENT, that's no problem; the constant value works on older versions, and there is no problem, since the value rather than the reference is used. However, in other cases this may lead to runtime crashes. This CL updates lint to look at the source files and flag field references. It excludes some constants that are known to be okay (such as referencing say android.os.Build.VERSION_CODES.JELLY_BEAN_MR1 (which is typically done precisely to conditionally branch based on the current device's version). It also ignores usages as case constants or in conditional if checks. We may need to do additional tweaks to this in the future, since unlike method and field references there are probably many cases where referencing these constants are fine on older devices, but there are also cases where it's dangerous. For now, we're erring on the side of warning the developer rather than being certain that it's not a problem. Change-Id: Ieba4c15d7fdda02756b7b7f5a1b79f7698c1915b
Diffstat (limited to 'lint')
-rw-r--r--lint/cli/src/test/java/com/android/tools/lint/MainTest.java8
-rw-r--r--lint/cli/src/test/java/com/android/tools/lint/checks/ApiDetectorTest.java90
-rw-r--r--lint/cli/src/test/java/com/android/tools/lint/checks/data/apicheck/ApiSourceCheck.class.databin0 -> 2133 bytes
-rw-r--r--lint/cli/src/test/java/com/android/tools/lint/checks/data/apicheck/ApiSourceCheck.java.txt123
-rw-r--r--lint/cli/src/test/java/com/android/tools/lint/checks/data/src/test/pkg/WrongAnnotation.java.txt5
-rw-r--r--lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/JavaVisitor.java3
-rw-r--r--lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/ClassContext.java8
-rw-r--r--lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/AnnotationDetector.java17
-rw-r--r--lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ApiDetector.java561
-rw-r--r--lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ApiLookup.java46
10 files changed, 837 insertions, 24 deletions
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 @@ public class MainTest extends AbstractCheckTest {
"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 @@ public class ApiDetectorTest extends AbstractCheckTest {
" ~~~~~~~~~~~~~~~~~~~\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 @@ public class ApiDetectorTest extends AbstractCheckTest {
" ~~~~~~~~~~~~~~~~~~~\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 @@ public class ApiDetectorTest extends AbstractCheckTest {
" ~~~~~~~~~~~~~~~~~~~\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 @@ public class ApiDetectorTest extends AbstractCheckTest {
" ~~~~~~~~~~~~~~~~~~~\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 @@ public class ApiDetectorTest extends AbstractCheckTest {
" ~~~~~~~~~~~~~~~~~~~\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 @@ public class ApiDetectorTest extends AbstractCheckTest {
"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 @@ public class WrongAnnotation {
@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 @@ public class JavaVisitor {
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.client.api.LintDriver;
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 @@ public class ClassContext extends Context {
*/
@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.ForwardingAstVisitor;
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 @@ public class AnnotationDetector extends Detector implements Detector.JavaScanner
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 @@ public class AnnotationDetector extends Detector implements Detector.JavaScanner
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.ANDROID_THEME_PREFIX;
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 static com.android.tools.lint.detector.api.Location.SearchDirection.NEARE
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.Element;
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 @@ public class ApiDetector extends ResourceXmlDetector implements Detector.ClassSc
"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 @@ public class ApiDetector extends ResourceXmlDetector implements Detector.ClassSc
private ApiLookup mApiDatabase;
private int mMinApi = -1;
+ private Set<String> mWarnedFields;
/** Constructs a new API check */
public ApiDetector() {
@@ -547,11 +600,13 @@ public class ApiDetector extends ResourceXmlDetector implements Detector.ClassSc
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 @@ public class ApiDetector extends ResourceXmlDetector implements Detector.ClassSc
}
}
- 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 @@ public class ApiDetector extends ResourceXmlDetector implements Detector.ClassSc
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 @@ public class ApiLookup {
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 @@ public class ApiLookup {
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 @@ public class ApiLookup {
}
/**
+ * 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.
*