Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* Copyright (C) 2012 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 com.android.tools.lint.checks;
import static com.android.SdkConstants.ANDROID_PREFIX;
import static com.android.SdkConstants.ANDROID_THEME_PREFIX;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_CLASS;
import static com.android.SdkConstants.ATTR_FULL_BACKUP_CONTENT;
import static com.android.SdkConstants.ATTR_HEIGHT;
import static com.android.SdkConstants.ATTR_ID;
import static com.android.SdkConstants.ATTR_LABEL_FOR;
import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.ATTR_PADDING_START;
import static com.android.SdkConstants.ATTR_PARENT;
import static com.android.SdkConstants.ATTR_TARGET_API;
import static com.android.SdkConstants.ATTR_TEXT_IS_SELECTABLE;
import static com.android.SdkConstants.ATTR_VALUE;
import static com.android.SdkConstants.ATTR_WIDTH;
import static com.android.SdkConstants.BUTTON;
import static com.android.SdkConstants.CHECK_BOX;
import static com.android.SdkConstants.CLASS_CONSTRUCTOR;
import static com.android.SdkConstants.CONSTRUCTOR_NAME;
import static com.android.SdkConstants.FQCN_TARGET_API;
import static com.android.SdkConstants.PREFIX_ANDROID;
import static com.android.SdkConstants.SUPPORT_ANNOTATIONS_PREFIX;
import static com.android.SdkConstants.SWITCH;
import static com.android.SdkConstants.TAG;
import static com.android.SdkConstants.TAG_ITEM;
import static com.android.SdkConstants.TAG_STYLE;
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.checks.RtlDetector.ATTR_SUPPORTS_RTL;
import static com.android.tools.lint.detector.api.ClassContext.getFqcn;
import static com.android.tools.lint.detector.api.LintUtils.getNextInstruction;
import static com.android.tools.lint.detector.api.LintUtils.skipParentheses;
import static com.android.tools.lint.detector.api.Location.SearchDirection.BACKWARD;
import static com.android.tools.lint.detector.api.Location.SearchDirection.EOL_NEAREST;
import static com.android.tools.lint.detector.api.Location.SearchDirection.FORWARD;
import static com.android.tools.lint.detector.api.Location.SearchDirection.NEAREST;
import static com.android.utils.SdkUtils.getResourceFieldName;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.repository.GradleVersion;
import com.android.repository.Revision;
import com.android.repository.api.LocalPackage;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.BuildToolInfo;
import com.android.sdklib.SdkVersionInfo;
import com.android.sdklib.repository.AndroidSdkHandler;
import com.android.tools.lint.client.api.IssueRegistry;
import com.android.tools.lint.client.api.JavaEvaluator;
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.ClassScanner;
import com.android.tools.lint.detector.api.Detector.JavaPsiScanner;
import com.android.tools.lint.detector.api.Implementation;
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.ResourceXmlDetector;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.TextFormat;
import com.android.tools.lint.detector.api.XmlContext;
import com.google.common.collect.Lists;
import com.intellij.psi.JavaElementVisitor;
import com.intellij.psi.JavaTokenType;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiAnnotationMemberValue;
import com.intellij.psi.PsiAnnotationParameterList;
import com.intellij.psi.PsiArrayInitializerMemberValue;
import com.intellij.psi.PsiAssignmentExpression;
import com.intellij.psi.PsiBinaryExpression;
import com.intellij.psi.PsiBlockStatement;
import com.intellij.psi.PsiCallExpression;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiClassType;
import com.intellij.psi.PsiCodeBlock;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiConditionalExpression;
import com.intellij.psi.PsiDisjunctionType;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiExpressionList;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiIfStatement;
import com.intellij.psi.PsiImportStatementBase;
import com.intellij.psi.PsiImportStaticStatement;
import com.intellij.psi.PsiJavaCodeReferenceElement;
import com.intellij.psi.PsiLiteral;
import com.intellij.psi.PsiLiteralExpression;
import com.intellij.psi.PsiLocalVariable;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiModifier;
import com.intellij.psi.PsiModifierList;
import com.intellij.psi.PsiModifierListOwner;
import com.intellij.psi.PsiNameValuePair;
import com.intellij.psi.PsiParameter;
import com.intellij.psi.PsiParameterList;
import com.intellij.psi.PsiPolyadicExpression;
import com.intellij.psi.PsiPrimitiveType;
import com.intellij.psi.PsiReferenceExpression;
import com.intellij.psi.PsiResourceList;
import com.intellij.psi.PsiReturnStatement;
import com.intellij.psi.PsiStatement;
import com.intellij.psi.PsiSwitchLabelStatement;
import com.intellij.psi.PsiTryStatement;
import com.intellij.psi.PsiType;
import com.intellij.psi.PsiTypeCastExpression;
import com.intellij.psi.PsiTypeElement;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiTreeUtil;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.IntInsnNode;
import org.objectweb.asm.tree.JumpInsnNode;
import org.objectweb.asm.tree.LabelNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.LocalVariableNode;
import org.objectweb.asm.tree.LookupSwitchInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TryCatchBlockNode;
import org.objectweb.asm.tree.analysis.AnalyzerException;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 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 ClassScanner, JavaPsiScanner {
/**
* 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
* classes until actually needed, so there is no need to flag these, and in fact,
* patterns used for supporting new and old versions sometimes declares these methods
* and only conditionally end up actually accessing methods and fields, so only check
* method and field accesses.
*/
private static final boolean CHECK_DECLARATIONS = false;
private static final boolean AOSP_BUILD = System.getenv("ANDROID_BUILD_TOP") != null; //$NON-NLS-1$
public static final String REQUIRES_API_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "RequiresApi"; //$NON-NLS-1$
/** Accessing an unsupported API */
@SuppressWarnings("unchecked")
public static final Issue UNSUPPORTED = Issue.create(
"NewApi", //$NON-NLS-1$
"Calling new methods on older versions",
"This check scans through all the Android API calls in the application and " +
"warns about any calls that are not available on *all* versions targeted " +
"by this application (according to its minimum SDK attribute in the manifest).\n" +
"\n" +
"If you really want to use this API and don't need to support older devices just " +
"set the `minSdkVersion` in your `build.gradle` or `AndroidManifest.xml` files.\n" +
"\n" +
"If your code is *deliberately* accessing newer APIs, and you have ensured " +
"(e.g. with conditional execution) that this code will only ever be called on a " +
"supported platform, then you can annotate your class or method with the " +
"`@TargetApi` annotation specifying the local minimum SDK to apply, such as " +
"`@TargetApi(11)`, such that this check considers 11 rather than your manifest " +
"file's minimum SDK as the required API level.\n" +
"\n" +
"If you are deliberately setting `android:` attributes in style definitions, " +
"make sure you place this in a `values-vNN` folder in order to avoid running " +
"into runtime conflicts on certain devices where manufacturers have added " +
"custom attributes whose ids conflict with the new ones on later platforms.\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.",
Category.CORRECTNESS,
6,
Severity.ERROR,
new Implementation(
ApiDetector.class,
EnumSet.of(Scope.CLASS_FILE, Scope.RESOURCE_FILE, Scope.MANIFEST),
Scope.RESOURCE_FILE_SCOPE,
Scope.CLASS_FILE_SCOPE,
Scope.MANIFEST_SCOPE));
/** Accessing an inlined API on older platforms */
public static final Issue INLINED = Issue.create(
"InlinedApi", //$NON-NLS-1$
"Using inlined constants on older versions",
"This check scans through all the Android API field references in the application " +
"and flags certain constants, such as static final integers and Strings, " +
"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 tbe guarded.\n" +
"\n" +
"If you really want to use this API and don't need to support older devices just " +
"set the `minSdkVersion` in your `build.gradle` or `AndroidManifest.xml` files." +
"\n" +
"If your code is *deliberately* accessing newer APIs, and you have ensured " +
"(e.g. with conditional execution) that this code will only ever be called on a " +
"supported platform, then you can annotate your class or method with the " +
"`@TargetApi` annotation specifying the local minimum SDK to apply, such as " +
"`@TargetApi(11)`, such that this check considers 11 rather than your manifest " +
"file's minimum SDK as the required API level.\n",
Category.CORRECTNESS,
6,
Severity.WARNING,
new Implementation(
ApiDetector.class,
Scope.JAVA_FILE_SCOPE));
/** Method conflicts with new inherited method */
public static final Issue OVERRIDE = Issue.create(
"Override", //$NON-NLS-1$
"Method conflicts with new inherited method",
"Suppose you are building against Android API 8, and you've subclassed Activity. " +
"In your subclass you add a new method called `isDestroyed`(). At some later point, " +
"a method of the same name and signature is added to Android. Your method will " +
"now override the Android method, and possibly break its contract. Your method " +
"is not calling `super.isDestroyed()`, since your compilation target doesn't " +
"know about the method.\n" +
"\n" +
"The above scenario is what this lint detector looks for. The above example is " +
"real, since `isDestroyed()` was added in API 17, but it will be true for *any* " +
"method you have added to a subclass of an Android class where your build target " +
"is lower than the version the method was introduced in.\n" +
"\n" +
"To fix this, either rename your method, or if you are really trying to augment " +
"the builtin method if available, switch to a higher build target where you can " +
"deliberately add `@Override` on your overriding method, and call `super` if " +
"appropriate etc.\n",
Category.CORRECTNESS,
6,
Severity.ERROR,
new Implementation(
ApiDetector.class,
Scope.CLASS_FILE_SCOPE));
/** Attribute unused on older versions */
public static final Issue UNUSED = Issue.create(
"UnusedAttribute", //$NON-NLS-1$
"Attribute unused on older versions",
"This check finds attributes set in XML files that were introduced in a version " +
"newer than the oldest version targeted by your application (with the " +
"`minSdkVersion` attribute).\n" +
"\n" +
"This is not an error; the application will simply ignore the attribute. However, " +
"if the attribute is important to the appearance of functionality of your " +
"application, you should consider finding an alternative way to achieve the " +
"same result with only available attributes, and then you can optionally create " +
"a copy of the layout in a layout-vNN folder which will be used on API NN or " +
"higher where you can take advantage of the newer attribute.\n" +
"\n" +
"Note: This check does not only apply to attributes. For example, some tags can be " +
"unused too, such as the new `` element in layouts introduced in API 21.",
Category.CORRECTNESS,
6,
Severity.WARNING,
new Implementation(
ApiDetector.class,
Scope.RESOURCE_FILE_SCOPE));
private static final String TARGET_API_VMSIG = '/' + TARGET_API + ';';
private static final String REQ_API_VMSIG = "/RequiresApi;";
private static final String SWITCH_TABLE_PREFIX = "$SWITCH_TABLE$"; //$NON-NLS-1$
private static final String ORDINAL_METHOD = "ordinal"; //$NON-NLS-1$
public static final String ENUM_SWITCH_PREFIX = "$SwitchMap$"; //$NON-NLS-1$
private static final String TAG_RIPPLE = "ripple";
private static final String TAG_VECTOR = "vector";
private static final String TAG_ANIMATED_VECTOR = "animated-vector";
private static final String TAG_ANIMATED_SELECTOR = "animated-selector";
private static final String SDK_INT = "SDK_INT";
private static final String ANDROID_OS_BUILD_VERSION = "android/os/Build$VERSION";
protected ApiLookup mApiDatabase;
private boolean mWarnedMissingDb;
private int mMinApi = -1;
/** Constructs a new API check */
public ApiDetector() {
}
@Override
public void beforeCheckProject(@NonNull Context context) {
if (mApiDatabase == null) {
mApiDatabase = ApiLookup.get(context.getClient());
// We can't look up the minimum API required by the project here:
// The manifest file hasn't been processed yet in the -before- project hook.
// For now it's initialized lazily in getMinSdk(Context), but the
// lint infrastructure should be fixed to parse manifest file up front.
if (mApiDatabase == null && !mWarnedMissingDb) {
mWarnedMissingDb = true;
context.report(IssueRegistry.LINT_ERROR, Location.create(context.file),
"Can't find API database; API check not performed");
} else {
// See if you don't have at least version 23.0.1 of platform tools installed
AndroidSdkHandler sdk = context.getClient().getSdk();
if (sdk == null) {
return;
}
LocalPackage pkgInfo = sdk.getLocalPackage(SdkConstants.FD_PLATFORM_TOOLS,
context.getClient().getRepositoryLogger());
if (pkgInfo == null) {
return;
}
Revision revision = pkgInfo.getVersion();
// The platform tools must be at at least the same revision
// as the compileSdkVersion!
// And as a special case, for 23, they must be at 23.0.1
// because 23.0.0 accidentally shipped without Android M APIs.
int compileSdkVersion = context.getProject().getBuildSdk();
if (compileSdkVersion == 23) {
if (revision.getMajor() > 23 || revision.getMajor() == 23
&& (revision.getMinor() > 0 || revision.getMicro() > 0)) {
return;
}
} else if (compileSdkVersion <= revision.getMajor()) {
return;
}
// Pick a location: when incrementally linting in the IDE, tie
// it to the current file
List currentFiles = context.getProject().getSubset();
Location location;
if (currentFiles != null && currentFiles.size() == 1) {
File file = currentFiles.get(0);
String contents = context.getClient().readFile(file);
int firstLineEnd = contents.indexOf('\n');
if (firstLineEnd == -1) {
firstLineEnd = contents.length();
}
location = Location.create(file,
new DefaultPosition(0, 0, 0), new
DefaultPosition(0, firstLineEnd, firstLineEnd));
} else {
location = Location.create(context.file);
}
context.report(UNSUPPORTED,
location,
String.format("The SDK platform-tools version (%1$s) is too old "
+ " to check APIs compiled with API %2$d; please update",
revision.toShortString(),
compileSdkVersion));
}
}
}
// ---- Implements XmlScanner ----
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return true;
}
@Override
public Collection getApplicableElements() {
return ALL;
}
@Override
public Collection getApplicableAttributes() {
return ALL;
}
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
if (mApiDatabase == null) {
return;
}
int attributeApiLevel = -1;
if (ANDROID_URI.equals(attribute.getNamespaceURI())) {
String name = attribute.getLocalName();
if (!(name.equals(ATTR_LAYOUT_WIDTH) && !(name.equals(ATTR_LAYOUT_HEIGHT)) &&
!(name.equals(ATTR_ID)))) {
String owner = "android/R$attr"; //$NON-NLS-1$
attributeApiLevel = mApiDatabase.getFieldVersion(owner, name);
int minSdk = getMinSdk(context);
if (attributeApiLevel > minSdk && attributeApiLevel > context.getFolderVersion()
&& attributeApiLevel > getLocalMinSdk(attribute.getOwnerElement())
&& !isBenignUnusedAttribute(name)
&& !isAlreadyWarnedDrawableFile(context, attribute, attributeApiLevel)) {
if (RtlDetector.isRtlAttributeName(name) || ATTR_SUPPORTS_RTL.equals(name)) {
// No need to warn for example that
// "layout_alignParentEnd will only be used in API level 17 and higher"
// since we have a dedicated RTL lint rule dealing with those attributes
// However, paddingStart in particular is known to cause crashes
// when used on TextViews (and subclasses of TextViews), on some
// devices, because vendor specific attributes conflict with the
// later-added framework resources, and these are apparently read
// by the text views.
//
// However, as of build tools 23.0.1 aapt works around this by packaging
// the resources differently.
if (name.equals(ATTR_PADDING_START)) {
BuildToolInfo buildToolInfo = context.getProject().getBuildTools();
Revision buildTools = buildToolInfo != null
? buildToolInfo.getRevision() : null;
boolean isOldBuildTools = buildTools != null &&
(buildTools.getMajor() < 23 || buildTools.getMajor() == 23
&& buildTools.getMinor() == 0 && buildTools.getMicro() == 0);
if ((buildTools == null || isOldBuildTools) &&
viewMayExtendTextView(attribute.getOwnerElement())) {
Location location = context.getLocation(attribute);
String message = String.format(
"Attribute `%1$s` referenced here can result in a crash on "
+ "some specific devices older than API %2$d "
+ "(current min is %3$d)",
attribute.getLocalName(), attributeApiLevel, minSdk);
//noinspection VariableNotUsedInsideIf
if (buildTools != null) {
message = String.format("Upgrade `buildToolsVersion` from "
+ "`%1$s` to at least `23.0.1`; if not, ",
buildTools.toShortString())
+ Character.toLowerCase(message.charAt(0))
+ message.substring(1);
}
context.report(UNSUPPORTED, attribute, location, message);
}
}
} else {
Location location = context.getLocation(attribute);
String message = String.format(
"Attribute `%1$s` is only used in API level %2$d and higher "
+ "(current min is %3$d)",
attribute.getLocalName(), attributeApiLevel, minSdk);
context.report(UNUSED, attribute, location, message);
}
}
}
// Special case:
// the dividers attribute is present in API 1, but it won't be read on older
// versions, so don't flag the common pattern
// android:divider="?android:attr/dividerHorizontal"
// since this will work just fine. See issue 67440 for more.
if (name.equals("divider")) {
return;
}
}
String value = attribute.getValue();
String owner = null;
String name = null;
String prefix;
if (value.startsWith(ANDROID_PREFIX)) {
prefix = ANDROID_PREFIX;
} else if (value.startsWith(ANDROID_THEME_PREFIX)) {
prefix = ANDROID_THEME_PREFIX;
if (context.getResourceFolderType() == ResourceFolderType.DRAWABLE) {
int api = 21;
int minSdk = getMinSdk(context);
if (api > minSdk && api > context.getFolderVersion()
&& api > getLocalMinSdk(attribute.getOwnerElement())) {
Location location = context.getLocation(attribute);
String message;
message = String.format(
"Using theme references in XML drawables requires API level %1$d "
+ "(current min is %2$d)", api,
minSdk);
context.report(UNSUPPORTED, attribute, location, message);
// Don't flag individual theme attribute requirements here, e.g. once
// we've told you that you need at least v21 to reference themes, we don't
// need to also tell you that ?android:selectableItemBackground requires
// API level 11
return;
}
}
} else if (value.startsWith(PREFIX_ANDROID) && ATTR_NAME.equals(attribute.getName())
&& TAG_ITEM.equals(attribute.getOwnerElement().getTagName())
&& attribute.getOwnerElement().getParentNode() != null
&& TAG_STYLE.equals(attribute.getOwnerElement().getParentNode().getNodeName())) {
owner = "android/R$attr"; //$NON-NLS-1$
name = value.substring(PREFIX_ANDROID.length());
prefix = null;
} else if (value.startsWith(PREFIX_ANDROID) && ATTR_PARENT.equals(attribute.getName())
&& TAG_STYLE.equals(attribute.getOwnerElement().getTagName())) {
owner = "android/R$style"; //$NON-NLS-1$
name = getResourceFieldName(value.substring(PREFIX_ANDROID.length()));
prefix = null;
} else {
return;
}
if (owner == null) {
// Convert @android:type/foo into android/R$type and "foo"
int index = value.indexOf('/', prefix.length());
if (index != -1) {
owner = "android/R$" //$NON-NLS-1$
+ value.substring(prefix.length(), index);
name = getResourceFieldName(value.substring(index + 1));
} else if (value.startsWith(ANDROID_THEME_PREFIX)) {
owner = "android/R$attr"; //$NON-NLS-1$
name = value.substring(ANDROID_THEME_PREFIX.length());
} else {
return;
}
}
int api = mApiDatabase.getFieldVersion(owner, name);
int minSdk = getMinSdk(context);
if (api > minSdk && api > context.getFolderVersion()
&& api > getLocalMinSdk(attribute.getOwnerElement())) {
// Don't complain about resource references in the tools namespace,
// such as for example "tools:layout="@android:layout/list_content",
// used only for designtime previews
if (TOOLS_URI.equals(attribute.getNamespaceURI())) {
return;
}
//noinspection StatementWithEmptyBody
if (attributeApiLevel >= api) {
// The attribute will only be *read* on platforms >= attributeApiLevel.
// If this isn't lower than the attribute reference's API level, it
// won't be a problem
} else if (attributeApiLevel > minSdk) {
String attributeName = attribute.getLocalName();
Location location = context.getLocation(attribute);
String message = String.format(
"`%1$s` requires API level %2$d (current min is %3$d), but note "
+ "that attribute `%4$s` is only used in API level %5$d "
+ "and higher",
name, api, minSdk, attributeName, attributeApiLevel);
context.report(UNSUPPORTED, attribute, location, message);
} else {
Location location = context.getLocation(attribute);
String message = String.format(
"`%1$s` requires API level %2$d (current min is %3$d)",
value, api, minSdk);
context.report(UNSUPPORTED, attribute, location, message);
}
}
}
/**
* Returns true if the view tag is possibly a text view. It may not be certain,
* but will err on the side of caution (for example, any custom view is considered
* to be a potential text view.)
*/
private static boolean viewMayExtendTextView(@NonNull Element element) {
String tag = element.getTagName();
if (tag.equals(VIEW_TAG)) {
tag = element.getAttribute(ATTR_CLASS);
if (tag == null || tag.isEmpty()) {
return false;
}
}
//noinspection SimplifiableIfStatement
if (tag.indexOf('.') != -1) {
// Custom views: not sure. Err on the side of caution.
return true;
}
return tag.contains("Text") // TextView, EditText, etc
|| tag.contains(BUTTON) // Button, ToggleButton, etc
|| tag.equals("DigitalClock")
|| tag.equals("Chronometer")
|| tag.equals(CHECK_BOX)
|| tag.equals(SWITCH);
}
/**
* Returns true if this attribute is in a drawable document with one of the
* root tags that require API 21
*/
private static boolean isAlreadyWarnedDrawableFile(@NonNull XmlContext context,
@NonNull Attr attribute, int attributeApiLevel) {
// Don't complain if it's in a drawable file where we've already
// flagged the root drawable type as being unsupported
if (context.getResourceFolderType() == ResourceFolderType.DRAWABLE
&& attributeApiLevel == 21) {
String root = attribute.getOwnerDocument().getDocumentElement().getTagName();
if (TAG_RIPPLE.equals(root)
|| TAG_VECTOR.equals(root)
|| TAG_ANIMATED_VECTOR.equals(root)
|| TAG_ANIMATED_SELECTOR.equals(root)) {
return true;
}
}
return false;
}
/**
* Is the given attribute a "benign" unused attribute, one we probably don't need to
* flag to the user as not applicable on all versions? These are typically attributes
* which add some nice platform behavior when available, but that are not critical
* and developers would not typically need to be aware of to try to implement workarounds
* on older platforms.
*/
public static boolean isBenignUnusedAttribute(@NonNull String name) {
return ATTR_LABEL_FOR.equals(name)
|| ATTR_TEXT_IS_SELECTABLE.equals(name)
|| "textAlignment".equals(name)
|| ATTR_FULL_BACKUP_CONTENT.equals(name);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
if (mApiDatabase == null) {
return;
}
String tag = element.getTagName();
ResourceFolderType folderType = context.getResourceFolderType();
if (folderType != ResourceFolderType.LAYOUT) {
if (folderType == ResourceFolderType.DRAWABLE) {
checkElement(context, element, TAG_VECTOR, 21, "1.4", UNSUPPORTED);
checkElement(context, element, TAG_RIPPLE, 21, null, UNSUPPORTED);
checkElement(context, element, TAG_ANIMATED_SELECTOR, 21, null, UNSUPPORTED);
checkElement(context, element, TAG_ANIMATED_VECTOR, 21, null, UNSUPPORTED);
checkElement(context, element, "drawable", 24, null, UNSUPPORTED);
if ("layer-list".equals(tag)) {
checkLevelList(context, element);
} else if (tag.contains(".")) {
checkElement(context, element, tag, 24, null, UNSUPPORTED);
}
}
if (element.getParentNode().getNodeType() != Node.ELEMENT_NODE) {
// Root node
return;
}
NodeList childNodes = element.getChildNodes();
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
Node textNode = childNodes.item(i);
if (textNode.getNodeType() == Node.TEXT_NODE) {
String text = textNode.getNodeValue();
if (text.contains(ANDROID_PREFIX)) {
text = text.trim();
// Convert @android:type/foo into android/R$type and "foo"
int index = text.indexOf('/', ANDROID_PREFIX.length());
if (index != -1) {
String typeString = text.substring(ANDROID_PREFIX.length(), index);
if (ResourceType.getEnum(typeString) != null) {
String owner = "android/R$" + typeString;
String name = getResourceFieldName(text.substring(index + 1));
int api = mApiDatabase.getFieldVersion(owner, name);
int minSdk = getMinSdk(context);
if (api > minSdk && api > context.getFolderVersion()
&& api > getLocalMinSdk(element)) {
Location location = context.getLocation(textNode);
String message = String.format(
"`%1$s` requires API level %2$d (current min is %3$d)",
text, api, minSdk);
context.report(UNSUPPORTED, element, location, message);
}
}
}
}
}
}
} else {
if (VIEW_TAG.equals(tag)) {
tag = element.getAttribute(ATTR_CLASS);
if (tag == null || tag.isEmpty()) {
return;
}
} else {
// TODO: Complain if is used at the root level!
checkElement(context, element, TAG, 21, null, UNUSED);
}
// Check widgets to make sure they're available in this version of the SDK.
if (tag.indexOf('.') != -1) {
// Custom views aren't in the index
return;
}
String fqn = "android/widget/" + tag; //$NON-NLS-1$
if (tag.equals("TextureView")) { //$NON-NLS-1$
fqn = "android/view/TextureView"; //$NON-NLS-1$
}
// TODO: Consider other widgets outside of android.widget.*
int api = mApiDatabase.getClassVersion(fqn);
int minSdk = getMinSdk(context);
if (api > minSdk && api > context.getFolderVersion()
&& api > getLocalMinSdk(element)) {
Location location = context.getLocation(element);
String message = String.format(
"View requires API level %1$d (current min is %2$d): `<%3$s>`",
api, minSdk, tag);
context.report(UNSUPPORTED, element, location, message);
}
}
}
/** Checks whether the given element is the given tag, and if so, whether it satisfied
* the minimum version that the given tag is supported in */
private void checkLevelList(@NonNull XmlContext context, @NonNull Element element) {
Node curr = element.getFirstChild();
while (curr != null) {
if (curr.getNodeType() == Node.ELEMENT_NODE
&& TAG_ITEM.equals(curr.getNodeName())) {
Element e = (Element) curr;
if (e.hasAttributeNS(ANDROID_URI, ATTR_WIDTH)
|| e.hasAttributeNS(ANDROID_URI, ATTR_HEIGHT)) {
int attributeApiLevel = 23; // Using width and height on layer-list children requires M
int minSdk = getMinSdk(context);
if (attributeApiLevel > minSdk
&& attributeApiLevel > context.getFolderVersion()
&& attributeApiLevel > getLocalMinSdk(element)) {
for (String attributeName : new String[] { ATTR_WIDTH, ATTR_HEIGHT}) {
Attr attribute = e.getAttributeNodeNS(ANDROID_URI, attributeName);
if (attribute == null) {
continue;
}
Location location = context.getLocation(attribute);
String message = String.format(
"Attribute `%1$s` is only used in API level %2$d and higher "
+ "(current min is %3$d)",
attribute.getLocalName(), attributeApiLevel, minSdk);
context.report(UNUSED, attribute, location, message);
}
}
}
}
curr = curr.getNextSibling();
}
}
/** Checks whether the given element is the given tag, and if so, whether it satisfied
* the minimum version that the given tag is supported in */
private void checkElement(@NonNull XmlContext context, @NonNull Element element,
@NonNull String tag, int api, @Nullable String gradleVersion, @NonNull Issue issue) {
if (tag.equals(element.getTagName())) {
int minSdk = getMinSdk(context);
if (api > minSdk
&& api > context.getFolderVersion()
&& api > getLocalMinSdk(element)
&& !featureProvidedByGradle(context, gradleVersion)) {
Location location = context.getLocation(element);
// For the tag we report it against the class= attribute
if ("drawable".equals(tag)) {
Attr attribute = element.getAttributeNode(ATTR_CLASS);
if (attribute == null) {
return;
}
location = context.getLocation(attribute);
tag = ATTR_CLASS;
}
String message;
if (issue == UNSUPPORTED) {
message = String.format(
"`<%1$s>` requires API level %2$d (current min is %3$d)", tag, api,
minSdk);
if (gradleVersion != null) {
message += String.format(
" or building with Android Gradle plugin %1$s or higher",
gradleVersion);
} else if (tag.contains(".")) {
message = String.format(
"Custom drawables requires API level %1$d (current min is %2$d)",
api, minSdk);
}
} else {
assert issue == UNUSED : issue;
message = String.format(
"`<%1$s>` is only used in API level %2$d and higher "
+ "(current min is %3$d)", tag, api, minSdk);
}
context.report(issue, element, location, message);
}
}
}
protected int getMinSdk(Context context) {
if (mMinApi == -1) {
AndroidVersion minSdkVersion = context.getMainProject().getMinSdkVersion();
mMinApi = minSdkVersion.getFeatureLevel();
}
return mMinApi;
}
// ---- Implements ClassScanner ----
@SuppressWarnings("rawtypes") // ASM API
@Override
public void checkClass(@NonNull final ClassContext context, @NonNull ClassNode classNode) {
if (mApiDatabase == null) {
return;
}
if (AOSP_BUILD && classNode.name.startsWith("android/support/")) { //$NON-NLS-1$
return;
}
// Requires util package (add prebuilts/tools/common/asm-tools/asm-debug-all-4.0.jar)
//classNode.accept(new TraceClassVisitor(new PrintWriter(System.out)));
int classMinSdk = getClassMinSdk(context, classNode);
if (classMinSdk == -1) {
classMinSdk = getMinSdk(context);
}
List methodList = classNode.methods;
if (methodList.isEmpty()) {
return;
}
boolean checkCalls = context.isEnabled(UNSUPPORTED)
|| context.isEnabled(INLINED);
boolean checkMethods = context.isEnabled(OVERRIDE)
&& context.getMainProject().getBuildSdk() >= 1;
String frameworkParent = null;
if (checkMethods) {
LintDriver driver = context.getDriver();
String owner = classNode.superName;
while (owner != null) {
// For virtual dispatch, walk up the inheritance chain checking
// each inherited method
if ((owner.startsWith("android/") //$NON-NLS-1$
&& !owner.startsWith("android/support/")) //$NON-NLS-1$
|| owner.startsWith("java/") //$NON-NLS-1$
|| owner.startsWith("javax/")) { //$NON-NLS-1$
frameworkParent = owner;
break;
}
owner = driver.getSuperClass(owner);
}
if (frameworkParent == null) {
checkMethods = false;
}
}
if (checkCalls) { // Check implements/extends
if (classNode.superName != null) {
String signature = classNode.superName;
checkExtendsClass(context, classNode, classMinSdk, signature);
}
if (classNode.interfaces != null) {
@SuppressWarnings("unchecked") // ASM API
List interfaceList = classNode.interfaces;
for (String signature : interfaceList) {
checkExtendsClass(context, classNode, classMinSdk, signature);
}
}
}
for (Object m : methodList) {
MethodNode method = (MethodNode) m;
int minSdk = getLocalMinSdk(method.invisibleAnnotations);
if (minSdk == -1) {
minSdk = classMinSdk;
}
InsnList nodes = method.instructions;
if (checkMethods && Character.isJavaIdentifierStart(method.name.charAt(0))) {
int buildSdk = context.getMainProject().getBuildSdk();
String name = method.name;
assert frameworkParent != null;
int api = mApiDatabase.getCallVersion(frameworkParent, name, method.desc);
if (api > buildSdk && buildSdk != -1) {
// TODO: Don't complain if it's annotated with @Override; that means
// somehow the build target isn't correct.
String fqcn;
String owner = classNode.name;
if (CONSTRUCTOR_NAME.equals(name)) {
fqcn = "new " + getFqcn(owner); //$NON-NLS-1$
} else {
fqcn = getFqcn(owner) + '#' + 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);
Location location = context.getLocation(method, classNode);
context.report(OVERRIDE, method, null, location, message);
}
}
if (!checkCalls) {
continue;
}
List tryCatchBlocks = method.tryCatchBlocks;
// single-catch blocks are already handled by an AST level check in ApiVisitor
if (tryCatchBlocks.size() > 1) {
List checked = Lists.newArrayList();
for (Object o : tryCatchBlocks) {
TryCatchBlockNode tryCatchBlock = (TryCatchBlockNode) o;
String className = tryCatchBlock.type;
if (className == null || checked.contains(className)) {
continue;
}
int api = mApiDatabase.getClassVersion(className);
if (api > minSdk) {
// Find instruction node
LabelNode label = tryCatchBlock.handler;
String fqcn = getFqcn(className);
String message = String.format(
"Class requires API level %1$d (current min is %2$d): `%3$s`",
api, minSdk, fqcn);
report(context, message, label, method,
className.substring(className.lastIndexOf('/') + 1), null,
SearchHints.create(EOL_NEAREST).matchJavaSymbol());
}
}
}
if (CHECK_DECLARATIONS) {
// Check types in parameter list and types of local variables
List localVariables = method.localVariables;
if (localVariables != null) {
for (Object v : localVariables) {
LocalVariableNode var = (LocalVariableNode) v;
String desc = var.desc;
if (desc.charAt(0) == 'L') {
// "Lpackage/Class;" ⇒ "package/Bar"
String className = desc.substring(1, desc.length() - 1);
int api = mApiDatabase.getClassVersion(className);
if (api > minSdk) {
String fqcn = getFqcn(className);
String message = String.format(
"Class requires API level %1$d (current min is %2$d): `%3$s`",
api, minSdk, fqcn);
report(context, message, var.start, method,
className.substring(className.lastIndexOf('/') + 1), null,
SearchHints.create(NEAREST).matchJavaSymbol());
}
}
}
}
// Check return type
// The parameter types are already handled as local variables so we can skip
// right to the return type.
// Check types in parameter list
String signature = method.desc;
if (signature != null) {
int args = signature.indexOf(')');
if (args != -1 && signature.charAt(args + 1) == 'L') {
String type = signature.substring(args + 2, signature.length() - 1);
int api = mApiDatabase.getClassVersion(type);
if (api > minSdk) {
String fqcn = getFqcn(type);
String message = String.format(
"Class requires API level %1$d (current min is %2$d): `%3$s`",
api, minSdk, fqcn);
AbstractInsnNode first = nodes.size() > 0 ? nodes.get(0) : null;
report(context, message, first, method, method.name, null,
SearchHints.create(BACKWARD).matchJavaSymbol());
}
}
}
}
for (int i = 0, n = nodes.size(); i < n; i++) {
AbstractInsnNode instruction = nodes.get(i);
int type = instruction.getType();
if (type == AbstractInsnNode.METHOD_INSN) {
MethodInsnNode node = (MethodInsnNode) instruction;
String name = node.name;
String owner = node.owner;
String desc = node.desc;
// No need to check methods in this local class; we know they
// won't be an API match
if (node.getOpcode() == Opcodes.INVOKEVIRTUAL
&& owner.equals(classNode.name)) {
owner = classNode.superName;
}
boolean checkingSuperClass = false;
while (owner != null) {
int api = mApiDatabase.getCallVersion(owner, name, desc);
if (api > minSdk) {
if (method.name.startsWith(SWITCH_TABLE_PREFIX)) {
// We're in a compiler-generated method to generate an
// array indexed by enum ordinal values to enum values. The enum
// itself must be requiring a higher API number than is
// currently used, but the call site for the switch statement
// will also be referencing it, so no need to report these
// calls.
break;
}
if (!checkingSuperClass
&& node.getOpcode() == Opcodes.INVOKEVIRTUAL
&& methodDefinedLocally(classNode, name, desc)) {
break;
}
String fqcn;
if (CONSTRUCTOR_NAME.equals(name)) {
fqcn = "new " + getFqcn(owner); //$NON-NLS-1$
} else {
fqcn = getFqcn(owner) + '#' + name;
}
String message = String.format(
"Call requires API level %1$d (current min is %2$d): `%3$s`",
api, minSdk, fqcn);
if (name.equals(ORDINAL_METHOD)
&& instruction.getNext() != null
&& instruction.getNext().getNext() != null
&& instruction.getNext().getOpcode() == Opcodes.IALOAD
&& instruction.getNext().getNext().getOpcode()
== Opcodes.TABLESWITCH) {
message = String.format(
"Enum for switch requires API level %1$d " +
"(current min is %2$d): `%3$s`",
api, minSdk, getFqcn(owner));
}
// 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 (instruction.getOpcode() == Opcodes.INVOKESPECIAL &&
name.equals(method.name) && desc.equals(method.desc) &&
// 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
!name.equals(CONSTRUCTOR_NAME)) {
break;
}
if (isWithinSdkConditional(context, classNode, method, instruction,
api)) {
break;
}
if (api == 19
&& owner.equals("java/lang/ReflectiveOperationException")
&& !method.tryCatchBlocks.isEmpty()) {
boolean direct = false;
for (Object o : method.tryCatchBlocks) {
if (((TryCatchBlockNode)o).type.equals("java/lang/ReflectiveOperationException")) {
direct = true;
break;
}
}
if (!direct) {
message = String.format("Multi-catch with these reflection "
+ "exceptions requires API level 19 (current min is"
+ " %2$d) because they get compiled to the common but "
+ "new super type `ReflectiveOperationException`. "
+ "As a workaround either create individual catch "
+ "statements, or catch `Exception`.",
api, minSdk);
}
}
if (api == 24
&& "java.util.concurrent.ConcurrentHashMap.KeySetView#iterator".equals(fqcn)) {
message += ". The `keySet()` method in `ConcurrentHashMap` "
+ "changed in a backwards incompatible way in Java 8; "
+ "to work around this issue, add an explicit cast to "
+ "`(Map)` before the `keySet()` call.";
}
report(context, message, node, method, name, null,
SearchHints.create(FORWARD).matchJavaSymbol());
break;
}
// For virtual dispatch, walk up the inheritance chain checking
// each inherited method
if (owner.startsWith("android/") //$NON-NLS-1$
|| owner.startsWith("javax/")) { //$NON-NLS-1$
// The API map has already inlined all inherited methods
// so no need to keep checking up the chain
// -- unless it's the support library which is also in
// the android/ namespace:
if (owner.startsWith("android/support/") && api == -1) { //$NON-NLS-1$
owner = context.getDriver().getSuperClass(owner);
} else {
owner = null;
}
} else if (owner.startsWith("java/")) { //$NON-NLS-1$
if (owner.equals("java/text/SimpleDateFormat")) {
checkSimpleDateFormat(context, method, node, minSdk);
}
// Already inlined; see comment above
owner = null;
} else if (node.getOpcode() == Opcodes.INVOKEVIRTUAL) {
owner = context.getDriver().getSuperClass(owner);
} else if (node.getOpcode() == Opcodes.INVOKESTATIC && api == -1) {
// Inherit through static classes as well
owner = context.getDriver().getSuperClass(owner);
} else {
owner = null;
}
checkingSuperClass = true;
}
} else if (type == AbstractInsnNode.FIELD_INSN) {
FieldInsnNode node = (FieldInsnNode) instruction;
String name = node.name;
String owner = node.owner;
int api = mApiDatabase.getFieldVersion(owner, name);
if (api > minSdk) {
if (method.name.startsWith(SWITCH_TABLE_PREFIX)) {
checkSwitchBlock(context, classNode, node, method, name, owner,
api, minSdk);
continue;
}
if (isSkippedEnumSwitch(context, classNode, method, node, owner, api)) {
continue;
}
if (isWithinSdkConditional(context, classNode, method, instruction, api)) {
continue;
}
String fqcn = 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());
}
} else if (type == AbstractInsnNode.LDC_INSN) {
LdcInsnNode node = (LdcInsnNode) instruction;
if (node.cst instanceof Type) {
Type t = (Type) node.cst;
String className = t.getInternalName();
int api = mApiDatabase.getClassVersion(className);
if (api > minSdk) {
String fqcn = getFqcn(className);
String message = String.format(
"Class requires API level %1$d (current min is %2$d): `%3$s`",
api, minSdk, fqcn);
report(context, message, node, method,
className.substring(className.lastIndexOf('/') + 1), null,
SearchHints.create(FORWARD).matchJavaSymbol());
}
}
}
}
}
}
private void checkExtendsClass(ClassContext context, ClassNode classNode, int classMinSdk,
String signature) {
int api = mApiDatabase.getClassVersion(signature);
if (api > classMinSdk) {
String fqcn = getFqcn(signature);
String message = String.format(
"Class requires API level %1$d (current min is %2$d): `%3$s`",
api, classMinSdk, fqcn);
String name = signature.substring(signature.lastIndexOf('/') + 1);
name = name.substring(name.lastIndexOf('$') + 1);
SearchHints hints = SearchHints.create(BACKWARD).matchJavaSymbol();
int lineNumber = ClassContext.findLineNumber(classNode);
Location location = context.getLocationForLine(lineNumber, name, null,
hints);
context.report(UNSUPPORTED, location, message);
}
}
private static void checkSimpleDateFormat(ClassContext context, MethodNode method,
MethodInsnNode node, int minSdk) {
if (minSdk >= 9) {
// Already OK
return;
}
if (node.name.equals(CONSTRUCTOR_NAME) && !node.desc.equals("()V")) { //$NON-NLS-1$
// Check first argument
AbstractInsnNode prev = LintUtils.getPrevInstruction(node);
if (prev != null && !node.desc.equals("(Ljava/lang/String;)V")) { //$NON-NLS-1$
prev = LintUtils.getPrevInstruction(prev);
}
if (prev != null && prev.getOpcode() == Opcodes.LDC) {
LdcInsnNode ldc = (LdcInsnNode) prev;
Object cst = ldc.cst;
if (cst instanceof String) {
String pattern = (String) cst;
boolean isEscaped = false;
for (int i = 0; i < pattern.length(); i++) {
char c = pattern.charAt(i);
if (c == '\'') {
isEscaped = !isEscaped;
} else if (!isEscaped && (c == 'L' || c == 'c')) {
String message = String.format(
"The pattern character '%1$c' requires API level 9 (current " +
"min is %2$d) : \"`%3$s`\"", c, minSdk, pattern);
report(context, message, node, method, pattern, null,
SearchHints.create(FORWARD));
return;
}
}
}
}
}
}
@SuppressWarnings("rawtypes") // ASM API
private static boolean methodDefinedLocally(ClassNode classNode, String name, String desc) {
List methodList = classNode.methods;
for (Object m : methodList) {
MethodNode method = (MethodNode) m;
if (name.equals(method.name) && desc.equals(method.desc)) {
return true;
}
}
return false;
}
@SuppressWarnings("rawtypes") // ASM API
private static void checkSwitchBlock(ClassContext context, ClassNode classNode,
FieldInsnNode field, MethodNode method, String name, String owner, int api,
int minSdk) {
// Switch statements on enums are tricky. The compiler will generate a method
// which returns an array of the enum constants, indexed by their ordinal() values.
// However, we only want to complain if the code is actually referencing one of
// the non-available enum fields.
//
// For the android.graphics.PorterDuff.Mode enum for example, the first few items
// in the array are populated like this:
//
// L0
// ALOAD 0
// GETSTATIC android/graphics/PorterDuff$Mode.ADD : Landroid/graphics/PorterDuff$Mode;
// INVOKEVIRTUAL android/graphics/PorterDuff$Mode.ordinal ()I
// ICONST_1
// IASTORE
// L1
// GOTO L3
// L2
// FRAME FULL [[I] [java/lang/NoSuchFieldError]
// POP
// L3
// FRAME SAME
// ALOAD 0
// GETSTATIC android/graphics/PorterDuff$Mode.CLEAR : Landroid/graphics/PorterDuff$Mode;
// INVOKEVIRTUAL android/graphics/PorterDuff$Mode.ordinal ()I
// ICONST_2
// IASTORE
// ...
// So if we for example find that the "ADD" field isn't accessible, since it requires
// API 11, we need to
// (1) First find out what its ordinal number is. We can look at the following
// instructions to discover this; it's the "ICONST_1" and "IASTORE" instructions.
// (After ICONST_5 it moves on to BIPUSH 6, BIPUSH 7, etc.)
// (2) Find the corresponding *usage* of this switch method. For the above enum,
// the switch ordinal lookup method will be called
// "$SWITCH_TABLE$android$graphics$PorterDuff$Mode" with desc "()[I".
// This means we will be looking for an invocation in some other method which looks
// like this:
// INVOKESTATIC (current class).$SWITCH_TABLE$android$graphics$PorterDuff$Mode ()[I
// (obviously, it can be invoked more than once)
// Note that it can be used more than once in this class and all sites should be
// checked!
// (3) Look up the corresponding table switch, which should look something like this:
// INVOKESTATIC (current class).$SWITCH_TABLE$android$graphics$PorterDuff$Mode ()[I
// ALOAD 0
// INVOKEVIRTUAL android/graphics/PorterDuff$Mode.ordinal ()I
// IALOAD
// LOOKUPSWITCH
// 2: L1
// 11: L2
// default: L3
// Here we need to see if the LOOKUPSWITCH instruction is referencing our target
// case. Above we were looking for the "ADD" case which had ordinal 1. Since this
// isn't explicitly referenced, we can ignore this field reference.
AbstractInsnNode next = field.getNext();
if (next == null || next.getOpcode() != Opcodes.INVOKEVIRTUAL) {
return;
}
next = next.getNext();
if (next == null) {
return;
}
int ordinal;
switch (next.getOpcode()) {
case Opcodes.ICONST_0: ordinal = 0; break;
case Opcodes.ICONST_1: ordinal = 1; break;
case Opcodes.ICONST_2: ordinal = 2; break;
case Opcodes.ICONST_3: ordinal = 3; break;
case Opcodes.ICONST_4: ordinal = 4; break;
case Opcodes.ICONST_5: ordinal = 5; break;
case Opcodes.BIPUSH: {
IntInsnNode iin = (IntInsnNode) next;
ordinal = iin.operand;
break;
}
default:
return;
}
// Find usages of this call site
List methodList = classNode.methods;
for (Object m : methodList) {
InsnList nodes = ((MethodNode) m).instructions;
for (int i = 0, n = nodes.size(); i < n; i++) {
AbstractInsnNode instruction = nodes.get(i);
if (instruction.getOpcode() != Opcodes.INVOKESTATIC){
continue;
}
MethodInsnNode node = (MethodInsnNode) instruction;
if (node.name.equals(method.name)
&& node.desc.equals(method.desc)
&& node.owner.equals(classNode.name)) {
// Find lookup switch
AbstractInsnNode target = getNextInstruction(node);
while (target != null) {
if (target.getOpcode() == Opcodes.LOOKUPSWITCH) {
LookupSwitchInsnNode lookup = (LookupSwitchInsnNode) target;
@SuppressWarnings("unchecked") // ASM API
List keys = lookup.keys;
if (keys != null && keys.contains(ordinal)) {
String fqcn = getFqcn(owner) + '#' + name;
String message = String.format(
"Enum value requires API level %1$d " +
"(current min is %2$d): `%3$s`",
api, minSdk, fqcn);
report(context, message, lookup, (MethodNode) m, name, null,
SearchHints.create(FORWARD).matchJavaSymbol());
// Break out of the inner target search only; the switch
// statement could be used in other places in this class as
// well and we want to report all problematic usages.
break;
}
}
target = getNextInstruction(target);
}
}
}
}
}
private static boolean isEnumSwitchInitializer(ClassNode classNode) {
@SuppressWarnings("rawtypes") // ASM API
List fieldList = classNode.fields;
for (Object f : fieldList) {
FieldNode field = (FieldNode) f;
if (field.name.startsWith(ENUM_SWITCH_PREFIX)) {
return true;
}
}
return false;
}
private static MethodNode findEnumSwitchUsage(ClassNode classNode, String owner) {
String target = ENUM_SWITCH_PREFIX + owner.replace('/', '$');
@SuppressWarnings("rawtypes") // ASM API
List methodList = classNode.methods;
for (Object f : methodList) {
MethodNode method = (MethodNode) f;
InsnList nodes = method.instructions;
for (int i = 0, n = nodes.size(); i < n; i++) {
AbstractInsnNode instruction = nodes.get(i);
if (instruction.getOpcode() == Opcodes.GETSTATIC) {
FieldInsnNode field = (FieldInsnNode) instruction;
if (field.name.equals(target)) {
return method;
}
}
}
}
return null;
}
private static boolean isSkippedEnumSwitch(ClassContext context, ClassNode classNode,
MethodNode method, FieldInsnNode node, String owner, int api) {
// Enum-style switches are handled in a different way: it generates
// an innerclass where the class initializer creates a mapping from
// the ordinals to the corresponding values.
// Here we need to check to see if the call site which *used* the
// table switch had a suppress node on it (or up that node's parent
// chain
AbstractInsnNode next = getNextInstruction(node);
if (next != null && next.getOpcode() == Opcodes.INVOKEVIRTUAL
&& CLASS_CONSTRUCTOR.equals(method.name)
&& ORDINAL_METHOD.equals(((MethodInsnNode) next).name)
&& classNode.outerClass != null
&& isEnumSwitchInitializer(classNode)) {
LintDriver driver = context.getDriver();
ClassNode outer = driver.getOuterClassNode(classNode);
if (outer != null) {
MethodNode switchUser = findEnumSwitchUsage(outer, owner);
if (switchUser != null) {
// Is the API check suppressed at the call site?
if (driver.isSuppressed(UNSUPPORTED, outer, switchUser,
null)) {
return true;
}
// Is there a @TargetAPI annotation on the method or
// class referencing this switch map class?
if (getLocalMinSdk(switchUser.invisibleAnnotations) >= api
|| getLocalMinSdk(outer.invisibleAnnotations) >= api) {
return true;
}
}
}
}
return false;
}
/**
* Return the {@code @TargetApi} level to use for the given {@code classNode};
* this will be the {@code @TargetApi} annotation on the class, or any outer
* methods (for anonymous inner classes) or outer classes (for inner classes)
* of the given class.
*/
private static int getClassMinSdk(ClassContext context, ClassNode classNode) {
int classMinSdk = getLocalMinSdk(classNode.invisibleAnnotations);
if (classMinSdk != -1) {
return classMinSdk;
}
LintDriver driver = context.getDriver();
while (classNode != null) {
ClassNode prev = classNode;
classNode = driver.getOuterClassNode(classNode);
if (classNode != null) {
// TODO: Should this be "curr" instead?
if (prev.outerMethod != null) {
@SuppressWarnings("rawtypes") // ASM API
List methods = classNode.methods;
for (Object m : methods) {
MethodNode method = (MethodNode) m;
if (method.name.equals(prev.outerMethod)
&& method.desc.equals(prev.outerMethodDesc)) {
// Found the outer method for this anonymous class; check method
// annotations on it, then continue up the class hierarchy
int methodMinSdk = getLocalMinSdk(method.invisibleAnnotations);
if (methodMinSdk != -1) {
return methodMinSdk;
}
break;
}
}
}
classMinSdk = getLocalMinSdk(classNode.invisibleAnnotations);
if (classMinSdk != -1) {
return classMinSdk;
}
}
}
return -1;
}
/**
* Returns the minimum SDK to use according to the given annotation list, or
* -1 if no annotation was found.
*
* @param annotations a list of annotation nodes from ASM
* @return the API level to use for this node, or -1
*/
@SuppressWarnings({"unchecked", "rawtypes"})
private static int getLocalMinSdk(List annotations) {
if (annotations != null) {
for (AnnotationNode annotation : (List)annotations) {
String desc = annotation.desc;
if (desc.endsWith(TARGET_API_VMSIG)) {
if (annotation.values != null) {
for (int i = 0, n = annotation.values.size(); i < n; i += 2) {
String key = (String) annotation.values.get(i);
if (key.equals(ATTR_VALUE)) { //$NON-NLS-1$
Object value = annotation.values.get(i + 1);
if (value instanceof Integer) {
return (Integer) value;
}
}
}
}
} else if (desc.endsWith(REQ_API_VMSIG)) {
if (annotation.values != null) {
for (int i = 0, n = annotation.values.size(); i < n; i += 2) {
String key = (String) annotation.values.get(i);
if (key.equals(ATTR_VALUE) || key.equals("api")) {
Object value = annotation.values.get(i + 1);
if (value instanceof Integer) {
int api = (Integer) value;
if (api > 1) {
return api;
}
}
}
}
}
}
}
}
return -1;
}
/**
* Returns the minimum SDK to use in the given element context, or -1 if no
* {@code tools:targetApi} attribute was found.
*
* @param element the element to look at, including parents
* @return the API level to use for this element, or -1
*/
private static int getLocalMinSdk(@NonNull Element element) {
//noinspection ConstantConditions
while (element != null) {
String targetApi = element.getAttributeNS(TOOLS_URI, ATTR_TARGET_API);
if (targetApi != null && !targetApi.isEmpty()) {
if (Character.isDigit(targetApi.charAt(0))) {
try {
return Integer.parseInt(targetApi);
} catch (NumberFormatException e) {
break;
}
} else {
return SdkVersionInfo.getApiByBuildCode(targetApi, true);
}
}
Node parent = element.getParentNode();
if (parent != null && parent.getNodeType() == Node.ELEMENT_NODE) {
element = (Element) parent;
} else {
break;
}
}
return -1;
}
/**
* Checks if the current project supports features added in {@code minGradleVersion} version of
* the Android gradle plugin.
*
* @param context Current context.
* @param minGradleVersionString Version in which support for a given feature was added, or null
* if it's not supported at build time.
*/
private static boolean featureProvidedByGradle(@NonNull XmlContext context,
@Nullable String minGradleVersionString) {
if (minGradleVersionString == null) {
return false;
}
GradleVersion gradleModelVersion = context.getProject().getGradleModelVersion();
if (gradleModelVersion != null) {
GradleVersion minVersion = GradleVersion.tryParse(minGradleVersionString);
if (minVersion != null
&& gradleModelVersion.compareIgnoringQualifiers(minVersion) >= 0) {
return true;
}
}
return false;
}
private static void report(final ClassContext context, String message, AbstractInsnNode node,
MethodNode method, String patternStart, String patternEnd, SearchHints hints) {
int lineNumber = node != null ? ClassContext.findLineNumber(node) : -1;
// If looking for a constructor, the string we'll see in the source is not the
// method name () but the class name
if (patternStart != null && patternStart.equals(CONSTRUCTOR_NAME)
&& node instanceof MethodInsnNode) {
if (hints != null) {
hints = hints.matchConstructor();
}
patternStart = ((MethodInsnNode) node).owner;
}
if (patternStart != null) {
int index = patternStart.lastIndexOf('$');
if (index != -1) {
patternStart = patternStart.substring(index + 1);
}
index = patternStart.lastIndexOf('/');
if (index != -1) {
patternStart = patternStart.substring(index + 1);
}
}
Location location = context.getLocationForLine(lineNumber, patternStart, patternEnd,
hints);
context.report(UNSUPPORTED, method, node, location, message);
}
// ---- Implements JavaScanner ----
@Nullable
@Override
public JavaElementVisitor createPsiVisitor(@NonNull JavaContext context) {
if (mApiDatabase == null) {
return new JavaElementVisitor() {
@Override
public void visitElement(PsiElement element) {
// No-op. Workaround for super currently calling
// ProgressIndicatorProvider.checkCanceled();
}
};
}
return new ApiVisitor(context);
}
@Override
public List> getApplicablePsiTypes() {
List> types = new ArrayList>(9);
types.add(PsiImportStaticStatement.class);
types.add(PsiReferenceExpression.class);
types.add(PsiLocalVariable.class);
types.add(PsiTryStatement.class);
types.add(PsiTypeCastExpression.class);
types.add(PsiAssignmentExpression.class);
types.add(PsiCallExpression.class);
types.add(PsiClass.class);
types.add(PsiMethod.class);
return types;
}
/**
* 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
* @param name the name of the constant
* @param owner the field owner
* @return true if the given usage is safe on older versions than the introduction
* level of the constant
*/
public static boolean isBenignConstantUsage(
@Nullable PsiElement 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;
}
if (owner.equals("android/widget/AbsListView") //$NON-NLS-1$
&& ((name.equals("CHOICE_MODE_NONE") //$NON-NLS-1$
|| name.equals("CHOICE_MODE_MULTIPLE") //$NON-NLS-1$
|| name.equals("CHOICE_MODE_SINGLE")))) { //$NON-NLS-1$
// android.widget.ListView#CHOICE_MODE_MULTIPLE and friends have API=1,
// but in API 11 it was moved up to the parent class AbsListView.
// Referencing AbsListView#CHOICE_MODE_MULTIPLE technically requires API 11,
// but the constant is the same as the older version, so accept this without
// warning.
return true;
}
// Gravity#START and Gravity#END are okay; these were specifically written to
// be backwards compatible (by using the same lower bits for START as LEFT and
// for END as RIGHT)
if ("android/view/Gravity".equals(owner) //$NON-NLS-1$
&& ("START".equals(name) || "END".equals(name))) { //$NON-NLS-1$ //$NON-NLS-2$
return true;
}
if (node == null) {
return false;
}
// 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
PsiElement curr = node.getParent();
while (curr != null) {
if (curr instanceof PsiSwitchLabelStatement) {
PsiExpression condition = ((PsiSwitchLabelStatement) curr).getCaseValue();
return condition != null && PsiTreeUtil.isAncestor(condition, node, false);
} else if (curr instanceof PsiIfStatement) {
PsiExpression condition = ((PsiIfStatement) curr).getCondition();
return condition != null && PsiTreeUtil.isAncestor(condition, node, false);
} else if (curr instanceof PsiConditionalExpression) {
PsiExpression condition = ((PsiConditionalExpression) curr).getCondition();
return PsiTreeUtil.isAncestor(condition, node, false);
} else if (curr instanceof PsiMethod || curr instanceof PsiClass) {
break;
}
curr = curr.getParent();
}
return false;
}
private final class ApiVisitor extends JavaElementVisitor {
private final JavaContext mContext;
private ApiVisitor(JavaContext context) {
mContext = context;
}
@Override
public void visitImportStaticStatement(PsiImportStaticStatement statement) {
if (!statement.isOnDemand()) {
PsiElement resolved = statement.resolve();
if (resolved instanceof PsiField) {
checkField(statement, (PsiField)resolved);
}
}
}
@Override
public void visitReferenceExpression(PsiReferenceExpression expression) {
if (skipParentheses(expression.getParent()) instanceof PsiReferenceExpression) {
// 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;
}
PsiElement resolved = expression.resolve();
if (resolved instanceof PsiField) {
checkField(expression, (PsiField)resolved);
}
}
@Override
public void visitTypeCastExpression(PsiTypeCastExpression expression) {
PsiTypeElement castTypeElement = expression.getCastType();
PsiExpression operand = expression.getOperand();
if (operand == null || castTypeElement == null) {
return;
}
PsiType operandType = operand.getType();
PsiType castType = castTypeElement.getType();
if (castType.equals(operandType)) {
return;
}
if (!(operandType instanceof PsiClassType)) {
return;
}
if (!(castType instanceof PsiClassType)) {
return;
}
PsiClassType classType = (PsiClassType)operandType;
PsiClassType interfaceType = (PsiClassType)castType;
checkCast(expression, classType, interfaceType);
}
private void checkCast(@NonNull PsiElement node, @NonNull PsiClassType classType, @NonNull PsiClassType interfaceType) {
if (classType.equals(interfaceType)) {
return;
}
JavaEvaluator evaluator = mContext.getEvaluator();
String classTypeInternal = evaluator.getInternalName(classType);
String interfaceTypeInternal = evaluator.getInternalName(interfaceType);
if ("java/lang/Object".equals(interfaceTypeInternal)) {
return;
}
int api = mApiDatabase.getValidCastVersion(classTypeInternal, interfaceTypeInternal);
if (api == -1) {
return;
}
int minSdk = getMinSdk(mContext);
if (api <= minSdk) {
return;
}
if (isSuppressed(api, node, minSdk)) {
return;
}
Location location = mContext.getLocation(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);
mContext.report(UNSUPPORTED, location, message);
}
@Override
public void visitMethod(PsiMethod method) {
// API check for default methods
if (method.getModifierList().hasExplicitModifier(PsiModifier.DEFAULT)) {
int api = 24; // minSdk for default methods
int minSdk = getMinSdk(mContext);
if (!isSuppressed(api, method, minSdk)) {
Location location = mContext.getLocation(method);
String message = String.format("Default method requires API level %1$d "
+ "(current min is %2$d)", api, minSdk);
mContext.report(UNSUPPORTED, method, location, message);
}
}
}
@Override
public void visitClass(PsiClass aClass) {
// Check for repeatable annotations
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(mContext);
if (!isSuppressed(api, aClass, minSdk)) {
Location location = mContext.getLocation(annotation);
String message = String.format("Repeatable annotation requires "
+ "API level %1$d (current min is %2$d)", api, minSdk);
mContext.report(UNSUPPORTED, annotation, location, message);
}
} else if ("java.lang.annotation.Target".equals(name)) {
PsiNameValuePair[] attributes = annotation.getParameterList()
.getAttributes();
for (PsiNameValuePair pair : attributes) {
PsiAnnotationMemberValue value = pair.getValue();
if (value instanceof PsiArrayInitializerMemberValue) {
PsiArrayInitializerMemberValue array
= (PsiArrayInitializerMemberValue) value;
for (PsiAnnotationMemberValue t : array.getInitializers()) {
checkAnnotationTarget(t, modifierList);
}
} else if (value != null) {
checkAnnotationTarget(value, modifierList);
}
}
}
}
}
}
}
private void checkAnnotationTarget(@NonNull PsiAnnotationMemberValue element,
PsiModifierList modifierList) {
if (element instanceof PsiReferenceExpression) {
PsiReferenceExpression ref = (PsiReferenceExpression) element;
if ("TYPE_PARAMETER".equals(ref.getReferenceName())
|| "TYPE_USE".equals(ref.getReferenceName())) {
PsiAnnotation retention = modifierList
.findAnnotation("java.lang.annotation.Retention");
if (retention == null ||
retention.getText().contains("RUNTIME")) {
Location location = mContext.getLocation(element);
String message = String.format("Type annotations are not "
+ "supported in Android: %1$s", ref.getReferenceName());
mContext.report(UNSUPPORTED, element, location, message);
}
}
}
}
@Override
public void visitCallExpression(PsiCallExpression expression) {
PsiMethod method = expression.resolveMethod();
if (method != null) {
PsiParameterList parameterList = method.getParameterList();
if (parameterList.getParametersCount() > 0) {
PsiParameter[] parameters = parameterList.getParameters();
PsiExpressionList argumentList = expression.getArgumentList();
if (argumentList != null) {
PsiExpression[] arguments = argumentList.getExpressions();
for (int i = 0; i < parameters.length; i++) {
PsiType parameterType = parameters[i].getType();
if (parameterType instanceof PsiClassType) {
if (i >= arguments.length) {
// We can end up with more arguments than parameters when
// there is a varargs call.
break;
}
PsiExpression argument = arguments[i];
PsiType argumentType = argument.getType();
if (argumentType == null || parameterType.equals(argumentType)
|| !(argumentType instanceof PsiClassType)) {
continue;
}
checkCast(argument, (PsiClassType) argumentType,
(PsiClassType) parameterType);
}
}
}
}
PsiModifierList modifierList = method.getModifierList();
if (!checkRequiresApi(expression, method, modifierList)) {
PsiClass containingClass = method.getContainingClass();
if (containingClass != null) {
modifierList = containingClass.getModifierList();
if (modifierList != null) {
checkRequiresApi(expression, method, modifierList);
}
}
}
}
}
// Look for @RequiresApi in modifier lists
private boolean checkRequiresApi(PsiCallExpression expression, PsiMethod method,
PsiModifierList modifierList) {
for (PsiAnnotation annotation : modifierList.getAnnotations()) {
if (REQUIRES_API_ANNOTATION.equals(annotation.getQualifiedName())) {
int api = (int) SupportAnnotationDetector.getLongAttribute(annotation,
ATTR_VALUE, -1);
if (api <= 1) {
// @RequiresApi has two aliasing attributes: api and value
api = (int) SupportAnnotationDetector.getLongAttribute(annotation,
"api", -1);
}
int minSdk = getMinSdk(mContext);
if (api > minSdk) {
int target = getTargetApi(expression);
if (target == -1 || api > target) {
if (ApiDetector.isWithinVersionCheckConditional(expression, api)) {
return true;
}
if (ApiDetector.isPrecededByVersionCheckExit(expression, api)) {
return true;
}
Location location;
location = mContext.getLocation(expression);
String fqcn = method.getName();
String message = String.format(
"Call requires API level %1$d (current min is %2$d): `%3$s`",
api, minSdk, fqcn);
mContext.report(UNSUPPORTED, location, message);
}
}
return true;
}
}
return false;
}
@Override
public void visitLocalVariable(PsiLocalVariable variable) {
PsiExpression initializer = variable.getInitializer();
if (initializer == null) {
return;
}
PsiType initializerType = initializer.getType();
if (!(initializerType instanceof PsiClassType)) {
return;
}
PsiType interfaceType = variable.getType();
if (initializerType.equals(interfaceType)) {
return;
}
if (!(interfaceType instanceof PsiClassType)) {
return;
}
checkCast(initializer, (PsiClassType)initializerType, (PsiClassType)interfaceType);
}
@Override
public void visitAssignmentExpression(PsiAssignmentExpression expression) {
PsiExpression rExpression = expression.getRExpression();
if (rExpression == null) {
return;
}
PsiType rhsType = rExpression.getType();
if (!(rhsType instanceof PsiClassType)) {
return;
}
PsiType interfaceType = expression.getLExpression().getType();
if (rhsType.equals(interfaceType)) {
return;
}
if (!(interfaceType instanceof PsiClassType)) {
return;
}
checkCast(rExpression, (PsiClassType)rhsType, (PsiClassType)interfaceType);
}
@Override
public void visitTryStatement(PsiTryStatement statement) {
PsiResourceList resourceList = statement.getResourceList();
//noinspection VariableNotUsedInsideIf
if (resourceList != null) {
int api = 19; // minSdk for try with resources
int minSdk = getMinSdk(mContext);
if (api > minSdk && api > getTargetApi(statement)) {
Location location = mContext.getLocation(statement);
String message = String.format("Try-with-resources requires "
+ "API level %1$d (current min is %2$d)", api, minSdk);
LintDriver driver = mContext.getDriver();
if (!driver.isSuppressed(mContext, UNSUPPORTED, statement)) {
mContext.report(UNSUPPORTED, statement, location, message);
}
}
}
for (PsiParameter parameter : statement.getCatchBlockParameters()) {
PsiTypeElement typeElement = parameter.getTypeElement();
if (typeElement != null) {
checkCatchTypeElement(statement, typeElement, typeElement.getType());
}
}
}
private void checkCatchTypeElement(@NonNull PsiTryStatement statement,
@NonNull PsiTypeElement typeElement,
@Nullable PsiType type) {
PsiClass resolved = null;
if (type instanceof PsiDisjunctionType) {
PsiDisjunctionType disjunctionType = (PsiDisjunctionType)type;
type = disjunctionType.getLeastUpperBound();
if (type instanceof PsiClassType) {
resolved = ((PsiClassType)type).resolve();
}
for (PsiElement child : typeElement.getChildren()) {
if (child instanceof PsiTypeElement) {
PsiTypeElement childTypeElement = (PsiTypeElement)child;
PsiType childType = childTypeElement.getType();
if (!type.equals(childType)) {
checkCatchTypeElement(statement, childTypeElement, childType);
}
}
}
} else if (type instanceof PsiClassType) {
resolved = ((PsiClassType)type).resolve();
}
if (resolved != null) {
String signature = mContext.getEvaluator().getInternalName(resolved);
int api = mApiDatabase.getClassVersion(signature);
if (api == -1) {
return;
}
int minSdk = getMinSdk(mContext);
if (api <= minSdk) {
return;
}
int target = getTargetApi(statement);
if (target != -1 && api <= target) {
return;
}
Location location;
location = mContext.getLocation(typeElement);
String fqcn = resolved.getName();
String message = String.format("Class requires API level %1$d (current min is %2$d): %3$s", api, minSdk, fqcn);
// Special case reflective operation exception which can be implicitly used
// with multi-catches: see issue 153406
if (api == 19 && "ReflectiveOperationException".equals(fqcn)) {
message = String.format("Multi-catch with these reflection exceptions requires API level 19 (current min is %2$d) " +
"because they get compiled to the common but new super type `ReflectiveOperationException`. " +
"As a workaround either create individual catch statements, or catch `Exception`.",
api, minSdk);
}
mContext.report(UNSUPPORTED, location, message);
}
}
/**
* 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 PsiElement node, @NonNull PsiField field) {
PsiType type = field.getType();
// Only look for compile time constants. See JLS 15.28 and JLS 13.4.9.
if (!(type instanceof PsiPrimitiveType) && !LintUtils.isString(type)) {
return false;
}
String name = field.getName();
PsiClass containingClass = field.getContainingClass();
if (containingClass == null || name == null) {
return false;
}
String owner = mContext.getEvaluator().getInternalName(containingClass);
int api = mApiDatabase.getFieldVersion(owner, name);
if (api != -1) {
int minSdk = getMinSdk(mContext);
if (api > minSdk
&& api > getTargetApi(node)) {
if (isBenignConstantUsage(node, name, owner)) {
return true;
}
String fqcn = getFqcn(owner) + '#' + name;
// For import statements, place the underlines only under the
// reference, not the import and static keywords
if (node instanceof PsiImportStatementBase) {
PsiJavaCodeReferenceElement reference
= ((PsiImportStatementBase) node).getImportReference();
if (reference != null) {
node = reference;
}
}
LintDriver driver = mContext.getDriver();
if (driver.isSuppressed(mContext, INLINED, node)) {
return true;
}
// backwards compatibility: lint used to use this issue type so respect
// older suppress annotations
if (driver.isSuppressed(mContext, UNSUPPORTED, node)) {
return true;
}
if (isWithinVersionCheckConditional(node, api)) {
return true;
}
if (isPrecededByVersionCheckExit(node, api)) {
return true;
}
String message = String.format(
"Field requires API level %1$d (current min is %2$d): `%3$s`",
api, minSdk, fqcn);
Location location = mContext.getLocation(node);
mContext.report(INLINED, node, location, message);
}
return true;
}
return false;
}
}
private static boolean isSuppressed(int api, PsiElement element, int minSdk) {
if (api <= minSdk) {
return true;
}
//if (mySeenTargetApi) {
int target = getTargetApi(element);
if (target != -1) {
if (api <= target) {
return true;
}
}
//}
// TODO: This MUST BE RESTORED
// if (context.getDriver().isSuppressed(UNSUPPORTED, element))
// if (/*mySeenSuppress &&*/
// (IntellijLintUtils.isSuppressed(element, myFile, UNSUPPORTED) || IntellijLintUtils.isSuppressed(element, myFile, INLINED))) {
// return true;
// }
if (isWithinVersionCheckConditional(element, api)) {
return true;
}
if (isPrecededByVersionCheckExit(element, api)) {
return true;
}
return false;
}
public static int getTargetApi(@Nullable PsiElement scope) {
while (scope != null) {
if (scope instanceof PsiModifierListOwner) {
PsiModifierList modifierList = ((PsiModifierListOwner) scope).getModifierList();
int targetApi = getTargetApi(modifierList);
if (targetApi != -1) {
return targetApi;
}
}
scope = scope.getParent();
if (scope instanceof PsiFile) {
break;
}
}
return -1;
}
/**
* Returns the API level for the given AST node if specified with
* an {@code @TargetApi} annotation.
*
* @param modifierList the modifier list to check
* @return the target API level, or -1 if not specified
*/
public static int getTargetApi(@Nullable PsiModifierList modifierList) {
if (modifierList == null) {
return -1;
}
for (PsiAnnotation annotation : modifierList.getAnnotations()) {
String fqcn = annotation.getQualifiedName();
if (fqcn != null &&
(fqcn.equals(FQCN_TARGET_API)
|| fqcn.equals(REQUIRES_API_ANNOTATION)
|| fqcn.equals(TARGET_API))) { // when missing imports
PsiAnnotationParameterList parameterList = annotation.getParameterList();
for (PsiNameValuePair pair : parameterList.getAttributes()) {
PsiAnnotationMemberValue v = pair.getValue();
if (v instanceof PsiLiteral) {
PsiLiteral literal = (PsiLiteral)v;
Object value = literal.getValue();
if (value instanceof Integer) {
return (Integer) value;
} else if (value instanceof String) {
return codeNameToApi((String) value);
}
} else if (v instanceof PsiArrayInitializerMemberValue) {
PsiArrayInitializerMemberValue mv = (PsiArrayInitializerMemberValue)v;
for (PsiAnnotationMemberValue mmv : mv.getInitializers()) {
if (mmv instanceof PsiLiteral) {
PsiLiteral literal = (PsiLiteral)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 PsiExpression) {
if (v instanceof PsiReferenceExpression) {
String name = ((PsiReferenceExpression)v).getQualifiedName();
return codeNameToApi(name);
} else {
return codeNameToApi(v.getText());
}
}
}
}
}
return -1;
}
public static int codeNameToApi(@NonNull String text) {
int dotIndex = text.lastIndexOf('.');
if (dotIndex != -1) {
text = text.substring(dotIndex + 1);
}
return SdkVersionInfo.getApiByBuildCode(text, true);
}
public static int getRequiredVersion(@NonNull Issue issue, @NonNull String errorMessage,
@NonNull TextFormat format) {
errorMessage = format.toText(errorMessage);
if (issue == UNSUPPORTED || issue == INLINED) {
Pattern pattern = Pattern.compile("\\s(\\d+)\\s"); //$NON-NLS-1$
Matcher matcher = pattern.matcher(errorMessage);
if (matcher.find()) {
return Integer.parseInt(matcher.group(1));
}
}
return -1;
}
private static boolean isWithinSdkConditional(
@NonNull ClassContext context,
@NonNull ClassNode classNode,
@NonNull MethodNode method,
@NonNull AbstractInsnNode call,
int requiredApi) {
assert requiredApi != -1;
if (!containsSimpleSdkCheck(method)) {
return false;
}
try {
// Search in the control graph, from beginning, up to the target call
// node, to see if it's reachable. The call graph is constructed in a
// special way: we include all control flow edges, *except* those that
// are satisfied by a SDK_INT version check (where the operand is a version
// that is at least as high as the one needed for the given call).
//
// If we can reach the call, that means that there is a way this call
// can be reached on some versions, and lint should flag the call/field lookup.
//
//
// Let's say you have code like this:
// if (SDK_INT >= LOLLIPOP) {
// // Call
// return property.hasAdjacentMapping();
// }
// ...
//
// The compiler will turn this into the following byte code:
//
// 0: getstatic #3; //Field android/os/Build$VERSION.SDK_INT:I
// 3: bipush 21
// 5: if_icmple 17
// 8: aload_1
// 9: invokeinterface #4, 1; //InterfaceMethod
// android/view/ViewDebug$ExportedProperty.hasAdjacentMapping:()Z
// 14: ifeq 17
// 17: ... code after if loop
//
// When the call graph is constructed, for an if branch we're called twice; once
// where the target is the next instruction (the one taken if byte code check is false)
// and one to the jump label (the one taken if the byte code condition is true).
//
// Notice how at the byte code level, the logic is reversed: the >= instruction
// is turned into "<" and we jump to the code *after* the if clause; otherwise
// it will just fall through. Therefore, if we take a byte code branch, that means
// that the SDK check was *not* satisfied, and conversely, the target call is reachable
// if we don't take the branch.
//
// Therefore, when we build the call graph, we will add call graph nodes for an
// if check if :
// (1) it is some other comparison than <, <= or !=.
// (2) if the byte code comparison check is *not* satisfied, this means that the the
// SDK check was successful and that the call graph should only include
// the jump edge
// (3) all other edges are added
//
// With a flow control graph like that, we can determine whether a target call
// is guarded by a given SDK check: that will be the case if we cannot reach
// the target call in the call graph
ApiCheckGraph graph = new ApiCheckGraph(requiredApi);
ControlFlowGraph.create(graph, classNode, method);
// Note: To debug unit tests, you may want to for example do
// ControlFlowGraph.Node callNode = graph.getNode(call);
// Set highlight = Sets.newHashSet(callNode);
// Files.write(graph.toDot(highlight), new File("/tmp/graph.gv"), Charsets.UTF_8);
// This will generate a graphviz file you can visualize with the "dot" utility
AbstractInsnNode first = method.instructions.get(0);
return !graph.isConnected(first, call);
} catch (AnalyzerException e) {
context.log(e, null);
}
return false;
}
private static boolean containsSimpleSdkCheck(@NonNull MethodNode method) {
// Look for a compiled version of "if (Build.VERSION.SDK_INT op N) {"
InsnList nodes = method.instructions;
for (int i = 0, n = nodes.size(); i < n; i++) {
AbstractInsnNode instruction = nodes.get(i);
if (isSdkVersionLookup(instruction)) {
AbstractInsnNode bipush = getNextInstruction(instruction);
if (bipush != null && bipush.getOpcode() == Opcodes.BIPUSH) {
AbstractInsnNode ifNode = getNextInstruction(bipush);
if (ifNode != null && ifNode.getType() == AbstractInsnNode.JUMP_INSN) {
return true;
}
}
}
}
return false;
}
private static boolean isSdkVersionLookup(@NonNull AbstractInsnNode instruction) {
if (instruction.getOpcode() == Opcodes.GETSTATIC) {
FieldInsnNode fieldNode = (FieldInsnNode) instruction;
return (SDK_INT.equals(fieldNode.name)
&& ANDROID_OS_BUILD_VERSION.equals(fieldNode.owner));
}
return false;
}
/**
* Control flow graph which skips control flow edges that check
* a given SDK_VERSION requirement that is not met by a given call
*/
private static class ApiCheckGraph extends ControlFlowGraph {
private final int mRequiredApi;
public ApiCheckGraph(int requiredApi) {
mRequiredApi = requiredApi;
}
@Override
protected void add(@NonNull AbstractInsnNode from, @NonNull AbstractInsnNode to) {
if (from.getType() == AbstractInsnNode.JUMP_INSN &&
from.getPrevious() != null &&
from.getPrevious().getType() == AbstractInsnNode.INT_INSN) {
IntInsnNode intNode = (IntInsnNode) from.getPrevious();
if (intNode.getPrevious() != null && isSdkVersionLookup(intNode.getPrevious())) {
JumpInsnNode jumpNode = (JumpInsnNode) from;
int api = intNode.operand;
boolean isJumpEdge = to == jumpNode.label;
boolean includeEdge;
switch (from.getOpcode()) {
case Opcodes.IF_ICMPNE:
includeEdge = api < mRequiredApi || isJumpEdge;
break;
case Opcodes.IF_ICMPLE:
includeEdge = api < mRequiredApi - 1 || isJumpEdge;
break;
case Opcodes.IF_ICMPLT:
includeEdge = api < mRequiredApi || isJumpEdge;
break;
case Opcodes.IF_ICMPGE:
includeEdge = api < mRequiredApi || !isJumpEdge;
break;
case Opcodes.IF_ICMPGT:
includeEdge = api < mRequiredApi - 1 || !isJumpEdge;
break;
default:
// unexpected comparison for int API level
includeEdge = true;
}
if (!includeEdge) {
return;
}
}
}
super.add(from, to);
}
}
public 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);
//noinspection VariableNotUsedInsideIf
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);
//noinspection VariableNotUsedInsideIf
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;
}
public 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;
Boolean isConditional = isVersionCheckConditional(api, prev, ifStatement);
if (isConditional != null) {
return isConditional;
}
} else if (current instanceof PsiPolyadicExpression && isAndedWithConditional(current, api, prev)) {
return true;
} else if (current instanceof PsiMethod || current instanceof PsiFile) {
return false;
}
prev = current;
current = current.getParent();
}
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) { } else { ... }
return level >= api && fromThen;
}
else if (tokenType == JavaTokenType.GT) {
// if (SDK_INT > ICE_CREAM_SANDWICH) { } else { ... }
return level >= api - 1 && fromThen;
}
else if (tokenType == JavaTokenType.LE) {
// if (SDK_INT <= ICE_CREAM_SANDWICH) { ... } else { }
return level >= api - 1 && fromElse;
}
else if (tokenType == JavaTokenType.LT) {
// if (SDK_INT < ICE_CREAM_SANDWICH) { ... } else { }
return level >= api && fromElse;
}
else if (tokenType == JavaTokenType.EQEQ) {
// if (SDK_INT == ICE_CREAM_SANDWICH) { } 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;
if (inner.getOperationTokenType() == JavaTokenType.ANDAND) {
return isAndedWithConditional(inner.getLOperand(), api, before) ||
inner.getROperand() != before && isAndedWithConditional(inner.getROperand(), api, before);
} else if (inner.getLOperand() instanceof PsiReferenceExpression &&
SDK_INT.equals(((PsiReferenceExpression)inner.getLOperand()).getReferenceName())) {
int level = -1;
IElementType tokenType = inner.getOperationTokenType();
PsiExpression right = inner.getROperand();
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) {
if (tokenType == JavaTokenType.GE) {
// if (SDK_INT >= ICE_CREAM_SANDWICH &&
return level >= api;
}
else if (tokenType == JavaTokenType.GT) {
// if (SDK_INT > ICE_CREAM_SANDWICH) &&
return level >= api - 1;
}
else if (tokenType == JavaTokenType.EQEQ) {
// if (SDK_INT == ICE_CREAM_SANDWICH) &&
return level >= api;
}
}
}
}
else if (element instanceof PsiPolyadicExpression) {
PsiPolyadicExpression ppe = (PsiPolyadicExpression)element;
if (ppe.getOperationTokenType() == JavaTokenType.ANDAND) {
for (PsiExpression operand : ppe.getOperands()) {
if (operand == before) {
break;
}
else if (isAndedWithConditional(operand, api, before)) {
return true;
}
}
}
}
return false;
}
}