Merge "Make the "will this code run on API n" analysis smarter" into studio-1.4-dev
automerge: 12a791c

* commit '12a791c1f5b4f749127d14d356bfaf65ece9b683':
  Make the "will this code run on API n" analysis smarter
diff --git a/android/src/org/jetbrains/android/inspections/lint/IntellijApiDetector.java b/android/src/org/jetbrains/android/inspections/lint/IntellijApiDetector.java
index a8800ec..d0f4365 100755
--- a/android/src/org/jetbrains/android/inspections/lint/IntellijApiDetector.java
+++ b/android/src/org/jetbrains/android/inspections/lint/IntellijApiDetector.java
@@ -21,7 +21,6 @@
 import com.android.tools.lint.checks.ApiDetector;
 import com.android.tools.lint.checks.ApiLookup;
 import com.android.tools.lint.detector.api.*;
-import com.intellij.codeInsight.ExceptionUtil;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.psi.*;
 import com.intellij.psi.impl.source.PsiClassReferenceType;
@@ -314,6 +313,9 @@
           if (isWithinVersionCheckConditional(element, api)) {
             continue;
           }
+          if (isPrecededByVersionCheckExit(element, api)) {
+            continue;
+          }
           location = IntellijLintUtils.getLocation(myContext.file, element);
         } else {
           location = IntellijLintUtils.getLocation(myContext.file, aClass);
@@ -486,6 +488,9 @@
       if (isWithinVersionCheckConditional(element, api)) {
         return true;
       }
+      if (isPrecededByVersionCheckExit(element, api)) {
+        return true;
+      }
 
       return false;
     }
@@ -678,83 +683,89 @@
     }
   }
 
