diff options
Diffstat (limited to 'lint')
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 Binary files differnew file mode 100644 index 0000000..726b8a5 --- /dev/null +++ b/lint/cli/src/test/java/com/android/tools/lint/checks/data/apicheck/ApiSourceCheck.class.data 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. * |