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 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 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 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