
com.android.tools.lint.checks.ApiDetector Maven / Gradle / Ivy
/*
* 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_PARENT;
import static com.android.SdkConstants.ATTR_TARGET_API;
import static com.android.SdkConstants.ATTR_TEXT_IS_SELECTABLE;
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.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 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.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.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.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.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.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
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.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",
"Finds API accesses to APIs that are not supported in all targeted API 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" +
"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-v11` 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",
"Finds inlined fields that may or may not work on older platforms",
"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",
"Finds method declarations that will accidentally override methods in later versions",
"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",
"Finds usages of attributes that will not be used (read) on all targeted 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 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 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$
protected ApiLookup mApiDatabase;
private boolean mWarnedMissingDb;
private int mMinApi = -1;
private Map>> 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", null);
}
}
// ---- 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)
// No need to warn for example that
// "layout_alignParentStart will only be used in API level 17 and higher"
// since we have a dedicated RTL lint rule dealing with those attributes
&& !RtlDetector.isRtlAttributeName(name)) {
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, null);
}
}
// 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 = getFieldName(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 = getFieldName(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, null);
} 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, null);
}
}
}
/**
* 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);
}
private static String getFieldName(String styleName) {
return styleName.replace('.', '_').replace('-', '_').replace(':', '_');
}
@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, "ripple", 21, UNSUPPORTED);
checkElement(context, element, "vector", 21, UNSUPPORTED);
checkElement(context, element, "animated-selector", 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 = text.substring(index + 1);
if (name.indexOf('.') != -1) {
name = name.replace('.', '_');
}
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, null);
}
}
}
}
}
} 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, 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, null);
}
}
}
/** 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, null);
}
}
}
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 " + 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, null);
}
}
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));
}
report(context, message, node, method, name, null,
SearchHints.create(FORWARD).matchJavaSymbol());
}
// 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;
}
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, null);
}
}
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 = 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 = LintUtils.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("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 () 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, null);
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (mPendingFields != null) {
for (List> list : mPendingFields.values()) {
for (Pair pair : list) {
String message = pair.getFirst();
Location location = pair.getSecond();
context.report(INLINED, location, message, null);
}
}
}
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> getApplicableNodeTypes() {
List> types =
new ArrayList>(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;
}
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 mClassToImport = Maps.newHashMap();
private List mStarImports;
private Set mLocalVars;
private lombok.ast.Node mCurrentMethod;
private Set mFields;
private List 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, location, message, null);
}
}
}
}
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> list = mPendingFields.get(fqcn);
if (list == null) {
list = new ArrayList>();
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 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 = getLocalMinSdk(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 = getLocalMinSdk(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 = getLocalMinSdk(declaration.astModifiers());
if (targetApi != -1) {
return targetApi;
}
} else if (type == ClassDeclaration.class) {
// Class
ClassDeclaration declaration = (ClassDeclaration) scope;
int targetApi = getLocalMinSdk(declaration.astModifiers());
if (targetApi != -1) {
return targetApi;
}
}
scope = scope.getParent();
}
return -1;
}
/**
* Returns true if the given AST modifier has a suppress annotation for the
* given issue (which can be null to check for the "all" annotation)
*
* @param modifiers the modifier to check
* @return true if the issue or all issues should be suppressed for this
* modifier
*/
private int getLocalMinSdk(@Nullable Modifiers modifiers) {
if (modifiers == null) {
return -1;
}
StrictListAccessor annotations = modifiers.astAnnotations();
if (annotations == null) {
return -1;
}
Iterator iterator = annotations.iterator();
while (iterator.hasNext()) {
Annotation annotation = iterator.next();
TypeReference t = annotation.astAnnotationTypeReference();
String typeName = t.getTypeName();
if (typeName.endsWith(TARGET_API)) {
StrictListAccessor values =
annotation.astElements();
if (values != null) {
Iterator valueIterator = values.iterator();
while (valueIterator.hasNext()) {
AnnotationElement element = valueIterator.next();
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;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy