blob: 8219e9ba8aec811b7d798ad0a94eab67bab2f5a4 [file] [log] [blame]
/*
* 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_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.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.PREFIX_ANDROID;
import static com.android.SdkConstants.R_CLASS;
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.detector.api.ClassContext.getFqcn;
import static com.android.tools.lint.detector.api.ClassContext.getInternalName;
import static com.android.tools.lint.detector.api.LintUtils.getNextInstruction;
import static com.android.tools.lint.detector.api.Location.SearchDirection.BACKWARD;
import static com.android.tools.lint.detector.api.Location.SearchDirection.FORWARD;
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.resources.ResourceFolderType;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.SdkVersionInfo;
import com.android.tools.lint.client.api.IssueRegistry;
import com.android.tools.lint.client.api.JavaParser;
import com.android.tools.lint.client.api.LintDriver;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.ClassContext;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.DefaultPosition;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.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.Position;
import com.android.tools.lint.detector.api.ResourceXmlDetector;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.detector.api.TextFormat;
import com.android.tools.lint.detector.api.XmlContext;
import com.android.utils.Pair;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
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.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.analysis.AnalyzerException;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.ast.Annotation;
import lombok.ast.AnnotationElement;
import lombok.ast.AnnotationValue;
import lombok.ast.AstVisitor;
import lombok.ast.BinaryExpression;
import lombok.ast.Case;
import lombok.ast.Catch;
import lombok.ast.ClassDeclaration;
import lombok.ast.ConstructorDeclaration;
import lombok.ast.ConstructorInvocation;
import lombok.ast.Expression;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.If;
import lombok.ast.ImportDeclaration;
import lombok.ast.InlineIfExpression;
import lombok.ast.IntegralLiteral;
import lombok.ast.MethodDeclaration;
import lombok.ast.MethodInvocation;
import lombok.ast.Modifiers;
import lombok.ast.Select;
import lombok.ast.StrictListAccessor;
import lombok.ast.StringLiteral;
import lombok.ast.SuperConstructorInvocation;
import lombok.ast.Switch;
import lombok.ast.Try;
import lombok.ast.TypeReference;
import lombok.ast.VariableDefinition;
import lombok.ast.VariableDefinitionEntry;
import lombok.ast.VariableReference;
/**
* Looks for usages of APIs that are not supported in all the versions targeted
* by this application (according to its minimum API requirement in the manifest).
*/
public class ApiDetector extends ResourceXmlDetector
implements Detector.ClassScanner, Detector.JavaScanner {
/**
* Whether we flag variable, field, parameter and return type declarations of a type
* not yet available. It appears Dalvik is very forgiving and doesn't try to preload
* 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$
/** 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));
/** Accessing an unsupported API */
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));
/** Accessing an inlined API on older platforms */
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 `<tag>` 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 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;
private Map<String, List<Pair<String, Location>>> mPendingFields;
/** Constructs a new API check */
public ApiDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.SLOW;
}
@Override
public void beforeCheckProject(@NonNull Context context) {
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");
}
}
// ---- Implements XmlScanner ----
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return true;
}
@Override
public Collection<String> getApplicableElements() {
return ALL;
}
@Override
public Collection<String> 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)) {
// 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:
if (name.equals(ATTR_PADDING_START) &&
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);
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;
} 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(SdkConstants.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);
}
@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_RIPPLE, 21, UNSUPPORTED);
checkElement(context, element, TAG_VECTOR, 21, UNSUPPORTED);
checkElement(context, element, TAG_ANIMATED_SELECTOR, 21, UNSUPPORTED);
checkElement(context, element, TAG_ANIMATED_VECTOR, 21, 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 owner = "android/R$" //$NON-NLS-1$
+ text.substring(ANDROID_PREFIX.length(), index);
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 <tag> is used at the root level!
checkElement(context, element, TAG, 21, 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 checkElement(@NonNull XmlContext context, @NonNull Element element,
@NonNull String tag, int api, @NonNull Issue issue) {
if (tag.equals(element.getTagName())) {
int minSdk = getMinSdk(context);
if (api > minSdk && api > context.getFolderVersion()
&& api > getLocalMinSdk(element)) {
Location location = context.getLocation(element);
String message;
if (issue == UNSUPPORTED) {
message = String.format(
"`<%1$s>` requires API level %2$d (current min is %3$d)", tag, 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<String> 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 " + ClassContext.getFqcn(owner); //$NON-NLS-1$
} else {
fqcn = ClassContext.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;
}
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 = ClassContext.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 = ClassContext.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 " + ClassContext.getFqcn(owner); //$NON-NLS-1$
} else {
fqcn = ClassContext.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, ClassContext.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;
}
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/")) { //$NON-NLS-1$
owner = context.getDriver().getSuperClass(owner);
} else {
owner = null;
}
} else if (owner.startsWith("java/")) { //$NON-NLS-1$
if (owner.equals(LocaleDetector.DATE_FORMAT_OWNER)) {
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 = ClassContext.getFqcn(owner) + '#' + name;
if (mPendingFields != null) {
mPendingFields.remove(fqcn);
}
String message = String.format(
"Field requires API level %1$d (current min is %2$d): `%3$s`",
api, minSdk, fqcn);
report(context, message, node, method, name, null,
SearchHints.create(FORWARD).matchJavaSymbol());
}
} else if (type == AbstractInsnNode.LDC_INSN) {
LdcInsnNode node = (LdcInsnNode) instruction;
if (node.cst instanceof Type) {
Type t = (Type) node.cst;
String className = t.getInternalName();
int api = mApiDatabase.getClassVersion(className);
if (api > minSdk) {
String fqcn = ClassContext.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 = ClassContext.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<Integer> keys = lookup.keys;
if (keys != null && keys.contains(ordinal)) {
String fqcn = ClassContext.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<AnnotationNode>)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("value")) { //$NON-NLS-1$
Object value = annotation.values.get(i + 1);
if (value instanceof Integer) {
return (Integer) value;
}
}
}
}
}
}
}
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) {
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 nufe) {
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;
}
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 (<init>) 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);
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (mPendingFields != null) {
for (List<Pair<String, Location>> list : mPendingFields.values()) {
for (Pair<String, Location> pair : list) {
String message = pair.getFirst();
Location location = pair.getSecond();
context.report(INLINED, location, message);
}
}
}
super.afterCheckProject(context);
}
// ---- Implements JavaScanner ----
@Nullable
@Override
public AstVisitor createJavaVisitor(@NonNull JavaContext context) {
if (mApiDatabase == null) {
return new ForwardingAstVisitor() {
};
}
return new ApiVisitor(context);
}
@Nullable
@Override
public List<Class<? extends lombok.ast.Node>> getApplicableNodeTypes() {
List<Class<? extends lombok.ast.Node>> types =
new ArrayList<Class<? extends lombok.ast.Node>>(2);
types.add(ImportDeclaration.class);
types.add(Select.class);
types.add(MethodDeclaration.class);
types.add(ConstructorDeclaration.class);
types.add(VariableDefinitionEntry.class);
types.add(VariableReference.class);
types.add(Try.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 lombok.ast.Node node,
@NonNull String name,
@NonNull String owner) {
if (owner.equals("android/os/Build$VERSION_CODES")) { //$NON-NLS-1$
// These constants are required for compilation, not execution
// and valid code checks it even on older platforms
return true;
}
if (owner.equals("android/view/ViewGroup$LayoutParams") //$NON-NLS-1$
&& name.equals("MATCH_PARENT")) { //$NON-NLS-1$
return true;
}
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
lombok.ast.Node curr = node.getParent();
while (curr != null) {
Class<? extends lombok.ast.Node> nodeType = curr.getClass();
if (nodeType == Case.class) {
Case caseStatement = (Case) curr;
Expression condition = caseStatement.astCondition();
return condition != null && isAncestor(condition, node);
} else if (nodeType == If.class) {
If ifStatement = (If) curr;
Expression condition = ifStatement.astCondition();
return condition != null && isAncestor(condition, node);
} else if (nodeType == InlineIfExpression.class) {
InlineIfExpression ifStatement = (InlineIfExpression) curr;
Expression condition = ifStatement.astCondition();
return condition != null && isAncestor(condition, node);
}
curr = curr.getParent();
}
return false;
}
private static boolean isAncestor(
@NonNull lombok.ast.Node ancestor,
@Nullable lombok.ast.Node node) {
while (node != null) {
if (node == ancestor) {
return true;
}
node = node.getParent();
}
return false;
}
private final class ApiVisitor extends ForwardingAstVisitor {
private JavaContext mContext;
private Map<String, String> mClassToImport = Maps.newHashMap();
private List<String> mStarImports;
private Set<String> mLocalVars;
private lombok.ast.Node mCurrentMethod;
private Set<String> mFields;
private List<String> mStaticStarImports;
private ApiVisitor(JavaContext context) {
mContext = context;
}
@Override
public boolean visitImportDeclaration(ImportDeclaration node) {
if (node.astStarImport()) {
// Similarly, if you're inheriting from a constants class, figure out
// how that works... :=(
String fqcn = node.asFullyQualifiedName();
int strip = fqcn.lastIndexOf('*');
if (strip != -1) {
strip = fqcn.lastIndexOf('.', strip);
if (strip != -1) {
String pkgName = getInternalName(fqcn.substring(0, strip));
if (ApiLookup.isRelevantOwner(pkgName)) {
if (node.astStaticImport()) {
if (mStaticStarImports == null) {
mStaticStarImports = Lists.newArrayList();
}
mStaticStarImports.add(pkgName);
} else {
if (mStarImports == null) {
mStarImports = Lists.newArrayList();
}
mStarImports.add(pkgName);
}
}
}
}
} else if (node.astStaticImport()) {
String fqcn = node.asFullyQualifiedName();
String fieldName = getInternalName(fqcn);
int index = fieldName.lastIndexOf('$');
if (index != -1) {
String owner = fieldName.substring(0, index);
String name = fieldName.substring(index + 1);
checkField(node, name, owner);
}
} else {
// Store in map -- if it's "one of ours"
// Use override detector's map for that purpose
String fqcn = node.asFullyQualifiedName();
int last = fqcn.lastIndexOf('.');
if (last != -1) {
String className = fqcn.substring(last + 1);
mClassToImport.put(className, fqcn);
}
}
return super.visitImportDeclaration(node);
}
@Override
public boolean visitSelect(Select node) {
boolean result = super.visitSelect(node);
if (node.getParent() instanceof Select) {
// We only want to look at the leaf expressions; e.g. if you have
// "foo.bar.baz" we only care about the select foo.bar.baz, not foo.bar
return result;
}
// See if this corresponds to a field reference. We assume it's a field if
// it's a select (x.y) and either the identifier y is capitalized (e.g.
// foo.VIEW_MASK) or if it's a member of an R class (R.id.foo).
String name = node.astIdentifier().astValue();
boolean isField = Character.isUpperCase(name.charAt(0));
if (!isField) {
// See if there's an R class
Select current = node;
while (current != null) {
Expression operand = current.astOperand();
if (operand instanceof Select) {
current = (Select) operand;
if (R_CLASS.equals(current.astIdentifier().astValue())) {
isField = true;
break;
}
} else if (operand instanceof VariableReference) {
VariableReference reference = (VariableReference) operand;
if (R_CLASS.equals(reference.astIdentifier().astValue())) {
isField = true;
}
break;
} else {
break;
}
}
}
if (isField) {
Expression operand = node.astOperand();
if (operand.getClass() == Select.class) {
// Possibly a fully qualified name in place
String cls = operand.toString();
// See if it's an imported class with an inner class
// (e.g. Manifest.permission.FIELD)
if (Character.isUpperCase(cls.charAt(0))) {
int firstDot = cls.indexOf('.');
if (firstDot != -1) {
String base = cls.substring(0, firstDot);
String fqcn = mClassToImport.get(base);
if (fqcn != null) {
// Yes imported
String owner = getInternalName(fqcn + cls.substring(firstDot));
checkField(node, name, owner);
return result;
}
// Might be a star import: have to iterate and check here
if (mStarImports != null) {
for (String packagePrefix : mStarImports) {
String owner = getInternalName(packagePrefix + '/' + cls);
if (checkField(node, name, owner)) {
mClassToImport.put(name, owner);
return result;
}
}
}
}
}
// See if it's a fully qualified reference in place
String owner = getInternalName(cls);
checkField(node, name, owner);
return result;
} else if (operand.getClass() == VariableReference.class) {
String className = ((VariableReference) operand).astIdentifier().astValue();
// Not a FQCN that we care about: look in imports
String fqcn = mClassToImport.get(className);
if (fqcn != null) {
// Yes imported
String owner = getInternalName(fqcn);
checkField(node, name, owner);
return result;
}
if (Character.isUpperCase(className.charAt(0))) {
// Might be a star import: have to iterate and check here
if (mStarImports != null) {
for (String packagePrefix : mStarImports) {
String owner = getInternalName(packagePrefix) + '/' + className;
if (checkField(node, name, owner)) {
mClassToImport.put(name, owner);
return result;
}
}
}
}
}
}
return result;
}
@Override
public boolean visitVariableReference(VariableReference node) {
boolean result = super.visitVariableReference(node);
if (node.getParent() != null) {
lombok.ast.Node parent = node.getParent();
Class<? extends lombok.ast.Node> parentClass = parent.getClass();
if (parentClass == Select.class
|| parentClass == Switch.class // look up on the switch expression type
|| parentClass == Case.class
|| parentClass == ConstructorInvocation.class
|| parentClass == SuperConstructorInvocation.class
|| parentClass == AnnotationElement.class) {
return result;
}
if (parent instanceof MethodInvocation &&
((MethodInvocation) parent).astOperand() == node) {
return result;
} else if (parent instanceof BinaryExpression) {
BinaryExpression expression = (BinaryExpression) parent;
if (expression.astLeft() == node) {
return result;
}
}
}
String name = node.astIdentifier().astValue();
if (Character.isUpperCase(name.charAt(0))
&& (mLocalVars == null || !mLocalVars.contains(name))
&& (mFields == null || !mFields.contains(name))) {
// Potential field reference: check it
if (mStaticStarImports != null) {
for (String owner : mStaticStarImports) {
if (checkField(node, name, owner)) {
break;
}
}
}
}
return result;
}
@Override
public boolean visitVariableDefinitionEntry(VariableDefinitionEntry node) {
if (mCurrentMethod != null) {
if (mLocalVars == null) {
mLocalVars = Sets.newHashSet();
}
mLocalVars.add(node.astName().astValue());
} else {
if (mFields == null) {
mFields = Sets.newHashSet();
}
mFields.add(node.astName().astValue());
}
return super.visitVariableDefinitionEntry(node);
}
@Override
public boolean visitMethodDeclaration(MethodDeclaration node) {
mLocalVars = null;
mCurrentMethod = node;
return super.visitMethodDeclaration(node);
}
@Override
public boolean visitConstructorDeclaration(ConstructorDeclaration node) {
mLocalVars = null;
mCurrentMethod = node;
return super.visitConstructorDeclaration(node);
}
@Override
public boolean visitTry(Try node) {
Object nativeNode = node.getNativeNode();
if (nativeNode != null && nativeNode.getClass().getName().equals(
"org.eclipse.jdt.internal.compiler.ast.TryStatement")) {
boolean isTryWithResources = false;
try {
Field field = nativeNode.getClass().getDeclaredField("resources");
Object value = field.get(nativeNode);
if (value instanceof Object[]) {
Object[] resources = (Object[]) value;
isTryWithResources = resources.length > 0;
}
} catch (NoSuchFieldException e) {
// Unexpected: ECJ parser internals have changed; can't detect try block
} catch (IllegalAccessException e) {
// Unexpected: ECJ parser internals have changed; can't detect try block
}
if (isTryWithResources) {
int minSdk = getMinSdk(mContext);
int api = 19; // minSdk for try with resources
if (api > minSdk && api > getLocalMinSdk(node)) {
Location location = mContext.getLocation(node);
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, node)) {
mContext.report(UNSUPPORTED, node, location, message);
}
}
} else {
// Special case: check types of catch block variables; these apparently
// need to be available at runtime even if there are no explicit calls
for (Catch c : node.astCatches()) {
VariableDefinition variableDefinition = c.astExceptionDeclaration();
TypeReference typeReference = variableDefinition.astTypeReference();
String fqcn = null;
JavaParser.ResolvedNode resolved = mContext.resolve(typeReference);
if (resolved != null) {
fqcn = resolved.getSignature();
} else if (typeReference.getTypeName().equals(
"ReflectiveOperationException")) {
fqcn = "java.lang.ReflectiveOperationException";
}
if (fqcn != null) {
String owner = getInternalName(fqcn);
int api = mApiDatabase.getClassVersion(owner);
int minSdk = getMinSdk(mContext);
if (api > minSdk && api > getLocalMinSdk(typeReference)) {
Location location = mContext.getLocation(typeReference);
String message = String.format(
"Class requires API level %1$d (current min is %2$d): `%3$s`",
api, minSdk, fqcn);
LintDriver driver = mContext.getDriver();
if (!driver.isSuppressed(mContext, UNSUPPORTED, typeReference)) {
mContext.report(UNSUPPORTED, typeReference, location, message);
}
}
}
}
}
}
return super.visitTry(node);
}
@Override
public void endVisit(lombok.ast.Node node) {
if (node == mCurrentMethod) {
mCurrentMethod = null;
}
super.endVisit(node);
}
/**
* Checks a Java source field reference. Returns true if the field is known
* regardless of whether it's an invalid field or not
*/
private boolean checkField(
@NonNull lombok.ast.Node node,
@NonNull String name,
@NonNull String owner) {
int api = mApiDatabase.getFieldVersion(owner, name);
if (api != -1) {
int minSdk = getMinSdk(mContext);
if (api > minSdk
&& api > getLocalMinSdk(node)) {
if (isBenignConstantUsage(node, name, owner)) {
return true;
}
Location location = mContext.getLocation(node);
String fqcn = getFqcn(owner) + '#' + name;
if (node instanceof ImportDeclaration) {
// Replace import statement location range with just
// the identifier part
ImportDeclaration d = (ImportDeclaration) node;
int startOffset = d.astParts().first().getPosition().getStart();
Position start = location.getStart();
int startColumn = start.getColumn();
int startLine = start.getLine();
start = new DefaultPosition(startLine,
startColumn + startOffset - start.getOffset(), startOffset);
int fqcnLength = fqcn.length();
Position end = new DefaultPosition(startLine,
start.getColumn() + fqcnLength,
start.getOffset() + fqcnLength);
location = Location.create(location.getFile(), start, end);
}
String message = String.format(
"Field requires API level %1$d (current min is %2$d): `%3$s`",
api, minSdk, fqcn);
LintDriver driver = mContext.getDriver();
if (driver.isSuppressed(mContext, INLINED, node)) {
return true;
}
// Also allow to suppress these issues with NewApi, since some
// fields used to get identified that way
if (driver.isSuppressed(mContext, UNSUPPORTED, node)) {
return true;
}
// We can't report the issue right away; we don't yet know if
// this is an actual inlined (static primitive or String) yet.
// So just make a note of it, and report these after the project
// checking has finished; any fields that aren't inlined will be
// cleared when they're noticed by the class check.
if (mPendingFields == null) {
mPendingFields = Maps.newHashMapWithExpectedSize(20);
}
List<Pair<String, Location>> list = mPendingFields.get(fqcn);
if (list == null) {
list = new ArrayList<Pair<String, Location>>();
mPendingFields.put(fqcn, list);
} else {
// See if this location already exists. This can happen if
// we have multiple references to an inlined field on the same
// line. Since the class file only gives us line information, we
// can't distinguish between these in the client as separate usages,
// so they end up being identical errors.
for (Pair<String, Location> pair : list) {
Location existingLocation = pair.getSecond();
if (location.getFile().equals(existingLocation.getFile())) {
Position start = location.getStart();
Position existingStart = existingLocation.getStart();
if (start != null && existingStart != null
&& start.getLine() == existingStart.getLine()) {
return true;
}
}
}
}
list.add(Pair.of(message, location));
}
return true;
}
return false;
}
/**
* Returns the minimum SDK to use according to the given AST node, or null
* if no {@code TargetApi} annotations were found
*
* @return the API level to use for this node, or -1
*/
public int getLocalMinSdk(@Nullable lombok.ast.Node scope) {
while (scope != null) {
Class<? extends lombok.ast.Node> type = scope.getClass();
// The Lombok AST uses a flat hierarchy of node type implementation classes
// so no need to do instanceof stuff here.
if (type == VariableDefinition.class) {
// Variable
VariableDefinition declaration = (VariableDefinition) scope;
int targetApi = getTargetApi(declaration.astModifiers());
if (targetApi != -1) {
return targetApi;
}
} else if (type == MethodDeclaration.class) {
// Method
// Look for annotations on the method
MethodDeclaration declaration = (MethodDeclaration) scope;
int targetApi = getTargetApi(declaration.astModifiers());
if (targetApi != -1) {
return targetApi;
}
} else if (type == ConstructorDeclaration.class) {
// Constructor
// Look for annotations on the method
ConstructorDeclaration declaration = (ConstructorDeclaration) scope;
int targetApi = getTargetApi(declaration.astModifiers());
if (targetApi != -1) {
return targetApi;
}
} else if (type == ClassDeclaration.class) {
// Class
ClassDeclaration declaration = (ClassDeclaration) scope;
int targetApi = getTargetApi(declaration.astModifiers());
if (targetApi != -1) {
return targetApi;
}
}
scope = scope.getParent();
}
return -1;
}
}
/**
* Returns the API level for the given AST node if specified with
* an {@code @TargetApi} annotation.
*
* @param modifiers the modifier to check
* @return the target API level, or -1 if not specified
*/
public static int getTargetApi(@Nullable Modifiers modifiers) {
if (modifiers == null) {
return -1;
}
StrictListAccessor<Annotation, Modifiers> annotations = modifiers.astAnnotations();
if (annotations == null) {
return -1;
}
for (Annotation annotation : annotations) {
TypeReference t = annotation.astAnnotationTypeReference();
String typeName = t.getTypeName();
if (typeName.endsWith(TARGET_API)) {
StrictListAccessor<AnnotationElement, Annotation> values =
annotation.astElements();
if (values != null) {
for (AnnotationElement element : values) {
AnnotationValue valueNode = element.astValue();
if (valueNode == null) {
continue;
}
if (valueNode instanceof IntegralLiteral) {
IntegralLiteral literal = (IntegralLiteral) valueNode;
return literal.astIntValue();
} else if (valueNode instanceof StringLiteral) {
String value = ((StringLiteral) valueNode).astValue();
return SdkVersionInfo.getApiByBuildCode(value, true);
} else if (valueNode instanceof Select) {
Select select = (Select) valueNode;
String codename = select.astIdentifier().astValue();
return SdkVersionInfo.getApiByBuildCode(codename, true);
} else if (valueNode instanceof VariableReference) {
VariableReference reference = (VariableReference) valueNode;
String codename = reference.astIdentifier().astValue();
return SdkVersionInfo.getApiByBuildCode(codename, true);
}
}
}
}
}
return -1;
}
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<ControlFlowGraph.Node> 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);
}
}
}