/* * 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; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.common.repository.GradleVersion; import com.android.repository.Revision; import com.android.repository.api.LocalPackage; import com.android.resources.ResourceFolderType; import com.android.resources.ResourceType; import com.android.sdklib.AndroidVersion; import com.android.sdklib.BuildToolInfo; import com.android.sdklib.SdkVersionInfo; import com.android.sdklib.repository.AndroidSdkHandler; import com.android.tools.klint.client.api.*; import com.android.tools.klint.detector.api.*; import com.android.tools.klint.detector.api.Detector.ClassScanner; import com.intellij.psi.*; import com.intellij.psi.util.MethodSignatureUtil; import com.intellij.psi.util.PsiTreeUtil; import org.jetbrains.android.inspections.klint.IntellijLintUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.uast.*; import org.jetbrains.uast.util.UastExpressionUtils; import org.jetbrains.uast.visitor.AbstractUastVisitor; import org.jetbrains.uast.visitor.UastVisitor; import org.w3c.dom.Attr; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.io.File; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import static com.android.SdkConstants.*; import static com.android.tools.klint.detector.api.ClassContext.getFqcn; import static com.android.utils.SdkUtils.getResourceFieldName; /** * Looks for usages of APIs that are not supported in all the versions targeted * by this application (according to its minimum API requirement in the manifest). */ public class ApiDetector extends ResourceXmlDetector implements ClassScanner, Detector.UastScanner { private static final String ATTR_WIDTH = "width"; private static final String ATTR_HEIGHT = "height"; private static final String ATTR_SUPPORTS_RTL = "supportsRtl"; public static final String REQUIRES_API_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "RequiresApi"; //$NON-NLS-1$ /** Accessing an unsupported API */ @SuppressWarnings("unchecked") public static final Issue UNSUPPORTED = Issue.create( "NewApi", //$NON-NLS-1$ "Calling new methods on older versions", "This check scans through all the Android API calls in the application and " + "warns about any calls that are not available on *all* versions targeted " + "by this application (according to its minimum SDK attribute in the manifest).\n" + "\n" + "If you really want to use this API and don't need to support older devices just " + "set the `minSdkVersion` in your `build.gradle` or `AndroidManifest.xml` files.\n" + "\n" + "If your code is *deliberately* accessing newer APIs, and you have ensured " + "(e.g. with conditional execution) that this code will only ever be called on a " + "supported platform, then you can annotate your class or method with the " + "`@TargetApi` annotation specifying the local minimum SDK to apply, such as " + "`@TargetApi(11)`, such that this check considers 11 rather than your manifest " + "file's minimum SDK as the required API level.\n" + "\n" + "If you are deliberately setting `android:` attributes in style definitions, " + "make sure you place this in a `values-vNN` folder in order to avoid running " + "into runtime conflicts on certain devices where manufacturers have added " + "custom attributes whose ids conflict with the new ones on later platforms.\n" + "\n" + "Similarly, you can use tools:targetApi=\"11\" in an XML file to indicate that " + "the element will only be inflated in an adequate context.", Category.CORRECTNESS, 6, Severity.ERROR, new Implementation( ApiDetector.class, EnumSet.of(Scope.JAVA_FILE, Scope.RESOURCE_FILE, Scope.MANIFEST), Scope.JAVA_FILE_SCOPE, Scope.RESOURCE_FILE_SCOPE, Scope.MANIFEST_SCOPE)); /** Accessing an inlined API on older platforms */ public static final Issue INLINED = Issue.create( "InlinedApi", //$NON-NLS-1$ "Using inlined constants on older versions", "This check scans through all the Android API field references in the application " + "and flags certain constants, such as static final integers and Strings, " + "which were introduced in later versions. These will actually be copied " + "into the class files rather than being referenced, which means that " + "the value is available even when running on older devices. In some " + "cases that's fine, and in other cases it can result in a runtime " + "crash or incorrect behavior. It depends on the context, so consider " + "the code carefully and device whether it's safe and can be suppressed " + "or whether the code needs tbe guarded.\n" + "\n" + "If you really want to use this API and don't need to support older devices just " + "set the `minSdkVersion` in your `build.gradle` or `AndroidManifest.xml` files." + "\n" + "If your code is *deliberately* accessing newer APIs, and you have ensured " + "(e.g. with conditional execution) that this code will only ever be called on a " + "supported platform, then you can annotate your class or method with the " + "`@TargetApi` annotation specifying the local minimum SDK to apply, such as " + "`@TargetApi(11)`, such that this check considers 11 rather than your manifest " + "file's minimum SDK as the required API level.\n", Category.CORRECTNESS, 6, Severity.WARNING, new Implementation( ApiDetector.class, Scope.JAVA_FILE_SCOPE)); /** Method conflicts with new inherited method */ public static final Issue OVERRIDE = Issue.create( "Override", //$NON-NLS-1$ "Method conflicts with new inherited method", "Suppose you are building against Android API 8, and you've subclassed Activity. " + "In your subclass you add a new method called `isDestroyed`(). At some later point, " + "a method of the same name and signature is added to Android. Your method will " + "now override the Android method, and possibly break its contract. Your method " + "is not calling `super.isDestroyed()`, since your compilation target doesn't " + "know about the method.\n" + "\n" + "The above scenario is what this lint detector looks for. The above example is " + "real, since `isDestroyed()` was added in API 17, but it will be true for *any* " + "method you have added to a subclass of an Android class where your build target " + "is lower than the version the method was introduced in.\n" + "\n" + "To fix this, either rename your method, or if you are really trying to augment " + "the builtin method if available, switch to a higher build target where you can " + "deliberately add `@Override` on your overriding method, and call `super` if " + "appropriate etc.\n", Category.CORRECTNESS, 6, Severity.ERROR, new Implementation( ApiDetector.class, Scope.CLASS_FILE_SCOPE)); /** Attribute unused on older versions */ public static final Issue UNUSED = Issue.create( "UnusedAttribute", //$NON-NLS-1$ "Attribute unused on older versions", "This check finds attributes set in XML files that were introduced in a version " + "newer than the oldest version targeted by your application (with the " + "`minSdkVersion` attribute).\n" + "\n" + "This is not an error; the application will simply ignore the attribute. However, " + "if the attribute is important to the appearance of functionality of your " + "application, you should consider finding an alternative way to achieve the " + "same result with only available attributes, and then you can optionally create " + "a copy of the layout in a layout-vNN folder which will be used on API NN or " + "higher where you can take advantage of the newer attribute.\n" + "\n" + "Note: This check does not only apply to attributes. For example, some tags can be " + "unused too, such as the new `<tag>` element in layouts introduced in API 21.", Category.CORRECTNESS, 6, Severity.WARNING, new Implementation( ApiDetector.class, Scope.RESOURCE_FILE_SCOPE)); private static final String 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 REFLECTIVE_OPERATION_EXCEPTION = "java.lang.ReflectiveOperationException"; public static final String ERROR = "error"; private ApiLookup mApiDatabase; private boolean mWarnedMissingDb; private int mMinApi = -1; /** Constructs a new API check */ public ApiDetector() { } @Override public void beforeCheckProject(@NonNull Context context) { if (mApiDatabase == null) { mApiDatabase = ApiLookup.get(context.getClient()); // We can't look up the minimum API required by the project here: // The manifest file hasn't been processed yet in the -before- project hook. // For now it's initialized lazily in getMinSdk(Context), but the // lint infrastructure should be fixed to parse manifest file up front. if (mApiDatabase == null && !mWarnedMissingDb) { mWarnedMissingDb = true; context.report(IssueRegistry.LINT_ERROR, Location.create(context.file), "Can't find API database; API check not performed"); } else { // See if you don't have at least version 23.0.1 of platform tools installed AndroidSdkHandler sdk = context.getClient().getSdk(); if (sdk == null) { return; } LocalPackage pkgInfo = sdk.getLocalPackage(SdkConstants.FD_PLATFORM_TOOLS, context.getClient().getRepositoryLogger()); if (pkgInfo == null) { return; } Revision revision = pkgInfo.getVersion(); // The platform tools must be at at least the same revision // as the compileSdkVersion! // And as a special case, for 23, they must be at 23.0.1 // because 23.0.0 accidentally shipped without Android M APIs. int compileSdkVersion = context.getProject().getBuildSdk(); if (compileSdkVersion == 23) { if (revision.getMajor() > 23 || revision.getMajor() == 23 && (revision.getMinor() > 0 || revision.getMicro() > 0)) { return; } } else if (compileSdkVersion <= revision.getMajor()) { return; } // Pick a location: when incrementally linting in the IDE, tie // it to the current file List<File> currentFiles = context.getProject().getSubset(); Location location; if (currentFiles != null && currentFiles.size() == 1) { File file = currentFiles.get(0); String contents = context.getClient().readFile(file); int firstLineEnd = contents.indexOf('\n'); if (firstLineEnd == -1) { firstLineEnd = contents.length(); } location = Location.create(file, new DefaultPosition(0, 0, 0), new DefaultPosition(0, firstLineEnd, firstLineEnd)); } else { location = Location.create(context.file); } context.report(UNSUPPORTED, location, String.format("The SDK platform-tools version (%1$s) is too old " + " to check APIs compiled with API %2$d; please update", revision.toShortString(), compileSdkVersion)); } } } // ---- Implements XmlScanner ---- @Override public boolean appliesTo(@NonNull ResourceFolderType folderType) { return true; } @Override public Collection<String> getApplicableElements() { return ALL; } @Override public Collection<String> getApplicableAttributes() { return ALL; } @Override public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { if (mApiDatabase == null) { return; } int attributeApiLevel = -1; if (ANDROID_URI.equals(attribute.getNamespaceURI())) { String name = attribute.getLocalName(); if (!(name.equals(ATTR_LAYOUT_WIDTH) && !(name.equals(ATTR_LAYOUT_HEIGHT)) && !(name.equals(ATTR_ID)))) { String owner = "android/R$attr"; //$NON-NLS-1$ attributeApiLevel = mApiDatabase.getFieldVersion(owner, name); int minSdk = getMinSdk(context); if (attributeApiLevel > minSdk && attributeApiLevel > context.getFolderVersion() && attributeApiLevel > getLocalMinSdk(attribute.getOwnerElement()) && !isBenignUnusedAttribute(name) && !isAlreadyWarnedDrawableFile(context, attribute, attributeApiLevel)) { if (RtlDetector.isRtlAttributeName(name) || ATTR_SUPPORTS_RTL.equals(name)) { // No need to warn for example that // "layout_alignParentEnd will only be used in API level 17 and higher" // since we have a dedicated RTL lint rule dealing with those attributes // However, paddingStart in particular is known to cause crashes // when used on TextViews (and subclasses of TextViews), on some // devices, because vendor specific attributes conflict with the // later-added framework resources, and these are apparently read // by the text views. // // However, as of build tools 23.0.1 aapt works around this by packaging // the resources differently. if (name.equals(ATTR_PADDING_START)) { BuildToolInfo buildToolInfo = context.getProject().getBuildTools(); Revision buildTools = buildToolInfo != null ? buildToolInfo.getRevision() : null; boolean isOldBuildTools = buildTools != null && (buildTools.getMajor() < 23 || buildTools.getMajor() == 23 && buildTools.getMinor() == 0 && buildTools.getMicro() == 0); if ((buildTools == null || isOldBuildTools) && viewMayExtendTextView(attribute.getOwnerElement())) { Location location = context.getLocation(attribute); String message = String.format( "Attribute `%1$s` referenced here can result in a crash on " + "some specific devices older than API %2$d " + "(current min is %3$d)", attribute.getLocalName(), attributeApiLevel, minSdk); //noinspection VariableNotUsedInsideIf if (buildTools != null) { message = String.format("Upgrade `buildToolsVersion` from " + "`%1$s` to at least `23.0.1`; if not, ", buildTools.toShortString()) + Character.toLowerCase(message.charAt(0)) + message.substring(1); } context.report(UNSUPPORTED, attribute, location, message); } } } else { Location location = context.getLocation(attribute); String message = String.format( "Attribute `%1$s` is only used in API level %2$d and higher " + "(current min is %3$d)", attribute.getLocalName(), attributeApiLevel, minSdk); context.report(UNUSED, attribute, location, message); } } } // Special case: // the dividers attribute is present in API 1, but it won't be read on older // versions, so don't flag the common pattern // android:divider="?android:attr/dividerHorizontal" // since this will work just fine. See issue 67440 for more. if (name.equals("divider")) { return; } } String value = attribute.getValue(); String owner = null; String name = null; String prefix; if (value.startsWith(ANDROID_PREFIX)) { prefix = ANDROID_PREFIX; } else if (value.startsWith(ANDROID_THEME_PREFIX)) { prefix = ANDROID_THEME_PREFIX; if (context.getResourceFolderType() == ResourceFolderType.DRAWABLE) { int api = 21; int minSdk = getMinSdk(context); if (api > minSdk && api > context.getFolderVersion() && api > getLocalMinSdk(attribute.getOwnerElement())) { Location location = context.getLocation(attribute); String message; message = String.format( "Using theme references in XML drawables requires API level %1$d " + "(current min is %2$d)", api, minSdk); context.report(UNSUPPORTED, attribute, location, message); // Don't flag individual theme attribute requirements here, e.g. once // we've told you that you need at least v21 to reference themes, we don't // need to also tell you that ?android:selectableItemBackground requires // API level 11 return; } } } else if (value.startsWith(PREFIX_ANDROID) && ATTR_NAME.equals(attribute.getName()) && TAG_ITEM.equals(attribute.getOwnerElement().getTagName()) && attribute.getOwnerElement().getParentNode() != null && TAG_STYLE.equals(attribute.getOwnerElement().getParentNode().getNodeName())) { owner = "android/R$attr"; //$NON-NLS-1$ name = value.substring(PREFIX_ANDROID.length()); prefix = null; } else if (value.startsWith(PREFIX_ANDROID) && ATTR_PARENT.equals(attribute.getName()) && TAG_STYLE.equals(attribute.getOwnerElement().getTagName())) { owner = "android/R$style"; //$NON-NLS-1$ name = getResourceFieldName(value.substring(PREFIX_ANDROID.length())); prefix = null; } else { return; } if (owner == null) { // Convert @android:type/foo into android/R$type and "foo" int index = value.indexOf('/', prefix.length()); if (index != -1) { owner = "android/R$" //$NON-NLS-1$ + value.substring(prefix.length(), index); name = getResourceFieldName(value.substring(index + 1)); } else if (value.startsWith(ANDROID_THEME_PREFIX)) { owner = "android/R$attr"; //$NON-NLS-1$ name = value.substring(ANDROID_THEME_PREFIX.length()); } else { return; } } int api = mApiDatabase.getFieldVersion(owner, name); int minSdk = getMinSdk(context); if (api > minSdk && api > context.getFolderVersion() && api > getLocalMinSdk(attribute.getOwnerElement())) { // Don't complain about resource references in the tools namespace, // such as for example "tools:layout="@android:layout/list_content", // used only for designtime previews if (TOOLS_URI.equals(attribute.getNamespaceURI())) { return; } //noinspection StatementWithEmptyBody if (attributeApiLevel >= api) { // The attribute will only be *read* on platforms >= attributeApiLevel. // If this isn't lower than the attribute reference's API level, it // won't be a problem } else if (attributeApiLevel > minSdk) { String attributeName = attribute.getLocalName(); Location location = context.getLocation(attribute); String message = String.format( "`%1$s` requires API level %2$d (current min is %3$d), but note " + "that attribute `%4$s` is only used in API level %5$d " + "and higher", name, api, minSdk, attributeName, attributeApiLevel); context.report(UNSUPPORTED, attribute, location, message); } else { Location location = context.getLocation(attribute); String message = String.format( "`%1$s` requires API level %2$d (current min is %3$d)", value, api, minSdk); context.report(UNSUPPORTED, attribute, location, message); } } } /** * Returns true if the view tag is possibly a text view. It may not be certain, * but will err on the side of caution (for example, any custom view is considered * to be a potential text view.) */ private static boolean viewMayExtendTextView(@NonNull Element element) { String tag = element.getTagName(); if (tag.equals(VIEW_TAG)) { tag = element.getAttribute(ATTR_CLASS); if (tag == null || tag.isEmpty()) { return false; } } //noinspection SimplifiableIfStatement if (tag.indexOf('.') != -1) { // Custom views: not sure. Err on the side of caution. return true; } return tag.contains("Text") // TextView, EditText, etc || tag.contains(BUTTON) // Button, ToggleButton, etc || tag.equals("DigitalClock") || tag.equals("Chronometer") || tag.equals(CHECK_BOX) || tag.equals(SWITCH); } /** * Returns true if this attribute is in a drawable document with one of the * root tags that require API 21 */ private static boolean isAlreadyWarnedDrawableFile(@NonNull XmlContext context, @NonNull Attr attribute, int attributeApiLevel) { // Don't complain if it's in a drawable file where we've already // flagged the root drawable type as being unsupported if (context.getResourceFolderType() == ResourceFolderType.DRAWABLE && attributeApiLevel == 21) { String root = attribute.getOwnerDocument().getDocumentElement().getTagName(); if (TAG_RIPPLE.equals(root) || TAG_VECTOR.equals(root) || TAG_ANIMATED_VECTOR.equals(root) || TAG_ANIMATED_SELECTOR.equals(root)) { return true; } } return false; } /** * Is the given attribute a "benign" unused attribute, one we probably don't need to * flag to the user as not applicable on all versions? These are typically attributes * which add some nice platform behavior when available, but that are not critical * and developers would not typically need to be aware of to try to implement workarounds * on older platforms. */ private static boolean isBenignUnusedAttribute(@NonNull String name) { return ATTR_LABEL_FOR.equals(name) || ATTR_TEXT_IS_SELECTABLE.equals(name) || "textAlignment".equals(name) || ATTR_FULL_BACKUP_CONTENT.equals(name); } @Override public void visitElement(@NonNull XmlContext context, @NonNull Element element) { if (mApiDatabase == null) { return; } String tag = element.getTagName(); ResourceFolderType folderType = context.getResourceFolderType(); if (folderType != ResourceFolderType.LAYOUT) { if (folderType == ResourceFolderType.DRAWABLE) { checkElement(context, element, TAG_VECTOR, 21, "1.4", UNSUPPORTED); checkElement(context, element, TAG_RIPPLE, 21, null, UNSUPPORTED); checkElement(context, element, TAG_ANIMATED_SELECTOR, 21, null, UNSUPPORTED); checkElement(context, element, TAG_ANIMATED_VECTOR, 21, null, UNSUPPORTED); checkElement(context, element, "drawable", 24, null, UNSUPPORTED); if ("layer-list".equals(tag)) { checkLevelList(context, element); } else if (tag.contains(".")) { checkElement(context, element, tag, 24, null, UNSUPPORTED); } } if (element.getParentNode().getNodeType() != Node.ELEMENT_NODE) { // Root node return; } NodeList childNodes = element.getChildNodes(); for (int i = 0, n = childNodes.getLength(); i < n; i++) { Node textNode = childNodes.item(i); if (textNode.getNodeType() == Node.TEXT_NODE) { String text = textNode.getNodeValue(); if (text.contains(ANDROID_PREFIX)) { text = text.trim(); // Convert @android:type/foo into android/R$type and "foo" int index = text.indexOf('/', ANDROID_PREFIX.length()); if (index != -1) { String typeString = text.substring(ANDROID_PREFIX.length(), index); if (ResourceType.getEnum(typeString) != null) { String owner = "android/R$" + typeString; String name = getResourceFieldName(text.substring(index + 1)); int api = mApiDatabase.getFieldVersion(owner, name); int minSdk = getMinSdk(context); if (api > minSdk && api > context.getFolderVersion() && api > getLocalMinSdk(element)) { Location location = context.getLocation(textNode); String message = String.format( "`%1$s` requires API level %2$d (current min is %3$d)", text, api, minSdk); context.report(UNSUPPORTED, element, location, message); } } } } } } } else { if (VIEW_TAG.equals(tag)) { tag = element.getAttribute(ATTR_CLASS); if (tag == null || tag.isEmpty()) { return; } } else { // TODO: Complain if <tag> is used at the root level! checkElement(context, element, TAG, 21, null, UNUSED); } // Check widgets to make sure they're available in this version of the SDK. if (tag.indexOf('.') != -1) { // Custom views aren't in the index return; } String fqn = "android/widget/" + tag; //$NON-NLS-1$ if (tag.equals("TextureView")) { //$NON-NLS-1$ fqn = "android/view/TextureView"; //$NON-NLS-1$ } // TODO: Consider other widgets outside of android.widget.* int api = mApiDatabase.getClassVersion(fqn); int minSdk = getMinSdk(context); if (api > minSdk && api > context.getFolderVersion() && api > getLocalMinSdk(element)) { Location location = context.getLocation(element); String message = String.format( "View requires API level %1$d (current min is %2$d): `<%3$s>`", api, minSdk, tag); context.report(UNSUPPORTED, element, location, message); } } } /** Checks whether the given element is the given tag, and if so, whether it satisfied * the minimum version that the given tag is supported in */ private void checkLevelList(@NonNull XmlContext context, @NonNull Element element) { Node curr = element.getFirstChild(); while (curr != null) { if (curr.getNodeType() == Node.ELEMENT_NODE && TAG_ITEM.equals(curr.getNodeName())) { Element e = (Element) curr; if (e.hasAttributeNS(ANDROID_URI, ATTR_WIDTH) || e.hasAttributeNS(ANDROID_URI, ATTR_HEIGHT)) { int attributeApiLevel = 23; // Using width and height on layer-list children requires M int minSdk = getMinSdk(context); if (attributeApiLevel > minSdk && attributeApiLevel > context.getFolderVersion() && attributeApiLevel > getLocalMinSdk(element)) { for (String attributeName : new String[] { ATTR_WIDTH, ATTR_HEIGHT}) { Attr attribute = e.getAttributeNodeNS(ANDROID_URI, attributeName); if (attribute == null) { continue; } Location location = context.getLocation(attribute); String message = String.format( "Attribute `%1$s` is only used in API level %2$d and higher " + "(current min is %3$d)", attribute.getLocalName(), attributeApiLevel, minSdk); context.report(UNUSED, attribute, location, message); } } } } curr = curr.getNextSibling(); } } /** Checks whether the given element is the given tag, and if so, whether it satisfied * the minimum version that the given tag is supported in */ private void checkElement(@NonNull XmlContext context, @NonNull Element element, @NonNull String tag, int api, @Nullable String gradleVersion, @NonNull Issue issue) { if (tag.equals(element.getTagName())) { int minSdk = getMinSdk(context); if (api > minSdk && api > context.getFolderVersion() && api > getLocalMinSdk(element) && !featureProvidedByGradle(context, gradleVersion)) { Location location = context.getLocation(element); // For the <drawable> tag we report it against the class= attribute if ("drawable".equals(tag)) { Attr attribute = element.getAttributeNode(ATTR_CLASS); if (attribute == null) { return; } location = context.getLocation(attribute); tag = ATTR_CLASS; } String message; if (issue == UNSUPPORTED) { message = String.format( "`<%1$s>` requires API level %2$d (current min is %3$d)", tag, api, minSdk); if (gradleVersion != null) { message += String.format( " or building with Android Gradle plugin %1$s or higher", gradleVersion); } else if (tag.contains(".")) { message = String.format( "Custom drawables requires API level %1$d (current min is %2$d)", api, minSdk); } } else { assert issue == UNUSED : issue; message = String.format( "`<%1$s>` is only used in API level %2$d and higher " + "(current min is %3$d)", tag, api, minSdk); } context.report(issue, element, location, message); } } } protected int getMinSdk(Context context) { if (mMinApi == -1) { AndroidVersion minSdkVersion = context.getMainProject().getMinSdkVersion(); mMinApi = minSdkVersion.getFeatureLevel(); } return mMinApi; } /** * Returns the minimum SDK to use in the given element context, or -1 if no * {@code tools:targetApi} attribute was found. * * @param element the element to look at, including parents * @return the API level to use for this element, or -1 */ private static int getLocalMinSdk(@NonNull Element element) { //noinspection ConstantConditions while (element != null) { String targetApi = element.getAttributeNS(TOOLS_URI, ATTR_TARGET_API); if (targetApi != null && !targetApi.isEmpty()) { if (Character.isDigit(targetApi.charAt(0))) { try { return Integer.parseInt(targetApi); } catch (NumberFormatException e) { break; } } else { return SdkVersionInfo.getApiByBuildCode(targetApi, true); } } Node parent = element.getParentNode(); if (parent != null && parent.getNodeType() == Node.ELEMENT_NODE) { element = (Element) parent; } else { break; } } return -1; } /** * Checks if the current project supports features added in {@code minGradleVersion} version of * the Android gradle plugin. * * @param context Current context. * @param minGradleVersionString Version in which support for a given feature was added, or null * if it's not supported at build time. */ private static boolean featureProvidedByGradle(@NonNull XmlContext context, @Nullable String minGradleVersionString) { if (minGradleVersionString == null) { return false; } GradleVersion gradleModelVersion = context.getProject().getGradleModelVersion(); if (gradleModelVersion != null) { GradleVersion minVersion = GradleVersion.tryParse(minGradleVersionString); if (minVersion != null && gradleModelVersion.compareIgnoringQualifiers(minVersion) >= 0) { return true; } } return false; } // ---- Implements UastScanner ---- @Nullable @Override public UastVisitor createUastVisitor(@NonNull JavaContext context) { if (mApiDatabase == null) { return new AbstractUastVisitor() { @Override public boolean visitElement(@NotNull UElement element) { // No-op. Workaround for super currently calling // ProgressIndicatorProvider.checkCanceled(); return super.visitElement(element); } }; } return new ApiVisitor(context); } @Nullable @Override public List<Class<? extends UElement>> getApplicableUastTypes() { List<Class<? extends UElement>> types = new ArrayList<Class<? extends UElement>>(9); types.add(UImportStatement.class); types.add(USimpleNameReferenceExpression.class); types.add(UVariable.class); types.add(UTryExpression.class); types.add(UBinaryExpressionWithType.class); types.add(UBinaryExpression.class); types.add(UCallExpression.class); types.add(UClass.class); types.add(UTypeReferenceExpression.class); types.add(UClassLiteralExpression.class); types.add(UMethod.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 */ private static boolean isBenignConstantUsage( @Nullable UElement 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 UElement curr = node.getUastParent(); while (curr != null) { if (curr instanceof USwitchClauseExpression) { List<UExpression> caseValues = ((USwitchClauseExpression) curr).getCaseValues(); if (caseValues != null) { for (UExpression condition : caseValues) { if (condition != null && UastUtils.isChildOf(node, condition, false)) { return true; } } } return false; } else if (curr instanceof UIfExpression) { UExpression condition = ((UIfExpression) curr).getCondition(); return UastUtils.isChildOf(node, condition, false); } else if (curr instanceof UMethod || curr instanceof UClass) { break; } curr = curr.getUastParent(); } return false; } public static int getRequiredVersion(String errorMessage) { Pattern pattern = Pattern.compile("\\s(\\d+)\\s"); Matcher matcher = pattern.matcher(errorMessage); if (matcher.find()) { return Integer.parseInt(matcher.group(1)); } return -1; } private final class ApiVisitor extends AbstractUastVisitor { private final JavaContext mContext; private ApiVisitor(JavaContext context) { mContext = context; } @Override public boolean visitTypeReferenceExpression(@NotNull UTypeReferenceExpression node) { checkType(node.getType(), node); return super.visitTypeReferenceExpression(node); } @Override public boolean visitClassLiteralExpression(@NotNull UClassLiteralExpression node) { checkType(node.getType(), node); return super.visitClassLiteralExpression(node); } @Override public boolean visitImportStatement(@NotNull UImportStatement statement) { if (!statement.isOnDemand()) { PsiElement resolved = statement.resolve(); if (resolved instanceof PsiField) { checkField(statement, (PsiField)resolved); } } return super.visitImportStatement(statement); } @Override public boolean visitSimpleNameReferenceExpression(@NotNull USimpleNameReferenceExpression node) { PsiElement resolved = node.resolve(); if (resolved instanceof PsiField) { checkField(node, (PsiField)resolved); } return super.visitSimpleNameReferenceExpression(node); } @Override public boolean visitBinaryExpressionWithType(@NotNull UBinaryExpressionWithType node) { visitTypeCastExpression(node); return super.visitBinaryExpressionWithType(node); } private void visitTypeCastExpression(UBinaryExpressionWithType expression) { UExpression operand = expression.getOperand(); PsiType operandType = operand.getExpressionType(); PsiType castType = expression.getType(); if (castType.equals(operandType)) { return; } if (!(operandType instanceof PsiClassType)) { return; } if (!(castType instanceof PsiClassType)) { return; } PsiClassType classType = (PsiClassType)operandType; PsiClassType interfaceType = (PsiClassType)castType; checkCast(expression, classType, interfaceType); } private void checkCast(@NonNull UElement node, @NonNull PsiClassType classType, @NonNull PsiClassType interfaceType) { if (classType.equals(interfaceType)) { return; } JavaEvaluator evaluator = mContext.getEvaluator(); String classTypeInternal = evaluator.getInternalName(classType); String interfaceTypeInternal = evaluator.getInternalName(interfaceType); if ("java/lang/Object".equals(interfaceTypeInternal)) { return; } int api = mApiDatabase.getValidCastVersion(classTypeInternal, interfaceTypeInternal); if (api == -1) { return; } int minSdk = getMinSdk(mContext); if (api <= minSdk) { return; } if (isSuppressed(api, node, minSdk, mContext, UNSUPPORTED)) { return; } Location location = mContext.getUastLocation(node); String message = String.format("Cast from %1$s to %2$s requires API level %3$d (current min is %4$d)", UastLintUtils.getClassName(classType), UastLintUtils.getClassName(interfaceType), api, minSdk); mContext.report(UNSUPPORTED, location, message); } @Override public boolean visitMethod(@NotNull UMethod method) { // API check for default methods if (method.getModifierList().hasExplicitModifier(PsiModifier.DEFAULT)) { int api = 24; // minSdk for default methods int minSdk = getMinSdk(mContext); if (!isSuppressed(api, method, minSdk, mContext, UNSUPPORTED)) { Location location = mContext.getLocation(method); String message = String.format("Default method requires API level %1$d " + "(current min is %2$d)", api, minSdk); mContext.reportUast(UNSUPPORTED, method, location, message); } } return super.visitMethod(method); } @Override public boolean visitClass(@NotNull UClass aClass) { // Check for repeatable annotations if (aClass.isAnnotationType()) { PsiModifierList modifierList = aClass.getModifierList(); if (modifierList != null) { for (PsiAnnotation annotation : modifierList.getAnnotations()) { String name = annotation.getQualifiedName(); if ("java.lang.annotation.Repeatable".equals(name)) { int api = 24; // minSdk for repeatable annotations int minSdk = getMinSdk(mContext); if (!isSuppressed(api, aClass, minSdk, mContext, UNSUPPORTED)) { Location location = mContext.getLocation(annotation); String message = String.format("Repeatable annotation requires " + "API level %1$d (current min is %2$d)", api, minSdk); mContext.report(UNSUPPORTED, annotation, location, message); } } else if ("java.lang.annotation.Target".equals(name)) { PsiNameValuePair[] attributes = annotation.getParameterList() .getAttributes(); for (PsiNameValuePair pair : attributes) { PsiAnnotationMemberValue value = pair.getValue(); if (value instanceof PsiArrayInitializerMemberValue) { PsiArrayInitializerMemberValue array = (PsiArrayInitializerMemberValue) value; for (PsiAnnotationMemberValue t : array.getInitializers()) { checkAnnotationTarget(t, modifierList); } } else if (value != null) { checkAnnotationTarget(value, modifierList); } } } } } } else { for (UTypeReferenceExpression t : aClass.getUastSuperTypes()) { checkType(t.getType(), t); } } return super.visitClass(aClass); } private void checkAnnotationTarget(@NonNull PsiAnnotationMemberValue element, PsiModifierList modifierList) { if (element instanceof UReferenceExpression) { UReferenceExpression ref = (UReferenceExpression) element; String referenceName = UastLintUtils.getReferenceName(ref); if ("TYPE_PARAMETER".equals(referenceName) || "TYPE_USE".equals(referenceName)) { PsiAnnotation retention = modifierList .findAnnotation("java.lang.annotation.Retention"); if (retention == null || retention.getText().contains("RUNTIME")) { Location location = mContext.getLocation(element); String message = String.format("Type annotations are not " + "supported in Android: %1$s", referenceName); mContext.report(UNSUPPORTED, element, location, message); } } } } @Override public boolean visitCallExpression(@NotNull UCallExpression expression) { checkMethodCallExpression(expression); return super.visitCallExpression(expression); } private void checkMethodCallExpression(@NotNull UCallExpression expression) { PsiMethod method = expression.resolve(); if (method != null) { PsiClass containingClass = method.getContainingClass(); if (containingClass == null) { return; } PsiParameterList parameterList = method.getParameterList(); if (parameterList.getParametersCount() > 0) { PsiParameter[] parameters = parameterList.getParameters(); List<UExpression> arguments = expression.getValueArguments(); for (int i = 0; i < parameters.length; i++) { PsiType parameterType = parameters[i].getType(); if (parameterType instanceof PsiClassType) { if (i >= arguments.size()) { // We can end up with more arguments than parameters when // there is a varargs call. break; } UExpression argument = arguments.get(i); PsiType argumentType = argument.getExpressionType(); if (argumentType == null || parameterType.equals(argumentType) || !(argumentType instanceof PsiClassType)) { continue; } checkCast(argument, (PsiClassType)argumentType, (PsiClassType)parameterType); } } } JavaEvaluator evaluator = mContext.getEvaluator(); String fqcn = containingClass.getQualifiedName(); String owner = evaluator.getInternalName(containingClass); if (owner == null) { return; // Couldn't resolve type } String name = IntellijLintUtils.getInternalMethodName(method); String desc = IntellijLintUtils.getInternalDescription(method, false, false); if (desc == null) { // Couldn't compute description of method for some reason; probably // failure to resolve parameter types return; } boolean hasApiAnnotation = false; int api = mApiDatabase.getCallVersion(owner, name, desc); if (api == -1) { api = getTargetApi(method.getModifierList()); if (api == -1 && method.isConstructor()) { api = getTargetApi(method.getContainingClass().getModifierList()); } if (api == -1) { return; } else { hasApiAnnotation = true; } } int minSdk = getMinSdk(mContext); if (api <= minSdk) { return; } // The lint API database contains two optimizations: // First, all members that were available in API 1 are omitted from the database, since that saves // about half of the size of the database, and for API check purposes, we don't need to distinguish // between "doesn't exist" and "available in all versions". // Second, all inherited members were inlined into each class, so that it doesn't have to do a // repeated search up the inheritance chain. // // Unfortunately, in this custom PSI detector, we look up the real resolved method, which can sometimes // have a different minimum API. // // For example, SQLiteDatabase had a close() method from API 1. Therefore, calling SQLiteDatabase is supported // in all versions. However, it extends SQLiteClosable, which in API 16 added "implements Closable". In // this detector, if we have the following code: // void test(SQLiteDatabase db) { db.close } // here the call expression will be the close method on type SQLiteClosable. And that will result in an API // requirement of API 16, since the close method it now resolves to is in API 16. // // To work around this, we can now look up the type of the call expression ("db" in the above, but it could // have been more complicated), and if that's a different type than the type of the method, we look up // *that* method from lint's database instead. Furthermore, it's possible for that method to return "-1" // and we can't tell if that means "doesn't exist" or "present in API 1", we then check the package prefix // to see whether we know it's an API method whose members should all have been inlined. if (!hasApiAnnotation && UastExpressionUtils.isMethodCall(expression)) { UExpression qualifier = expression.getReceiver(); if (qualifier != null && !(qualifier instanceof UThisExpression) && !(qualifier instanceof USuperExpression)) { PsiType type = qualifier.getExpressionType(); if (type != null && type instanceof PsiClassType) { String expressionOwner = evaluator.getInternalName((PsiClassType)type); if (expressionOwner != null && !expressionOwner.equals(owner)) { int specificApi = mApiDatabase.getCallVersion(expressionOwner, name, desc); if (specificApi == -1) { if (ApiLookup.isRelevantOwner(expressionOwner)) { return; } } else if (specificApi <= minSdk) { return; } else { // For example, for Bundle#getString(String,String) the API level is 12, whereas for // BaseBundle#getString(String,String) the API level is 21. If the code specified a Bundle instead of // a BaseBundle, reported the Bundle level in the error message instead. if (specificApi < api) { api = specificApi; fqcn = expressionOwner.replace('/', '.'); } api = Math.min(specificApi, api); } } } } else { // Unqualified call; need to search in our super hierarchy PsiClass cls = null; PsiType receiverType = expression.getReceiverType(); if (receiverType instanceof PsiClassType) { cls = ((PsiClassType) receiverType).resolve(); } while (cls != null) { if (cls instanceof PsiAnonymousClass) { // If it's an unqualified call in an anonymous class, we need to rely on the // resolve method to find out whether the method is picked up from the anonymous // class chain or any outer classes boolean found = false; PsiClassType anonymousBaseType = ((PsiAnonymousClass)cls).getBaseClassType(); PsiClass anonymousBase = anonymousBaseType.resolve(); if (anonymousBase != null && anonymousBase.isInheritor(containingClass, true)) { cls = anonymousBase; found = true; } else { PsiClass surroundingBaseType = PsiTreeUtil.getParentOfType(cls, PsiClass.class, true); if (surroundingBaseType != null && surroundingBaseType.isInheritor(containingClass, true)) { cls = surroundingBaseType; found = true; } } if (!found) { break; } } String expressionOwner = evaluator.getInternalName(cls); if (expressionOwner == null) { break; } int specificApi = mApiDatabase.getCallVersion(expressionOwner, name, desc); if (specificApi == -1) { if (ApiLookup.isRelevantOwner(expressionOwner)) { return; } } else if (specificApi <= minSdk) { return; } else { if (specificApi < api) { api = specificApi; fqcn = expressionOwner.replace('/', '.'); } api = Math.min(specificApi, api); break; } cls = cls.getSuperClass(); } } } if (isSuppressed(api, expression, minSdk, mContext, UNSUPPORTED)) { return; } // 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 (UastExpressionUtils.isMethodCall(expression)) { if (expression.getReceiver() instanceof USuperExpression) { PsiMethod containingMethod = UastUtils.getContainingMethod(expression); if (containingMethod != null && name.equals(containingMethod.getName()) && MethodSignatureUtil.areSignaturesEqual(method, containingMethod) // 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 && !method.isConstructor()) { return; } } } UElement locationNode = expression.getMethodIdentifier(); if (locationNode == null) { locationNode = expression; } Location location = mContext.getUastLocation(locationNode); String message = String.format("Call requires API level %1$d (current min is %2$d): %3$s", api, minSdk, fqcn + '#' + method.getName()); mContext.report(UNSUPPORTED, location, message); } } @Override public boolean visitLocalVariable(ULocalVariable variable) { UExpression initializer = variable.getUastInitializer(); if (initializer == null) { return true; } PsiType initializerType = initializer.getExpressionType(); if (!(initializerType instanceof PsiClassType)) { return true; } PsiType interfaceType = variable.getType(); if (initializerType.equals(interfaceType)) { return true; } if (!(interfaceType instanceof PsiClassType)) { return true; } checkCast(initializer, (PsiClassType)initializerType, (PsiClassType)interfaceType); return true; } @Override public boolean visitBinaryExpression(@NotNull UBinaryExpression node) { if (UastExpressionUtils.isAssignment(node)) { visitAssignmentExpression(node); } return super.visitBinaryExpression(node); } private void visitAssignmentExpression(UBinaryExpression expression) { UExpression rExpression = expression.getRightOperand(); PsiType rhsType = rExpression.getExpressionType(); if (!(rhsType instanceof PsiClassType)) { return; } PsiType interfaceType = expression.getLeftOperand().getExpressionType(); if (rhsType.equals(interfaceType)) { return; } if (!(interfaceType instanceof PsiClassType)) { return; } checkCast(rExpression, (PsiClassType)rhsType, (PsiClassType)interfaceType); } @Override public boolean visitTryExpression(@NotNull UTryExpression statement) { if (statement.getHasResources()) { int api = 19; // minSdk for try with resources int minSdk = getMinSdk(mContext); if (api > minSdk && api > getTargetApi(statement)) { Location location = mContext.getUastLocation(statement); String message = String.format("Try-with-resources requires " + "API level %1$d (current min is %2$d)", api, minSdk); LintDriver driver = mContext.getDriver(); if (!driver.isSuppressed(mContext, UNSUPPORTED, statement)) { mContext.report(UNSUPPORTED, statement, location, message); } } } for (UCatchClause catchClause : statement.getCatchClauses()) { // Special case reflective operation exception which can be implicitly used // with multi-catches: see issue 153406 int minSdk = getMinSdk(mContext); if(minSdk < 19 && isMultiCatchReflectiveOperationException(catchClause)) { String message = String.format("Multi-catch with these reflection exceptions requires API level 19 (current min is %d) " + "because they get compiled to the common but new super type `ReflectiveOperationException`. " + "As a workaround either create individual catch statements, or catch `Exception`.", minSdk); mContext.report(UNSUPPORTED, getCatchParametersLocation(mContext, catchClause), message); continue; } for (UTypeReferenceExpression typeReference : catchClause.getTypeReferences()) { checkCatchTypeElement(statement, typeReference, typeReference.getType()); } } return super.visitTryExpression(statement); } private void checkCatchTypeElement(@NonNull UTryExpression statement, @NonNull UTypeReferenceExpression typeReference, @Nullable PsiType type) { PsiClass resolved = null; if (type instanceof PsiClassType) { resolved = ((PsiClassType) type).resolve(); } if (resolved != null) { String signature = mContext.getEvaluator().getInternalName(resolved); int api = mApiDatabase.getClassVersion(signature); if (api == -1) { return; } int minSdk = getMinSdk(mContext); if (api <= minSdk) { return; } int target = getTargetApi(statement); if (target != -1 && api <= target) { return; } Location location = mContext.getUastLocation(typeReference); String fqcn = resolved.getQualifiedName(); String message = String.format("Class requires API level %1$d (current min is %2$d): %3$s", api, minSdk, fqcn); mContext.report(UNSUPPORTED, location, message); } } private void checkType(PsiType type, UElement element) { if (!(type instanceof PsiClassType)) { return; } PsiClass resolved = ((PsiClassType) type).resolve(); if (resolved == null) { return; } String signature = mContext.getEvaluator().getInternalName(resolved); int api = mApiDatabase.getClassVersion(signature); if (api == -1) { return; } int minSdk = getMinSdk(mContext); if (api <= minSdk) { return; } if (isSuppressed(api, element, minSdk, mContext, UNSUPPORTED)) { return; } Location location = mContext.getUastLocation(element); String fqcn = resolved.getQualifiedName(); String message = String.format("Class requires API level %1$d (current min is %2$d): %3$s", api, minSdk, fqcn); mContext.report(UNSUPPORTED, location, message); } /** * Checks a Java source field reference. Returns true if the field is known * regardless of whether it's an invalid field or not */ private boolean checkField(@NonNull UElement node, @NonNull PsiField field) { PsiType type = field.getType(); Issue issue; if ((type instanceof PsiPrimitiveType) || LintUtils.isString(type)) { issue = INLINED; } else { issue = UNSUPPORTED; } String name = field.getName(); PsiClass containingClass = field.getContainingClass(); if (containingClass == null || name == null) { return false; } String owner = mContext.getEvaluator().getInternalName(containingClass); int api = mApiDatabase.getFieldVersion(owner, name); if (api != -1) { int minSdk = getMinSdk(mContext); if (api > minSdk && api > getTargetApi(node)) { if (isBenignConstantUsage(node, name, owner)) { return true; } String fqcn = getFqcn(owner) + '#' + name; // For import statements, place the underlines only under the // reference, not the import and static keywords if (node instanceof UImportStatement) { UElement reference = ((UImportStatement) node).getImportReference(); if (reference != null) { node = reference; } } LintDriver driver = mContext.getDriver(); if (driver.isSuppressed(mContext, INLINED, node)) { return true; } // backwards compatibility: lint used to use this issue type so respect // older suppress annotations if (driver.isSuppressed(mContext, UNSUPPORTED, node)) { return true; } if (isWithinVersionCheckConditional(node, api, mContext)) { return true; } if (isPrecededByVersionCheckExit(node, api, mContext)) { return true; } String message = String.format( "Field requires API level %1$d (current min is %2$d): `%3$s`", api, minSdk, fqcn); Location location = mContext.getUastLocation(node); mContext.report(issue, node, location, message); } return true; } return false; } } private static boolean isSuppressed(int api, UElement element, int minSdk, JavaContext context, Issue issue) { if (api <= minSdk) { return true; } int target = getTargetApi(element); if (target != -1) { if (api <= target) { return true; } } LintDriver driver = context.getDriver(); if(driver.isSuppressed(context, issue, element)) { return true; } if (isWithinVersionCheckConditional(element, api, context)) { return true; } if (isPrecededByVersionCheckExit(element, api, context)) { return true; } return false; } private static int getTargetApi(@Nullable UElement scope) { while (scope != null) { if (scope instanceof PsiModifierListOwner) { PsiModifierList modifierList = ((PsiModifierListOwner) scope).getModifierList(); int targetApi = getTargetApi(modifierList); if (targetApi != -1) { return targetApi; } } scope = scope.getUastParent(); if (scope instanceof PsiFile) { break; } } return -1; } /** * Returns the API level for the given AST node if specified with * an {@code @TargetApi} annotation. * * @param modifierList the modifier list to check * @return the target API level, or -1 if not specified */ public static int getTargetApi(@Nullable PsiModifierList modifierList) { if (modifierList == null) { return -1; } for (PsiAnnotation annotation : modifierList.getAnnotations()) { String fqcn = annotation.getQualifiedName(); if (fqcn != null && (fqcn.equals(FQCN_TARGET_API) || fqcn.equals(REQUIRES_API_ANNOTATION) || fqcn.equals(TARGET_API))) { // when missing imports PsiAnnotationParameterList parameterList = annotation.getParameterList(); for (PsiNameValuePair pair : parameterList.getAttributes()) { PsiAnnotationMemberValue v = pair.getValue(); if (v instanceof PsiLiteral) { PsiLiteral literal = (PsiLiteral)v; Object value = literal.getValue(); if (value instanceof Integer) { return (Integer) value; } else if (value instanceof String) { return codeNameToApi((String) value); } } else if (v instanceof PsiArrayInitializerMemberValue) { PsiArrayInitializerMemberValue mv = (PsiArrayInitializerMemberValue)v; for (PsiAnnotationMemberValue mmv : mv.getInitializers()) { if (mmv instanceof PsiLiteral) { PsiLiteral literal = (PsiLiteral)mmv; Object value = literal.getValue(); if (value instanceof Integer) { return (Integer) value; } else if (value instanceof String) { return codeNameToApi((String) value); } } } } else if (v instanceof PsiExpression) { // PsiExpression nodes are not present in light classes, so // we can use Java PSI api to get the qualified name if (v instanceof PsiReferenceExpression) { String name = ((PsiReferenceExpression)v).getQualifiedName(); return codeNameToApi(name); } else { return codeNameToApi(v.getText()); } } } } } return -1; } private static int codeNameToApi(@NonNull String text) { int dotIndex = text.lastIndexOf('.'); if (dotIndex != -1) { text = text.substring(dotIndex + 1); } return SdkVersionInfo.getApiByBuildCode(text, true); } private static class VersionCheckWithExitFinder extends AbstractUastVisitor { private final UExpression mExpression; private final UElement mEndElement; private final int mApi; private final JavaContext mContext; private boolean mFound = false; private boolean mDone = false; public VersionCheckWithExitFinder(UExpression expression, UElement endElement, int api, JavaContext context) { mExpression = expression; mEndElement = endElement; mApi = api; mContext = context; } @Override public boolean visitElement(@NotNull UElement node) { if (mDone) { return true; } if (node.equals(mEndElement)) { mDone = true; } return mDone || !mExpression.equals(node); } @Override public boolean visitIfExpression(@NotNull UIfExpression ifStatement) { if (mDone) { return true; } UExpression thenBranch = ifStatement.getThenExpression(); UExpression elseBranch = ifStatement.getElseExpression(); if (thenBranch != null) { Boolean level = isVersionCheckConditional(mApi, thenBranch, ifStatement, mContext); //noinspection VariableNotUsedInsideIf if (level != null) { // See if the body does an immediate return if (isUnconditionalReturn(thenBranch)) { mFound = true; mDone = true; } } } if (elseBranch != null) { Boolean level = isVersionCheckConditional(mApi, elseBranch, ifStatement, mContext); //noinspection VariableNotUsedInsideIf if (level != null) { if (isUnconditionalReturn(elseBranch)) { mFound = true; mDone = true; } } } return true; } public boolean found() { return mFound; } } private static boolean isPrecededByVersionCheckExit( UElement element, int api, JavaContext context ) { //noinspection unchecked UExpression currentExpression = UastUtils.getParentOfType(element, UExpression.class, true, UMethod.class, UClass.class); while(currentExpression != null) { VersionCheckWithExitFinder visitor = new VersionCheckWithExitFinder( currentExpression, element, api, context); currentExpression.accept(visitor); if (visitor.found()) { return true; } element = currentExpression; //noinspection unchecked currentExpression = UastUtils.getParentOfType(currentExpression, UExpression.class, true, UMethod.class, UClass.class); } return false; } private static boolean isUnconditionalReturn(UExpression expression) { if (expression instanceof UBlockExpression) { List<UExpression> expressions = ((UBlockExpression) expression).getExpressions(); return !expressions.isEmpty() && (isUnconditionalReturn(expressions.get(expressions.size() - 1))); } return expression instanceof UReturnExpression || expression instanceof UThrowExpression || (expression instanceof UCallExpression && ERROR.equals(((UCallExpression)expression).getMethodName())); } private static boolean isWithinVersionCheckConditional( UElement element, int api, JavaContext context ) { UElement current = element.getUastParent(); UElement prev = element; while (current != null) { if (current instanceof UIfExpression) { UIfExpression ifStatement = (UIfExpression) current; Boolean isConditional = isVersionCheckConditional(api, prev, ifStatement, context); if (isConditional != null) { return isConditional; } } else if (current instanceof UBinaryExpression) { if (isAndedWithConditional(current, api, prev) || isOredWithConditional(current, api, prev)) { return true; } } else if (current instanceof UMethod || current instanceof UFile) { return false; } prev = current; current = current.getUastParent(); } return false; } @Nullable private static Boolean isVersionCheckConditional( int api, UElement prev, UIfExpression ifStatement, @NonNull JavaContext context) { UExpression condition = ifStatement.getCondition(); if (condition != prev && condition instanceof UBinaryExpression) { Boolean isConditional = isVersionCheckConditional(api, prev, ifStatement, (UBinaryExpression) condition); if (isConditional != null) { return isConditional; } } else if (condition instanceof UCallExpression) { UCallExpression call = (UCallExpression) condition; PsiMethod method = call.resolve(); if (method != null && !method.hasModifierProperty(PsiModifier.ABSTRACT)) { UExpression body = context.getUastContext().getMethodBody(method); List<UExpression> expressions; if (body instanceof UBlockExpression) { expressions = ((UBlockExpression) body).getExpressions(); } else { expressions = Collections.singletonList(body); } if (expressions.size() == 1) { UExpression statement = expressions.get(0); if (statement instanceof UReturnExpression) { UReturnExpression returnStatement = (UReturnExpression) statement; UExpression returnValue = returnStatement.getReturnExpression(); if (returnValue instanceof UBinaryExpression) { Boolean isConditional = isVersionCheckConditional(api, null, null, (UBinaryExpression) returnValue); if (isConditional != null) { return isConditional; } } } } } } return null; } @Nullable private static Boolean isVersionCheckConditional( int api, @Nullable UElement prev, @Nullable UIfExpression ifStatement, @NonNull UBinaryExpression binary) { UastBinaryOperator tokenType = binary.getOperator(); if (tokenType == UastBinaryOperator.GREATER || tokenType == UastBinaryOperator.GREATER_OR_EQUALS || tokenType == UastBinaryOperator.LESS_OR_EQUALS || tokenType == UastBinaryOperator.LESS || tokenType == UastBinaryOperator.EQUALS || tokenType == UastBinaryOperator.IDENTITY_EQUALS) { UExpression left = binary.getLeftOperand(); if (left instanceof UReferenceExpression) { UReferenceExpression ref = (UReferenceExpression)left; if (SDK_INT.equals(ref.getResolvedName())) { UExpression right = binary.getRightOperand(); int level = -1; if (right instanceof UReferenceExpression) { UReferenceExpression ref2 = (UReferenceExpression)right; String codeName = ref2.getResolvedName(); if (codeName == null) { return false; } level = SdkVersionInfo.getApiByBuildCode(codeName, true); } else if (right instanceof ULiteralExpression) { ULiteralExpression lit = (ULiteralExpression)right; Object value = lit.getValue(); if (value instanceof Integer) { level = (Integer) value; } } if (level != -1) { boolean fromThen = ifStatement == null || prev == ifStatement.getThenExpression(); boolean fromElse = ifStatement != null && prev == ifStatement.getElseExpression(); assert fromThen == !fromElse; if (tokenType == UastBinaryOperator.GREATER_OR_EQUALS) { // if (SDK_INT >= ICE_CREAM_SANDWICH) { <call> } else { ... } return level >= api && fromThen; } else if (tokenType == UastBinaryOperator.GREATER) { // if (SDK_INT > ICE_CREAM_SANDWICH) { <call> } else { ... } return level >= api - 1 && fromThen; } else if (tokenType == UastBinaryOperator.LESS_OR_EQUALS) { // if (SDK_INT <= ICE_CREAM_SANDWICH) { ... } else { <call> } return level >= api - 1 && fromElse; } else if (tokenType == UastBinaryOperator.LESS) { // if (SDK_INT < ICE_CREAM_SANDWICH) { ... } else { <call> } return level >= api && fromElse; } else if (tokenType == UastBinaryOperator.EQUALS || tokenType == UastBinaryOperator.IDENTITY_EQUALS) { // if (SDK_INT == ICE_CREAM_SANDWICH) { <call> } else { } return level >= api && fromThen; } else { assert false : tokenType; } } } } } else if (tokenType == UastBinaryOperator.LOGICAL_AND && (ifStatement != null && prev == ifStatement.getThenExpression())) { if (isAndedWithConditional(ifStatement.getCondition(), api, prev)) { return true; } } return null; } private static Location getCatchParametersLocation(JavaContext context, UCatchClause catchClause) { List<UTypeReferenceExpression> types = catchClause.getTypeReferences(); if (types.isEmpty()) { return Location.NONE; } Location first = context.getUastLocation(types.get(0)); if (types.size() < 2) { return first; } Location last = context.getUastLocation(types.get(types.size() - 1)); File file = first.getFile(); Position start = first.getStart(); Position end = last.getEnd(); if (start == null) { return Location.create(file); } return Location.create(file, start, end); } private static boolean isMultiCatchReflectiveOperationException(UCatchClause catchClause) { List<PsiType> types = catchClause.getTypes(); if (types.size() < 2) { return false; } for (PsiType t : types) { if(!isSubclassOfReflectiveOperationException(t)) { return false; } } return true; } private static boolean isAndedWithConditional(UElement element, int api, @Nullable UElement target) { if (element instanceof UBinaryExpression) { UBinaryExpression inner = (UBinaryExpression) element; if (inner.getOperator() == UastBinaryOperator.LOGICAL_AND) { return isAndedWithConditional(inner.getLeftOperand(), api, target) || inner.getRightOperand() != target && isAndedWithConditional(inner.getRightOperand(), api, target); } else if (inner.getLeftOperand() instanceof UReferenceExpression && SDK_INT.equals(((UReferenceExpression)inner.getLeftOperand()).getResolvedName())) { UastOperator tokenType = inner.getOperator(); UExpression right = inner.getRightOperand(); int level = getApiLevel(right); if (level != -1) { if (tokenType == UastBinaryOperator.GREATER_OR_EQUALS) { // if (SDK_INT >= ICE_CREAM_SANDWICH && <call> return level >= api; } else if (tokenType == UastBinaryOperator.GREATER) { // if (SDK_INT > ICE_CREAM_SANDWICH) && <call> return level >= api - 1; } else if (tokenType == UastBinaryOperator.EQUALS || tokenType == UastBinaryOperator.IDENTITY_EQUALS) { // if (SDK_INT == ICE_CREAM_SANDWICH) && <call> return level >= api; } } } } return false; } private static boolean isOredWithConditional(UElement element, int api, @Nullable UElement target) { if (element instanceof UBinaryExpression) { UBinaryExpression inner = (UBinaryExpression) element; if (inner.getOperator() == UastBinaryOperator.LOGICAL_OR) { return isOredWithConditional(inner.getLeftOperand(), api, target) || inner.getRightOperand() != target && isOredWithConditional(inner.getRightOperand(), api, target); } else if (inner.getLeftOperand() instanceof UReferenceExpression && SDK_INT.equals(((UReferenceExpression)inner.getLeftOperand()).getResolvedName())) { UastOperator tokenType = inner.getOperator(); UExpression right = inner.getRightOperand(); int level = getApiLevel(right); if (level != -1) { if (tokenType == UastBinaryOperator.LESS_OR_EQUALS) { // if (SDK_INT <= ICE_CREAM_SANDWICH || <call> return level >= api - 1; } else if (tokenType == UastBinaryOperator.LESS) { // if (SDK_INT < ICE_CREAM_SANDWICH) || <call> return level >= api; } else if (tokenType == UastBinaryOperator.NOT_EQUALS) { // if (SDK_INT < ICE_CREAM_SANDWICH) || <call> return level == api; } } } } return false; } private static int getApiLevel(UElement apiLevelElement) { if (apiLevelElement instanceof UReferenceExpression) { UReferenceExpression ref2 = (UReferenceExpression)apiLevelElement; String codeName = ref2.getResolvedName(); if (codeName == null) { return -1; } return SdkVersionInfo.getApiByBuildCode(codeName, true); } else if (apiLevelElement instanceof ULiteralExpression) { ULiteralExpression lit = (ULiteralExpression)apiLevelElement; Object value = lit.getValue(); if (value instanceof Integer) { return ((Integer)value).intValue(); } } return -1; } private static boolean isSubclassOfReflectiveOperationException(PsiType type) { for (PsiType t : type.getSuperTypes()) { if (REFLECTIVE_OPERATION_EXCEPTION.equals(t.getCanonicalText())) { return true; } } return false; } }