Fixed several bugs in Android lint ApiDetector (KT-15002, KT-12024, KT-14737, KT-14825, KT-12023, KT-15018)

Merged IntelliJApiDetector to ApiDetector

#KT-15002 Fixed
#KT-12024 Fixed
#KT-14737 Fixed
#KT-14825 Fixed
#KT-12023 Fixed
#KT-15018 Fixed

(cherry picked from commit e830e8b55fa2e354303a6c604b3905285a05b9db)
This commit is contained in:
Vyacheslav Gerasimov
2017-01-24 13:38:06 +03:00
parent d0f1b81bfa
commit 4fe040eeb9
4 changed files with 531 additions and 2052 deletions
+178 -18
View File
@@ -4,6 +4,7 @@
// INSPECTION_CLASS3: org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintOverrideInspection
import android.animation.RectEvaluator
import android.annotation.SuppressLint
import android.annotation.TargetApi
import org.w3c.dom.DOMError
import org.w3c.dom.DOMErrorHandler
@@ -14,6 +15,7 @@ import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
import android.app.Activity
import android.app.ApplicationErrorReport
import android.graphics.drawable.VectorDrawable
import android.graphics.Path
import android.graphics.PorterDuff
import android.graphics.Rect
@@ -22,7 +24,7 @@ import android.widget.*
import dalvik.bytecode.OpcodeInfo
import android.os.Build.VERSION
import android.os.Build.VERSION.SDK_INT
import <warning descr="Field requires API level 4 (current min is 1): `android.os.Build.VERSION#SDK_INT`">android.os.Build.VERSION.SDK_INT</warning>
import android.os.Build.VERSION_CODES
import android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH
import android.os.Build.VERSION_CODES.JELLY_BEAN
@@ -30,7 +32,7 @@ import android.os.Bundle
import android.system.ErrnoException
import android.widget.TextView
@Suppress("SENSELESS_COMPARISON", "UNUSED_EXPRESSION", "UsePropertyAccessSyntax", "UNUSED_VARIABLE", "unused", "UNUSED_PARAMETER", "DEPRECATION")
@Suppress("SENSELESS_COMPARISON", "UNUSED_EXPRESSION", "UsePropertyAccessSyntax", "UNUSED_VARIABLE", "unused", "UNUSED_PARAMETER", "DEPRECATION", "USELESS_CAST")
class ApiCallTest: Activity() {
fun method(chronometer: Chronometer, locator: DOMLocator) {
@@ -39,8 +41,7 @@ class ApiCallTest: Activity() {
// Ok
Bundle().getInt("")
// Ok, this constant is inlined
View.<warning descr="Field requires API level 16 (current min is 1): android.view.View#SYSTEM_UI_FLAG_FULLSCREEN">SYSTEM_UI_FLAG_FULLSCREEN</warning>
View.<warning descr="Field requires API level 16 (current min is 1): `android.view.View#SYSTEM_UI_FLAG_FULLSCREEN`">SYSTEM_UI_FLAG_FULLSCREEN</warning>
// Virtual call
<error descr="Call requires API level 11 (current min is 1): android.app.Activity#getActionBar">getActionBar</error>() // API 11
@@ -48,7 +49,7 @@ class ApiCallTest: Activity() {
// Class references (no call or field access)
val error: DOMError? = null // API 8
val clz = DOMErrorHandler::class // API 8
val clz = <error descr="Class requires API level 8 (current min is 1): org.w3c.dom.DOMErrorHandler">DOMErrorHandler::class</error> // API 8
// Method call
chronometer.<error descr="Call requires API level 3 (current min is 1): android.widget.Chronometer#getOnChronometerTickListener">onChronometerTickListener</error> // API 3
@@ -56,21 +57,21 @@ class ApiCallTest: Activity() {
// Inherited method call (from TextView
chronometer.<error descr="Call requires API level 11 (current min is 1): android.widget.TextView#setTextIsSelectable">setTextIsSelectable</error>(true) // API 11
GridLayout::class
<error descr="Class requires API level 14 (current min is 1): android.widget.GridLayout">GridLayout::class</error>
// Field access
val field = OpcodeInfo.<warning descr="Field requires API level 11 (current min is 1): dalvik.bytecode.OpcodeInfo#MAXIMUM_VALUE">MAXIMUM_VALUE</warning> // API 11
val field = OpcodeInfo.<warning descr="Field requires API level 11 (current min is 1): `dalvik.bytecode.OpcodeInfo#MAXIMUM_VALUE`">MAXIMUM_VALUE</warning> // API 11
val fillParent = LayoutParams.FILL_PARENT // API 1
// This is a final int, which means it gets inlined
val matchParent = LayoutParams.MATCH_PARENT // API 8
// Field access: non final
val batteryInfo = report!!.<error descr="Field requires API level 14 (current min is 1): android.app.ApplicationErrorReport#batteryInfo">batteryInfo</error>
val batteryInfo = report!!.<error descr="Field requires API level 14 (current min is 1): `android.app.ApplicationErrorReport#batteryInfo`">batteryInfo</error>
// Enum access
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP) {
val mode = PorterDuff.Mode.<error descr="Field requires API level 11 (current min is 1): android.graphics.PorterDuff.Mode#OVERLAY">OVERLAY</error> // API 11
val mode = PorterDuff.Mode.<error descr="Field requires API level 11 (current min is 1): `android.graphics.PorterDuff.Mode#OVERLAY`">OVERLAY</error> // API 11
}
}
@@ -115,12 +116,6 @@ class ApiCallTest: Activity() {
}
fun test(priority: Boolean, layout: ViewGroup) {
if (layout is LinearLayout) {}
layout as? LinearLayout
if (layout is GridLayout) {}
layout as? GridLayout
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
GridLayout(null).getOrientation(); // Not flagged
} else {
@@ -250,14 +245,22 @@ class ApiCallTest: Activity() {
//ERROR
}
if (SDK_INT >= JELLY_BEAN && textView.<error descr="Call requires API level 14 (current min is 1): android.widget.TextView#isSuggestionsEnabled">isSuggestionsEnabled</error>()) {
if (SDK_INT >= JELLY_BEAN && textView.isSuggestionsEnabled()) {
//NO ERROR
}
if (SDK_INT >= JELLY_BEAN && textView.<error descr="Call requires API level 14 (current min is 1): android.widget.TextView#isSuggestionsEnabled">isSuggestionsEnabled</error>) {
if (SDK_INT >= JELLY_BEAN && textView.isSuggestionsEnabled) {
//NO ERROR
}
if (SDK_INT >= JELLY_BEAN && (textView.text != "" || textView.isSuggestionsEnabled)) {
//NO ERROR
}
if (SDK_INT < JELLY_BEAN && (textView.text != "" || textView.<error descr="Call requires API level 14 (current min is 1): android.widget.TextView#isSuggestionsEnabled">isSuggestionsEnabled</error>)) {
//ERROR
}
if (SDK_INT < JELLY_BEAN && textView.<error descr="Call requires API level 14 (current min is 1): android.widget.TextView#isSuggestionsEnabled">isSuggestionsEnabled</error>()) {
//ERROR
}
@@ -265,10 +268,108 @@ class ApiCallTest: Activity() {
if (SDK_INT < JELLY_BEAN && textView.<error descr="Call requires API level 14 (current min is 1): android.widget.TextView#isSuggestionsEnabled">isSuggestionsEnabled</error>) {
//ERROR
}
if (SDK_INT < JELLY_BEAN || textView.isSuggestionsEnabled) {
//NO ERROR
}
if (SDK_INT > JELLY_BEAN || textView.<error descr="Call requires API level 14 (current min is 1): android.widget.TextView#isSuggestionsEnabled">isSuggestionsEnabled</error>) {
//ERROR
}
// getActionBar() API 11
if (SDK_INT <= 10 || getActionBar() == null) {
//NO ERROR
}
if (SDK_INT < 10 || <error descr="Call requires API level 11 (current min is 1): android.app.Activity#getActionBar">getActionBar</error>() == null) {
//ERROR
}
if (SDK_INT < 11 || getActionBar() == null) {
//NO ERROR
}
if (SDK_INT != 11 || getActionBar() == null) {
//NO ERROR
}
if (SDK_INT != 12 || <error descr="Call requires API level 11 (current min is 1): android.app.Activity#getActionBar">getActionBar</error>() == null) {
//ERROR
}
if (SDK_INT <= 11 || getActionBar() == null) {
//NO ERROR
}
if (SDK_INT < 12 || getActionBar() == null) {
//NO ERROR
}
if (SDK_INT <= 12 || getActionBar() == null) {
//NO ERROR
}
if (SDK_INT < 9 || <error descr="Call requires API level 11 (current min is 1): android.app.Activity#getActionBar">getActionBar</error>() == null) {
//ERROR
}
if (SDK_INT <= 9 || <error descr="Call requires API level 11 (current min is 1): android.app.Activity#getActionBar">getActionBar</error>() == null) {
//ERROR
}
}
fun testReturn() {
if (SDK_INT < 11) {
return
}
// No Error
val actionBar = getActionBar()
}
fun testThrow() {
if (SDK_INT < 11) {
throw IllegalStateException()
}
// No Error
val actionBar = getActionBar()
}
fun testError() {
if (SDK_INT < 11) {
error("Api")
}
// No Error
val actionBar = getActionBar()
}
fun testWithoutAnnotation(textView: TextView) {
if (textView.<error descr="Call requires API level 14 (current min is 1): android.widget.TextView#isSuggestionsEnabled">isSuggestionsEnabled</error>()) {
}
if (textView.<error descr="Call requires API level 14 (current min is 1): android.widget.TextView#isSuggestionsEnabled">isSuggestionsEnabled</error>) {
}
}
@TargetApi(JELLY_BEAN)
fun testWithAnnotation(textView: TextView) {
fun testWithTargetApiAnnotation(textView: TextView) {
if (textView.isSuggestionsEnabled()) {
//NO ERROR, annotation
}
if (textView.isSuggestionsEnabled) {
//NO ERROR, annotation
}
}
@SuppressLint("NewApi")
fun testWithSuppressLintAnnotation(textView: TextView) {
if (textView.isSuggestionsEnabled()) {
//NO ERROR, annotation
}
@@ -291,6 +392,59 @@ class ApiCallTest: Activity() {
Path().<error descr="Call requires API level 21 (current min is 1): android.graphics.Path#addOval">addOval</error>(0f, 0f, 0f, 0f, Path.Direction.CW)
}
// KT-14737 False error with short-circuit evaluation
fun testShortCircuitEvaluation() {
<error descr="Call requires API level 21 (current min is 1): android.content.Context#getDrawable">getDrawable</error>(0) // error here as expected
if(Build.VERSION.SDK_INT >= 23
&& null == getDrawable(0)) // error here should not occur
{
getDrawable(0) // no error here as expected
}
}
// KT-1482 Kotlin Lint: "Calling new methods on older versions" does not report call on receiver in extension function
private fun Bundle.caseE1a() { <error descr="Call requires API level 18 (current min is 1): android.os.Bundle#getBinder">getBinder</error>("") }
private fun Bundle.caseE1c() { this.<error descr="Call requires API level 18 (current min is 1): android.os.Bundle#getBinder">getBinder</error>("") }
private fun caseE1b(bundle: Bundle) { bundle.<error descr="Call requires API level 18 (current min is 1): android.os.Bundle#getBinder">getBinder</error>("") }
// KT-12023 Kotlin Lint: Cast doesn't trigger minSdk error
fun testCast(layout: ViewGroup) {
if (layout is LinearLayout) {} // OK API 1
layout as? LinearLayout // OK API 1
layout as LinearLayout // OK API 1
if (layout !is GridLayout) {} // TODO: should be error, fixed in uast > 1.0.8
layout as? GridLayout // TODO: should be error, fixed in uast > 1.0.8
layout as GridLayout // TODO: should be error, fixed in uast > 1.0.8
val grid = layout as? GridLayout // TODO: should be error, fixed in uast > 1.0.8
val linear = layout as LinearLayout // OK API 1
}
// TODO: should be error
class ErrorVectorDravable : VectorDrawable()
@TargetApi(21)
class MyVectorDravable : VectorDrawable()
fun testTypes() {
<error descr="Call requires API level 14 (current min is 1): android.widget.GridLayout#GridLayout">GridLayout</error>(this)
val c = <error descr="Class requires API level 21 (current min is 1): android.graphics.drawable.VectorDrawable">VectorDrawable::class</error>.java
}
fun testCallWithApiAnnotation(textView: TextView) {
<error descr="Call requires API level 21 (current min is 1): ApiCallTest.MyVectorDravable#MyVectorDravable">MyVectorDravable</error>()
<error descr="Call requires API level 16 (current min is 1): ApiCallTest#testWithTargetApiAnnotation">testWithTargetApiAnnotation</error>(textView)
}
companion object : Activity() {
fun test() {
<error descr="Call requires API level 21 (current min is 1): android.content.Context#getDrawable">getDrawable</error>(0)
}
}
// Return type
internal // API 14
val gridLayout: GridLayout?
@@ -298,4 +452,10 @@ class ApiCallTest: Activity() {
private val report: ApplicationErrorReport?
get() = null
}
object O: Activity() {
fun test() {
<error descr="Call requires API level 21 (current min is 1): android.content.Context#getDrawable">getDrawable</error>(0)
}
}
File diff suppressed because it is too large Load Diff
@@ -1,885 +0,0 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.android.inspections.klint;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.checks.ApiDetector;
import com.android.tools.klint.checks.ApiLookup;
import com.android.tools.klint.client.api.UastLintUtils;
import com.android.tools.klint.detector.api.*;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.PsiClassReferenceType;
import com.intellij.psi.util.MethodSignatureUtil;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.*;
import org.jetbrains.uast.expressions.UTypeReferenceExpression;
import org.jetbrains.uast.util.UastExpressionUtils;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
import org.jetbrains.uast.visitor.UastVisitor;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import static org.jetbrains.android.inspections.klint.IntellijLintUtils.SUPPRESS_LINT_FQCN;
import static org.jetbrains.android.inspections.klint.IntellijLintUtils.SUPPRESS_WARNINGS_FQCN;
/**
* Intellij-specific version of the {@link ApiDetector} which uses the PSI structure
* to check accesses
* <p>
* TODO:
* <ul>
* <li> Port this part from the bytecode based check:
* if (owner.equals("java/text/SimpleDateFormat")) {
* checkSimpleDateFormat(context, method, node, minSdk);
* }
* </li>
* <li>Compare to the bytecode based results</li>
* </ul>
*/
public class IntellijApiDetector extends ApiDetector {
@SuppressWarnings("unchecked")
public static final Implementation IMPLEMENTATION = new Implementation(
IntellijApiDetector.class,
EnumSet.of(Scope.RESOURCE_FILE, Scope.MANIFEST, Scope.JAVA_FILE),
Scope.MANIFEST_SCOPE,
Scope.RESOURCE_FILE_SCOPE,
Scope.JAVA_FILE_SCOPE
);
@NonNls
private static final String TARGET_API_FQCN = "android.annotation.TargetApi";
@Override
public List<Class<? extends UElement>> getApplicableUastTypes() {
return Collections.<Class<? extends UElement>>singletonList(UFile.class);
}
// TODO: Reuse the parent ApiVisitor to share more code between these two!
@Override
public UastVisitor createUastVisitor(@NonNull final JavaContext context) {
return new AbstractUastVisitor() {
@Override
public boolean visitFile(@NotNull UFile file) {
List<UClass> classes = file.getClasses();
if (!classes.isEmpty()) {
// TODO: This is weird; I should just perform the per class checks as part of visitClass!!
file.accept(new ApiCheckVisitor(context, classes.get(0), file));
}
return super.visitFile(file);
}
};
}
private static int getTargetApi(@NonNull UElement e, @NonNull UFile file) {
UElement element = e;
// Search upwards for target api annotations
while (element != null && element != file) {
if (element instanceof UAnnotated) {
UAnnotated owner = (UAnnotated)element;
UAnnotation annotation = owner.findAnnotation(TARGET_API_FQCN);
if (annotation == null) {
annotation = owner.findAnnotation(REQUIRES_API_ANNOTATION);
}
if (annotation != null) {
for (UNamedExpression pair : annotation.getAttributeValues()) {
UExpression v = pair.getExpression();
if (v instanceof ULiteralExpression) {
ULiteralExpression literal = (ULiteralExpression)v;
Object value = literal.getValue();
if (value instanceof Integer) {
return (Integer) value;
} else if (value instanceof String) {
return codeNameToApi((String) value);
}
} else if (UastExpressionUtils.isArrayInitializer(v)) {
UCallExpression mv = (UCallExpression)v;
for (UExpression mmv : mv.getValueArguments()) {
if (mmv instanceof ULiteralExpression) {
ULiteralExpression literal = (ULiteralExpression)mmv;
Object value = literal.getValue();
if (value instanceof Integer) {
return (Integer) value;
} else if (value instanceof String) {
return codeNameToApi((String) value);
}
}
}
} else if (v instanceof UResolvable) {
PsiElement resolved = ((UResolvable) v).resolve();
if (resolved != null) {
String fqcn = UastLintUtils.getQualifiedName(resolved);
return codeNameToApi(fqcn);
} else {
return codeNameToApi(v.asRenderString());
}
}
}
}
}
element = element.getContainingElement();
}
return -1;
}
private class ApiCheckVisitor extends AbstractUastVisitor {
private final JavaContext myContext;
private boolean mySeenSuppress;
private boolean mySeenTargetApi;
private final UClass myClass;
private final UFile myFile;
private final boolean myCheckAccess;
private boolean myCheckOverride;
private String myFrameworkParent;
public ApiCheckVisitor(JavaContext context, UClass clz, UFile file) {
myContext = context;
myClass = clz;
myFile = file;
myCheckAccess = context.isEnabled(UNSUPPORTED) || context.isEnabled(INLINED);
myCheckOverride = context.isEnabled(OVERRIDE)
&& context.getMainProject().getBuildSdk() >= 1;
int depth = 0;
if (myCheckOverride) {
myFrameworkParent = null;
UClass superClass = myClass.getUastSuperClass();
while (superClass != null) {
String fqcn = superClass.getQualifiedName();
if (fqcn == null) {
myCheckOverride = false;
} else if (fqcn.startsWith("android.") //$NON-NLS-1$
|| fqcn.startsWith("java.") //$NON-NLS-1$
|| fqcn.startsWith("javax.")) { //$NON-NLS-1$
if (!fqcn.equals(CommonClassNames.JAVA_LANG_OBJECT)) {
myFrameworkParent = ClassContext.getInternalName(fqcn);
}
break;
}
superClass = superClass.getUastSuperClass();
depth++;
if (depth == 500) {
// Shouldn't happen in practice; this prevents the IDE from
// hanging if the user has accidentally typed in an incorrect
// super class which creates a cycle.
break;
}
}
if (myFrameworkParent == null) {
myCheckOverride = false;
}
}
}
@Override
public boolean visitAnnotation(@NotNull UAnnotation annotation) {
String fqcn = annotation.getQualifiedName();
if (TARGET_API_FQCN.equals(fqcn) || REQUIRES_API_ANNOTATION.equals(fqcn)) {
mySeenTargetApi = true;
}
else if (SUPPRESS_LINT_FQCN.equals(fqcn) || SUPPRESS_WARNINGS_FQCN.equals(fqcn)) {
mySeenSuppress = true;
}
return super.visitAnnotation(annotation);
}
@Override
public boolean visitMethod(@NotNull UMethod method) {
// API check for default methods
if (method.getModifierList().hasExplicitModifier(PsiModifier.DEFAULT)) {
int api = 24; // minSdk for default methods
int minSdk = getMinSdk(myContext);
if (!isSuppressed(api, method, minSdk)) {
Location location = IntellijLintUtils.getUastLocation(myContext.file, method);
String message = String.format("Default method requires API level %1$d (current min is %2$d)", api, minSdk);
myContext.report(UNSUPPORTED, location, message);
}
}
if (!myCheckOverride) {
return super.visitMethod(method);
}
int buildSdk = myContext.getMainProject().getBuildSdk();
String name = method.getName();
assert myFrameworkParent != null;
String desc = IntellijLintUtils.getInternalDescription(method, false, false);
if (desc == null) {
// Couldn't compute description of method for some reason; probably
// failure to resolve parameter types
return super.visitMethod(method);
}
int api = mApiDatabase.getCallVersion(myFrameworkParent, name, desc);
if (api > buildSdk && buildSdk != -1) {
if (mySeenSuppress &&
IntellijLintUtils.isSuppressed(method, myFile.getPsi(), OVERRIDE)) {
return super.visitMethod(method);
}
// TODO: Don't complain if it's annotated with @Override; that means
// somehow the build target isn't correct.
String fqcn;
PsiClass containingClass = method.getContainingClass();
if (containingClass != null) {
String className = containingClass.getName();
String fullClassName = containingClass.getQualifiedName();
if (fullClassName != null) {
className = fullClassName;
}
fqcn = className + '#' + name;
} else {
fqcn = name;
}
String message = String.format(
"This method is not overriding anything with the current build " +
"target, but will in API level %1$d (current target is %2$d): %3$s",
api, buildSdk, fqcn);
PsiElement locationNode = method.getNameIdentifier();
if (locationNode == null) {
locationNode = method;
}
Location location = IntellijLintUtils.getLocation(myContext.file, locationNode);
myContext.report(OVERRIDE, location, message);
}
return super.visitMethod(method);
}
@Override
public boolean visitClass(@NotNull UClass aClass) {
if (!myCheckAccess) {
return super.visitClass(aClass);
}
if (aClass.isAnnotationType()) {
PsiModifierList modifierList = aClass.getModifierList();
if (modifierList != null) {
for (PsiAnnotation annotation : modifierList.getAnnotations()) {
String name = annotation.getQualifiedName();
if ("java.lang.annotation.Repeatable".equals(name)) {
int api = 24; // minSdk for repeatable annotations
int minSdk = getMinSdk(myContext);
if (!isSuppressed(api, aClass, minSdk)) {
Location location = IntellijLintUtils.getLocation(myContext.file, annotation);
String message = String.format("Repeatable annotation requires API level %1$d (current min is %2$d)", api, minSdk);
myContext.report(UNSUPPORTED, location, message);
}
}
}
}
}
for (PsiClassType type : aClass.getSuperTypes()) {
String signature = IntellijLintUtils.getInternalName(type);
if (signature == null) {
continue;
}
int api = mApiDatabase.getClassVersion(signature);
if (api == -1) {
continue;
}
int minSdk = getMinSdk(myContext);
if (api <= minSdk) {
continue;
}
if (mySeenTargetApi) {
int target = getTargetApi(aClass, myFile);
if (target != -1) {
if (api <= target) {
continue;
}
}
}
if (mySeenSuppress && IntellijLintUtils.isSuppressed(aClass, myFile.getPsi(), UNSUPPORTED)) {
continue;
}
Location location;
if (type instanceof PsiClassReferenceType) {
PsiReference reference = ((PsiClassReferenceType)type).getReference();
PsiElement element = reference.getElement();
if (isWithinVersionCheckConditional(aClass, api, myContext)) {
continue;
}
if (isPrecededByVersionCheckExit(aClass, api, myContext)) {
continue;
}
location = IntellijLintUtils.getLocation(myContext.file, element);
} else {
location = IntellijLintUtils.getLocation(myContext.file, aClass); //TODO to super type reference
}
String fqcn = type.getClassName();
String message = String.format("Class requires API level %1$d (current min is %2$d): %3$s", api, minSdk, fqcn);
myContext.report(UNSUPPORTED, location, message);
}
return super.visitClass(aClass);
}
@Override
public boolean visitSimpleNameReferenceExpression(@NotNull USimpleNameReferenceExpression expression) {
if (!myCheckAccess) {
return super.visitSimpleNameReferenceExpression(expression);
}
PsiElement resolved = expression.resolve();
if (resolved != null) {
if (resolved instanceof PsiField) {
PsiField field = (PsiField)resolved;
PsiClass containingClass = field.getContainingClass();
if (containingClass == null) {
return super.visitSimpleNameReferenceExpression(expression);
}
String owner = IntellijLintUtils.getInternalName(containingClass);
if (owner == null) {
return super.visitSimpleNameReferenceExpression(expression); // Couldn't resolve type
}
String name = field.getName();
if (name == null) {
return super.visitSimpleNameReferenceExpression(expression);
}
int api = mApiDatabase.getFieldVersion(owner, name);
if (api == -1) {
return super.visitSimpleNameReferenceExpression(expression);
}
int minSdk = getMinSdk(myContext);
if (isSuppressed(api, expression, minSdk)) {
return super.visitSimpleNameReferenceExpression(expression);
}
Location location = IntellijLintUtils.getUastLocation(myContext.file, expression);
String fqcn = containingClass.getQualifiedName();
String message = String.format(
"Field requires API level %1$d (current min is %2$d): %3$s",
api, minSdk, fqcn + '#' + name);
Issue issue = UNSUPPORTED;
// When accessing primitive types or Strings, the values get copied into
// the class files (e.g. get inlined) which has a separate issue type:
// INLINED.
PsiType type = field.getType();
if (PsiType.INT.equals(type) || PsiType.CHAR.equals(type) || PsiType.BOOLEAN.equals(type)
|| PsiType.DOUBLE.equals(type) || PsiType.FLOAT.equals(type) || PsiType.BYTE.equals(type)
|| type.equalsToText(CommonClassNames.JAVA_LANG_STRING)) {
issue = INLINED;
// Some usages of inlined constants are okay:
if (isBenignConstantUsage(expression, name, owner)) {
return super.visitSimpleNameReferenceExpression(expression);
}
}
myContext.report(issue, location, message);
}
}
return super.visitSimpleNameReferenceExpression(expression);
}
@Override
public boolean visitTryExpression(@NotNull UTryExpression statement) {
if (statement.isResources()) {
int api = 19; // minSdk for try with resources
int minSdk = getMinSdk(myContext);
if (isSuppressed(api, statement, minSdk)) {
return super.visitTryExpression(statement);
}
Location location = IntellijLintUtils.getUastLocation(myContext.file, statement.getTryClause());
String message = String.format("Try-with-resources requires API level %1$d (current min is %2$d)", api, minSdk);
myContext.report(UNSUPPORTED, location, message);
}
for (UCatchClause catchClause : statement.getCatchClauses()) {
// Special case reflective operation exception which can be implicitly used
// with multi-catches: see issue 153406
int minSdk = getMinSdk(myContext);
if(minSdk < 19 && isMultiCatchReflectiveOperationException(catchClause)) {
String message = String.format("Multi-catch with these reflection exceptions requires API level 19 (current min is %d) " +
"because they get compiled to the common but new super type `ReflectiveOperationException`. " +
"As a workaround either create individual catch statements, or catch `Exception`.",
minSdk);
myContext.report(UNSUPPORTED, getCatchParametersLocation(myContext, catchClause), message);
continue;
}
for (UTypeReferenceExpression typeReference : catchClause.getTypeReferences()) {
checkCatchTypeElement(statement, typeReference, typeReference.getType());
}
}
return super.visitTryExpression(statement);
}
private void checkCatchTypeElement(@NonNull UTryExpression statement,
@NonNull UTypeReferenceExpression typeReference,
@Nullable PsiType type) {
PsiClass resolved = null;
if (type instanceof PsiClassType) {
resolved = ((PsiClassType) type).resolve();
}
if (resolved != null) {
String signature = myContext.getEvaluator().getInternalName(resolved);
int api = mApiDatabase.getClassVersion(signature);
if (api == -1) {
return;
}
int minSdk = getMinSdk(myContext);
if (api <= minSdk) {
return;
}
int target = getTargetApi(statement);
if (target != -1 && api <= target) {
return;
}
Location location = myContext.getUastLocation(typeReference);
String fqcn = resolved.getQualifiedName();
String message = String.format("Class requires API level %1$d (current min is %2$d): %3$s",
api, minSdk, fqcn);
myContext.report(UNSUPPORTED, location, message);
}
}
private boolean isSuppressed(int api, UElement element, int minSdk) {
if (api <= minSdk) {
return true;
}
if (mySeenTargetApi) {
int target = getTargetApi(element, myFile);
if (target != -1) {
if (api <= target) {
return true;
}
}
}
if (mySeenSuppress &&
(IntellijLintUtils.isSuppressed(element, myFile, UNSUPPORTED) || IntellijLintUtils.isSuppressed(element, myFile, INLINED))) {
return true;
}
if (isWithinVersionCheckConditional(element, api, myContext)) {
return true;
}
if (isPrecededByVersionCheckExit(element, api, myContext)) {
return true;
}
return false;
}
@Override
public boolean visitBinaryExpressionWithType(@NotNull UBinaryExpressionWithType expression) {
if (myCheckAccess) {
UExpression operand = expression.getOperand();
PsiType operandType = operand.getExpressionType();
PsiType castType = expression.getType();
if (castType.equals(operandType)) {
return super.visitBinaryExpressionWithType(expression);
}
if (!(operandType instanceof PsiClassType)) {
return super.visitBinaryExpressionWithType(expression);
}
if (!(castType instanceof PsiClassType)) {
return super.visitBinaryExpressionWithType(expression);
}
PsiClassType classType = (PsiClassType)operandType;
PsiClassType interfaceType = (PsiClassType)castType;
checkCast(expression, classType, interfaceType);
}
return super.visitBinaryExpressionWithType(expression);
}
private void checkCast(@NotNull UElement node, @NotNull PsiClassType classType, @NotNull PsiClassType interfaceType) {
if (classType.equals(interfaceType)) {
return;
}
String classTypeInternal = IntellijLintUtils.getInternalName(classType);
String interfaceTypeInternal = IntellijLintUtils.getInternalName(interfaceType);
if (classTypeInternal == null || interfaceTypeInternal == null || "java/lang/Object".equals(interfaceTypeInternal)) {
return; // Couldn't resolve type
}
int api = mApiDatabase.getValidCastVersion(classTypeInternal, interfaceTypeInternal);
if (api == -1) {
return;
}
int minSdk = getMinSdk(myContext);
if (api <= minSdk) {
return;
}
if (isSuppressed(api, node, minSdk)) {
return;
}
Location location = IntellijLintUtils.getUastLocation(myContext.file, node);
String message = String.format("Cast from %1$s to %2$s requires API level %3$d (current min is %4$d)",
classType.getClassName(), interfaceType.getClassName(), api, minSdk);
myContext.report(UNSUPPORTED, location, message);
}
@Override
public boolean visitVariable(@NotNull UVariable variable) {
if (variable instanceof ULocalVariable) {
if (!myCheckAccess) {
return super.visitVariable(variable);
}
UExpression initializer = variable.getUastInitializer();
if (initializer == null) {
return super.visitVariable(variable);
}
PsiType initializerType = initializer.getExpressionType();
if (initializerType == null || !(initializerType instanceof PsiClassType)) {
return super.visitVariable(variable);
}
PsiType interfaceType = variable.getType();
if (initializerType.equals(interfaceType)) {
return super.visitVariable(variable);
}
if (!(interfaceType instanceof PsiClassType)) {
return super.visitVariable(variable);
}
checkCast(initializer, (PsiClassType)initializerType, (PsiClassType)interfaceType);
}
return super.visitVariable(variable);
}
@Override
public boolean visitBinaryExpression(@NotNull UBinaryExpression expression) {
if (expression.getOperator() instanceof UastBinaryOperator.AssignOperator) {
if (!myCheckAccess) {
return super.visitBinaryExpression(expression);
}
UExpression rExpression = expression.getRightOperand();
PsiType rhsType = rExpression.getExpressionType();
if (rhsType == null || !(rhsType instanceof PsiClassType)) {
return super.visitBinaryExpression(expression);
}
PsiType interfaceType = expression.getLeftOperand().getExpressionType();
if (rhsType.equals(interfaceType)) {
return super.visitBinaryExpression(expression);
}
if (!(interfaceType instanceof PsiClassType)) {
return super.visitBinaryExpression(expression);
}
checkCast(rExpression, (PsiClassType)rhsType, (PsiClassType)interfaceType);
}
return super.visitBinaryExpression(expression);
}
@Override
public boolean visitForEachExpression(@NotNull UForEachExpression statement) {
// The for each method will implicitly call iterator() on the
// Iterable that is used in the for each loop; make sure that
// the API level for that
if (!myCheckAccess) {
return super.visitForEachExpression(statement);
}
UExpression value = statement.getIteratedValue();
PsiType type = value.getExpressionType();
if (type instanceof PsiClassType) {
String expressionOwner = IntellijLintUtils.getInternalName((PsiClassType)type);
if (expressionOwner != null) {
int api = mApiDatabase.getClassVersion(expressionOwner);
if (api == -1) {
return super.visitForEachExpression(statement);
}
int minSdk = getMinSdk(myContext);
if (api <= minSdk) {
return super.visitForEachExpression(statement);
}
if (mySeenTargetApi) {
int target = getTargetApi(statement, myFile);
if (target != -1) {
if (api <= target) {
return super.visitForEachExpression(statement);
}
}
}
if (mySeenSuppress && IntellijLintUtils.isSuppressed(statement, myFile, UNSUPPORTED)) {
return super.visitForEachExpression(statement);
}
if (isWithinVersionCheckConditional(statement, api, myContext)) {
return super.visitForEachExpression(statement);
}
if (isPrecededByVersionCheckExit(statement, api, myContext)) {
return super.visitForEachExpression(statement);
}
Location location = IntellijLintUtils.getUastLocation(myContext.file, value);
String message = String.format("The type of the for loop iterated value is %1$s, which requires API level %2$d" +
" (current min is %3$d)", type.getCanonicalText(), api, minSdk);
// Add specific check ConcurrentHashMap#keySet and add workaround text.
// This was an unfortunate incompatible API change in Open JDK 8, which is
// not an issue for the Android SDK but is relevant if you're using a
// Java library.
if (value instanceof PsiMethodCallExpression) {
PsiMethodCallExpression valueCall = (PsiMethodCallExpression)value;
if ("keySet".equals(valueCall.getMethodExpression().getReferenceName())) {
PsiMethod keySet = valueCall.resolveMethod();
if (keySet != null && keySet.getContainingClass() != null &&
"java.util.concurrent.ConcurrentHashMap".equals(keySet.getContainingClass().getQualifiedName())) {
message += "; to work around this, add an explicit cast to (Map) before the `keySet` call.";
}
}
}
myContext.report(UNSUPPORTED, location, message);
}
}
return super.visitForEachExpression(statement);
}
@Override
public boolean visitCallExpression(@NotNull UCallExpression node) {
checkMethodCallExpression(node);
return super.visitCallExpression(node);
}
private void checkMethodCallExpression(@NotNull UCallExpression expression) {
super.visitCallExpression(expression);
if (!myCheckAccess) {
return;
}
PsiMethod method = expression.resolve();
if (method != null) {
PsiClass containingClass = method.getContainingClass();
if (containingClass == null) {
return;
}
PsiParameterList parameterList = method.getParameterList();
if (parameterList.getParametersCount() > 0) {
PsiParameter[] parameters = parameterList.getParameters();
List<UExpression> arguments = expression.getValueArguments();
for (int i = 0; i < parameters.length; i++) {
PsiType parameterType = parameters[i].getType();
if (parameterType instanceof PsiClassType) {
if (i >= arguments.size()) {
// We can end up with more arguments than parameters when
// there is a varargs call.
break;
}
UExpression argument = arguments.get(i);
PsiType argumentType = argument.getExpressionType();
if (argumentType == null || parameterType.equals(argumentType) || !(argumentType instanceof PsiClassType)) {
continue;
}
checkCast(argument, (PsiClassType)argumentType, (PsiClassType)parameterType);
}
}
}
String fqcn = containingClass.getQualifiedName();
String owner = IntellijLintUtils.getInternalName(containingClass);
if (owner == null) {
return; // Couldn't resolve type
}
String name = IntellijLintUtils.getInternalMethodName(method);
String desc = IntellijLintUtils.getInternalDescription(method, false, false);
if (desc == null) {
// Couldn't compute description of method for some reason; probably
// failure to resolve parameter types
return;
}
int api = mApiDatabase.getCallVersion(owner, name, desc);
if (api == -1) {
return;
}
int minSdk = getMinSdk(myContext);
if (api <= minSdk) {
return;
}
// The lint API database contains two optimizations:
// First, all members that were available in API 1 are omitted from the database, since that saves
// about half of the size of the database, and for API check purposes, we don't need to distinguish
// between "doesn't exist" and "available in all versions".
// Second, all inherited members were inlined into each class, so that it doesn't have to do a
// repeated search up the inheritance chain.
//
// Unfortunately, in this custom PSI detector, we look up the real resolved method, which can sometimes
// have a different minimum API.
//
// For example, SQLiteDatabase had a close() method from API 1. Therefore, calling SQLiteDatabase is supported
// in all versions. However, it extends SQLiteClosable, which in API 16 added "implements Closable". In
// this detector, if we have the following code:
// void test(SQLiteDatabase db) { db.close }
// here the call expression will be the close method on type SQLiteClosable. And that will result in an API
// requirement of API 16, since the close method it now resolves to is in API 16.
//
// To work around this, we can now look up the type of the call expression ("db" in the above, but it could
// have been more complicated), and if that's a different type than the type of the method, we look up
// *that* method from lint's database instead. Furthermore, it's possible for that method to return "-1"
// and we can't tell if that means "doesn't exist" or "present in API 1", we then check the package prefix
// to see whether we know it's an API method whose members should all have been inlined.
if (UastExpressionUtils.isMethodCall(expression)) {
UExpression qualifier = expression.getReceiver();
if (qualifier != null && !(qualifier instanceof UThisExpression) && !(qualifier instanceof USuperExpression)) {
PsiType type = qualifier.getExpressionType();
if (type != null && type instanceof PsiClassType) {
String expressionOwner = IntellijLintUtils.getInternalName((PsiClassType)type);
if (expressionOwner != null && !expressionOwner.equals(owner)) {
int specificApi = mApiDatabase.getCallVersion(expressionOwner, name, desc);
if (specificApi == -1) {
if (ApiLookup.isRelevantOwner(expressionOwner)) {
return;
}
} else if (specificApi <= minSdk) {
return;
} else {
// For example, for Bundle#getString(String,String) the API level is 12, whereas for
// BaseBundle#getString(String,String) the API level is 21. If the code specified a Bundle instead of
// a BaseBundle, reported the Bundle level in the error message instead.
if (specificApi < api) {
api = specificApi;
fqcn = expressionOwner.replace('/', '.');
}
api = Math.min(specificApi, api);
}
}
}
} else {
// Unqualified call; need to search in our super hierarchy
PsiClass cls = UastUtils.getContainingClass(expression);
//noinspection ConstantConditions
if (qualifier instanceof UThisExpression || qualifier instanceof USuperExpression) {
UInstanceExpression pte = (UInstanceExpression) qualifier;
PsiElement resolved = pte.resolve();
if (resolved instanceof PsiClass) {
cls = (PsiClass)resolved;
}
}
while (cls != null) {
if (cls instanceof PsiAnonymousClass) {
// If it's an unqualified call in an anonymous class, we need to rely on the
// resolve method to find out whether the method is picked up from the anonymous
// class chain or any outer classes
boolean found = false;
PsiClassType anonymousBaseType = ((PsiAnonymousClass)cls).getBaseClassType();
PsiClass anonymousBase = anonymousBaseType.resolve();
if (anonymousBase != null && anonymousBase.isInheritor(containingClass, true)) {
cls = anonymousBase;
found = true;
} else {
PsiClass surroundingBaseType = PsiTreeUtil.getParentOfType(cls, PsiClass.class, true);
if (surroundingBaseType != null && surroundingBaseType.isInheritor(containingClass, true)) {
cls = surroundingBaseType;
found = true;
}
}
if (!found) {
break;
}
}
String expressionOwner = IntellijLintUtils.getInternalName(cls);
if (expressionOwner == null) {
break;
}
int specificApi = mApiDatabase.getCallVersion(expressionOwner, name, desc);
if (specificApi == -1) {
if (ApiLookup.isRelevantOwner(expressionOwner)) {
return;
}
} else if (specificApi <= minSdk) {
return;
} else {
if (specificApi < api) {
api = specificApi;
fqcn = expressionOwner.replace('/', '.');
}
api = Math.min(specificApi, api);
break;
}
cls = cls.getSuperClass();
}
}
}
if (isSuppressed(api, expression, minSdk)) {
return;
}
// If you're simply calling super.X from method X, even if method X is in a higher API level than the minSdk, we're
// generally safe; that method should only be called by the framework on the right API levels. (There is a danger of
// somebody calling that method locally in other contexts, but this is hopefully unlikely.)
if (UastExpressionUtils.isMethodCall(expression)) {
if (expression.getReceiver() instanceof USuperExpression) {
PsiMethod containingMethod = UastUtils.getContainingMethod(expression);
if (containingMethod != null && name.equals(containingMethod.getName())
&& MethodSignatureUtil.areSignaturesEqual(method, containingMethod)
// We specifically exclude constructors from this check, because we do want to flag constructors requiring the
// new API level; it's highly likely that the constructor is called by local code so you should specifically
// investigate this as a developer
&& !method.isConstructor()) {
return;
}
}
}
UElement locationNode = expression.getMethodIdentifier();
if (locationNode == null) {
locationNode = expression;
}
Location location = IntellijLintUtils.getUastLocation(myContext.file, locationNode);
String message = String.format("Call requires API level %1$d (current min is %2$d): %3$s", api, minSdk,
fqcn + '#' + method.getName());
myContext.report(UNSUPPORTED, location, message);
}
}
}
}
@@ -69,7 +69,7 @@ public class IntellijLintIssueRegistry extends BuiltinIssueRegistry {
EnumSet<Scope> scope = implementation.getScope();
Class<? extends Detector> detectorClass = implementation.getDetectorClass();
if (detectorClass == ApiDetector.class) {
issue.setImplementation(IntellijApiDetector.IMPLEMENTATION);
//issue.setImplementation(IntellijApiDetector.IMPLEMENTATION);
} else if (detectorClass == ViewTypeDetector.class) {
issue.setImplementation(IntellijViewTypeDetector.IMPLEMENTATION);
}