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)) {