+  private static boolean isPrecededByVersionCheckExit(PsiElement element, int api) {
+    PsiElement current = PsiTreeUtil.getParentOfType(element, PsiStatement.class);
+    if (current != null) {
+      PsiElement prev = getPreviousStatement(current);
+      if (prev == null) {
+        //noinspection unchecked
+        current = PsiTreeUtil.getParentOfType(current, PsiStatement.class, true, PsiMethod.class, PsiClass.class);
+      } else {
+        current = prev;
+      }
+    }
+    while (current != null) {
+      if (current instanceof PsiIfStatement) {
+        PsiIfStatement ifStatement = (PsiIfStatement)current;
+        PsiStatement thenBranch = ifStatement.getThenBranch();
+        PsiStatement elseBranch = ifStatement.getElseBranch();
+        if (thenBranch != null) {
+          Boolean level = isVersionCheckConditional(api, thenBranch, ifStatement);
+          if (level != null) {
+            // See if the body does an immediate return
+            if (isUnconditionalReturn(thenBranch)) {
+              return true;
+            }
+          }
+        }
+        if (elseBranch != null) {
+          Boolean level = isVersionCheckConditional(api, elseBranch, ifStatement);
+          if (level != null) {
+            if (isUnconditionalReturn(elseBranch)) {
+              return true;
+            }
+          }
+        }
+      }
+      PsiElement prev = getPreviousStatement(current);
+      if (prev == null) {
+        //noinspection unchecked
+        current = PsiTreeUtil.getParentOfType(current, PsiStatement.class, true, PsiMethod.class, PsiClass.class);
+        if (current == null) {
+          return false;
+        }
+      } else {
+        current = prev;
+      }
+    }
+
+    return false;
+  }
+
+  private static boolean isUnconditionalReturn(PsiStatement statement) {
+    if (statement instanceof PsiBlockStatement) {
+      PsiBlockStatement blockStatement = (PsiBlockStatement)statement;
+      PsiCodeBlock block = blockStatement.getCodeBlock();
+      PsiStatement[] statements = block.getStatements();
+      if (statements.length == 1 && statements[0] instanceof PsiReturnStatement) {
+        return true;
+      }
+    }
+    if (statement instanceof PsiReturnStatement) {
+      return true;
+    }
+    return false;
+  }
+
+
+  @Nullable
+  public static PsiStatement getPreviousStatement(PsiElement element) {
+    final PsiElement prevStatement = PsiTreeUtil.skipSiblingsBackward(element, PsiWhiteSpace.class, PsiComment.class);
+    return prevStatement instanceof PsiStatement ? (PsiStatement)prevStatement : null;
+  }
+
   private static boolean isWithinVersionCheckConditional(PsiElement element, int api) {
     PsiElement current = element.getParent();
     PsiElement prev = element;
     while (current != null) {
       if (current instanceof PsiIfStatement) {
         PsiIfStatement ifStatement = (PsiIfStatement)current;
-        PsiExpression condition = ifStatement.getCondition();
-        if (condition != prev && condition instanceof PsiBinaryExpression) {
-          PsiBinaryExpression binary = (PsiBinaryExpression)condition;
-          IElementType tokenType = binary.getOperationTokenType();
-          if (tokenType == JavaTokenType.GT || tokenType == JavaTokenType.GE ||
-              tokenType == JavaTokenType.LE || tokenType == JavaTokenType.LT ||
-              tokenType == JavaTokenType.EQEQ) {
-            PsiExpression left = binary.getLOperand();
-            if (left instanceof PsiReferenceExpression) {
-              PsiReferenceExpression ref = (PsiReferenceExpression)left;
-              if (SDK_INT.equals(ref.getReferenceName())) {
-                PsiExpression right = binary.getROperand();
-                int level = -1;
-                if (right instanceof PsiReferenceExpression) {
-                  PsiReferenceExpression ref2 = (PsiReferenceExpression)right;
-                  String codeName = ref2.getReferenceName();
-                  if (codeName == null) {
-                    return false;
-                  }
-                  level = SdkVersionInfo.getApiByBuildCode(codeName, true);
-                } else if (right instanceof PsiLiteralExpression) {
-                  PsiLiteralExpression lit = (PsiLiteralExpression)right;
-                  Object value = lit.getValue();
-                  if (value instanceof Integer) {
-                    level = ((Integer)value).intValue();
-                  }
-                }
-                if (level != -1) {
-                  boolean fromThen = prev == ifStatement.getThenBranch();
-                  boolean fromElse = prev == ifStatement.getElseBranch();
-                  assert fromThen == !fromElse;
-                  if (tokenType == JavaTokenType.GE) {
-                    // if (SDK_INT >= ICE_CREAM_SANDWICH) { <call> } else { ... }
-                    return level >= api && fromThen;
-                  }
-                  else if (tokenType == JavaTokenType.GT) {
-                    // if (SDK_INT > ICE_CREAM_SANDWICH) { <call> } else { ... }
-                    return level >= api - 1 && fromThen;
-                  }
-                  else if (tokenType == JavaTokenType.LE) {
-                    // if (SDK_INT <= ICE_CREAM_SANDWICH) { ... } else { <call> }
-                    return level >= api - 1 && fromElse;
-                  }
-                  else if (tokenType == JavaTokenType.LT) {
-                    // if (SDK_INT < ICE_CREAM_SANDWICH) { ... } else { <call> }
-                    return level >= api && fromElse;
-                  }
-                  else if (tokenType == JavaTokenType.EQEQ) {
-                      // if (SDK_INT == ICE_CREAM_SANDWICH) { <call> } else {  }
-                      return level >= api && fromThen;
-                  } else {
-                    assert false : tokenType;
-                  }
-                }
-              }
-            }
-          } else if (tokenType == JavaTokenType.ANDAND && (prev == ifStatement.getThenBranch())) {
-            if (isAndedWithConditional(ifStatement.getCondition(), api, prev)) {
-              return true;
-            }
-          }
-        } else if (condition instanceof PsiPolyadicExpression) {
-          PsiPolyadicExpression ppe = (PsiPolyadicExpression)condition;
-          if (ppe.getOperationTokenType() == JavaTokenType.ANDAND && (prev == ifStatement.getThenBranch())) {
-            if (isAndedWithConditional(ppe, api, prev)) {
-              return true;
-            }
-          }
+        Boolean isConditional = isVersionCheckConditional(api, prev, ifStatement);
+        if (isConditional != null) {
+          return isConditional;
         }
       } else if (current instanceof PsiPolyadicExpression && isAndedWithConditional(current, api, prev)) {
-          return true;
+        return true;
       } else if (current instanceof PsiMethod || current instanceof PsiFile) {
         return false;
       }
@@ -765,6 +776,113 @@
     return false;
   }
 
+  @Nullable
+  private static Boolean isVersionCheckConditional(int api, PsiElement prev, PsiIfStatement ifStatement) {
+    PsiExpression condition = ifStatement.getCondition();
+    if (condition != prev && condition instanceof PsiBinaryExpression) {
+      Boolean isConditional = isVersionCheckConditional(api, prev, ifStatement, (PsiBinaryExpression)condition);
+      if (isConditional != null) {
+        return isConditional;
+      }
+    } else if (condition instanceof PsiPolyadicExpression) {
+      PsiPolyadicExpression ppe = (PsiPolyadicExpression)condition;
+      if (ppe.getOperationTokenType() == JavaTokenType.ANDAND && (prev == ifStatement.getThenBranch())) {
+        if (isAndedWithConditional(ppe, api, prev)) {
+          return true;
+        }
+      }
+    } else if (condition instanceof PsiMethodCallExpression) {
+      PsiMethodCallExpression call = (PsiMethodCallExpression) condition;
+      PsiMethod method = call.resolveMethod();
+      if (method != null) {
+        PsiCodeBlock body = method.getBody();
+        if (body != null) {
+          PsiStatement[] statements = body.getStatements();
+          if (statements.length == 1) {
+            PsiStatement statement = statements[0];
+            if (statement instanceof PsiReturnStatement) {
+              PsiReturnStatement returnStatement = (PsiReturnStatement) statement;
+              PsiExpression returnValue = returnStatement.getReturnValue();
+              if (returnValue instanceof PsiBinaryExpression) {
+                Boolean isConditional = isVersionCheckConditional(api, null, null, (PsiBinaryExpression)returnValue);
+                if (isConditional != null) {
+                  return isConditional;
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+    return null;
+  }
+
+  @Nullable
+  private static Boolean isVersionCheckConditional(int api,
+                                                   @Nullable PsiElement prev,
+                                                   @Nullable PsiIfStatement ifStatement,
+                                                   @NonNull PsiBinaryExpression binary) {
+    IElementType tokenType = binary.getOperationTokenType();
+    if (tokenType == JavaTokenType.GT || tokenType == JavaTokenType.GE ||
+        tokenType == JavaTokenType.LE || tokenType == JavaTokenType.LT ||
+        tokenType == JavaTokenType.EQEQ) {
+      PsiExpression left = binary.getLOperand();
+      if (left instanceof PsiReferenceExpression) {
+        PsiReferenceExpression ref = (PsiReferenceExpression)left;
+        if (SDK_INT.equals(ref.getReferenceName())) {
+          PsiExpression right = binary.getROperand();
+          int level = -1;
+          if (right instanceof PsiReferenceExpression) {
+            PsiReferenceExpression ref2 = (PsiReferenceExpression)right;
+            String codeName = ref2.getReferenceName();
+            if (codeName == null) {
+              return false;
+            }
+            level = SdkVersionInfo.getApiByBuildCode(codeName, true);
+          } else if (right instanceof PsiLiteralExpression) {
+            PsiLiteralExpression lit = (PsiLiteralExpression)right;
+            Object value = lit.getValue();
+            if (value instanceof Integer) {
+              level = ((Integer)value).intValue();
+            }
+          }
+          if (level != -1) {
+            boolean fromThen = ifStatement == null || prev == ifStatement.getThenBranch();
+            boolean fromElse = ifStatement != null && prev == ifStatement.getElseBranch();
+            assert fromThen == !fromElse;
+            if (tokenType == JavaTokenType.GE) {
+              // if (SDK_INT >= ICE_CREAM_SANDWICH) { <call> } else { ... }
+              return level >= api && fromThen;
+            }
+            else if (tokenType == JavaTokenType.GT) {
+              // if (SDK_INT > ICE_CREAM_SANDWICH) { <call> } else { ... }
+              return level >= api - 1 && fromThen;
+            }
+            else if (tokenType == JavaTokenType.LE) {
+              // if (SDK_INT <= ICE_CREAM_SANDWICH) { ... } else { <call> }
+              return level >= api - 1 && fromElse;
+            }
+            else if (tokenType == JavaTokenType.LT) {
+              // if (SDK_INT < ICE_CREAM_SANDWICH) { ... } else { <call> }
+              return level >= api && fromElse;
+            }
+            else if (tokenType == JavaTokenType.EQEQ) {
+                // if (SDK_INT == ICE_CREAM_SANDWICH) { <call> } else {  }
+                return level >= api && fromThen;
+            } else {
+              assert false : tokenType;
+            }
+          }
+        }
+      }
+    } else if (tokenType == JavaTokenType.ANDAND && (ifStatement != null && prev == ifStatement.getThenBranch())) {
+      if (isAndedWithConditional(ifStatement.getCondition(), api, prev)) {
+        return true;
+      }
+    }
+    return null;
+  }
+
   private static boolean isAndedWithConditional(PsiElement element, int api, @Nullable PsiElement before) {
     if (element instanceof PsiBinaryExpression) {
       PsiBinaryExpression inner = (PsiBinaryExpression)element;
diff --git a/android/testData/apiCheck/EarlyExit.java b/android/testData/apiCheck/EarlyExit.java
new file mode 100644
index 0000000..f57a572
--- /dev/null
+++ b/android/testData/apiCheck/EarlyExit.java
@@ -0,0 +1,55 @@
+package test.pkg;
+
+import android.os.Build;
+import android.widget.GridLayout;
+
+public class Class {
+    public void testEarlyExit1() {
+        // https://code.google.com/p/android/issues/detail?id=37728
+        if (Build.VERSION.SDK_INT < 11) return;
+
+        new GridLayout(null); // OK
+    }
+
+    public void testEarlyExit2() {
+        if (Utils.isLollipop()) {
+            return;
+        }
+
+        new GridLayout(null); // OK
+    }
+
+    public void testEarlyExit3(boolean nested) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            return;
+        }
+
+        if (nested) {
+            new GridLayout(null); // OK
+        }
+    }
+
+    public void testEarlyExit4(boolean nested) {
+        if (nested) {
+            if (Utils.isLollipop()) {
+                return;
+            }
+        }
+
+        <error descr="Call requires API level 14 (current min is 1): android.widget.GridLayout#GridLayout">new GridLayout(null)</error>; // ERROR
+
+        if (Utils.isLollipop()) { // too late
+            //noinspection UnnecessaryReturnStatement
+            return;
+        }
+    }
+
+    private static class Utils {
+        public static boolean isLollipop() {
+            return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
+        }
+        public static boolean isGingerbread() {
+            return Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD;
+        }
+    }
+}
diff --git a/android/testData/apiCheck/VersionUtility.java b/android/testData/apiCheck/VersionUtility.java
new file mode 100644
index 0000000..a8646c5
--- /dev/null
+++ b/android/testData/apiCheck/VersionUtility.java
@@ -0,0 +1,24 @@
+package test.pkg;
+
+import android.os.Build;
+import android.widget.GridLayout;
+
+public class Class {
+    public void checkApi() {
+        if (Utils.isLollipop()) {
+            new GridLayout(null); // OK
+        }
+        if (Utils.isGingerbread()) {
+            <error descr="Call requires API level 14 (current min is 1): android.widget.GridLayout#GridLayout">new GridLayout(null)</error>; // ERROR
+        }
+    }
+
+    private static class Utils {
+        public static boolean isLollipop() {
+            return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
+        }
+        public static boolean isGingerbread() {
+            return Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD;
+        }
+    }
+}
diff --git a/android/testSrc/org/jetbrains/android/inspections/lint/IntellijApiDetectorTest.java b/android/testSrc/org/jetbrains/android/inspections/lint/IntellijApiDetectorTest.java
index ad93c38..e4939d5 100644
--- a/android/testSrc/org/jetbrains/android/inspections/lint/IntellijApiDetectorTest.java
+++ b/android/testSrc/org/jetbrains/android/inspections/lint/IntellijApiDetectorTest.java
@@ -112,6 +112,22 @@
     doTest(inspection, null);
   }
 
+  public void testVersionUtility() throws Exception {
+    // Regression test for https://code.google.com/p/android/issues/detail?id=178686
+    // Makes sure the version conditional lookup peeks into surrounding method calls to see
+    // if they're providing version checks.
+    AndroidLintNewApiInspection inspection = new AndroidLintNewApiInspection();
+    doTest(inspection, null);
+  }
+
+  public void testEarlyExit() throws Exception {
+    // Regression test for https://code.google.com/p/android/issues/detail?id=37728
+    // Makes sure that if a method exits earlier in the method, we don't flag
+    // API checks after that
+    AndroidLintNewApiInspection inspection = new AndroidLintNewApiInspection();
+    doTest(inspection, null);
+  }
+
   public void testReflectiveOperationException() throws Exception {
     AndroidSdkData sdkData = AndroidSdkUtils.tryToChooseAndroidSdk();
     if (sdkData == null || !ConfigureAndroidModuleStep.isJdk7Supported(sdkData)) {