diff --git a/idea/src/META-INF/android-lint.xml b/idea/src/META-INF/android-lint.xml
index 4318ec2d1d6..52523fcf0b3 100644
--- a/idea/src/META-INF/android-lint.xml
+++ b/idea/src/META-INF/android-lint.xml
@@ -75,7 +75,6 @@
-
diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/LintUtils.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/LintUtils.java
index f74876e4848..0820559d84e 100755
--- a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/LintUtils.java
+++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/LintUtils.java
@@ -784,7 +784,7 @@ public class LintUtils {
boolean imported = false;
for (UImportStatement importStatement : compilationUnit.getImportStatements()) {
- String fqn = importStatement.getNameToImport();
+ String fqn = importStatement.getFqNameToImport();
if (fqn == null) {
continue;
}
diff --git a/plugins/lint/lint-checks/lint-checks.iml b/plugins/lint/lint-checks/lint-checks.iml
index 358e50b450b..4348c78f8f4 100644
--- a/plugins/lint/lint-checks/lint-checks.iml
+++ b/plugins/lint/lint-checks/lint-checks.iml
@@ -13,5 +13,6 @@
+
\ No newline at end of file
diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiDetector.java
deleted file mode 100755
index 580c8eae575..00000000000
--- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiDetector.java
+++ /dev/null
@@ -1,2176 +0,0 @@
-/*
- * 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.klint.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.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.klint.detector.api.LintUtils.getNextInstruction;
-import static com.android.tools.klint.detector.api.Location.SearchDirection.BACKWARD;
-import static com.android.tools.klint.detector.api.Location.SearchDirection.FORWARD;
-import static com.android.tools.klint.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.klint.client.api.IssueRegistry;
-import com.android.tools.klint.client.api.LintDriver;
-import com.android.tools.klint.detector.api.Category;
-import com.android.tools.klint.detector.api.ClassContext;
-import com.android.tools.klint.detector.api.Context;
-import com.android.tools.klint.detector.api.Detector;
-import com.android.tools.klint.detector.api.Implementation;
-import com.android.tools.klint.detector.api.Issue;
-import com.android.tools.klint.detector.api.LintUtils;
-import com.android.tools.klint.detector.api.Location;
-import com.android.tools.klint.detector.api.Location.SearchHints;
-import com.android.tools.klint.detector.api.ResourceXmlDetector;
-import com.android.tools.klint.detector.api.Scope;
-import com.android.tools.klint.detector.api.Severity;
-import com.android.tools.klint.detector.api.Speed;
-import com.android.tools.klint.detector.api.TextFormat;
-import com.android.tools.klint.detector.api.XmlContext;
-import com.android.utils.Pair;
-
-import org.jetbrains.uast.*;
-import org.jetbrains.uast.check.UastScanner;
-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.util.Collection;
-import java.util.EnumSet;
-import java.util.List;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Looks for usages of APIs that are not supported in all the versions targeted
- * by this application (according to its minimum API requirement in the manifest).
- */
-public class ApiDetector extends ResourceXmlDetector
- implements Detector.ClassScanner, UastScanner {
-
- /**
- * 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 `` 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>> 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 getApplicableElements() {
- return ALL;
- }
-
- @Override
- public Collection getApplicableAttributes() {
- return ALL;
- }
-
- @Override
- public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
- if (mApiDatabase == null) {
- return;
- }
-
- int attributeApiLevel = -1;
- if (ANDROID_URI.equals(attribute.getNamespaceURI())) {
- String name = attribute.getLocalName();
- if (!(name.equals(ATTR_LAYOUT_WIDTH) && !(name.equals(ATTR_LAYOUT_HEIGHT)) &&
- !(name.equals(ATTR_ID)))) {
- String owner = "android/R$attr"; //$NON-NLS-1$
- attributeApiLevel = mApiDatabase.getFieldVersion(owner, name);
- int minSdk = getMinSdk(context);
- if (attributeApiLevel > minSdk && attributeApiLevel > context.getFolderVersion()
- && attributeApiLevel > getLocalMinSdk(attribute.getOwnerElement())
- && !isBenignUnusedAttribute(name)
- && !isAlreadyWarnedDrawableFile(context, attribute, attributeApiLevel)) {
- if (RtlDetector.isRtlAttributeName(name)) {
- // 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 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 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$
- // 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 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)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);
- }
-
- @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);
- }
- }
- }
-
- super.afterCheckProject(context);
- }
-
-// ---- Implements UastScanner ----
-
-/* @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 UElement node,
- @NonNull String name,
- @NonNull String owner) {
- return true;
- }
-
- /*
- 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 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);
- }
- }
- } 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, 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> 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 = 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 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 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 getTargetApi(List annotations) {
- if (annotations == null) {
- return -1;
- }
-
- for (UAnnotation annotation : annotations) {
- if (annotation.matchesName(TARGET_API)) {
- for (UNamedExpression element : annotation.getValueArguments()) {
- UExpression valueNode = element.getExpression();
- if (UastLiteralUtils.isIntegralLiteral(valueNode)) {
- return (int) UastLiteralUtils.getLongValue((ULiteralExpression) valueNode);
- } else if (UastLiteralUtils.isStringLiteral(valueNode)) {
- String value = (String) ((ULiteralExpression) valueNode).getValue();
- return SdkVersionInfo.getApiByBuildCode(value, true);
- } else if (valueNode instanceof UQualifiedExpression) {
- UQualifiedExpression select = (UQualifiedExpression) valueNode;
- String codename = select.getSelector().renderString();
- return SdkVersionInfo.getApiByBuildCode(codename, true);
- } else if (valueNode instanceof USimpleReferenceExpression) {
- USimpleReferenceExpression reference = (USimpleReferenceExpression) valueNode;
- String codename = reference.getIdentifier();
- 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) { return false; } /*
- assert requiredApi != -1;
-
- if (!containsSimpleSdkCheck(method)) {
- return false;
- }
-
- try {
- // Search in the control graph, from beginning, up to the target call
- // node, to see if it's reachable. The call graph is constructed in a
- // special way: we include all control flow edges, *except* those that
- // are satisfied by a SDK_INT version check (where the operand is a version
- // that is at least as high as the one needed for the given call).
- //
- // If we can reach the call, that means that there is a way this call
- // can be reached on some versions, and lint should flag the call/field lookup.
- //
- //
- // Let's say you have code like this:
- // if (SDK_INT >= LOLLIPOP) {
- // // Call
- // return property.hasAdjacentMapping();
- // }
- // ...
- //
- // The compiler will turn this into the following byte code:
- //
- // 0: getstatic #3; //Field android/os/Build$VERSION.SDK_INT:I
- // 3: bipush 21
- // 5: if_icmple 17
- // 8: aload_1
- // 9: invokeinterface #4, 1; //InterfaceMethod
- // android/view/ViewDebug$ExportedProperty.hasAdjacentMapping:()Z
- // 14: ifeq 17
- // 17: ... code after if loop
- //
- // When the call graph is constructed, for an if branch we're called twice; once
- // where the target is the next instruction (the one taken if byte code check is false)
- // and one to the jump label (the one taken if the byte code condition is true).
- //
- // Notice how at the byte code level, the logic is reversed: the >= instruction
- // is turned into "<" and we jump to the code *after* the if clause; otherwise
- // it will just fall through. Therefore, if we take a byte code branch, that means
- // that the SDK check was *not* satisfied, and conversely, the target call is reachable
- // if we don't take the branch.
- //
- // Therefore, when we build the call graph, we will add call graph nodes for an
- // if check if :
- // (1) it is some other comparison than <, <= or !=.
- // (2) if the byte code comparison check is *not* satisfied, this means that the the
- // SDK check was successful and that the call graph should only include
- // the jump edge
- // (3) all other edges are added
- //
- // With a flow control graph like that, we can determine whether a target call
- // is guarded by a given SDK check: that will be the case if we cannot reach
- // the target call in the call graph
-
- ApiCheckGraph graph = new ApiCheckGraph(requiredApi);
- ControlFlowGraph.create(graph, classNode, method);
-
- // Note: To debug unit tests, you may want to for example do
- // ControlFlowGraph.Node callNode = graph.getNode(call);
- // Set highlight = Sets.newHashSet(callNode);
- // Files.write(graph.toDot(highlight), new File("/tmp/graph.gv"), Charsets.UTF_8);
- // This will generate a graphviz file you can visualize with the "dot" utility
- AbstractInsnNode first = method.instructions.get(0);
- return !graph.isConnected(first, call);
- } catch (AnalyzerException e) {
- context.log(e, null);
- }
-
- return false;
- }
-
- private static boolean containsSimpleSdkCheck(@NonNull MethodNode method) {
- // Look for a compiled version of "if (Build.VERSION.SDK_INT op N) {"
- InsnList nodes = method.instructions;
- for (int i = 0, n = nodes.size(); i < n; i++) {
- AbstractInsnNode instruction = nodes.get(i);
- if (isSdkVersionLookup(instruction)) {
- AbstractInsnNode bipush = getNextInstruction(instruction);
- if (bipush != null && bipush.getOpcode() == Opcodes.BIPUSH) {
- AbstractInsnNode ifNode = getNextInstruction(bipush);
- if (ifNode != null && ifNode.getType() == AbstractInsnNode.JUMP_INSN) {
- return true;
- }
- }
- }
- }
-
- return false;
- }
-
- private static boolean isSdkVersionLookup(@NonNull AbstractInsnNode instruction) {
- if (instruction.getOpcode() == Opcodes.GETSTATIC) {
- FieldInsnNode fieldNode = (FieldInsnNode) instruction;
- return (SDK_INT.equals(fieldNode.name)
- && ANDROID_OS_BUILD_VERSION.equals(fieldNode.owner));
- }
- return false;
- }
-
- *//**
- * Control flow graph which skips control flow edges that check
- * a given SDK_VERSION requirement that is not met by a given call
- *//*
- private static class ApiCheckGraph extends ControlFlowGraph {
- private final int mRequiredApi;
-
- public ApiCheckGraph(int requiredApi) {
- mRequiredApi = requiredApi;
- }
-
- @Override
- protected void add(@NonNull AbstractInsnNode from, @NonNull AbstractInsnNode to) {
- if (from.getType() == AbstractInsnNode.JUMP_INSN &&
- from.getPrevious() != null &&
- from.getPrevious().getType() == AbstractInsnNode.INT_INSN) {
- IntInsnNode intNode = (IntInsnNode) from.getPrevious();
- if (intNode.getPrevious() != null && isSdkVersionLookup(intNode.getPrevious())) {
- JumpInsnNode jumpNode = (JumpInsnNode) from;
- int api = intNode.operand;
- boolean isJumpEdge = to == jumpNode.label;
- boolean includeEdge;
- switch (from.getOpcode()) {
- case Opcodes.IF_ICMPNE:
- includeEdge = api < mRequiredApi || isJumpEdge;
- break;
- case Opcodes.IF_ICMPLE:
- includeEdge = api < mRequiredApi - 1 || isJumpEdge;
- break;
- case Opcodes.IF_ICMPLT:
- includeEdge = api < mRequiredApi || isJumpEdge;
- break;
-
- case Opcodes.IF_ICMPGE:
- includeEdge = api < mRequiredApi || !isJumpEdge;
- break;
- case Opcodes.IF_ICMPGT:
- includeEdge = api < mRequiredApi - 1 || !isJumpEdge;
- break;
- default:
- // unexpected comparison for int API level
- includeEdge = true;
- }
- if (!includeEdge) {
- return;
- }
- }
- }
-
- super.add(from, to);
- }
- }*/
-}
diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiDetector.kt b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiDetector.kt
new file mode 100755
index 00000000000..7e1ade99d44
--- /dev/null
+++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiDetector.kt
@@ -0,0 +1,434 @@
+/*
+ * 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.klint.checks
+
+import com.android.SdkConstants.TARGET_API
+import com.android.annotations.NonNull
+import com.android.sdklib.SdkVersionInfo
+import com.android.tools.klint.client.api.IssueRegistry
+import com.android.tools.klint.detector.api.*
+import org.jetbrains.uast.*
+import org.jetbrains.uast.check.UastAndroidContext
+import org.jetbrains.uast.check.UastScanner
+import org.jetbrains.uast.visitor.UastVisitor
+import java.util.*
+
+/**
+ * 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).
+ */
+open class ApiDetector : Detector(), UastScanner {
+
+ protected var mApiDatabase: ApiLookup? = null
+ private var mWarnedMissingDb: Boolean = false
+
+ @NonNull
+ override fun getSpeed(): Speed {
+ return Speed.SLOW
+ }
+
+ override fun beforeCheckProject(@NonNull context: Context) {
+ mApiDatabase = ApiLookup.get(context.client)
+
+ if (mApiDatabase == null && !mWarnedMissingDb) {
+ mWarnedMissingDb = true
+ context.report(IssueRegistry.LINT_ERROR, Location.create(context.file),
+ "Can't find API database; API check not performed")
+ }
+ }
+
+ override fun createUastVisitor(context: UastAndroidContext): UastVisitor {
+ return ApiVersionVisitor(context)
+ }
+
+ private inner class ApiVersionVisitor(val context: UastAndroidContext) : UastVisitor() {
+ private var mMinApi = -1
+
+ override fun visitCallExpression(node: UCallExpression): Boolean {
+ when (node.kind) {
+ UastCallKind.FUNCTION_CALL -> checkVersion(context, node, node.functionReference?.resolve(context))
+ UastCallKind.CONSTRUCTOR_CALL -> checkVersion(context, node, node.classReference?.resolve(context))
+ }
+
+ return super.visitCallExpression(node)
+ }
+
+ override fun visitSimpleReferenceExpression(node: USimpleReferenceExpression): Boolean {
+ checkVersion(context, node, node.resolve(context) as? UVariable)
+ return super.visitSimpleReferenceExpression(node)
+ }
+
+ override fun visitClass(node: UClass): Boolean {
+ val nameElement = node.nameElement
+
+ if (nameElement != null) {
+ for (type in node.superTypes) {
+ checkVersion(context, nameElement, type.resolveClass(context))
+ }
+ }
+
+ return super.visitClass(node)
+ }
+
+ override fun visitClassLiteralExpression(node: UClassLiteralExpression): Boolean {
+ val clazz = node.type.resolve(context)
+ if (clazz != null) {
+ checkVersion(context, node, clazz)
+ }
+ return super.visitClassLiteralExpression(node)
+ }
+
+ override fun visitFunction(node: UFunction): Boolean {
+ if (!node.hasModifier(UastModifier.OVERRIDE)) {
+ val parentClass = node.parent as? UClass ?: return false
+
+ val db = mApiDatabase ?: return false
+ val desc = node.bytecodeDescriptor ?: return false
+
+ val buildSdk = context.lintContext.getMainProject().getBuildSdk()
+ if (buildSdk == -1) return false
+
+ for (type in parentClass.superTypes) {
+ val clazz = type.resolve(context) ?: continue
+ if (!isSdkClass(clazz)) continue
+ val internalName = clazz.internalName ?: continue
+
+ val methodSdkLevel = db.getCallVersion(internalName, node.name, desc)
+ if (methodSdkLevel != -1 && methodSdkLevel > buildSdk) {
+ val message = "This method is not overriding anything with the current build " +
+ "target, but will in API level $methodSdkLevel (current target is $buildSdk): `${node.name}`"
+ context.report(OVERRIDE, node, context.getLocation(node.nameElement), message);
+ }
+ }
+ }
+
+ return false
+ }
+
+ private fun checkIfSpecialCase(declaration: UDeclaration, owner: UClass): Boolean {
+ if (declaration is UVariable && owner.fqName == "android.os.Build.VERSION_CODES") return true
+ if (declaration.name == "MATCH_PARENT" && owner.fqName == "android.view.ViewGroup.LayoutParams") return true
+ if ((declaration.name == "CHOICE_MODE_NONE" || declaration.name == "CHOICE_MODE_MULTIPLE"
+ || declaration.name == "CHOICE_MODE_SINGLE") && owner.fqName == "android.widget.AbsListView") return true
+ if ((declaration.name == "START" || declaration.name == "END") && owner.fqName == "android.view.Gravity") return true
+
+ return false
+ }
+
+ private fun checkVersion(context: UastAndroidContext, node: UElement, declaration: UDeclaration?) {
+ if (declaration == null) return
+ val db = mApiDatabase ?: return
+
+ val projectMinSdk = getMinSdk(context)
+ if (projectMinSdk == -1) return
+
+ fun check(declarationSdkLevel: Int, parentClass: UClass?) {
+ if (declarationSdkLevel == -1) return
+ if (parentClass != null && checkIfSpecialCase(declaration, parentClass)) return
+ if (isCheckedExplicitly(context, declarationSdkLevel, node)) return
+
+ if (projectMinSdk < declarationSdkLevel) {
+ val subject = when (declaration) {
+ is UFunction -> "Call"
+ is UVariable -> "Field"
+ is UClass -> "Class"
+ else -> "Call"
+ }
+ val message = "$subject requires API level $declarationSdkLevel" +
+ " (current min is $projectMinSdk): `${declaration.name}`"
+ context.report(UNSUPPORTED, node, context.getLocation(node), message)
+ return
+ }
+ }
+
+ fun checkAosp(clazz: UClass) = AOSP_BUILD && clazz.fqName?.startsWith("android.support.") ?: false
+
+ if (declaration is UClass) {
+ if (!isSdkClass(declaration) || checkAosp(declaration)) return
+ val internalName = declaration.internalName ?: return
+ check(db.getClassVersion(internalName), null)
+ }
+
+ val parentClass = declaration.parent as? UClass ?: return
+ if (!isSdkClass(parentClass) || checkAosp(parentClass)) return
+ val parentInternalName = parentClass.internalName ?: return
+
+ when (declaration) {
+ is UFunction -> {
+ val descriptor = declaration.bytecodeDescriptor ?: return
+ check(db.getCallVersion(parentInternalName, declaration.name, descriptor), parentClass)
+ }
+ is UVariable -> {
+ if (declaration.kind != UastVariableKind.MEMBER) return
+ check(db.getFieldVersion(parentInternalName, declaration.name), parentClass)
+ }
+ }
+ }
+
+ private fun isSdkClass(clazz: UClass): Boolean {
+ val fqName = clazz.fqName ?: return false
+ return fqName.startsWith("android.")
+ || fqName.startsWith("java.")
+ || fqName.startsWith("javax.")
+ || fqName.startsWith("dalvik.")
+ }
+
+ protected fun getMinSdk(context: UastAndroidContext): Int {
+ if (mMinApi == -1) {
+ val minSdkVersion = context.lintContext.mainProject.minSdkVersion
+ mMinApi = minSdkVersion.featureLevel
+ }
+
+ return mMinApi
+ }
+ }
+
+ companion object {
+ private val AOSP_BUILD = System.getenv("ANDROID_BUILD_TOP") != null
+ private val SDK_INT_FQNAME = "android.os.Build.VERSION.SDK_INT"
+
+ tailrec fun getLocalMinSdk(scope: UElement?): Int {
+ if (scope == null) return -1
+
+ if (scope is UAnnotated) {
+ val targetApi = getTargetApi(scope.annotations)
+ if (targetApi != -1) return targetApi
+ }
+
+ return getLocalMinSdk(scope.parent)
+ }
+
+ fun getTargetApi(annotations: List): Int {
+ for (annotation in annotations) {
+ if (annotation.matchesName(TARGET_API)) {
+ for (element in annotation.valueArguments) {
+ val valueNode = element.expression
+ if (valueNode.isIntegralLiteral()) {
+ return (valueNode as ULiteralExpression).getLongValue().toInt()
+ } else if (valueNode.isStringLiteral()) {
+ val value = (valueNode as ULiteralExpression).value as String
+ return SdkVersionInfo.getApiByBuildCode(value, true)
+ } else if (valueNode is UQualifiedExpression) {
+ val codename = valueNode.selector.renderString()
+ return SdkVersionInfo.getApiByBuildCode(codename, true)
+ } else if (valueNode is USimpleReferenceExpression) {
+ val codename = valueNode.identifier;
+ return SdkVersionInfo.getApiByBuildCode(codename, true)
+ }
+ }
+ }
+ }
+
+ return -1;
+ }
+
+ fun isCheckedExplicitly(context: UastAndroidContext, requiredVersion: Int, node: UElement): Boolean {
+ tailrec fun UExpression.isSdkIntReference(): Boolean = when (this) {
+ is UParenthesizedExpression -> expression.isSdkIntReference()
+ is USimpleReferenceExpression -> resolve(context)?.fqName == SDK_INT_FQNAME
+ is UQualifiedExpression -> resolve(context)?.fqName == SDK_INT_FQNAME
+ else -> false
+ }
+
+ fun checkCondition(node: UExpression, invertCondition: Boolean = false): Boolean? {
+ if (node is UBinaryExpression && node.operator is UastBinaryOperator.ComparationOperator) {
+ var invert: Boolean = invertCondition
+
+ val value: UExpression
+ if (node.leftOperand.isSdkIntReference()) {
+ value = node.rightOperand
+ }
+ else if (node.rightOperand.isSdkIntReference()) {
+ invert = !invert
+ value = node.leftOperand
+ }
+ else return false
+ fun inv(cond: Boolean) = if (invert) !cond else cond
+
+ tailrec fun evaluateValue(value: UExpression?): Int? {
+ if (value == null) return null
+
+ return (value.evaluate() as? Number)?.toInt() ?: if (value is UResolvable) {
+ val declaration = value.resolve(context) ?: return null
+ if (declaration is UVariable)
+ evaluateValue(declaration.initializer)
+ else
+ null
+ } else null
+ }
+
+ val sdkLevel = evaluateValue(value) ?: return null
+
+ return when (node.operator) {
+ UastBinaryOperator.GREATER -> inv(sdkLevel > requiredVersion)
+ UastBinaryOperator.GREATER_OR_EQUAL -> sdkLevel == requiredVersion || inv(sdkLevel > requiredVersion)
+ UastBinaryOperator.LESS -> inv(sdkLevel < requiredVersion)
+ UastBinaryOperator.LESS_OR_EQUAL -> sdkLevel == requiredVersion || inv(sdkLevel < requiredVersion)
+ UastBinaryOperator.EQUALS -> requiredVersion == sdkLevel
+ else -> null
+ }
+ }
+
+ return when (node) {
+ is UBinaryExpression -> if (node.operator is UastBinaryOperator.LogicalOperator) {
+ checkCondition(node.leftOperand) ?: checkCondition(node.rightOperand)
+ } else null
+ is UUnaryExpression -> if (node.operator == UastPrefixOperator.LOGICAL_NOT) {
+ checkCondition(node.operand, true)
+ } else null
+ is UParenthesizedExpression -> checkCondition(node.expression)
+ else -> null
+ }
+ }
+
+ tailrec fun check(node: UElement?, prev: UElement?, context: UastAndroidContext): Boolean {
+ return when (node) {
+ null -> false
+ is UIfExpression -> {
+ val cond = checkCondition(node.condition) ?: return false
+ if ((cond && prev == node.thenBranch) || (!cond && prev == node.elseBranch))
+ true
+ else
+ check(node.parent, node, context)
+ }
+ is UExpressionSwitchClauseExpression -> {
+ if (checkCondition(node.caseValue) == true)
+ true
+ else
+ check(node.parent, node, context)
+ }
+ is UBinaryExpression -> {
+ if (prev == node.rightOperand
+ && node.operator is UastBinaryOperator.LogicalOperator
+ && checkCondition(node.leftOperand) == true)
+ true
+ else
+ check(node.parent, node, context)
+ }
+ is UFunction -> false
+ is UVariable -> if (node.kind == UastVariableKind.MEMBER) false else check(node.parent, node, context)
+ is UClass -> false
+ else -> check(node.parent, node, context)
+ }
+ }
+
+ val minSdk = getLocalMinSdk(node)
+ if (minSdk != -1 && minSdk >= requiredVersion) return true
+
+ return check(node, null, context)
+ }
+
+ /** Accessing an unsupported API */
+ @SuppressWarnings("unchecked")
+ @JvmField
+ val 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,
+ Implementation(
+ ApiDetector::class.java,
+ 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 */
+ @JvmField
+ val 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,
+ Implementation(
+ ApiDetector::class.java,
+ Scope.JAVA_FILE_SCOPE))
+
+ /** Accessing an unsupported API */
+ @JvmField
+ val 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,
+ Implementation(
+ ApiDetector::class.java,
+ Scope.JAVA_FILE_SCOPE))
+ }
+}
\ No newline at end of file
diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/JavaPerformanceDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/JavaPerformanceDetector.java
index c527591586d..2d132169bb8 100644
--- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/JavaPerformanceDetector.java
+++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/JavaPerformanceDetector.java
@@ -201,9 +201,9 @@ public class JavaPerformanceDetector extends Detector implements UastScanner {
// TODO: Should we handle factory method constructions of HashMaps as well,
// e.g. via Guava? This is a bit trickier since we need to infer the type
// arguments from the calling context.
- if (clazz.matchesFqName(HASH_MAP)) {
+ if (clazz.matchesName(HASH_MAP)) {
checkHashMap(node);
- } else if (clazz.matchesFqName(SPARSE_ARRAY)) {
+ } else if (clazz.matchesName(SPARSE_ARRAY)) {
checkSparseArray(node);
}
}
@@ -481,29 +481,29 @@ public class JavaPerformanceDetector extends Detector implements UastScanner {
List types = node.getTypeArguments();
if (types.size() == 2) {
UType first = types.get(0);
- String typeName = first.getName();
Project mainProject = mContext.getLintContext().getMainProject();
int minSdk = mainProject.getMinSdk();
- if (typeName.equals(INTEGER) || typeName.equals(BYTE)) {
- String valueType = types.get(1).getName();
- if (valueType.equals(INTEGER)) {
+ if (first.isInt() || first.isByte()) {
+ UType valueType = types.get(1);
+ String valueTypeText = valueType.getName();
+ if (valueType.isInt()) {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
"Use new `SparseIntArray(...)` instead for better performance");
- } else if (valueType.equals(LONG) && minSdk >= 18) {
+ } else if (valueType.isLong() && minSdk >= 18) {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
"Use `new SparseLongArray(...)` instead for better performance");
- } else if (valueType.equals(BOOLEAN)) {
+ } else if (valueType.isBoolean()) {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
"Use `new SparseBooleanArray(...)` instead for better performance");
} else {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
String.format(
"Use `new SparseArray<%1$s>(...)` instead for better performance",
- valueType));
+ valueTypeText));
}
- } else if (typeName.equals(LONG) && (minSdk >= 16 ||
+ } else if (first.isLong() && (minSdk >= 16 ||
Boolean.TRUE == mainProject.dependsOn(
SUPPORT_LIB_ARTIFACT))) {
boolean useBuiltin = minSdk >= 16;
@@ -521,11 +521,10 @@ public class JavaPerformanceDetector extends Detector implements UastScanner {
List types = node.getTypeArguments();
if (types.size() == 1) {
UType first = types.get(0);
- String valueType = first.getName();
- if (valueType.equals(INTEGER)) {
+ if (first.isInt()) {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
"Use `new SparseIntArray(...)` instead for better performance");
- } else if (valueType.equals(BOOLEAN)) {
+ } else if (first.isBoolean()) {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
"Use `new SparseBooleanArray(...)` instead for better performance");
}
diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/LogDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/LogDetector.java
index 6f09a0f3bc6..dd6767eab02 100644
--- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/LogDetector.java
+++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/LogDetector.java
@@ -207,11 +207,17 @@ public class LogDetector extends Detector implements UastScanner {
while (curr != null) {
if (curr instanceof UIfExpression) {
UIfExpression ifNode = (UIfExpression) curr;
- if (ifNode.getCondition() instanceof UCallExpression) {
- UCallExpression call = (UCallExpression) ifNode.getCondition();
+ UExpression condition = ifNode.getCondition();
+ if (condition instanceof UCallExpression) {
+ UCallExpression call = (UCallExpression) condition;
if (IS_LOGGABLE.equals(call.getFunctionName())) {
checkTagConsistent(context, logCall, call);
}
+ } else if (condition instanceof UQualifiedExpression) {
+ UCallExpression call = UastUtils.getCallElementFromQualified((UQualifiedExpression) condition);
+ if (call != null && IS_LOGGABLE.equals(call.getFunctionName())) {
+ checkTagConsistent(context, logCall, call);
+ }
}
return true;
@@ -250,31 +256,39 @@ public class LogDetector extends Detector implements UastScanner {
JavaContext lintContext = context.getLintContext();
if (logTag != null) {
- if (!isLoggableTag.toString().equals(logTag.toString()) &&
- isLoggableTag instanceof UResolvable &&
- logTag instanceof UResolvable) {
- UDeclaration resolved1 = ((UResolvable) isLoggableTag).resolve(context);
- UDeclaration resolved2 = ((UResolvable) logTag).resolve(context);
- if ((resolved1 == null || resolved2 == null || !resolved1.equals(resolved2))
- && lintContext.isEnabled(WRONG_TAG)) {
- Location location = UastAndroidUtils.getLocation(logTag);
- Location alternate = UastAndroidUtils.getLocation(isLoggableTag);
- if (location != null && alternate != null) {
- alternate.setMessage("Conflicting tag");
- location.setSecondary(alternate);
- String isLoggableDescription = resolved1 != null ? resolved1
- .getName()
- : isLoggableTag.toString();
- String logCallDescription = resolved2 != null ? resolved2.getName()
- : logTag.toString();
- String message = String.format(
- "Mismatched tags: the `%1$s()` and `isLoggable()` calls typically " +
- "should pass the same tag: `%2$s` versus `%3$s`",
- logCallName,
- isLoggableDescription,
- logCallDescription);
- context.report(WRONG_TAG, call, location, message);
- }
+ String isLoggableTagString = null;
+ String logTagString = null;
+ boolean isOk = true;
+
+ Object isLoggableTagValue = isLoggableTag.evaluate();
+ Object logTagValue = logTag.evaluate();
+
+ if (isLoggableTagValue instanceof String && logTagValue instanceof String) {
+ isLoggableTagString = (String) isLoggableTagValue;
+ logTagString = (String) logTagValue;
+ isOk = isLoggableTagString.equals(logTagString);
+ } else if (isLoggableTag instanceof UResolvable && logTag instanceof UResolvable) {
+ UDeclaration isLoggableTagResolved = ((UResolvable) isLoggableTag).resolve(context);
+ UDeclaration logTagResolved = ((UResolvable) logTag).resolve(context);
+ if (isLoggableTagResolved != null && logTagResolved != null) {
+ isOk = isLoggableTagResolved.equals(logTagResolved);
+ }
+ }
+
+ if (!isOk) {
+ Location location = UastAndroidUtils.getLocation(logTag);
+ Location alternate = UastAndroidUtils.getLocation(isLoggableTag);
+ if (location != null && alternate != null) {
+ alternate.setMessage("Conflicting tag");
+ location.setSecondary(alternate);
+
+ String message = String.format(
+ "Mismatched tags: the `%1$s()` and `isLoggable()` calls typically " +
+ "should pass the same tag: `%2$s` versus `%3$s`",
+ logCallName,
+ isLoggableTagValue,
+ logTagValue);
+ context.report(WRONG_TAG, call, location, message);
}
}
}
diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/OverrideConcreteDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/OverrideConcreteDetector.java
index ea1ac70b0d9..cfa04973cec 100644
--- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/OverrideConcreteDetector.java
+++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/OverrideConcreteDetector.java
@@ -145,7 +145,7 @@ public class OverrideConcreteDetector extends Detector implements UastScanner {
private static int getTargetApi(UClass node) {
while (node != null) {
- int targetApi = ApiDetector.getTargetApi(node.getAnnotations());
+ int targetApi = ApiDetector.Companion.getTargetApi(node.getAnnotations());
if (targetApi != -1) {
return targetApi;
}
diff --git a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/WrongImportDetector.java b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/WrongImportDetector.java
index 6d91d9e1fee..115dbed5469 100644
--- a/plugins/lint/lint-checks/src/com/android/tools/klint/checks/WrongImportDetector.java
+++ b/plugins/lint/lint-checks/src/com/android/tools/klint/checks/WrongImportDetector.java
@@ -90,7 +90,7 @@ public class WrongImportDetector extends Detector implements UastScanner {
@Override
public boolean visitImportStatement(@NotNull UImportStatement node) {
- String fqn = node.getNameToImport();
+ String fqn = node.getFqNameToImport();
if (fqn != null && fqn.equals("android.R")) { //$NON-NLS-1$
Location location = UastAndroidUtils.getLocation(node);
mContext.report(ISSUE, node, location,
diff --git a/plugins/uast-common/src/org/jetbrains/uast/UastUtils.kt b/plugins/uast-common/src/org/jetbrains/uast/UastUtils.kt
index 3b7bcd2abb5..f63bbaf75e8 100644
--- a/plugins/uast-common/src/org/jetbrains/uast/UastUtils.kt
+++ b/plugins/uast-common/src/org/jetbrains/uast/UastUtils.kt
@@ -44,8 +44,6 @@ tailrec fun UElement?.getContainingDeclaration(): UDeclaration? {
return parent.getContainingDeclaration()
}
-fun UClass.findProperty(name: String) = declarations.firstOrNull { it is UVariable && it.name == name } as? UVariable
-
fun UElement.handleTraverse(handler: UastHandler) {
handler(this)
this.traverse(handler)
@@ -80,7 +78,6 @@ fun List.handleTraverseList(handler: UastHandler) {
fun UClass.findFunctions(name: String) = declarations.filter { it is UFunction && it.matchesName(name) } as List
fun UCallExpression.getReceiver(): UExpression? = (this.parent as? UQualifiedExpression)?.receiver
-fun UCallExpression.getReceiverOrEmpty(): UExpression = (this.parent as? UQualifiedExpression)?.receiver ?: EmptyExpression(this)
fun UElement.resolveIfCan(context: UastContext): UDeclaration? = (this as? UResolvable)?.resolve(context)
@@ -97,6 +94,15 @@ fun UClass.getAllDeclarations(context: UastContext): List = mutabl
fun UClass.getAllFunctions(context: UastContext) = getAllDeclarations(context).filterIsInstance()
+tailrec fun UQualifiedExpression.getCallElementFromQualified(): UCallExpression? {
+ val selector = this.selector
+ return when (selector) {
+ is UQualifiedExpression -> selector.getCallElementFromQualified()
+ is UCallExpression -> selector
+ else -> null
+ }
+}
+
fun UCallExpression.getQualifiedCallElement(): UExpression {
fun findParent(element: UExpression?): UExpression? = when (element) {
is UQualifiedExpression -> findParent(element.parent as? UExpression) ?: element