/* * Copyright (C) 2015 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.annotations.NonNull; import com.android.annotations.Nullable; import com.android.resources.ResourceType; import com.android.sdklib.AndroidVersion; import com.android.tools.klint.checks.PermissionFinder.Operation; import com.android.tools.klint.checks.PermissionFinder.Result; import com.android.tools.klint.checks.PermissionHolder.SetPermissionLookup; import com.android.tools.klint.client.api.ExternalReferenceExpression; import com.android.tools.klint.client.api.JavaEvaluator; import com.android.tools.klint.client.api.LintClient; import com.android.tools.klint.client.api.UastLintUtils; import com.android.tools.klint.detector.api.*; import com.android.utils.XmlUtils; import com.google.common.base.Joiner; import com.google.common.collect.Sets; import com.intellij.psi.*; import org.jetbrains.uast.*; import org.jetbrains.uast.java.JavaUAnnotation; import org.jetbrains.uast.util.UastExpressionUtils; import org.jetbrains.uast.visitor.AbstractUastVisitor; import org.jetbrains.uast.visitor.UastVisitor; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.io.File; import java.util.*; import static com.android.SdkConstants.*; import static com.android.resources.ResourceType.*; import static com.android.tools.klint.checks.PermissionFinder.Operation.*; import static com.android.tools.klint.checks.PermissionRequirement.*; import static com.android.tools.klint.detector.api.ResourceEvaluator.*; import static org.jetbrains.uast.UastUtils.getQualifiedParentOrThis; /** * Looks up annotations on method calls and enforces the various things they * express, e.g. for {@code @CheckReturn} it makes sure the return value is used, * for {@code ColorInt} it ensures that a proper color integer is passed in, etc. * * TODO: Throw in some annotation usage checks here too; e.g. specifying @Size without parameters, * specifying toInclusive without setting to, combining @ColorInt with any @ResourceTypeRes, * using @CheckResult on a void method, etc. */ @SuppressWarnings("WeakerAccess") public class SupportAnnotationDetector extends Detector implements Detector.UastScanner { public static final Implementation IMPLEMENTATION = new Implementation(SupportAnnotationDetector.class, Scope.JAVA_FILE_SCOPE); /** Method result should be used */ public static final Issue RANGE = Issue.create( "Range", //$NON-NLS-1$ "Outside Range", "Some parameters are required to in a particular numerical range; this check " + "makes sure that arguments passed fall within the range. For arrays, Strings " + "and collections this refers to the size or length.", Category.CORRECTNESS, 6, Severity.ERROR, IMPLEMENTATION); /** * Attempting to set a resource id as a color */ public static final Issue RESOURCE_TYPE = Issue.create( "ResourceType", //$NON-NLS-1$ "Wrong Resource Type", "Ensures that resource id's passed to APIs are of the right type; for example, " + "calling `Resources.getColor(R.string.name)` is wrong.", Category.CORRECTNESS, 7, Severity.FATAL, IMPLEMENTATION); /** Attempting to set a resource id as a color */ public static final Issue COLOR_USAGE = Issue.create( "ResourceAsColor", //$NON-NLS-1$ "Should pass resolved color instead of resource id", "Methods that take a color in the form of an integer should be passed " + "an RGB triple, not the actual color resource id. You must call " + "`getResources().getColor(resource)` to resolve the actual color value first.", Category.CORRECTNESS, 7, Severity.ERROR, IMPLEMENTATION); /** Passing the wrong constant to an int or String method */ public static final Issue TYPE_DEF = Issue.create( "WrongConstant", //$NON-NLS-1$ "Incorrect constant", "Ensures that when parameter in a method only allows a specific set " + "of constants, calls obey those rules.", Category.SECURITY, 6, Severity.ERROR, IMPLEMENTATION); /** Method result should be used */ public static final Issue CHECK_RESULT = Issue.create( "CheckResult", //$NON-NLS-1$ "Ignoring results", "Some methods have no side effects, an calling them without doing something " + "without the result is suspicious. ", Category.CORRECTNESS, 6, Severity.WARNING, IMPLEMENTATION); /** Failing to enforce security by just calling check permission */ public static final Issue CHECK_PERMISSION = Issue.create( "UseCheckPermission", //$NON-NLS-1$ "Using the result of check permission calls", "You normally want to use the result of checking a permission; these methods " + "return whether the permission is held; they do not throw an error if the permission " + "is not granted. Code which does not do anything with the return value probably " + "meant to be calling the enforce methods instead, e.g. rather than " + "`Context#checkCallingPermission` it should call `Context#enforceCallingPermission`.", Category.SECURITY, 6, Severity.WARNING, IMPLEMENTATION); /** Method result should be used */ public static final Issue MISSING_PERMISSION = Issue.create( "MissingPermission", //$NON-NLS-1$ "Missing Permissions", "This check scans through your code and libraries and looks at the APIs being used, " + "and checks this against the set of permissions required to access those APIs. If " + "the code using those APIs is called at runtime, then the program will crash.\n" + "\n" + "Furthermore, for permissions that are revocable (with targetSdkVersion 23), client " + "code must also be prepared to handle the calls throwing an exception if the user " + "rejects the request for permission at runtime.", Category.CORRECTNESS, 9, Severity.ERROR, IMPLEMENTATION); /** Passing the wrong constant to an int or String method */ public static final Issue THREAD = Issue.create( "WrongThread", //$NON-NLS-1$ "Wrong Thread", "Ensures that a method which expects to be called on a specific thread, is actually " + "called from that thread. For example, calls on methods in widgets should always " + "be made on the UI thread.", Category.CORRECTNESS, 6, Severity.ERROR, IMPLEMENTATION) .addMoreInfo( "http://developer.android.com/guide/components/processes-and-threads.html#Threads"); public static final String CHECK_RESULT_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "CheckResult"; //$NON-NLS-1$ public static final String INT_RANGE_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "IntRange"; //$NON-NLS-1$ public static final String FLOAT_RANGE_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "FloatRange"; //$NON-NLS-1$ public static final String SIZE_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "Size"; //$NON-NLS-1$ public static final String PERMISSION_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "RequiresPermission"; //$NON-NLS-1$ public static final String UI_THREAD_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "UiThread"; //$NON-NLS-1$ public static final String MAIN_THREAD_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "MainThread"; //$NON-NLS-1$ public static final String WORKER_THREAD_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "WorkerThread"; //$NON-NLS-1$ public static final String BINDER_THREAD_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "BinderThread"; //$NON-NLS-1$ public static final String ANY_THREAD_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "AnyThread"; //$NON-NLS-1$ public static final String PERMISSION_ANNOTATION_READ = PERMISSION_ANNOTATION + ".Read"; //$NON-NLS-1$ public static final String PERMISSION_ANNOTATION_WRITE = PERMISSION_ANNOTATION + ".Write"; //$NON-NLS-1$ public static final String THREAD_SUFFIX = "Thread"; public static final String ATTR_SUGGEST = "suggest"; public static final String ATTR_TO = "to"; public static final String ATTR_FROM = "from"; public static final String ATTR_FROM_INCLUSIVE = "fromInclusive"; public static final String ATTR_TO_INCLUSIVE = "toInclusive"; public static final String ATTR_MULTIPLE = "multiple"; public static final String ATTR_MIN = "min"; public static final String ATTR_MAX = "max"; public static final String ATTR_ALL_OF = "allOf"; public static final String ATTR_ANY_OF = "anyOf"; public static final String ATTR_CONDITIONAL = "conditional"; public static final String SECURITY_EXCEPTION = "java.lang.SecurityException"; /** * Constructs a new {@link SupportAnnotationDetector} check */ public SupportAnnotationDetector() { } private void checkMethodAnnotation( @NonNull JavaContext context, @NonNull PsiMethod method, @NonNull UCallExpression call, @NonNull UAnnotation annotation, @NonNull List<UAnnotation> allMethodAnnotations, @NonNull List<UAnnotation> allClassAnnotations) { String signature = annotation.getQualifiedName(); if (signature == null) { return; } if (CHECK_RESULT_ANNOTATION.equals(signature) // support findbugs annotation too || signature.endsWith(".CheckReturnValue")) { checkResult(context, call, method, annotation); } else if (signature.equals(PERMISSION_ANNOTATION)) { PermissionRequirement requirement = PermissionRequirement.create(annotation); checkPermission(context, call, method, null, requirement); } else if (signature.endsWith(THREAD_SUFFIX) && signature.startsWith(SUPPORT_ANNOTATIONS_PREFIX)) { checkThreading(context, call, method, signature, annotation, allMethodAnnotations, allClassAnnotations); } } private void checkParameterAnnotations( @NonNull JavaContext context, @NonNull UExpression argument, @NonNull UCallExpression call, @NonNull PsiMethod method, @NonNull List<UAnnotation> annotations) { boolean handledResourceTypes = false; for (UAnnotation annotation : annotations) { String signature = annotation.getQualifiedName(); if (signature == null) { continue; } if (COLOR_INT_ANNOTATION.equals(signature)) { checkColor(context, argument); } else if (signature.equals(PX_ANNOTATION)) { checkPx(context, argument); } else if (signature.equals(INT_RANGE_ANNOTATION)) { checkIntRange(context, annotation, argument, annotations); } else if (signature.equals(FLOAT_RANGE_ANNOTATION)) { checkFloatRange(context, annotation, argument); } else if (signature.equals(SIZE_ANNOTATION)) { checkSize(context, annotation, argument); } else if (signature.startsWith(PERMISSION_ANNOTATION)) { // PERMISSION_ANNOTATION, PERMISSION_ANNOTATION_READ, PERMISSION_ANNOTATION_WRITE // When specified on a parameter, that indicates that we're dealing with // a permission requirement on this *method* which depends on the value // supplied by this parameter checkParameterPermission(context, signature, call, method, argument); } else { // We only run @IntDef, @StringDef and @<Type>Res checks if we're not // running inside Android Studio / IntelliJ where there are already inspections // covering the same warnings (using IntelliJ's own data flow analysis); we // don't want to (a) create redundant warnings or (b) work harder than we // have to if (signature.equals(INT_DEF_ANNOTATION)) { boolean flag = getAnnotationBooleanValue(annotation, TYPE_DEF_FLAG_ATTRIBUTE) == Boolean.TRUE; checkTypeDefConstant(context, annotation, argument, null, flag, annotations); } else if (signature.equals(STRING_DEF_ANNOTATION)) { checkTypeDefConstant(context, annotation, argument, null, false, annotations); } else if (signature.endsWith(RES_SUFFIX)) { if (handledResourceTypes) { continue; } handledResourceTypes = true; EnumSet<ResourceType> types = null; // Handle all resource type annotations in one go: there could be multiple // resource type annotations specified on the same element; we need to // know about them all up front. for (UAnnotation a : annotations) { String s = a.getQualifiedName(); if (s != null && s.endsWith(RES_SUFFIX)) { String typeString = s.substring(SUPPORT_ANNOTATIONS_PREFIX.length(), s.length() - RES_SUFFIX.length()).toLowerCase(Locale.US); ResourceType type = ResourceType.getEnum(typeString); if (type != null) { if (types == null) { types = EnumSet.of(type); } else { types.add(type); } } else if (typeString.equals("any")) { // @AnyRes types = getAnyRes(); break; } } } if (types != null) { checkResourceType(context, argument, types, call, method); } } } } } private static EnumSet<ResourceType> getAnyRes() { EnumSet<ResourceType> types = EnumSet.allOf(ResourceType.class); types.remove(ResourceEvaluator.COLOR_INT_MARKER_TYPE); types.remove(ResourceEvaluator.PX_MARKER_TYPE); return types; } private void checkParameterPermission( @NonNull JavaContext context, @NonNull String signature, @NonNull UElement call, @NonNull PsiMethod method, @NonNull UExpression argument) { Operation operation = null; if (signature.equals(PERMISSION_ANNOTATION_READ)) { operation = READ; } else if (signature.equals(PERMISSION_ANNOTATION_WRITE)) { operation = WRITE; } else { PsiType type = argument.getExpressionType(); if (type != null && CLASS_INTENT.equals(type.getCanonicalText())) { operation = ACTION; } } if (operation == null) { return; } Result result = PermissionFinder.findRequiredPermissions(operation, context, argument); if (result != null) { checkPermission(context, call, method, result, result.requirement); } } private static void checkColor(@NonNull JavaContext context, @NonNull UElement argument) { if (argument instanceof UIfExpression) { UIfExpression expression = (UIfExpression) argument; if (expression.getThenExpression() != null) { checkColor(context, expression.getThenExpression()); } if (expression.getElseExpression() != null) { checkColor(context, expression.getElseExpression()); } return; } EnumSet<ResourceType> types = ResourceEvaluator.getResourceTypes(context, argument); if (types != null && types.contains(COLOR) && !isIgnoredInIde(COLOR_USAGE, context, argument)) { String message = String.format( "Should pass resolved color instead of resource id here: " + "`getResources().getColor(%1$s)`", argument.asSourceString()); context.report(COLOR_USAGE, argument, context.getUastLocation(argument), message); } } private static void checkPx(@NonNull JavaContext context, @NonNull UElement argument) { if (argument instanceof UIfExpression) { UIfExpression expression = (UIfExpression) argument; if (expression.getThenExpression() != null) { checkPx(context, expression.getThenExpression()); } if (expression.getElseExpression() != null) { checkPx(context, expression.getElseExpression()); } return; } EnumSet<ResourceType> types = ResourceEvaluator.getResourceTypes(context, argument); if (types != null && types.contains(DIMEN)) { String message = String.format( "Should pass resolved pixel dimension instead of resource id here: " + "`getResources().getDimension*(%1$s)`", argument.asSourceString()); context.report(COLOR_USAGE, argument, context.getUastLocation(argument), message); } } private static boolean isIgnoredInIde(@NonNull Issue issue, @NonNull JavaContext context, @NonNull UElement node) { // Historically, the IDE would treat *all* support annotation warnings as // handled by the id "ResourceType", so look for that id too for issues // deliberately suppressed prior to Android Studio 2.0. Issue synonym = Issue.create("ResourceType", issue.getBriefDescription(TextFormat.RAW), issue.getExplanation(TextFormat.RAW), issue.getCategory(), issue.getPriority(), issue.getDefaultSeverity(), issue.getImplementation()); return context.getDriver().isSuppressed(context, synonym, node); } private void checkPermission( @NonNull JavaContext context, @NonNull UElement node, @Nullable PsiMethod method, @Nullable Result result, @NonNull PermissionRequirement requirement) { if (requirement.isConditional()) { return; } PermissionHolder permissions = getPermissions(context); if (!requirement.isSatisfied(permissions)) { // See if it looks like we're holding the permission implicitly by @RequirePermission // annotations in the surrounding context permissions = addLocalPermissions(permissions, node); if (!requirement.isSatisfied(permissions)) { if (isIgnoredInIde(MISSING_PERMISSION, context, node)) { return; } Operation operation; String name; if (result != null) { name = result.name; operation = result.operation; } else { assert method != null; PsiClass containingClass = method.getContainingClass(); if (containingClass != null) { name = containingClass.getName() + "." + method.getName(); } else { name = method.getName(); } operation = Operation.CALL; } String message = getMissingPermissionMessage(requirement, name, permissions, operation); context.report(MISSING_PERMISSION, node, context.getUastLocation(node), message); } } else if (requirement.isRevocable(permissions) && context.getMainProject().getTargetSdkVersion().getFeatureLevel() >= 23) { boolean handlesMissingPermission = handlesSecurityException(node); // If not, check to see if the code is deliberately checking to see if the // given permission is available. if (!handlesMissingPermission) { UMethod methodNode = UastUtils.getParentOfType(node, UMethod.class, true); if (methodNode != null) { CheckPermissionVisitor visitor = new CheckPermissionVisitor(node); methodNode.accept(visitor); handlesMissingPermission = visitor.checksPermission(); } } if (!handlesMissingPermission && !isIgnoredInIde(MISSING_PERMISSION, context, node)) { String message = getUnhandledPermissionMessage(); context.report(MISSING_PERMISSION, node, context.getUastLocation(node), message); } } } private static boolean handlesSecurityException(@NonNull UElement node) { // Ensure that the caller is handling a security exception // First check to see if we're inside a try/catch which catches a SecurityException // (or some wider exception than that). Check for nested try/catches too. UElement parent = node; while (true) { UTryExpression tryCatch = UastUtils.getParentOfType(parent, UTryExpression.class, true); if (tryCatch == null) { break; } else { for (UCatchClause catchClause : tryCatch.getCatchClauses()) { if (containsSecurityException(catchClause.getTypes())) { return true; } } parent = tryCatch; } } // If not, check to see if the method itself declares that it throws a // SecurityException or something wider. UMethod declaration = UastUtils.getParentOfType(parent, UMethod.class, false); if (declaration != null) { PsiClassType[] thrownTypes = declaration.getThrowsList().getReferencedTypes(); if (containsSecurityException(Arrays.asList(thrownTypes))) { return true; } } return false; } @NonNull private static PermissionHolder addLocalPermissions( @NonNull PermissionHolder permissions, @NonNull UElement node ) { // Accumulate @RequirePermissions available in the local context UMethod method = UastUtils.getParentOfType(node, UMethod.class, true); if (method == null) { return permissions; } UAnnotation annotation = method.findAnnotation(PERMISSION_ANNOTATION); permissions = mergeAnnotationPermissions(permissions, annotation); UClass containingClass = UastUtils.getContainingUClass(method); if (containingClass != null) { annotation = containingClass.findAnnotation(PERMISSION_ANNOTATION); permissions = mergeAnnotationPermissions(permissions, annotation); } return permissions; } @NonNull private static PermissionHolder mergeAnnotationPermissions( @NonNull PermissionHolder permissions, @Nullable UAnnotation annotation ) { if (annotation != null) { PermissionRequirement requirement = PermissionRequirement.create(annotation); permissions = SetPermissionLookup.join(permissions, requirement); } return permissions; } /** Returns the error message shown when a given call is missing one or more permissions */ public static String getMissingPermissionMessage(@NonNull PermissionRequirement requirement, @NonNull String callName, @NonNull PermissionHolder permissions, @NonNull Operation operation) { return String.format("Missing permissions required %1$s %2$s: %3$s", operation.prefix(), callName, requirement.describeMissingPermissions(permissions)); } /** Returns the error message shown when a revocable permission call is not properly handled */ public static String getUnhandledPermissionMessage() { return "Call requires permission which may be rejected by user: code should explicitly " + "check to see if permission is available (with `checkPermission`) or explicitly " + "handle a potential `SecurityException`"; } /** * Visitor which looks through a method, up to a given call (the one requiring a * permission) and checks whether it's preceeded by a call to checkPermission or * checkCallingPermission or enforcePermission etc. * <p> * Currently it only looks for the presence of this check; it does not perform * flow analysis to determine whether the check actually affects program flow * up to the permission call, or whether the check permission is checking for * permissions sufficient to satisfy the permission requirement of the target call, * or whether the check return value (== PERMISSION_GRANTED vs != PERMISSION_GRANTED) * is handled correctly, etc. */ private static class CheckPermissionVisitor extends AbstractUastVisitor { private boolean mChecksPermission; private boolean mDone; private final UElement mTarget; public CheckPermissionVisitor(@NonNull UElement target) { mTarget = target; } @Override public boolean visitElement(UElement node) { return mDone || super.visitElement(node); } @Override public boolean visitCallExpression(UCallExpression node) { if (UastExpressionUtils.isMethodCall(node)) { visitMethodCallExpression(node); } return super.visitCallExpression(node); } private void visitMethodCallExpression(UCallExpression node) { if (node == mTarget) { mDone = true; } String name = node.getMethodName(); if (name != null && (name.startsWith("check") || name.startsWith("enforce")) && name.endsWith("Permission")) { mChecksPermission = true; mDone = true; } } public boolean checksPermission() { return mChecksPermission; } } private static boolean containsSecurityException( @NonNull List<? extends PsiType> types) { for (PsiType type : types) { if (type instanceof PsiClassType) { PsiClass cls = ((PsiClassType) type).resolve(); // In earlier versions we checked not just for java.lang.SecurityException but // any super type as well, however that probably hides warnings in cases where // users don't want that; see http://b.android.com/182165 //return context.getEvaluator().extendsClass(cls, "java.lang.SecurityException", false); if (cls != null && SECURITY_EXCEPTION.equals(cls.getQualifiedName())) { return true; } } } return false; } private PermissionHolder mPermissions; private PermissionHolder getPermissions( @NonNull JavaContext context) { if (mPermissions == null) { Set<String> permissions = Sets.newHashSetWithExpectedSize(30); Set<String> revocable = Sets.newHashSetWithExpectedSize(4); LintClient client = context.getClient(); // Gather permissions from all projects that contribute to the // main project. Project mainProject = context.getMainProject(); for (File manifest : mainProject.getManifestFiles()) { addPermissions(client, permissions, revocable, manifest); } for (Project library : mainProject.getAllLibraries()) { for (File manifest : library.getManifestFiles()) { addPermissions(client, permissions, revocable, manifest); } } AndroidVersion minSdkVersion = mainProject.getMinSdkVersion(); AndroidVersion targetSdkVersion = mainProject.getTargetSdkVersion(); mPermissions = new SetPermissionLookup(permissions, revocable, minSdkVersion, targetSdkVersion); } return mPermissions; } private static void addPermissions(@NonNull LintClient client, @NonNull Set<String> permissions, @NonNull Set<String> revocable, @NonNull File manifest) { Document document = XmlUtils.parseDocumentSilently(client.readFile(manifest), true); if (document == null) { return; } Element root = document.getDocumentElement(); if (root == null) { return; } NodeList children = root.getChildNodes(); for (int i = 0, n = children.getLength(); i < n; i++) { Node item = children.item(i); if (item.getNodeType() != Node.ELEMENT_NODE) { continue; } String nodeName = item.getNodeName(); if (nodeName.equals(TAG_USES_PERMISSION) || nodeName.equals(TAG_USES_PERMISSION_SDK_23) || nodeName.equals(TAG_USES_PERMISSION_SDK_M)) { Element element = (Element)item; String name = element.getAttributeNS(ANDROID_URI, ATTR_NAME); if (!name.isEmpty()) { permissions.add(name); } } else if (nodeName.equals(TAG_PERMISSION)) { Element element = (Element)item; String protectionLevel = element.getAttributeNS(ANDROID_URI, ATTR_PROTECTION_LEVEL); if (VALUE_DANGEROUS.equals(protectionLevel)) { String name = element.getAttributeNS(ANDROID_URI, ATTR_NAME); if (!name.isEmpty()) { revocable.add(name); } } } } } private static void checkResult(@NonNull JavaContext context, @NonNull UCallExpression node, @NonNull PsiMethod method, @NonNull UAnnotation annotation) { if (isExpressionValueUnused(node)) { String methodName = JavaContext.getMethodName(node); String suggested = getAnnotationStringValue(annotation, ATTR_SUGGEST); // Failing to check permissions is a potential security issue (and had an existing // dedicated issue id before which people may already have configured with a // custom severity in their LintOptions etc) so continue to use that issue // (which also has category Security rather than Correctness) for these: Issue issue = CHECK_RESULT; if (methodName != null && methodName.startsWith("check") && methodName.contains("Permission")) { issue = CHECK_PERMISSION; } if (isIgnoredInIde(issue, context, node)) { return; } String message = String.format("The result of `%1$s` is not used", methodName); if (suggested != null) { // TODO: Resolve suggest attribute (e.g. prefix annotation class if it starts // with "#" etc? message = String.format( "The result of `%1$s` is not used; did you mean to call `%2$s`?", methodName, suggested); } else if ("intersect".equals(methodName) && context.getEvaluator().isMemberInClass(method, "android.graphics.Rect")) { message += ". If the rectangles do not intersect, no change is made and the " + "original rectangle is not modified. These methods return false to " + "indicate that this has happened."; } context.report(issue, node, context.getUastLocation(node), message); } } private static boolean isExpressionValueUnused(UExpression expression) { return getQualifiedParentOrThis(expression).getUastParent() instanceof UBlockExpression; } private static void checkThreading( @NonNull JavaContext context, @NonNull UElement node, @NonNull PsiMethod method, @NonNull String signature, @NonNull UAnnotation annotation, @NonNull List<UAnnotation> allMethodAnnotations, @NonNull List<UAnnotation> allClassAnnotations) { List<String> threadContext = getThreadContext(context, node); if (threadContext != null && !isCompatibleThread(threadContext, signature) && !isIgnoredInIde(THREAD, context, node)) { // If the annotation is specified on the class, ignore this requirement // if there is another annotation specified on the method. if (containsAnnotation(allClassAnnotations, annotation)) { if (containsThreadingAnnotation(allMethodAnnotations)) { return; } // Make sure ALL the other context annotations are acceptable! } else { assert containsAnnotation(allMethodAnnotations, annotation); // See if any of the *other* annotations are compatible. Boolean isFirst = null; for (UAnnotation other : allMethodAnnotations) { if (other == annotation) { if (isFirst == null) { isFirst = true; } continue; } else if (!isThreadingAnnotation(other)) { continue; } if (isFirst == null) { // We'll be called for each annotation on the method. // For each one we're checking *all* annotations on the target. // Therefore, when we're seeing the second, third, etc annotation // on the method we've already checked them, so return here. return; } String s = other.getQualifiedName(); if (s != null && isCompatibleThread(threadContext, s)) { return; } } } String name = method.getName(); if ((name.startsWith("post") ) && context.getEvaluator().isMemberInClass(method, CLASS_VIEW)) { // The post()/postDelayed() methods are (currently) missing // metadata (@AnyThread); they're on a class marked @UiThread // but these specific methods are not @UiThread. return; } List<String> targetThreads = getThreads(context, method); if (targetThreads == null) { targetThreads = Collections.singletonList(signature); } String message = String.format( "%1$s %2$s must be called from the `%3$s` thread, currently inferred thread is `%4$s` thread", method.isConstructor() ? "Constructor" : "Method", method.getName(), describeThreads(targetThreads, true), describeThreads(threadContext, false)); context.report(THREAD, node, context.getUastLocation(node), message); } } public static boolean containsAnnotation( @NonNull List<UAnnotation> array, @NonNull UAnnotation annotation) { for (UAnnotation a : array) { if (a == annotation) { return true; } } return false; } public static boolean containsThreadingAnnotation(@NonNull List<UAnnotation> array) { for (UAnnotation annotation : array) { if (isThreadingAnnotation(annotation)) { return true; } } return false; } public static boolean isThreadingAnnotation(@NonNull UAnnotation annotation) { String signature = annotation.getQualifiedName(); return signature != null && signature.endsWith(THREAD_SUFFIX) && signature.startsWith(SUPPORT_ANNOTATIONS_PREFIX); } @NonNull public static String describeThreads(@NonNull List<String> annotations, boolean any) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < annotations.size(); i++) { if (i > 0) { if (i == annotations.size() - 1) { if (any) { sb.append(" or "); } else { sb.append(" and "); } } else { sb.append(", "); } } sb.append(describeThread(annotations.get(i))); } return sb.toString(); } @NonNull public static String describeThread(@NonNull String annotation) { if (annotation.equals(UI_THREAD_ANNOTATION)) { return "UI"; } else if (annotation.equals(MAIN_THREAD_ANNOTATION)) { return "main"; } else if (annotation.equals(BINDER_THREAD_ANNOTATION)) { return "binder"; } else if (annotation.equals(WORKER_THREAD_ANNOTATION)) { return "worker"; } else if (annotation.equals(ANY_THREAD_ANNOTATION)) { return "any"; } else { return "other"; } } /** returns true if the two threads are compatible */ public static boolean isCompatibleThread(@NonNull List<String> callers, @NonNull String callee) { // ALL calling contexts must be valid assert !callers.isEmpty(); for (String caller : callers) { if (!isCompatibleThread(caller, callee)) { return false; } } return true; } /** returns true if the two threads are compatible */ public static boolean isCompatibleThread(@NonNull String caller, @NonNull String callee) { if (callee.equals(caller)) { return true; } if (callee.equals(ANY_THREAD_ANNOTATION)) { return true; } // Allow @UiThread and @MainThread to be combined if (callee.equals(UI_THREAD_ANNOTATION)) { if (caller.equals(MAIN_THREAD_ANNOTATION)) { return true; } } else if (callee.equals(MAIN_THREAD_ANNOTATION)) { if (caller.equals(UI_THREAD_ANNOTATION)) { return true; } } return false; } /** Attempts to infer the current thread context at the site of the given method call */ @Nullable private static List<String> getThreadContext(@NonNull JavaContext context, @NonNull UElement methodCall) { //noinspection unchecked PsiMethod method = UastUtils.getParentOfType(methodCall, UMethod.class, true, UAnonymousClass.class); return getThreads(context, method); } /** Attempts to infer the current thread context at the site of the given method call */ @Nullable private static List<String> getThreads(@NonNull JavaContext context, @Nullable PsiMethod method) { if (method != null) { List<String> result = null; PsiClass cls = method.getContainingClass(); while (method != null) { for (PsiAnnotation annotation : method.getModifierList().getAnnotations()) { String name = annotation.getQualifiedName(); if (name != null && name.startsWith(SUPPORT_ANNOTATIONS_PREFIX) && name.endsWith(THREAD_SUFFIX)) { if (result == null) { result = new ArrayList<String>(4); } result.add(name); } } if (result != null) { // We don't accumulate up the chain: one method replaces the requirements // of its super methods. return result; } method = context.getEvaluator().getSuperMethod(method); } // See if we're extending a class with a known threading context while (cls != null) { PsiModifierList modifierList = cls.getModifierList(); if (modifierList != null) { for (PsiAnnotation annotation : modifierList.getAnnotations()) { String name = annotation.getQualifiedName(); if (name != null && name.startsWith(SUPPORT_ANNOTATIONS_PREFIX) && name.endsWith(THREAD_SUFFIX)) { if (result == null) { result = new ArrayList<String>(4); } result.add(name); } } if (result != null) { // We don't accumulate up the chain: one class replaces the requirements // of its super classes. return result; } } cls = cls.getSuperClass(); } } // In the future, we could also try to infer the threading context using // other heuristics. For example, if we're in a method with unknown threading // context, but we see that the method is called by another method with a known // threading context, we can infer that that threading context is the context for // this thread too (assuming the call is direct). return null; } private static boolean isNumber(@NonNull UElement argument) { if (argument instanceof ULiteralExpression) { Object value = ((ULiteralExpression) argument).getValue(); return value instanceof Number; } else if (argument instanceof UPrefixExpression) { UPrefixExpression expression = (UPrefixExpression) argument; UExpression operand = expression.getOperand(); return isNumber(operand); } else { return false; } } private static boolean isZero(@NonNull UElement argument) { if (argument instanceof ULiteralExpression) { Object value = ((ULiteralExpression) argument).getValue(); return value instanceof Number && ((Number)value).intValue() == 0; } return false; } private static boolean isMinusOne(@NonNull UElement argument) { if (argument instanceof UPrefixExpression) { UPrefixExpression expression = (UPrefixExpression) argument; UExpression operand = expression.getOperand(); if (operand instanceof ULiteralExpression && expression.getOperator() == UastPrefixOperator.UNARY_MINUS) { Object value = ((ULiteralExpression) operand).getValue(); return value instanceof Number && ((Number) value).intValue() == 1; } else { return false; } } else { return false; } } private static void checkResourceType( @NonNull JavaContext context, @NonNull UElement argument, @NonNull EnumSet<ResourceType> expectedType, @NonNull UCallExpression call, @NonNull PsiMethod calledMethod) { EnumSet<ResourceType> actual = ResourceEvaluator.getResourceTypes(context, argument); if (actual == null && (!isNumber(argument) || isZero(argument) || isMinusOne(argument)) ) { return; } else if (actual != null && (!Sets.intersection(actual, expectedType).isEmpty() || expectedType.contains(DRAWABLE) && (actual.contains(COLOR) || actual.contains(MIPMAP)))) { return; } if (isIgnoredInIde(RESOURCE_TYPE, context, argument)) { return; } if (expectedType.contains(ResourceType.STYLEABLE) && (expectedType.size() == 1) && JavaEvaluator.isMemberInClass(calledMethod, "android.content.res.TypedArray") && typeArrayFromArrayLiteral(call.getReceiver(), context)) { // You're generally supposed to provide a styleable to the TypedArray methods, // but you're also allowed to supply an integer array return; } String message; if (actual != null && actual.size() == 1 && actual.contains( ResourceEvaluator.COLOR_INT_MARKER_TYPE)) { message = "Expected a color resource id (`R.color.`) but received an RGB integer"; } else if (expectedType.contains(ResourceEvaluator.COLOR_INT_MARKER_TYPE)) { message = String.format("Should pass resolved color instead of resource id here: " + "`getResources().getColor(%1$s)`", argument.asSourceString()); } else if (actual != null && actual.size() == 1 && actual.contains( ResourceEvaluator.PX_MARKER_TYPE)) { message = "Expected a dimension resource id (`R.color.`) but received a pixel integer"; } else if (expectedType.contains(ResourceEvaluator.PX_MARKER_TYPE)) { message = String.format("Should pass resolved pixel size instead of resource id here: " + "`getResources().getDimension*(%1$s)`", argument.asSourceString()); } else if (expectedType.size() < ResourceType.getNames().length - 2) { // -2: marker types message = String.format("Expected resource of type %1$s", Joiner.on(" or ").join(expectedType)); } else { message = "Expected resource identifier (`R`.type.`name`)"; } context.report(RESOURCE_TYPE, argument, context.getUastLocation(argument), message); } /** * Returns true if the node is pointing to a TypedArray whose value was obtained * from an array literal */ public static boolean typeArrayFromArrayLiteral( @Nullable UElement node, @NonNull JavaContext context) { if (isMethodCall(node)) { UCallExpression expression = (UCallExpression) node; assert expression != null; String name = expression.getMethodName(); if (name != null && "obtainStyledAttributes".equals(name)) { List<UExpression> expressions = expression.getValueArguments(); if (!expressions.isEmpty()) { int arg; if (expressions.size() == 1) { // obtainStyledAttributes(int[] attrs) arg = 0; } else if (expressions.size() == 2) { // obtainStyledAttributes(AttributeSet set, int[] attrs) // obtainStyledAttributes(int resid, int[] attrs) for (arg = 0; arg < expressions.size(); arg++) { PsiType type = expressions.get(arg).getExpressionType(); if (type instanceof PsiArrayType) { break; } } if (arg == expressions.size()) { return false; } } else if (expressions.size() == 4) { // obtainStyledAttributes(AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) arg = 1; } else { return false; } return ConstantEvaluator.isArrayLiteral(expressions.get(arg), context); } } return false; } else if (node instanceof UReferenceExpression) { PsiElement resolved = ((UReferenceExpression) node).resolve(); if (resolved instanceof PsiVariable) { PsiVariable variable = (PsiVariable) resolved; UExpression lastAssignment = UastLintUtils.findLastAssignment(variable, node, context); if (lastAssignment != null) { return typeArrayFromArrayLiteral(lastAssignment, context); } } } else if (UastExpressionUtils.isNewArrayWithInitializer(node)) { return true; } else if (UastExpressionUtils.isNewArrayWithDimensions(node)) { return true; } else if (node instanceof UParenthesizedExpression) { UParenthesizedExpression parenthesizedExpression = (UParenthesizedExpression) node; UExpression expression = parenthesizedExpression.getExpression(); return typeArrayFromArrayLiteral(expression, context); } else if (UastExpressionUtils.isTypeCast(node)) { UBinaryExpressionWithType castExpression = (UBinaryExpressionWithType) node; assert castExpression != null; UExpression operand = castExpression.getOperand(); return typeArrayFromArrayLiteral(operand, context); } return false; } private static boolean isMethodCall(UElement node) { if (node instanceof UQualifiedReferenceExpression) { UExpression last = getLastInQualifiedChain((UQualifiedReferenceExpression) node); return UastExpressionUtils.isMethodCall(last); } return UastExpressionUtils.isMethodCall(node); } @NonNull private static UExpression getLastInQualifiedChain(@NonNull UQualifiedReferenceExpression node) { UExpression last = node.getSelector(); while (last instanceof UQualifiedReferenceExpression) { last = ((UQualifiedReferenceExpression) last).getSelector(); } return last; } private static void checkIntRange( @NonNull JavaContext context, @NonNull UAnnotation annotation, @NonNull UElement argument, @NonNull List<UAnnotation> allAnnotations) { String message = getIntRangeError(context, annotation, argument); if (message != null) { if (findIntDef(allAnnotations) != null) { // Don't flag int range errors if there is an int def annotation there too; // there could be a valid @IntDef constant. (The @IntDef check will // perform range validation by calling getIntRange.) return; } if (isIgnoredInIde(RANGE, context, argument)) { return; } context.report(RANGE, argument, context.getUastLocation(argument), message); } } @Nullable private static String getIntRangeError( @NonNull JavaContext context, @NonNull UAnnotation annotation, @NonNull UElement argument) { if (UastExpressionUtils.isNewArrayWithInitializer(argument)) { UCallExpression newExpression = (UCallExpression) argument; for (UExpression expression : newExpression.getValueArguments()) { String error = getIntRangeError(context, annotation, expression); if (error != null) { return error; } } } Object object = ConstantEvaluator.evaluate(context, argument); if (!(object instanceof Number)) { return null; } long value = ((Number)object).longValue(); long from = getLongAttribute(annotation, ATTR_FROM, Long.MIN_VALUE); long to = getLongAttribute(annotation, ATTR_TO, Long.MAX_VALUE); return getIntRangeError(value, from, to); } /** * Checks whether a given integer value is in the allowed range, and if so returns * null; otherwise returns a suitable error message. */ private static String getIntRangeError(long value, long from, long to) { String message = null; if (value < from || value > to) { StringBuilder sb = new StringBuilder(20); if (value < from) { sb.append("Value must be \u2265 "); sb.append(Long.toString(from)); } else { assert value > to; sb.append("Value must be \u2264 "); sb.append(Long.toString(to)); } sb.append(" (was ").append(value).append(')'); message = sb.toString(); } return message; } private static void checkFloatRange( @NonNull JavaContext context, @NonNull UAnnotation annotation, @NonNull UElement argument) { Object object = ConstantEvaluator.evaluate(context, argument); if (!(object instanceof Number)) { return; } double value = ((Number)object).doubleValue(); double from = getDoubleAttribute(annotation, ATTR_FROM, Double.NEGATIVE_INFINITY); double to = getDoubleAttribute(annotation, ATTR_TO, Double.POSITIVE_INFINITY); boolean fromInclusive = getBoolean(annotation, ATTR_FROM_INCLUSIVE, true); boolean toInclusive = getBoolean(annotation, ATTR_TO_INCLUSIVE, true); String message = getFloatRangeError(value, from, to, fromInclusive, toInclusive, argument); if (message != null && !isIgnoredInIde(RANGE, context, argument)) { context.report(RANGE, argument, context.getUastLocation(argument), message); } } /** * Checks whether a given floating point value is in the allowed range, and if so returns * null; otherwise returns a suitable error message. */ @Nullable private static String getFloatRangeError(double value, double from, double to, boolean fromInclusive, boolean toInclusive, @NonNull UElement node) { if (!((fromInclusive && value >= from || !fromInclusive && value > from) && (toInclusive && value <= to || !toInclusive && value < to))) { StringBuilder sb = new StringBuilder(20); if (from != Double.NEGATIVE_INFINITY) { if (to != Double.POSITIVE_INFINITY) { if (fromInclusive && value < from || !fromInclusive && value <= from) { sb.append("Value must be "); if (fromInclusive) { sb.append('\u2265'); // >= sign } else { sb.append('>'); } sb.append(' '); sb.append(Double.toString(from)); } else { assert toInclusive && value > to || !toInclusive && value >= to; sb.append("Value must be "); if (toInclusive) { sb.append('\u2264'); // <= sign } else { sb.append('<'); } sb.append(' '); sb.append(Double.toString(to)); } } else { sb.append("Value must be "); if (fromInclusive) { sb.append('\u2265'); // >= sign } else { sb.append('>'); } sb.append(' '); sb.append(Double.toString(from)); } } else if (to != Double.POSITIVE_INFINITY) { sb.append("Value must be "); if (toInclusive) { sb.append('\u2264'); // <= sign } else { sb.append('<'); } sb.append(' '); sb.append(Double.toString(to)); } sb.append(" (was "); if (node instanceof ULiteralExpression) { // Use source text instead to avoid rounding errors involved in conversion, e.g // Error: Value must be > 2.5 (was 2.490000009536743) [Range] // printAtLeastExclusive(2.49f); // ERROR // ~~~~~ String str = node.asSourceString(); if (str.endsWith("f") || str.endsWith("F")) { str = str.substring(0, str.length() - 1); } sb.append(str); } else { sb.append(value); } sb.append(')'); return sb.toString(); } return null; } private static void checkSize( @NonNull JavaContext context, @NonNull UAnnotation annotation, @NonNull UElement argument) { int actual; boolean isString = false; // TODO: Collections syntax, e.g. Arrays.asList ⇒ param count, emptyList=0, singleton=1, etc // TODO: Flow analysis // No flow analysis for this check yet, only checking literals passed in as parameters if (UastExpressionUtils.isNewArrayWithInitializer(argument)) { actual = ((UCallExpression) argument).getValueArgumentCount(); } else { Object object = ConstantEvaluator.evaluate(context, argument); // Check string length if (object instanceof String) { actual = ((String)object).length(); isString = true; } else { return; } } long exact = getLongAttribute(annotation, ATTR_VALUE, -1); long min = getLongAttribute(annotation, ATTR_MIN, Long.MIN_VALUE); long max = getLongAttribute(annotation, ATTR_MAX, Long.MAX_VALUE); long multiple = getLongAttribute(annotation, ATTR_MULTIPLE, 1); String unit; if (isString) { unit = "length"; } else { unit = "size"; } String message = getSizeError(actual, exact, min, max, multiple, unit); if (message != null && !isIgnoredInIde(RANGE, context, argument)) { context.report(RANGE, argument, context.getUastLocation(argument), message); } } /** * Checks whether a given size follows the given constraints, and if so returns * null; otherwise returns a suitable error message. */ private static String getSizeError(long actual, long exact, long min, long max, long multiple, @NonNull String unit) { String message = null; if (exact != -1) { if (exact != actual) { message = String.format("Expected %1$s %2$d (was %3$d)", unit, exact, actual); } } else if (actual < min || actual > max) { StringBuilder sb = new StringBuilder(20); if (actual < min) { sb.append("Expected ").append(unit).append(" \u2265 "); sb.append(Long.toString(min)); } else { assert actual > max; sb.append("Expected ").append(unit).append(" \u2264 "); sb.append(Long.toString(max)); } sb.append(" (was ").append(actual).append(')'); message = sb.toString(); } else if (actual % multiple != 0) { message = String.format("Expected %1$s to be a multiple of %2$d (was %3$d " + "and should be either %4$d or %5$d)", unit, multiple, actual, (actual / multiple) * multiple, (actual / multiple + 1) * multiple); } return message; } @Nullable private static UAnnotation findIntRange( @NonNull List<UAnnotation> annotations) { for (UAnnotation annotation : annotations) { if (INT_RANGE_ANNOTATION.equals(annotation.getQualifiedName())) { return annotation; } } return null; } @Nullable static UAnnotation findIntDef(@NonNull List<UAnnotation> annotations) { for (UAnnotation annotation : annotations) { if (INT_DEF_ANNOTATION.equals(annotation.getQualifiedName())) { return annotation; } } return null; } private static void checkTypeDefConstant( @NonNull JavaContext context, @NonNull UAnnotation annotation, @Nullable UElement argument, @Nullable UElement errorNode, boolean flag, @NonNull List<UAnnotation> allAnnotations) { if (argument == null) { return; } if (argument instanceof ULiteralExpression) { Object value = ((ULiteralExpression) argument).getValue(); if (value == null) { // Accepted for @StringDef //noinspection UnnecessaryReturnStatement return; } else if (value instanceof String) { String string = (String) value; checkTypeDefConstant(context, annotation, argument, errorNode, false, string, allAnnotations); } else if (value instanceof Integer || value instanceof Long) { long v = value instanceof Long ? ((Long) value) : ((Integer) value).longValue(); if (flag && v == 0) { // Accepted for a flag @IntDef return; } checkTypeDefConstant(context, annotation, argument, errorNode, flag, value, allAnnotations); } } else if (isMinusOne(argument)) { // -1 is accepted unconditionally for flags if (!flag) { reportTypeDef(context, annotation, argument, errorNode, allAnnotations); } } else if (argument instanceof UPrefixExpression) { UPrefixExpression expression = (UPrefixExpression) argument; if (flag) { checkTypeDefConstant(context, annotation, expression.getOperand(), errorNode, true, allAnnotations); } else { UastOperator operator = expression.getOperator(); if (operator == UastPrefixOperator.BITWISE_NOT) { if (isIgnoredInIde(TYPE_DEF, context, expression)) { return; } context.report(TYPE_DEF, expression, context.getUastLocation(expression), "Flag not allowed here"); } else if (operator == UastPrefixOperator.UNARY_MINUS) { reportTypeDef(context, annotation, argument, errorNode, allAnnotations); } } } else if (argument instanceof UParenthesizedExpression) { UExpression expression = ((UParenthesizedExpression) argument).getExpression(); if (expression != null) { checkTypeDefConstant(context, annotation, expression, errorNode, flag, allAnnotations); } } else if (argument instanceof UIfExpression) { UIfExpression expression = (UIfExpression) argument; if (expression.getThenExpression() != null) { checkTypeDefConstant(context, annotation, expression.getThenExpression(), errorNode, flag, allAnnotations); } if (expression.getElseExpression() != null) { checkTypeDefConstant(context, annotation, expression.getElseExpression(), errorNode, flag, allAnnotations); } } else if (argument instanceof UBinaryExpression) { // If it's ?: then check both the if and else clauses UBinaryExpression expression = (UBinaryExpression) argument; if (flag) { checkTypeDefConstant(context, annotation, expression.getLeftOperand(), errorNode, true, allAnnotations); checkTypeDefConstant(context, annotation, expression.getRightOperand(), errorNode, true, allAnnotations); } else { UastBinaryOperator operator = expression.getOperator(); if (operator == UastBinaryOperator.BITWISE_AND || operator == UastBinaryOperator.BITWISE_OR || operator == UastBinaryOperator.BITWISE_XOR) { if (isIgnoredInIde(TYPE_DEF, context, expression)) { return; } context.report(TYPE_DEF, expression, context.getUastLocation(expression), "Flag not allowed here"); } } } if (argument instanceof UReferenceExpression) { PsiElement resolved = ((UReferenceExpression) argument).resolve(); if (resolved instanceof PsiVariable) { PsiVariable variable = (PsiVariable) resolved; if (variable.getType() instanceof PsiArrayType) { // It's pointing to an array reference; we can't check these individual // elements (because we can't jump from ResolvedNodes to AST elements; this // is part of the motivation for the PSI change in lint 2.0), but we also // don't want to flag it as invalid. return; } // If it's a constant (static/final) check that it's one of the allowed ones if (variable.hasModifierProperty(PsiModifier.STATIC) && variable.hasModifierProperty(PsiModifier.FINAL)) { checkTypeDefConstant(context, annotation, argument, errorNode != null ? errorNode : argument, flag, resolved, allAnnotations); } else { UExpression lastAssignment = UastLintUtils.findLastAssignment(variable, argument, context); if (lastAssignment != null) { checkTypeDefConstant(context, annotation, lastAssignment, errorNode != null ? errorNode : argument, flag, allAnnotations); } } } } else if (UastExpressionUtils.isNewArrayWithInitializer(argument)) { UCallExpression arrayInitializer = (UCallExpression) argument; PsiType type = arrayInitializer.getExpressionType(); if (type != null) { type = type.getDeepComponentType(); } if (PsiType.INT.equals(type) || PsiType.LONG.equals(type)) { for (UExpression expression : arrayInitializer.getValueArguments()) { checkTypeDefConstant(context, annotation, expression, errorNode, flag, allAnnotations); } } } } private static void checkTypeDefConstant(@NonNull JavaContext context, @NonNull UAnnotation annotation, @NonNull UElement argument, @Nullable UElement errorNode, boolean flag, Object value, @NonNull List<UAnnotation> allAnnotations) { UAnnotation rangeAnnotation = findIntRange(allAnnotations); if (rangeAnnotation != null) { // Allow @IntRange on this number if (getIntRangeError(context, rangeAnnotation, argument) == null) { return; } } UExpression allowed = getAnnotationValue(annotation); if (allowed == null) { return; } if (UastExpressionUtils.isArrayInitializer(allowed)) { UCallExpression initializerExpression = (UCallExpression) allowed; List<UExpression> initializers = initializerExpression.getValueArguments(); for (UExpression expression : initializers) { if (expression instanceof ULiteralExpression) { if (value.equals(((ULiteralExpression)expression).getValue())) { return; } } else if (expression instanceof ExternalReferenceExpression) { PsiElement resolved = UastLintUtils.resolve( (ExternalReferenceExpression) expression, argument); if (resolved != null && resolved.equals(value)) { return; } } else if (expression instanceof UReferenceExpression) { PsiElement resolved = ((UReferenceExpression) expression).resolve(); if (resolved != null && resolved.equals(value)) { return; } } } if (value instanceof PsiField) { PsiField astNode = (PsiField)value; UExpression initializer = context.getUastContext().getInitializerBody(astNode); if (initializer != null) { checkTypeDefConstant(context, annotation, initializer, errorNode, flag, allAnnotations); return; } } reportTypeDef(context, argument, errorNode, flag, initializers, allAnnotations); } } private static void reportTypeDef( @NonNull JavaContext context, @NonNull UAnnotation annotation, @NonNull UElement argument, @Nullable UElement errorNode, @NonNull List<UAnnotation> allAnnotations ) { // reportTypeDef(context, argument, errorNode, false, allowedValues, allAnnotations); UExpression allowed = getAnnotationValue(annotation); if (UastExpressionUtils.isArrayInitializer(allowed)) { UCallExpression initializerExpression = (UCallExpression) allowed; List<UExpression> initializers = initializerExpression.getValueArguments(); reportTypeDef(context, argument, errorNode, false, initializers, allAnnotations); } } private static void reportTypeDef(@NonNull JavaContext context, @NonNull UElement node, @Nullable UElement errorNode, boolean flag, @NonNull List<UExpression> allowedValues, @NonNull List<UAnnotation> allAnnotations) { if (errorNode == null) { errorNode = node; } if (isIgnoredInIde(TYPE_DEF, context, errorNode)) { return; } String values = listAllowedValues(node, allowedValues); String message; if (flag) { message = "Must be one or more of: " + values; } else { message = "Must be one of: " + values; } UAnnotation rangeAnnotation = findIntRange(allAnnotations); if (rangeAnnotation != null) { // Allow @IntRange on this number String rangeError = getIntRangeError(context, rangeAnnotation, node); if (rangeError != null && !rangeError.isEmpty()) { message += " or " + Character.toLowerCase(rangeError.charAt(0)) + rangeError.substring(1); } } context.report(TYPE_DEF, errorNode, context.getUastLocation(errorNode), message); } @Nullable private static UExpression getAnnotationValue(@NonNull UAnnotation annotation) { UExpression value = annotation.findDeclaredAttributeValue(ATTR_VALUE); if (value == null) { value = annotation.findDeclaredAttributeValue(null); } return value; } private static String listAllowedValues(@NonNull UElement context, @NonNull List<UExpression> allowedValues) { StringBuilder sb = new StringBuilder(); for (UExpression allowedValue : allowedValues) { String s = null; PsiElement resolved = null; if (allowedValue instanceof ExternalReferenceExpression) { resolved = UastLintUtils.resolve( (ExternalReferenceExpression) allowedValue, context); } else if (allowedValue instanceof UReferenceExpression) { resolved = ((UReferenceExpression) allowedValue).resolve(); } if (resolved instanceof PsiField) { PsiField field = (PsiField) resolved; String containingClassName = field.getContainingClass() != null ? field.getContainingClass().getName() : null; if (containingClassName == null) { continue; } s = containingClassName + "." + field.getName(); } if (s == null) { s = allowedValue.asSourceString(); } if (sb.length() > 0) { sb.append(", "); } sb.append(s); } return sb.toString(); } static double getDoubleAttribute(@NonNull UAnnotation annotation, @NonNull String name, double defaultValue) { Double value = getAnnotationDoubleValue(annotation, name); if (value != null) { return value; } return defaultValue; } static long getLongAttribute(@NonNull UAnnotation annotation, @NonNull String name, long defaultValue) { Long value = getAnnotationLongValue(annotation, name); if (value != null) { return value; } return defaultValue; } static boolean getBoolean(@NonNull UAnnotation annotation, @NonNull String name, boolean defaultValue) { Boolean value = getAnnotationBooleanValue(annotation, name); if (value != null) { return value; } return defaultValue; } @NonNull static PsiAnnotation[] filterRelevantAnnotations( @NonNull JavaEvaluator evaluator, @NonNull PsiAnnotation[] annotations) { List<PsiAnnotation> result = null; int length = annotations.length; if (length == 0) { return annotations; } for (PsiAnnotation annotation : annotations) { String signature = annotation.getQualifiedName(); if (signature == null || signature.startsWith("java.")) { // @Override, @SuppressWarnings etc. Ignore continue; } if (signature.startsWith(SUPPORT_ANNOTATIONS_PREFIX)) { // Bail on the nullness annotations early since they're the most commonly // defined ones. They're not analyzed in lint yet. if (signature.endsWith(".Nullable") || signature.endsWith(".NonNull")) { continue; } // Common case: there's just one annotation; no need to create a list copy if (length == 1) { return annotations; } if (result == null) { result = new ArrayList<PsiAnnotation>(2); } result.add(annotation); } // Special case @IntDef and @StringDef: These are used on annotations // themselves. For example, you create a new annotation named @foo.bar.Baz, // annotate it with @IntDef, and then use @foo.bar.Baz in your signatures. // Here we want to map from @foo.bar.Baz to the corresponding int def. // Don't need to compute this if performing @IntDef or @StringDef lookup PsiJavaCodeReferenceElement ref = annotation.getNameReferenceElement(); if (ref == null) { continue; } PsiElement resolved = ref.resolve(); if (!(resolved instanceof PsiClass) || !((PsiClass)resolved).isAnnotationType()) { continue; } PsiClass cls = (PsiClass)resolved; PsiAnnotation[] innerAnnotations = evaluator.getAllAnnotations(cls); for (int j = 0; j < innerAnnotations.length; j++) { PsiAnnotation inner = innerAnnotations[j]; String a = inner.getQualifiedName(); if (a == null || a.startsWith("java.")) { // @Override, @SuppressWarnings etc. Ignore continue; } if (a.equals(INT_DEF_ANNOTATION) || a.equals(PERMISSION_ANNOTATION) || a.equals(INT_RANGE_ANNOTATION) || a.equals(STRING_DEF_ANNOTATION)) { if (length == 1 && j == innerAnnotations.length - 1 && result == null) { return innerAnnotations; } if (result == null) { result = new ArrayList<PsiAnnotation>(2); } result.add(inner); } } } return result != null ? result.toArray(PsiAnnotation.EMPTY_ARRAY) : PsiAnnotation.EMPTY_ARRAY; } // ---- Implements UastScanner ---- @Nullable @Override public List<Class<? extends UElement>> getApplicableUastTypes() { List<Class<? extends UElement>> types = new ArrayList<Class<? extends UElement>>(3); types.add(UCallExpression.class); types.add(UVariable.class); return types; } @Nullable @Override public UastVisitor createUastVisitor(@NonNull JavaContext context) { return new CallVisitor(context); } private class CallVisitor extends AbstractUastVisitor { private final JavaContext mContext; public CallVisitor(JavaContext context) { mContext = context; } @Override public boolean visitCallExpression(UCallExpression call) { PsiMethod method = call.resolve(); if (method != null) { checkCall(method, call); } return super.visitCallExpression(call); } @Override public boolean visitVariable(UVariable node) { if (node instanceof UEnumConstant) { UEnumConstant constant = (UEnumConstant) node; PsiMethod method = constant.resolveMethod(); checkCall(method, constant); } return super.visitVariable(node); } public void checkCall(PsiMethod method, UCallExpression call) { JavaEvaluator evaluator = mContext.getEvaluator(); List<UAnnotation> methodAnnotations; { PsiAnnotation[] annotations = evaluator.getAllAnnotations(method); methodAnnotations = JavaUAnnotation.wrap(filterRelevantAnnotations(evaluator, annotations)); } // Look for annotations on the class as well: these trickle // down to all the methods in the class PsiClass containingClass = method.getContainingClass(); List<UAnnotation> classAnnotations; if (containingClass != null) { PsiAnnotation[] annotations = evaluator.getAllAnnotations(containingClass); classAnnotations = JavaUAnnotation.wrap(filterRelevantAnnotations(evaluator, annotations)); } else { classAnnotations = Collections.emptyList(); } for (UAnnotation annotation : methodAnnotations) { checkMethodAnnotation(mContext, method, call, annotation, methodAnnotations, classAnnotations); } if (!classAnnotations.isEmpty()) { for (UAnnotation annotation : classAnnotations) { checkMethodAnnotation(mContext, method, call, annotation, methodAnnotations, classAnnotations); } } List<UExpression> arguments = call.getValueArguments(); PsiParameterList parameterList = method.getParameterList(); PsiParameter[] parameters = parameterList.getParameters(); List<UAnnotation> annotations = null; for (int i = 0, n = Math.min(parameters.length, arguments.size()); i < n; i++) { UExpression argument = arguments.get(i); PsiParameter parameter = parameters[i]; annotations = JavaUAnnotation.wrap( filterRelevantAnnotations(evaluator, evaluator.getAllAnnotations(parameter))); checkParameterAnnotations(mContext, argument, call, method, annotations); } if (annotations != null) { // last parameter is varargs (same parameter annotations) for (int i = parameters.length; i < arguments.size(); i++) { UExpression argument = arguments.get(i); checkParameterAnnotations(mContext, argument, call, method, annotations); } } } } }