| /* |
| * Copyright 2000-2014 JetBrains s.r.o. |
| * |
| * 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 org.jetbrains.android.inspections; |
| |
| import com.android.annotations.NonNull; |
| import com.android.resources.ResourceType; |
| import com.android.tools.idea.model.AndroidModuleInfo; |
| import com.android.tools.idea.model.DeclaredPermissionsLookup; |
| import com.android.tools.lint.checks.PermissionFinder; |
| import com.android.tools.lint.checks.PermissionHolder; |
| import com.android.tools.lint.checks.PermissionRequirement; |
| import com.google.common.base.Joiner; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| import com.intellij.analysis.AnalysisScope; |
| import com.intellij.codeInsight.AnnotationUtil; |
| import com.intellij.codeInsight.ExceptionUtil; |
| import com.intellij.codeInsight.FileModificationService; |
| import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer; |
| import com.intellij.codeInspection.*; |
| import com.intellij.ide.util.treeView.AbstractTreeNode; |
| import com.intellij.openapi.editor.Document; |
| import com.intellij.openapi.fileEditor.FileDocumentManager; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.Comparing; |
| import com.intellij.openapi.util.Key; |
| import com.intellij.openapi.util.TextRange; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.openapi.vfs.ReadonlyStatusHandler; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.psi.*; |
| import com.intellij.psi.codeStyle.CodeStyleManager; |
| import com.intellij.psi.codeStyle.JavaCodeStyleManager; |
| import com.intellij.psi.impl.JavaConstantExpressionEvaluator; |
| import com.intellij.psi.search.GlobalSearchScope; |
| import com.intellij.psi.search.LocalSearchScope; |
| import com.intellij.psi.tree.IElementType; |
| import com.intellij.psi.util.*; |
| import com.intellij.psi.xml.XmlTag; |
| import com.intellij.slicer.DuplicateMap; |
| import com.intellij.slicer.SliceAnalysisParams; |
| import com.intellij.slicer.SliceRootNode; |
| import com.intellij.slicer.SliceUsage; |
| import com.intellij.util.Function; |
| import com.intellij.util.Processor; |
| import com.intellij.util.containers.ContainerUtil; |
| import com.siyeh.ig.psiutils.ExpressionUtils; |
| import gnu.trove.THashSet; |
| import lombok.ast.BinaryOperator; |
| import lombok.ast.NullLiteral; |
| import org.intellij.lang.annotations.MagicConstant; |
| import org.jetbrains.android.dom.manifest.Manifest; |
| import org.jetbrains.android.facet.AndroidFacet; |
| import org.jetbrains.android.facet.AndroidRootUtil; |
| import org.jetbrains.android.inspections.lint.LombokPsiParser; |
| import org.jetbrains.android.util.AndroidUtils; |
| import org.jetbrains.annotations.Nls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.*; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| import static com.android.SdkConstants.*; |
| import static com.android.tools.lint.checks.PermissionFinder.Operation.*; |
| import static com.android.tools.lint.checks.SupportAnnotationDetector.*; |
| import static com.intellij.psi.CommonClassNames.DEFAULT_PACKAGE; |
| import static com.intellij.psi.CommonClassNames.JAVA_LANG_STRING; |
| import static com.intellij.psi.util.PsiFormatUtilBase.SHOW_CONTAINING_CLASS; |
| import static com.intellij.psi.util.PsiFormatUtilBase.SHOW_NAME; |
| |
| /** |
| * A custom version of the IntelliJ |
| * {@link com.intellij.codeInspection.magicConstant.MagicConstantInspection}, |
| * with two changes: |
| * <ol> |
| * <li> |
| * Checks for proper resource types, e.g. ensuring that you call |
| * getResources().getDimension(id) with an ID that is a constant from |
| * R.dimen, not for example R.string. |
| * </li> |
| * <li> |
| * Checks for typedef annotations. These are precisely like the IntelliJ |
| * MagicConstant annotations, but with different annotation names and |
| * fields (for example for the integer case there is an explicit flags= boolean |
| * annotation attribute. |
| * </li> |
| * </ol> |
| * <p> |
| * We didn't necessarily need to customize the inspection itself to handle the |
| * second case; instead, we could have translated the annotations.zip file at |
| * Gradle sync time to generate a derived annotations file with the annotation |
| * names and parameters rewritten as IntelliJ annotations. However, doing |
| * a resource type check is similar enough the the inspector needs to do nearly |
| * everything else anyway (check the same kinds of calls, load the external annotations |
| * for each resolved element, and so on) so with all that code in place we might |
| * as well also support the android support annotations natively. This also has |
| * the advantage that it will work with non-Gradle (IntelliJ and Maven) projects. |
| * <p> |
| * Since a lot of the code is identical to the IntelliJ magic constant inspection, |
| * I have left the code identical to that inspection as much as possible, in order |
| * to facilitate diffing the two classes and migrating future changes to the base |
| * inspection over to this one. |
| * <p> |
| * To diff this class with the inspection it was based on, check out tag |
| * idea/135.445 in the IntelliJ community edition git repository. |
| * <p> |
| * This means the code isn't as clear as possible; I just added a {@link ResourceType} |
| * field to the AllowedValues inner class to pass around the required ResourceType, |
| * which if non-null is the set of resource types we're enforcing rather than the |
| * other values held in the object. |
| * <p> |
| * The main methods that were modified are: |
| * <ul> |
| * <li> |
| * {@link #getAllowedValues(PsiModifierListOwner, PsiType, Set)}: |
| * Changed to look for IntDef/StringDef instead of MagicConstant, as well as look for the Resource type annotations |
| * ({@code }@StringRes}, {@code @IdRes}, etc) and for these we have to loop since you can specify more than one. |
| * </li> |
| * <li> |
| * {@code getAllowedValuesFromMagic()}: Changed to extract attributes from |
| * the support library's IntDef and TypeDef annotations and split into |
| * {@link #getAllowedValuesFromTypedef} and {@link #getResourceTypeFromAnnotation(String)} |
| * </li> |
| * <li> |
| * Created a new class, Constraint, which AllowedValues extends. AllowedValues now represents a value object |
| * for typedef constants. There are additional new subclasses which represent other constraints, such as |
| * ResourceTypeAllowedValues, IntRangeAllowedValues, etc. |
| * </li> |
| * <li> |
| * {@link #isAllowed}: Added checking for other types of allowed values, and if applicable, |
| * call new methods to check these, similar to the existing constant checks. |
| * </li> |
| * <li> |
| * Removed {@code checkAnnotationsJarAttached()}, since we will always provide SDK annotations |
| * for use by the type def collector in the SDK. (Also removed the code to call it and |
| * to clean up the associated key.) |
| * </li> |
| * <li> |
| * Sprinkled {@code @Nullable} annotations on various parameters and return values to |
| * remove warnings |
| * </li> |
| * <li> |
| * Removed {@code readFromClass} and code associated with picking all fields from |
| * a class |
| * </li> |
| * <li> |
| * Plugin registration: Increased severity to error, and changed category to Android. |
| * </li> |
| * <li> |
| * Stripped out {@code parseBeanInfo} |
| * </li> |
| * </ul> |
| * <p> |
| */ |
| public class ResourceTypeInspection extends BaseJavaLocalInspectionTool { |
| |
| @NotNull |
| @Override |
| public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, |
| boolean isOnTheFly, |
| @NotNull LocalInspectionToolSession session) { |
| AndroidFacet facet = AndroidFacet.getInstance(holder.getFile()); |
| if (facet == null) { |
| // No-op outside of Android modules |
| return new PsiElementVisitor() {}; |
| } |
| |
| return new JavaElementVisitor() { |
| @Override |
| public void visitCallExpression(PsiCallExpression callExpression) { |
| checkCall(callExpression, holder); |
| } |
| |
| @Override |
| public void visitEnumConstant(PsiEnumConstant enumConstant) { |
| checkCall(enumConstant, holder); |
| } |
| |
| @Override |
| public void visitAssignmentExpression(PsiAssignmentExpression expression) { |
| PsiExpression r = expression.getRExpression(); |
| if (r == null) return; |
| PsiExpression l = expression.getLExpression(); |
| if (!(l instanceof PsiReferenceExpression)) return; |
| PsiElement resolved = ((PsiReferenceExpression)l).resolve(); |
| if (!(resolved instanceof PsiModifierListOwner)) return; |
| PsiModifierListOwner owner = (PsiModifierListOwner)resolved; |
| PsiType type = expression.getType(); |
| checkExpression(r, owner, type, holder); |
| } |
| |
| @Override |
| public void visitReturnStatement(PsiReturnStatement statement) { |
| PsiExpression value = statement.getReturnValue(); |
| if (value == null) return; |
| @SuppressWarnings("unchecked") |
| PsiElement element = PsiTreeUtil.getParentOfType(statement, PsiMethod.class, PsiLambdaExpression.class); |
| PsiMethod method = element instanceof PsiMethod ? (PsiMethod)element : LambdaUtil.getFunctionalInterfaceMethod(element); |
| if (method == null) return; |
| checkExpression(value, method, value.getType(), holder); |
| } |
| |
| @Override |
| public void visitNameValuePair(PsiNameValuePair pair) { |
| PsiAnnotationMemberValue value = pair.getValue(); |
| if (!(value instanceof PsiExpression)) return; |
| PsiReference ref = pair.getReference(); |
| if (ref == null) return; |
| PsiMethod method = (PsiMethod)ref.resolve(); |
| if (method == null) return; |
| checkExpression((PsiExpression)value, method, method.getReturnType(), holder); |
| } |
| |
| @Override |
| public void visitBinaryExpression(PsiBinaryExpression expression) { |
| IElementType tokenType = expression.getOperationTokenType(); |
| if (tokenType != JavaTokenType.EQEQ && tokenType != JavaTokenType.NE) return; |
| PsiExpression l = expression.getLOperand(); |
| PsiExpression r = expression.getROperand(); |
| if (r == null) return; |
| checkBinary(l, r); |
| checkBinary(r, l); |
| } |
| |
| private void checkBinary(PsiExpression l, PsiExpression r) { |
| if (l instanceof PsiReference) { |
| PsiElement resolved = ((PsiReference)l).resolve(); |
| if (resolved instanceof PsiModifierListOwner) { |
| checkExpression(r, (PsiModifierListOwner)resolved, getType((PsiModifierListOwner)resolved), holder); |
| } |
| } |
| else if (l instanceof PsiMethodCallExpression) { |
| PsiMethod method = ((PsiMethodCallExpression)l).resolveMethod(); |
| if (method != null) { |
| checkExpression(r, method, method.getReturnType(), holder); |
| } |
| } |
| } |
| }; |
| } |
| |
| private static void checkExpression(PsiExpression expression, |
| PsiModifierListOwner owner, |
| @Nullable PsiType type, |
| ProblemsHolder holder) { |
| Constraints allowed = getAllowedValues(owner, type, null); |
| if (allowed == null) return; |
| //noinspection ConstantConditions |
| PsiElement scope = PsiUtil.getTopLevelEnclosingCodeBlock(expression, null); |
| if (scope == null) scope = expression; |
| |
| checkConstraints(scope, expression, allowed, holder); |
| } |
| |
| private static void checkConstraints(@NotNull PsiElement scope, |
| @NotNull PsiExpression expression, |
| @NotNull Constraints constraints, |
| @NotNull ProblemsHolder holder) { |
| if (expression.getTextRange().isEmpty()) { |
| return; |
| } |
| PsiManager manager = expression.getManager(); |
| if (constraints.next != null && ExpressionUtils.isLiteral(expression)) { |
| // The only possible combination of constraints is @IntDef and @IntRange, which allows you |
| // to specify that an argument must be one of a set of constants, OR, a number in a range. |
| if (!isAllowed(scope, expression, constraints, manager, null)) { |
| if (!isAllowed(scope, expression, constraints.next, manager, null)) { |
| registerProblem(expression, constraints, holder); |
| } |
| } else if (!isAllowed(scope, expression, constraints.next, manager, null)) { |
| registerProblem(expression, constraints.next, holder); |
| } |
| } else { |
| if (!isAllowed(scope, expression, constraints, manager, null)) { |
| registerProblem(expression, constraints, holder); |
| } |
| } |
| } |
| |
| private static void checkCall(@NotNull PsiCall methodCall, @NotNull ProblemsHolder holder) { |
| PsiMethod method = methodCall.resolveMethod(); |
| if (method == null) return; |
| PsiParameter[] parameters = method.getParameterList().getParameters(); |
| PsiExpressionList argumentList = methodCall.getArgumentList(); |
| if (argumentList == null) return; |
| PsiExpression[] arguments = argumentList.getExpressions(); |
| for (int i = 0; i < parameters.length; i++) { |
| PsiParameter parameter = parameters[i]; |
| Constraints values = getAllowedValues(parameter, parameter.getType(), null); |
| if (values == null) continue; |
| if (i >= arguments.length) break; |
| PsiExpression argument = arguments[i]; |
| argument = PsiUtil.deparenthesizeExpression(argument); |
| if (argument == null) continue; |
| |
| checkConstraints(parameter.getDeclarationScope(), argument, values, holder); |
| } |
| |
| checkMethodAnnotations(methodCall, holder, method); |
| } |
| |
| private static void checkMethodAnnotations(PsiCall methodCall, ProblemsHolder holder, PsiMethod method) { |
| PsiAnnotation[] annotations = getAllAnnotations(method); |
| for (PsiAnnotation annotation : annotations) { |
| String qualifiedName = annotation.getQualifiedName(); |
| if (qualifiedName == null) { |
| continue; |
| } else if (!qualifiedName.startsWith(SUPPORT_ANNOTATIONS_PREFIX)) { |
| if (qualifiedName.startsWith(DEFAULT_PACKAGE)) { // java.lang.Override, java.lang.SuppressWarnings etc, ignore |
| continue; |
| } else { |
| // Look for annotation that itself is annotated; we allow this for the @RequiresPermission annotation |
| PsiJavaCodeReferenceElement ref = annotation.getNameReferenceElement(); |
| PsiElement resolved = ref == null ? null : ref.resolve(); |
| if (!(resolved instanceof PsiClass) || !((PsiClass)resolved).isAnnotationType()) { |
| continue; |
| } |
| PsiClass cls = (PsiClass)resolved; |
| annotations = getAllAnnotations(cls); |
| for (PsiAnnotation a : annotations) { |
| qualifiedName = a.getQualifiedName(); |
| if (qualifiedName != null && qualifiedName.endsWith(PERMISSION_ANNOTATION)) { |
| checkPermissionRequirement(methodCall, holder, method, a); |
| } |
| } |
| |
| continue; |
| } |
| } |
| |
| if (PERMISSION_ANNOTATION.equals(qualifiedName)) { |
| checkPermissionRequirement(methodCall, holder, method, annotation); |
| } else if (CHECK_RESULT_ANNOTATION.equals(qualifiedName)) { |
| checkReturnValueUsage(methodCall, holder, method); |
| } else if (qualifiedName.endsWith(THREAD_SUFFIX)) { |
| checkThreadAnnotation(methodCall, holder, method, qualifiedName); |
| } |
| } |
| |
| PsiClass cls = method.getContainingClass(); |
| if (cls != null) { |
| annotations = getAllAnnotations(cls); |
| for (PsiAnnotation annotation : annotations) { |
| String qualifiedName = annotation.getQualifiedName(); |
| if (qualifiedName != null && qualifiedName.endsWith(THREAD_SUFFIX)) { |
| checkThreadAnnotation(methodCall, holder, method, qualifiedName); |
| } else if (qualifiedName != null && !qualifiedName.startsWith(DEFAULT_PACKAGE)) { |
| // Look for annotation that itself is annotated; we allow this for the @RequiresPermission annotation |
| PsiJavaCodeReferenceElement ref = annotation.getNameReferenceElement(); |
| PsiElement resolved = ref == null ? null : ref.resolve(); |
| if (!(resolved instanceof PsiClass) || !((PsiClass)resolved).isAnnotationType()) { |
| continue; |
| } |
| cls = (PsiClass)resolved; |
| annotations = getAllAnnotations(cls); |
| for (PsiAnnotation a : annotations) { |
| qualifiedName = a.getQualifiedName(); |
| if (qualifiedName != null && qualifiedName.endsWith(PERMISSION_ANNOTATION)) { |
| checkPermissionRequirement(methodCall, holder, method, a); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| @Nullable |
| private static PermissionFinder.Result search(@NonNull PsiElement node, @NonNull PermissionFinder.Operation operation) { |
| if (node instanceof NullLiteral) { |
| return null; |
| } else if (node instanceof PsiTypeCastExpression) { |
| PsiTypeCastExpression cast = (PsiTypeCastExpression) node; |
| final PsiExpression operand = cast.getOperand(); |
| if (operand != null) { |
| return search(operand, operation); |
| } |
| } else if (node instanceof PsiNewExpression && operation == PermissionFinder.Operation.ACTION) { |
| // Identifies "new Intent(argument)" calls and, if found, continues |
| // resolving the argument instead looking for the action definition |
| PsiNewExpression call = (PsiNewExpression)node; |
| PsiJavaCodeReferenceElement classOrAnonymousClassReference = call.getClassOrAnonymousClassReference(); |
| if (classOrAnonymousClassReference != null) { |
| String qualifiedName = classOrAnonymousClassReference.getQualifiedName(); |
| if (CLASS_INTENT.equals(qualifiedName)) { |
| PsiExpressionList argumentList = call.getArgumentList(); |
| if (argumentList != null) { |
| PsiExpression[] expressions = argumentList.getExpressions(); |
| if (expressions.length > 0) { |
| return search(expressions[0], operation); |
| } |
| } |
| } |
| } |
| return null; |
| } else if (node instanceof PsiJavaReference) { |
| PsiElement resolved = ((PsiJavaReference)node).resolve(); |
| if (resolved instanceof PsiField) { |
| PsiField field = (PsiField)resolved; |
| if (operation == PermissionFinder.Operation.ACTION) { |
| for (PsiAnnotation annotation : getAllAnnotations(field)) { |
| if (PERMISSION_ANNOTATION.equals(annotation.getQualifiedName())) { |
| return getPermissionRequirement(field, annotation, operation); |
| } |
| } |
| } |
| else if (operation == PermissionFinder.Operation.READ || operation == PermissionFinder.Operation.WRITE) { |
| String fqn = operation == PermissionFinder.Operation.READ ? PERMISSION_ANNOTATION_READ : PERMISSION_ANNOTATION_WRITE; |
| PsiAnnotation annotation = null; |
| for (PsiAnnotation a : getAllAnnotations(field)) { |
| if (fqn.equals(a.getQualifiedName())) { |
| annotation = null; |
| if (AnnotationUtil.isExternalAnnotation(a)) { |
| // The complex annotations used for read/write cannot be |
| // expressed in the external annotations format, so they're inlined. |
| // (See Extractor.AnnotationData#write). |
| // |
| // Instead we've inlined the fields of the annotation on the |
| // outer one: |
| annotation = a; |
| break; |
| } else { |
| final PsiAnnotationMemberValue o = a.findAttributeValue(ATTR_VALUE); |
| if (o instanceof PsiAnnotation) { |
| annotation = (PsiAnnotation)o; |
| if (PERMISSION_ANNOTATION.equals(annotation.getQualifiedName())) { |
| break; |
| } |
| } |
| } |
| } |
| } |
| if (annotation != null) { |
| return getPermissionRequirement(field, annotation, operation); |
| } else { |
| PsiExpression initializer = field.getInitializer(); |
| if (initializer instanceof PsiMethodCallExpression) { |
| PsiMethodCallExpression call = (PsiMethodCallExpression)initializer; |
| if (call.getMethodExpression().getQualifiedName().equals("Uri.withAppendedPath")) { |
| PsiExpression[] expressions = call.getArgumentList().getExpressions(); |
| if (expressions.length == 2) { |
| return search(expressions[0], operation); |
| } |
| } |
| } |
| } |
| } |
| else { |
| assert false : operation; |
| } |
| } else if (resolved instanceof PsiLocalVariable) { |
| PsiStatement statement = PsiTreeUtil.getParentOfType(node, PsiStatement.class, false); |
| while (statement != null) { |
| if (statement instanceof PsiDeclarationStatement) { |
| PsiDeclarationStatement declaration = (PsiDeclarationStatement)statement; |
| PsiElement[] declaredElements = declaration.getDeclaredElements(); |
| for (PsiElement declared : declaredElements) { |
| if (declared == resolved && declared instanceof PsiLocalVariable) { |
| // Found the declaration of the target variable; look at the right hand side to determine how |
| // to proceed. |
| PsiExpression initializer = ((PsiLocalVariable)declared).getInitializer(); |
| if (initializer != null) { |
| return search(initializer, operation); |
| } |
| } |
| } |
| } else if (statement instanceof PsiExpressionStatement) { |
| PsiExpression expression = ((PsiExpressionStatement)statement).getExpression(); |
| if (expression instanceof PsiAssignmentExpression) { |
| PsiAssignmentExpression assignment = (PsiAssignmentExpression)expression; |
| if (assignment.getLExpression() instanceof PsiReferenceExpression) { |
| PsiElement variable = ((PsiReferenceExpression)assignment.getLExpression()).resolve(); |
| if (variable == resolved) { |
| PsiExpression value = assignment.getRExpression(); |
| if (value != null) { |
| return search(value, operation); |
| } |
| } |
| } |
| } |
| } else if (statement instanceof PsiBinaryExpression) { |
| PsiBinaryExpression binary = (PsiBinaryExpression)statement; |
| if (binary.getOperationTokenType() == JavaTokenType.EQ) { |
| // Look at the LHS to see how to proceed |
| if (binary.getLOperand() == resolved && binary.getROperand() != null) { |
| // Found assignment: delegate to rhs. |
| return search(binary.getROperand(), operation); |
| } |
| } |
| } |
| statement = PsiTreeUtil.getPrevSiblingOfType(statement, PsiStatement.class); |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| @NonNull |
| private static PermissionFinder.Result getPermissionRequirement(@NonNull PsiField field, |
| @NonNull PsiAnnotation annotation, |
| @NonNull PermissionFinder.Operation operation) { |
| PermissionRequirement requirement = PermissionRequirement.create(null, LombokPsiParser.createResolvedAnnotation(annotation)); |
| PsiClass containingClass = field.getContainingClass(); |
| String name; |
| if (containingClass != null) { |
| name = containingClass.getName() + "." + field.getName(); |
| } else { |
| name = field.getName(); |
| } |
| return new PermissionFinder.Result(operation, requirement, name); |
| } |
| |
| private static void checkThreadAnnotation(PsiCall methodCall, ProblemsHolder holder, PsiMethod method, String qualifiedName) { |
| String threadContext = getThreadContext(methodCall); |
| if (threadContext != null && !isCompatibleThread(threadContext, qualifiedName)) { |
| String message = String.format("Method %1$s must be called from the %2$s thread, currently inferred thread is %3$s", |
| method.getName(), describeThread(qualifiedName), describeThread(threadContext)); |
| holder.registerProblem(methodCall, message); |
| } |
| } |
| |
| /** Attempts to infer the current thread context at the site of the given method call */ |
| @Nullable |
| private static String getThreadContext(PsiCall methodCall) { |
| PsiMethod method = PsiTreeUtil.getParentOfType(methodCall, PsiMethod.class, true); |
| if (method != null) { |
| PsiAnnotation[] annotations = getAllAnnotations(method); |
| for (PsiAnnotation annotation : annotations) { |
| String qualifiedName = annotation.getQualifiedName(); |
| if (qualifiedName == null) { |
| continue; |
| } |
| |
| if (qualifiedName.startsWith(SUPPORT_ANNOTATIONS_PREFIX) && qualifiedName.endsWith(THREAD_SUFFIX)) { |
| return qualifiedName; |
| } |
| } |
| |
| // See if we're extending a class with a known threading context |
| PsiClass cls = method.getContainingClass(); |
| if (cls != null) { |
| annotations = getAllAnnotations(cls); |
| for (PsiAnnotation annotation : annotations) { |
| String qualifiedName = annotation.getQualifiedName(); |
| if (qualifiedName == null) { |
| continue; |
| } |
| |
| if (qualifiedName.startsWith(SUPPORT_ANNOTATIONS_PREFIX) && qualifiedName.endsWith(THREAD_SUFFIX)) { |
| return qualifiedName; |
| } |
| } |
| } |
| } |
| |
| // TODO: Other heuristics I could use here are: |
| // - if extending view, use @UiThread |
| // - look at other calls in the method, and try to infer it based on those? (risky since if I have a method with two thread |
| // annotated calls and one of them is wrong, who do I choose as the authority? |
| |
| return null; |
| } |
| |
| private static void checkPermissionRequirement(PsiCall methodCall, |
| ProblemsHolder holder, |
| PsiMethod method, |
| PsiAnnotation annotation) { |
| PermissionRequirement requirement = PermissionRequirement.create(null, LombokPsiParser.createResolvedAnnotation(annotation)); |
| checkPermissionRequirement(methodCall, holder, method, null, requirement); |
| } |
| |
| private static void checkPermissionRequirement(PsiCall methodCall, |
| ProblemsHolder holder, |
| @Nullable PsiMethod method, |
| @Nullable PermissionFinder.Result result, |
| PermissionRequirement requirement) { |
| if (!requirement.isConditional()) { |
| Project project = methodCall.getProject(); |
| final AndroidFacet facet = AndroidFacet.getInstance(methodCall); |
| assert facet != null; // already checked early on in the inspection visitor |
| PermissionHolder lookup = DeclaredPermissionsLookup.getPermissionHolder(facet.getModule()); |
| if (!requirement.isSatisfied(lookup)) { |
| // See if it looks like we're holding the permission implicitly by @RequirePermission |
| // annotations in the surrounding context |
| lookup = addLocalPermissions(lookup, methodCall); |
| if (requirement.isSatisfied(lookup)) { |
| return; |
| } |
| |
| String methodName; |
| PermissionFinder.Operation operation; |
| if (result != null) { |
| operation = result.operation; |
| methodName = result.name; |
| } else { |
| assert method != null; |
| operation = PermissionFinder.Operation.CALL; |
| PsiClass containingClass = method.getContainingClass(); |
| methodName = containingClass != null ? containingClass.getName() + "." + method.getName() : method.getName(); |
| } |
| String message = getMissingPermissionMessage(requirement, methodName, lookup, operation); |
| LocalQuickFix[] fixes = LocalQuickFix.EMPTY_ARRAY; |
| List<LocalQuickFix> list = Lists.newArrayList(); |
| for (String permissionName : requirement.getMissingPermissions(lookup)) { |
| list.add(new AddPermissionFix(facet, permissionName)); |
| } |
| if (!list.isEmpty()) { |
| fixes = list.toArray(new LocalQuickFix[list.size()]); |
| } |
| holder.registerProblem(methodCall, message, fixes); |
| } else if (requirement.isRevocable(lookup) && AndroidModuleInfo.get(facet).getTargetSdkVersion().getFeatureLevel() >= 23) { |
| JavaPsiFacade psiFacade = JavaPsiFacade.getInstance(project); |
| PsiClass securityException = psiFacade.findClass("java.lang.SecurityException", GlobalSearchScope.allScope(project)); |
| if (securityException != null && ExceptionUtil.isHandled(PsiTypesUtil.getClassType(securityException), methodCall)) { |
| return; |
| } |
| |
| PsiMethod methodNode = PsiTreeUtil.getParentOfType(methodCall, PsiMethod.class, true); |
| if (methodNode != null) { |
| //noinspection unchecked |
| for (PsiMethodCallExpression call : PsiTreeUtil.collectElementsOfType(methodNode, PsiMethodCallExpression.class)) { |
| String name = call.getMethodExpression().getReferenceName(); |
| if (name != null && name.endsWith("Permission") && (name.startsWith("check") || name.startsWith("enforce"))) { |
| return; |
| } |
| } |
| |
| Set<String> revocablePermissions = requirement.getRevocablePermissions(lookup); |
| AddCheckPermissionFix fix = new AddCheckPermissionFix(facet, requirement, methodCall, revocablePermissions); |
| holder.registerProblem(methodCall, getUnhandledPermissionMessage(), fix); |
| } |
| } |
| } |
| } |
| |
| private static PermissionHolder addLocalPermissions(PermissionHolder lookup, PsiCall call) { |
| // Accumulate @RequirePermissions available in the local context |
| PsiMethod method = PsiTreeUtil.getParentOfType(call, PsiMethod.class); |
| if (method == null) { |
| return lookup; |
| } |
| PsiAnnotation annotation = AnnotationUtil.findAnnotationInHierarchy(method, Collections.singleton(PERMISSION_ANNOTATION)); |
| if (annotation == null) { |
| return lookup; |
| } |
| |
| PermissionRequirement requirement = PermissionRequirement.create(null, LombokPsiParser.createResolvedAnnotation(annotation)); |
| return PermissionHolder.SetPermissionLookup.join(lookup, requirement); |
| } |
| |
| private static void checkReturnValueUsage(PsiCall methodCall, ProblemsHolder holder, PsiMethod method) { |
| if (methodCall.getParent() instanceof PsiExpressionStatement) { |
| PsiAnnotation annotation = AnnotationUtil.findAnnotation(method, CHECK_RESULT_ANNOTATION); |
| if (annotation == null) { |
| return; |
| } |
| String message = String.format("The result '%1$s' is not used", method.getName()); |
| PsiAnnotationMemberValue value = annotation.findAttributeValue(ATTR_SUGGEST); |
| if (value instanceof PsiLiteral) { |
| PsiLiteral literal = (PsiLiteral)value; |
| Object literalValue = literal.getValue(); |
| if (literalValue instanceof String) { |
| String suggest = (String)literalValue; |
| // TODO: Resolve suggest attribute (e.g. prefix annotation class if it starts |
| // with "#" etc)? |
| if (!suggest.isEmpty()) { |
| String name = suggest; |
| if (name.startsWith("#")) { |
| name = name.substring(1); |
| } |
| message = String.format("The result of '%1$s' is not used; did you mean to call '%2$s'?", method.getName(), name); |
| if (suggest.startsWith("#") && methodCall instanceof PsiMethodCallExpression) { |
| holder.registerProblem(methodCall, message, new ReplaceCallFix((PsiMethodCallExpression)methodCall, suggest)); |
| return; |
| } |
| } |
| } |
| } |
| holder.registerProblem(methodCall, message); |
| } |
| } |
| |
| private static class AddPermissionFix implements LocalQuickFix { |
| private final AndroidFacet myFacet; |
| private final String myPermissionName; |
| |
| public AddPermissionFix(AndroidFacet facet, String permissionName) { |
| myFacet = facet; |
| myPermissionName = permissionName; |
| } |
| |
| @Nls |
| @NotNull |
| @Override |
| public String getName() { |
| return String.format("Add Permission %1$s", myPermissionName.substring(myPermissionName.lastIndexOf('.')+1)); |
| } |
| |
| @NotNull |
| @Override |
| public String getFamilyName() { |
| return "Add Permissions"; |
| } |
| |
| @Override |
| public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { |
| final VirtualFile manifestFile = AndroidRootUtil.getPrimaryManifestFile(myFacet); |
| if (manifestFile == null || !ReadonlyStatusHandler.ensureFilesWritable(myFacet.getModule().getProject(), manifestFile)) { |
| return; |
| } |
| |
| final Manifest manifest = AndroidUtils.loadDomElement(myFacet.getModule(), manifestFile, Manifest.class); |
| if (manifest == null) { |
| return; |
| } |
| |
| // I tried manipulating the file using DOM apis, using this: |
| // Permission permission = manifest.addPermission(); |
| // permission.getName().setValue(myPermissionName); |
| // (which required adding |
| // Permission addPermission(); |
| // to org.jetbrains.android.dom.manifest.Manifest). |
| // |
| // However, that will append a <permission name="something"/> element to the |
| // *end* of the manifest, which is not right (and will trigger a lint warning). |
| // So, instead we manipulate the XML document directly via PSI. (This is |
| // incidentally also how the AndroidModuleBuilder#configureManifest method works.) |
| final XmlTag manifestTag = manifest.getXmlTag(); |
| if (manifestTag == null) { |
| return; |
| } |
| |
| XmlTag permissionTag = manifestTag.createChildTag(TAG_USES_PERMISSION, "", null, false); |
| if (permissionTag != null) { |
| |
| XmlTag before = null; |
| // Find best insert position: |
| // (1) attempt to insert alphabetically among any permission tags |
| // (2) if no permission tags are found, put it before the application tag |
| for (XmlTag tag : manifestTag.getSubTags()) { |
| String tagName = tag.getName(); |
| if (tagName.equals(TAG_APPLICATION)) { |
| before = tag; |
| break; |
| } else if (tagName.equals(TAG_USES_PERMISSION)) { |
| String name = tag.getAttributeValue(ATTR_NAME, ANDROID_URI); |
| if (name != null && name.compareTo(myPermissionName) > 0) { |
| before = tag; |
| break; |
| } |
| } |
| } |
| if (before == null) { |
| permissionTag = manifestTag.addSubTag(permissionTag, false); |
| } else { |
| permissionTag = (XmlTag)manifestTag.addBefore(permissionTag, before); |
| } |
| |
| // Do this *after* adding the tag to the document; otherwise, setting the |
| // namespace prefix will not work correctly |
| permissionTag.setAttribute(ATTR_NAME, ANDROID_URI, myPermissionName); |
| |
| CodeStyleManager.getInstance(project).reformat(permissionTag); |
| |
| DeclaredPermissionsLookup.getInstance(project).reset(); |
| FileDocumentManager.getInstance().saveAllDocuments(); |
| PsiFile containingFile = permissionTag.getContainingFile(); |
| // No edits were made to the current document, so trigger a rescan to ensure |
| // that the inspection discovers that there is now a new available inspection |
| if (containingFile != null) { |
| DaemonCodeAnalyzer.getInstance(project).restart(); |
| } |
| } |
| } |
| } |
| |
| private static class AddCheckPermissionFix implements LocalQuickFix { |
| private final AndroidFacet myFacet; |
| private final PermissionRequirement myRequirement; |
| private final Set<String> myRevocablePermissions; |
| private final PsiCall myCall; |
| |
| public AddCheckPermissionFix(AndroidFacet facet, PermissionRequirement requirement, PsiCall call, |
| Set<String> revocablePermissions) { |
| myFacet = facet; |
| myRequirement = requirement; |
| myCall = call; |
| myRevocablePermissions = revocablePermissions; |
| } |
| |
| @Nls |
| @NotNull |
| @Override |
| public String getName() { |
| return "Add Permission Check"; |
| } |
| |
| @NotNull |
| @Override |
| public String getFamilyName() { |
| return getName(); |
| } |
| |
| @Override |
| public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { |
| // Find the statement containing the method call; |
| PsiStatement statement = PsiTreeUtil.getParentOfType(myCall, PsiStatement.class, true); |
| if (statement == null) { |
| return; |
| } |
| PsiElement parent = statement.getParent(); |
| if (parent == null) { |
| return; // highly unlikely |
| } |
| |
| JavaPsiFacade facade = JavaPsiFacade.getInstance(project); |
| PsiClass manifest = facade.findClass("android.Manifest.permission", GlobalSearchScope.moduleWithLibrariesScope(myFacet.getModule())); |
| Map<String, PsiField> permissionNames; |
| |
| if (manifest != null) { |
| PsiField[] fields = manifest.getFields(); |
| permissionNames = Maps.newHashMapWithExpectedSize(fields.length); |
| for (PsiField field : fields) { |
| PsiExpression initializer = field.getInitializer(); |
| if (initializer instanceof PsiLiteralExpression) { |
| Object value = ((PsiLiteralExpression)initializer).getValue(); |
| if (value instanceof String) { |
| permissionNames.put((String)value, field); |
| } |
| } |
| } |
| } else { |
| permissionNames = Collections.emptyMap(); |
| } |
| |
| // Look up the operator combining the requirements, and *reverse it*. |
| // That's because we're adding a check to exit if the permissions are *not* met. |
| // For example, take the case of location permissions: you need COARSE OR FINE. |
| // In that case, we check that you do not have COARSE, *and* that you do not have FINE, |
| // before we exit. |
| BinaryOperator operator = myRequirement.getOperator(); |
| if (operator == null || operator == BinaryOperator.LOGICAL_AND) { |
| operator = BinaryOperator.LOGICAL_OR; |
| } else if (operator == BinaryOperator.LOGICAL_OR) { |
| operator = BinaryOperator.LOGICAL_AND; |
| } |
| |
| PsiElementFactory factory = facade.getElementFactory(); |
| StringBuilder sb = new StringBuilder(200); |
| sb.append("if ("); |
| boolean first = true; |
| for (String permission : myRevocablePermissions) { |
| if (first) { |
| first = false; |
| } else { |
| sb.append(' '); |
| sb.append(operator.getSymbol()); |
| sb.append(' '); |
| } |
| sb.append("checkSelfPermission("); |
| |
| // Try to map permission strings back to field references! |
| PsiField field = permissionNames.get(permission); |
| if (field != null && field.getContainingClass() != null) { |
| sb.append(field.getContainingClass().getQualifiedName()).append('.').append(field.getName()); |
| } else { |
| sb.append('"').append(permission).append('"'); |
| } |
| sb.append(") != android.content.pm.PackageManager.PERMISSION_GRANTED"); |
| } |
| sb.append(") {\n"); |
| sb.append(" // TODO: Consider calling\n" + |
| " // public void requestPermissions(@NonNull String[] permissions, int requestCode)\n" + |
| " // here to request the missing permissions, and then overriding\n" + |
| " // public void onRequestPermissionsResult(int requestCode, String[] permissions,\n" + |
| " // int[] grantResults)\n" + |
| " // to handle the case where the user grants the permission. See the documentation\n" + |
| " // for Activity#requestPermissions for more details.\n"); |
| PsiMethod method = PsiTreeUtil.getParentOfType(myCall, PsiMethod.class, true); |
| if (method != null && !PsiType.VOID.equals(method.getReturnType())) { |
| sb.append("return TODO;\n"); |
| } else { |
| sb.append("return;\n"); |
| } |
| sb.append("}\n"); |
| String code = sb.toString(); |
| |
| PsiStatement check = factory.createStatementFromText(code, myCall); |
| JavaCodeStyleManager.getInstance(project).shortenClassReferences(check); |
| parent.addBefore(check, statement); |
| |
| // Reformat from start of newly added element to end of statement following it |
| CodeStyleManager.getInstance(project).reformatRange(parent, check.getTextOffset(), |
| statement.getTextOffset() + statement.getTextLength()); |
| } |
| } |
| |
| private static class ReplaceCallFix implements LocalQuickFix { |
| |
| private final PsiMethodCallExpression myMethodCall; |
| private final String mySuggest; |
| |
| public ReplaceCallFix(PsiMethodCallExpression methodCall, String suggest) { |
| myMethodCall = methodCall; |
| mySuggest = suggest; |
| } |
| |
| @Nls |
| @NotNull |
| @Override |
| public String getName() { |
| return String.format("Call %1$s instead", getMethodName()); |
| } |
| |
| @NotNull |
| @Override |
| public String getFamilyName() { |
| return "Replace Calls"; |
| } |
| |
| private String getMethodName() { |
| assert mySuggest.startsWith("#"); |
| int start = 1; |
| int parameters = mySuggest.indexOf('(', start); |
| if (parameters == -1) { |
| parameters = mySuggest.length(); |
| } |
| return mySuggest.substring(start, parameters).trim(); |
| } |
| |
| @Override |
| public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { |
| if (!myMethodCall.isValid()) { |
| return; |
| } |
| String name = getMethodName(); |
| final PsiFile file = myMethodCall.getContainingFile(); |
| if (file == null || !FileModificationService.getInstance().prepareFileForWrite(file)) { |
| return; |
| } |
| |
| Document document = FileDocumentManager.getInstance().getDocument(file.getVirtualFile()); |
| if (document != null) { |
| PsiReferenceExpression methodExpression = myMethodCall.getMethodExpression(); |
| PsiElement referenceNameElement = methodExpression.getReferenceNameElement(); |
| if (referenceNameElement != null) { |
| TextRange range = referenceNameElement.getTextRange(); |
| if (range != null) { |
| // Also need to insert a message parameter |
| // Currently hardcoded for the check*Permission to enforce*Permission code path. It's |
| // tricky to figure out in general how to map existing parameters to new |
| // parameters. Consider using MethodSignatureInsertHandler. |
| if (name.startsWith("enforce") && methodExpression.getReferenceName() != null |
| && methodExpression.getReferenceName().startsWith("check")) { |
| PsiExpressionList argumentList = myMethodCall.getArgumentList(); |
| int offset = argumentList.getTextOffset() + argumentList.getTextLength() - 1; |
| document.insertString(offset, ", \"TODO: message if thrown\""); |
| } |
| |
| // Replace method call |
| document.replaceString(range.getStartOffset(), range.getEndOffset(), name); |
| } |
| } |
| } |
| } |
| } |
| |
| static class Constraints { |
| public boolean isSubsetOf(@NotNull Constraints other, @NotNull PsiManager manager) { |
| return false; |
| } |
| |
| /** Linked list next reference, when more than one applies */ |
| @Nullable public Constraints next; |
| } |
| |
| /** |
| * A typedef constraint. Then name is kept as "AllowedValues" to keep all the surrounding code |
| * which references this class unchanged (since it's based on MagicConstantInspection, so we |
| * can more easily diff and incorporate recent MagicConstantInspection changes.) |
| */ |
| static class AllowedValues extends Constraints { |
| final PsiAnnotationMemberValue[] values; |
| final boolean canBeOred; |
| |
| private AllowedValues(@NotNull PsiAnnotationMemberValue[] values, boolean canBeOred) { |
| this.values = values; |
| this.canBeOred = canBeOred; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) return true; |
| if (o == null || getClass() != o.getClass()) return false; |
| |
| AllowedValues a2 = (AllowedValues)o; |
| if (canBeOred != a2.canBeOred) { |
| return false; |
| } |
| Set<PsiAnnotationMemberValue> v1 = new THashSet<PsiAnnotationMemberValue>(Arrays.asList(values)); |
| Set<PsiAnnotationMemberValue> v2 = new THashSet<PsiAnnotationMemberValue>(Arrays.asList(a2.values)); |
| if (v1.size() != v2.size()) { |
| return false; |
| } |
| for (PsiAnnotationMemberValue value : v1) { |
| for (PsiAnnotationMemberValue value2 : v2) { |
| if (same(value, value2, value.getManager())) { |
| v2.remove(value2); |
| break; |
| } |
| } |
| } |
| return v2.isEmpty(); |
| } |
| |
| @Override |
| public int hashCode() { |
| int result = Arrays.hashCode(values); |
| result = 31 * result + (canBeOred ? 1 : 0); |
| return result; |
| } |
| |
| @Override |
| public boolean isSubsetOf(@NotNull Constraints other, @NotNull PsiManager manager) { |
| if (!(other instanceof AllowedValues)) { |
| return false; |
| } |
| AllowedValues o = (AllowedValues)other; |
| for (PsiAnnotationMemberValue value : values) { |
| boolean found = false; |
| for (PsiAnnotationMemberValue otherValue : o.values) { |
| if (same(value, otherValue, manager)) { |
| found = true; |
| break; |
| } |
| } |
| if (!found) return false; |
| } |
| return true; |
| } |
| } |
| |
| static class ResourceTypeAllowedValues extends Constraints { |
| /** |
| * Type of Android resource that we must be passing. An empty list means no |
| * resource type is allowed; this is currently used for {@code @ColorInt}, |
| * stating that not only is it <b>not</b> supposed to be a {@code R.color.name}, |
| * but it should correspond to an ARGB integer. |
| */ |
| @NotNull |
| final List<ResourceType> types; |
| |
| public ResourceTypeAllowedValues(@NotNull List<ResourceType> types) { |
| this.types = types; |
| } |
| |
| /** Returns true if this resource type constraint allows a type of the given name */ |
| public boolean isTypeAllowed(@NotNull String typeName) { |
| for (ResourceType type : types) { |
| if (type.getName().equals(typeName) || |
| type == ResourceType.DRAWABLE && |
| (ResourceType.COLOR.getName().equals(typeName) || ResourceType.MIPMAP.getName().equals(typeName))) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Returns true if the resource type constraint is compatible with the other resource type |
| * constraint |
| * |
| * @param other the resource type constraint to compare it to |
| * @return true if the two resource constraints are compatible |
| */ |
| public boolean isCompatibleWith(@NotNull ResourceTypeAllowedValues other) { |
| // Happy if *any* of the resource types on the annotation matches any of the |
| // annotations allowed for this API |
| if (other.types.isEmpty() && types.isEmpty()) { |
| // Passing in a method call whose return value is @ColorInt |
| // to a parameter which is @ColorInt: OK |
| return true; |
| } |
| for (ResourceType t1 : other.types) { |
| for (ResourceType t2 : types) { |
| if (t1 == t2) { |
| return true; |
| } |
| } |
| } |
| |
| return false; |
| } |
| } |
| |
| static class RangeAllowedValues extends Constraints { |
| @NotNull |
| public String describe(@NotNull PsiExpression argument) { |
| assert false; |
| return ""; |
| } |
| |
| @MagicConstant(intValues={VALID,INVALID,UNCERTAIN}) |
| public int isValid(@NotNull PsiExpression argument) { |
| return UNCERTAIN; |
| } |
| |
| @Nullable |
| protected Number guessSize(@NotNull PsiExpression argument) { |
| if (argument instanceof PsiLiteral) { |
| PsiLiteral literal = (PsiLiteral)argument; |
| Object v = literal.getValue(); |
| if (v instanceof Number) { |
| return (Number)v; |
| } |
| } else if (argument instanceof PsiBinaryExpression) { |
| Object v = JavaConstantExpressionEvaluator.computeConstantExpression(argument, false); |
| if (v instanceof Number) { |
| return (Number)v; |
| } |
| } else if (argument instanceof PsiReferenceExpression) { |
| PsiReferenceExpression ref = (PsiReferenceExpression)argument; |
| PsiElement resolved = ref.resolve(); |
| if (resolved instanceof PsiField) { |
| PsiField field = (PsiField)resolved; |
| PsiExpression initializer = field.getInitializer(); |
| if (initializer != null) { |
| return guessSize(initializer); |
| } |
| } else if (resolved instanceof PsiLocalVariable) { |
| PsiLocalVariable variable = (PsiLocalVariable) resolved; |
| PsiExpression initializer = variable.getInitializer(); |
| if (initializer != null) { |
| return guessSize(initializer); |
| } |
| } |
| } else if (argument instanceof PsiPrefixExpression) { |
| PsiPrefixExpression prefix = (PsiPrefixExpression)argument; |
| if (prefix.getOperationTokenType() == JavaTokenType.MINUS) { |
| PsiExpression operand = prefix.getOperand(); |
| if (operand != null) { |
| Number number = guessSize(operand); |
| if (number != null) { |
| if (number instanceof Long) { |
| return -number.longValue(); |
| } else if (number instanceof Integer) { |
| return -number.intValue(); |
| } else if (number instanceof Double) { |
| return -number.doubleValue(); |
| } else if (number instanceof Float) { |
| return -number.floatValue(); |
| } else if (number instanceof Short) { |
| return -number.shortValue(); |
| } else if (number instanceof Byte) { |
| return -number.byteValue(); |
| } |
| } |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Checks whether the given range is compatible with this one. |
| * We err on the side of caution. E.g. if we have |
| * <pre> |
| * method(x) |
| * </pre> |
| * and the parameter declaration says that x is between 0 and 10, |
| * and then we have a parameter which is known to be in the range 5 to 15, |
| * here we consider this a compatible range; we don't flag this as |
| * an error. If however, the ranges don't overlap, *then* we complain. |
| */ |
| public int isCompatibleWith(@NotNull RangeAllowedValues other) { |
| return UNCERTAIN; |
| } |
| } |
| |
| static class IntRangeConstraint extends RangeAllowedValues { |
| final long from; |
| final long to; |
| |
| public IntRangeConstraint(@NotNull PsiAnnotation annotation) { |
| PsiAnnotationMemberValue fromValue = annotation.findDeclaredAttributeValue(ATTR_FROM); |
| PsiAnnotationMemberValue toValue = annotation.findDeclaredAttributeValue(ATTR_TO); |
| from = getLongValue(fromValue, Long.MIN_VALUE); |
| to = getLongValue(toValue, Long.MAX_VALUE); |
| } |
| |
| @Override |
| @MagicConstant(intValues = {VALID, INVALID, UNCERTAIN}) |
| public int isValid(@NotNull PsiExpression argument) { |
| Number literalValue = guessSize(argument); |
| if (literalValue != null) { |
| long value = literalValue.longValue(); |
| return value >= from && value <= to ? VALID : INVALID; |
| } |
| |
| return UNCERTAIN; |
| } |
| |
| @NotNull |
| @Override |
| public String describe(@NotNull PsiExpression argument) { |
| StringBuilder sb = new StringBuilder(20); |
| if (to == Long.MAX_VALUE) { |
| sb.append("Value must be \u2265 "); |
| sb.append(Long.toString(from)); |
| } |
| else if (from == Long.MIN_VALUE) { |
| sb.append("Value must be \u2264 "); |
| sb.append(Long.toString(to)); |
| } |
| else { |
| sb.append("Value must be \u2265 "); |
| sb.append(Long.toString(from)); |
| sb.append(" and \u2264 "); |
| sb.append(Long.toString(to)); |
| } |
| Number actual = guessSize(argument); |
| if (actual != null) { |
| sb.append(" (was ").append(Integer.toString(actual.intValue())).append(')'); |
| } |
| return sb.toString(); |
| } |
| |
| @Override |
| public int isCompatibleWith(@NotNull RangeAllowedValues other) { |
| if (other instanceof IntRangeConstraint) { |
| IntRangeConstraint otherRange = (IntRangeConstraint)other; |
| return otherRange.from > to || otherRange.to < from ? INVALID : VALID; |
| } else if (other instanceof FloatRangeConstraint) { |
| FloatRangeConstraint otherRange = (FloatRangeConstraint)other; |
| return otherRange.from > to || otherRange.to < from ? INVALID : VALID; |
| } |
| return UNCERTAIN; |
| } |
| } |
| |
| static class FloatRangeConstraint extends RangeAllowedValues { |
| final double from; |
| final double to; |
| final boolean fromInclusive; |
| final boolean toInclusive; |
| |
| public FloatRangeConstraint(@NotNull PsiAnnotation annotation) { |
| PsiAnnotationMemberValue fromValue = annotation.findDeclaredAttributeValue(ATTR_FROM); |
| PsiAnnotationMemberValue toValue = annotation.findDeclaredAttributeValue(ATTR_TO); |
| PsiAnnotationMemberValue fromInclusiveValue = annotation.findDeclaredAttributeValue(ATTR_FROM_INCLUSIVE); |
| PsiAnnotationMemberValue toInclusiveValue = annotation.findDeclaredAttributeValue(ATTR_TO_INCLUSIVE); |
| from = getDoubleValue(fromValue, Double.NEGATIVE_INFINITY); |
| to = getDoubleValue(toValue, Double.POSITIVE_INFINITY); |
| fromInclusive = getBooleanValue(fromInclusiveValue, true); |
| toInclusive = getBooleanValue(toInclusiveValue, true); |
| } |
| |
| @Override |
| @MagicConstant(intValues={VALID,INVALID,UNCERTAIN}) |
| public int isValid(@NotNull PsiExpression argument) { |
| Number number = guessSize(argument); |
| if (number != null) { |
| double value = number.doubleValue(); |
| if (!((fromInclusive && value >= from || !fromInclusive && value > from) && |
| (toInclusive && value <= to || !toInclusive && value < to))) { |
| return INVALID; |
| } |
| return VALID; |
| } |
| return UNCERTAIN; |
| } |
| |
| @NotNull |
| @Override |
| public String describe(@NotNull PsiExpression argument) { |
| StringBuilder sb = new StringBuilder(20); |
| if (from != Double.NEGATIVE_INFINITY) { |
| if (to != Double.POSITIVE_INFINITY) { |
| sb.append("Value must be "); |
| if (fromInclusive) { |
| sb.append('\u2265'); // >= sign |
| } else { |
| sb.append('>'); |
| } |
| sb.append(' '); |
| sb.append(Double.toString(from)); |
| sb.append(" and "); |
| 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)); |
| } |
| Number actual = guessSize(argument); |
| if (actual != null) { |
| sb.append(" (was "); |
| // Try to avoid going through an actual double which introduces |
| // potential rounding ugliness (e.g. source can say "2.49f" and this is printed as "2.490000009536743") |
| if (argument instanceof PsiLiteral) { |
| PsiLiteral literal = (PsiLiteral)argument; |
| sb.append(literal.getText()); |
| } else { |
| sb.append(Double.toString(actual.doubleValue())); |
| } |
| sb.append(')'); |
| } |
| return sb.toString(); |
| } |
| |
| @Override |
| public int isCompatibleWith(@NotNull RangeAllowedValues other) { |
| if (other instanceof FloatRangeConstraint) { |
| FloatRangeConstraint otherRange = (FloatRangeConstraint)other; |
| return otherRange.from > to || otherRange.to < from ? INVALID : VALID; |
| } else if (other instanceof IntRangeConstraint) { |
| IntRangeConstraint otherRange = (IntRangeConstraint)other; |
| return otherRange.from > to || otherRange.to < from ? INVALID : VALID; |
| } |
| return UNCERTAIN; |
| } |
| } |
| |
| static class SizeConstraint extends RangeAllowedValues { |
| final long exact; |
| final long min; |
| final long max; |
| final long multiple; |
| |
| public SizeConstraint(@NotNull PsiAnnotation annotation) { |
| PsiAnnotationMemberValue exactValue = annotation.findAttributeValue(ATTR_VALUE); |
| PsiAnnotationMemberValue fromValue = annotation.findDeclaredAttributeValue(ATTR_MIN); |
| PsiAnnotationMemberValue toValue = annotation.findDeclaredAttributeValue(ATTR_MAX); |
| PsiAnnotationMemberValue multipleValue = annotation.findDeclaredAttributeValue(ATTR_MULTIPLE); |
| exact = getLongValue(exactValue, -1); |
| min = getLongValue(fromValue, Long.MIN_VALUE); |
| max = getLongValue(toValue, Long.MAX_VALUE); |
| multiple = getLongValue(multipleValue, 1); |
| } |
| |
| @Override |
| @MagicConstant(intValues={VALID,INVALID,UNCERTAIN}) |
| public int isValid(@NotNull PsiExpression argument) { |
| Number size = guessSize(argument); |
| if (size == null) { |
| return UNCERTAIN; |
| } |
| int actual = size.intValue(); |
| if (exact != -1) { |
| if (exact != actual) { |
| return INVALID; |
| } |
| } else if (actual < min || actual > max || actual % multiple != 0) { |
| return INVALID; |
| } |
| |
| return VALID; |
| } |
| |
| @Override |
| protected Number guessSize(@NotNull PsiExpression argument) { |
| if (argument instanceof PsiNewExpression) { |
| PsiNewExpression pne = (PsiNewExpression)argument; |
| PsiArrayInitializerExpression initializer = pne.getArrayInitializer(); |
| if (initializer != null) { |
| return initializer.getInitializers().length; |
| } |
| PsiExpression[] dimensions = pne.getArrayDimensions(); |
| if (dimensions.length > 0) { |
| PsiExpression dimension = dimensions[0]; |
| return super.guessSize(dimension); |
| } |
| } else if (argument instanceof PsiLiteral) { |
| PsiLiteral literal = (PsiLiteral)argument; |
| Object o = literal.getValue(); |
| if (o instanceof String) { |
| return ((String)o).length(); |
| } |
| } else if (argument instanceof PsiBinaryExpression) { |
| Object v = JavaConstantExpressionEvaluator.computeConstantExpression(argument, false); |
| if (v instanceof String) { |
| return ((String)v).length(); |
| } |
| } else if (argument instanceof PsiReferenceExpression) { |
| PsiReferenceExpression ref = (PsiReferenceExpression)argument; |
| PsiElement resolved = ref.resolve(); |
| if (resolved instanceof PsiField) { |
| PsiField field = (PsiField)resolved; |
| PsiExpression initializer = field.getInitializer(); |
| if (initializer != null) { |
| return guessSize(initializer); |
| } |
| } else if (resolved instanceof PsiLocalVariable) { |
| PsiLocalVariable variable = (PsiLocalVariable) resolved; |
| PsiExpression initializer = variable.getInitializer(); |
| if (initializer != null) { |
| return guessSize(initializer); |
| } |
| } |
| } |
| return null; |
| } |
| |
| @NotNull |
| @Override |
| public String describe(@NotNull PsiExpression argument) { |
| StringBuilder sb = new StringBuilder(20); |
| if (argument.getType() != null && argument.getType().getCanonicalText().equals(JAVA_LANG_STRING)) { |
| sb.append("Length"); |
| } else { |
| sb.append("Size"); |
| } |
| sb.append(" must be"); |
| if (exact != -1) { |
| sb.append(" exactly "); |
| sb.append(Long.toString(exact)); |
| return sb.toString(); |
| } |
| boolean continued = true; |
| if (min != Long.MIN_VALUE && max != Long.MAX_VALUE) { |
| sb.append(" at least "); |
| sb.append(Long.toString(min)); |
| sb.append(" and at most "); |
| sb.append(Long.toString(max)); |
| } else if (min != Long.MIN_VALUE) { |
| sb.append(" at least "); |
| sb.append(Long.toString(min)); |
| } else if (max != Long.MAX_VALUE) { |
| sb.append(" at most "); |
| sb.append(Long.toString(max)); |
| } else { |
| continued = false; |
| } |
| if (multiple != 1) { |
| if (continued) { |
| sb.append(" and"); |
| } |
| sb.append(" a multiple of "); |
| sb.append(Long.toString(multiple)); |
| } |
| Number actual = guessSize(argument); |
| if (actual != null) { |
| sb.append(" (was ").append(Integer.toString(actual.intValue())).append(')'); |
| } |
| return sb.toString(); |
| } |
| |
| @Override |
| public int isCompatibleWith(@NotNull RangeAllowedValues other) { |
| if (other instanceof SizeConstraint) { |
| SizeConstraint otherRange = (SizeConstraint)other; |
| if ((exact != -1 || otherRange.exact != -1) && exact != otherRange.exact) { |
| return INVALID; |
| } |
| return otherRange.min > max || otherRange.max < min ? INVALID : VALID; |
| } |
| return UNCERTAIN; |
| } |
| } |
| |
| static class IndirectPermission extends Constraints { |
| public final String signature; |
| @Nullable public PermissionFinder.Result result; |
| |
| public IndirectPermission(String signature) { |
| this.signature = signature; |
| } |
| } |
| |
| @Nullable |
| private static Constraints getAllowedValuesFromTypedef(@NotNull PsiType type, |
| @NotNull PsiAnnotation magic, |
| @NotNull PsiManager manager) { |
| PsiAnnotationMemberValue[] allowedValues; |
| final boolean canBeOred; |
| boolean isInt = TypeConversionUtil.getTypeRank(type) <= TypeConversionUtil.LONG_RANK; |
| boolean isString = !isInt && type.equals(PsiType.getJavaLangString(manager, GlobalSearchScope.allScope(manager.getProject()))); |
| if (isInt || isString) { |
| PsiAnnotationMemberValue intValues = magic.findAttributeValue(TYPE_DEF_VALUE_ATTRIBUTE); |
| allowedValues = intValues instanceof PsiArrayInitializerMemberValue ? ((PsiArrayInitializerMemberValue)intValues).getInitializers() : PsiAnnotationMemberValue.EMPTY_ARRAY; |
| |
| if (isInt) { |
| PsiAnnotationMemberValue orValue = magic.findAttributeValue(TYPE_DEF_FLAG_ATTRIBUTE); |
| canBeOred = orValue instanceof PsiLiteral && Boolean.TRUE.equals(((PsiLiteral)orValue).getValue()); |
| } else { |
| canBeOred = false; |
| } |
| } else { |
| return null; //other types not supported |
| } |
| |
| if (allowedValues.length != 0) { |
| return new AllowedValues(allowedValues, canBeOred); |
| } |
| |
| return null; |
| } |
| |
| @Nullable |
| public static ResourceType getResourceTypeFromAnnotation(@NotNull String qualifiedName) { |
| String resourceTypeName = |
| Character.toLowerCase(qualifiedName.charAt(SUPPORT_ANNOTATIONS_PREFIX.length())) + |
| qualifiedName.substring(SUPPORT_ANNOTATIONS_PREFIX.length() + 1, qualifiedName.length() - RES_SUFFIX.length()); |
| return ResourceType.getEnum(resourceTypeName); |
| } |
| |
| @Nullable |
| private static Constraints merge(@Nullable Constraints head, @Nullable Constraints tail) { |
| if (head != null) { |
| if (tail != null) { |
| head.next = tail; |
| |
| // The only valid combination of multiple constraints are @IntDef and @IntRange. |
| // In this case, always arrange for the IntDef constraint to be processed first |
| if (tail instanceof AllowedValues) { |
| head.next = tail.next; |
| tail.next = head; |
| head = tail; |
| } |
| |
| return head; |
| } else { |
| return head; |
| } |
| } |
| return tail; |
| } |
| |
| @Nullable |
| public static Constraints getAllowedValues(@NotNull PsiModifierListOwner element, @Nullable PsiType type, @Nullable Set<PsiClass> visited) { |
| PsiAnnotation[] annotations = getAllAnnotations(element); |
| PsiManager manager = element.getManager(); |
| List<ResourceType> resourceTypes = null; |
| Constraints constraint = null; |
| for (PsiAnnotation annotation : annotations) { |
| String qualifiedName = annotation.getQualifiedName(); |
| if (qualifiedName == null) { |
| continue; |
| } |
| |
| if (qualifiedName.startsWith(SUPPORT_ANNOTATIONS_PREFIX) || qualifiedName.startsWith("test.pkg.")) { |
| if (INT_DEF_ANNOTATION.equals(qualifiedName) || STRING_DEF_ANNOTATION.equals(qualifiedName)) { |
| if (type != null) { |
| constraint = merge(getAllowedValuesFromTypedef(type, annotation, manager), constraint); |
| } |
| } |
| else if (INT_RANGE_ANNOTATION.equals(qualifiedName) || "test.pkg.IntRange".equals(qualifiedName)) { |
| constraint = merge(new IntRangeConstraint(annotation), constraint); |
| } |
| else if (FLOAT_RANGE_ANNOTATION.equals(qualifiedName)) { |
| constraint = merge(new FloatRangeConstraint(annotation), constraint); |
| } |
| else if (SIZE_ANNOTATION.equals(qualifiedName)) { |
| constraint = merge(new SizeConstraint(annotation), constraint); |
| } |
| else if (COLOR_INT_ANNOTATION.equals(qualifiedName)) { |
| constraint = merge(new ResourceTypeAllowedValues(Collections.<ResourceType>emptyList()), constraint); |
| } |
| else if (qualifiedName.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 |
| constraint = merge(new IndirectPermission(qualifiedName), constraint); |
| } |
| else if (qualifiedName.endsWith(RES_SUFFIX)) { |
| ResourceType resourceType = getResourceTypeFromAnnotation(qualifiedName); |
| if (resourceType != null) { |
| if (resourceTypes == null) { |
| resourceTypes = Lists.newArrayList(); |
| } |
| resourceTypes.add(resourceType); |
| } |
| } |
| } |
| |
| if (constraint == null) { |
| PsiJavaCodeReferenceElement ref = annotation.getNameReferenceElement(); |
| PsiElement resolved = ref == null ? null : ref.resolve(); |
| if (!(resolved instanceof PsiClass) || !((PsiClass)resolved).isAnnotationType()) continue; |
| PsiClass aClass = (PsiClass)resolved; |
| if (visited == null) visited = new THashSet<PsiClass>(); |
| if (!visited.add(aClass)) continue; |
| constraint = merge(getAllowedValues(aClass, type, visited), constraint); |
| } |
| } |
| |
| if (resourceTypes != null) { |
| constraint = merge(new ResourceTypeAllowedValues(resourceTypes), constraint); |
| } |
| |
| return constraint; |
| } |
| |
| public static PsiAnnotation[] getAllAnnotations(final PsiModifierListOwner element) { |
| return CachedValuesManager.getCachedValue(element, new CachedValueProvider<PsiAnnotation[]>() { |
| @Nullable |
| @Override |
| public Result<PsiAnnotation[]> compute() { |
| return Result.create(AnnotationUtil.getAllAnnotations(element, true, null), PsiModificationTracker.MODIFICATION_COUNT); |
| } |
| }); |
| } |
| |
| @Nullable |
| private static PsiType getType(@NotNull PsiModifierListOwner element) { |
| return element instanceof PsiVariable ? ((PsiVariable)element).getType() : element instanceof PsiMethod ? ((PsiMethod)element).getReturnType() : null; |
| } |
| |
| private static void registerProblem(@NotNull PsiExpression argument, @NotNull Constraints constraint, |
| @NotNull ProblemsHolder holder) { |
| if (constraint instanceof IndirectPermission) { |
| PsiMethodCallExpression call = PsiTreeUtil.getParentOfType(argument, PsiMethodCallExpression.class); |
| IndirectPermission ip = (IndirectPermission)constraint; |
| if (call != null && ip.result != null) { |
| checkPermissionRequirement(call, holder, null, ip.result, ip.result.requirement); |
| } |
| } |
| else if (constraint instanceof ResourceTypeAllowedValues) { |
| List<ResourceType> types = ((ResourceTypeAllowedValues)constraint).types; |
| String message; |
| if (types.isEmpty()) { |
| message = String.format("Should pass resolved color instead of resource id here: `getResources().getColor(%1$s)`", |
| argument.getText()); |
| } |
| else if (types.size() == 1) { |
| message = "Expected resource of type " + types.get(0); |
| } |
| else { |
| message = "Expected resource type to be one of " + Joiner.on(", ").join(types); |
| } |
| holder.registerProblem(argument, message); |
| } |
| else if (constraint instanceof RangeAllowedValues) { |
| String message = ((RangeAllowedValues)constraint).describe(argument); |
| holder.registerProblem(argument, message); |
| } |
| else { |
| assert constraint instanceof AllowedValues; |
| AllowedValues typedef = (AllowedValues)constraint; |
| Function<PsiAnnotationMemberValue, String> formatter = new Function<PsiAnnotationMemberValue, String>() { |
| @Override |
| public String fun(PsiAnnotationMemberValue value) { |
| if (value instanceof PsiReferenceExpression) { |
| PsiElement resolved = ((PsiReferenceExpression)value).resolve(); |
| if (resolved instanceof PsiVariable) { |
| return PsiFormatUtil.formatVariable((PsiVariable)resolved, SHOW_NAME | SHOW_CONTAINING_CLASS, |
| PsiSubstitutor.EMPTY); |
| } |
| } |
| return value.getText(); |
| } |
| }; |
| String values = StringUtil.join(typedef.values, formatter, ", "); |
| String message; |
| if (typedef.canBeOred) { |
| message = "Must be one or more of: " + values; |
| } |
| else { |
| message = "Must be one of: " + values; |
| } |
| |
| // @IntDef and @IntRange can be combined |
| if (constraint.next instanceof RangeAllowedValues) { |
| message += " or " + StringUtil.decapitalize(((RangeAllowedValues)constraint.next).describe(argument)); |
| } |
| |
| holder.registerProblem(argument, message); |
| } |
| } |
| |
| private static boolean isAllowed(@NotNull final PsiElement scope, |
| @NotNull final PsiExpression argument, |
| @NotNull final Constraints constraints, |
| @NotNull final PsiManager manager, |
| @Nullable final Set<PsiExpression> visited) { |
| if (constraints instanceof ResourceTypeAllowedValues) { |
| return isResourceTypeAllowed(scope, argument, (ResourceTypeAllowedValues)constraints, manager, visited); |
| } else if (constraints instanceof RangeAllowedValues) { |
| return isInRange(scope, argument, (RangeAllowedValues)constraints, manager, visited); |
| } else if (constraints instanceof IndirectPermission) { |
| return isGrantedPermission(argument, (IndirectPermission)constraints); |
| } else { |
| assert constraints instanceof AllowedValues; |
| final AllowedValues a = (AllowedValues)constraints; |
| |
| if (isGoodExpression(argument, a, scope, manager, visited)) return true; |
| |
| boolean allowed = processValuesFlownTo(argument, scope, manager, new Processor<PsiExpression>() { |
| @Override |
| public boolean process(PsiExpression expression) { |
| return isGoodExpression(expression, a, scope, manager, visited); |
| } |
| }); |
| |
| return allowed; |
| } |
| } |
| |
| private static boolean isGoodExpression(@NotNull PsiExpression e, |
| @NotNull AllowedValues allowedValues, |
| @NotNull PsiElement scope, |
| @NotNull PsiManager manager, |
| @Nullable Set<PsiExpression> visited) { |
| PsiExpression expression = PsiUtil.deparenthesizeExpression(e); |
| if (expression == null) return true; |
| if (visited == null) visited = new THashSet<PsiExpression>(); |
| if (!visited.add(expression)) return true; |
| if (expression instanceof PsiConditionalExpression) { |
| PsiExpression thenExpression = ((PsiConditionalExpression)expression).getThenExpression(); |
| boolean thenAllowed = thenExpression == null || isAllowed(scope, thenExpression, allowedValues, manager, visited); |
| if (!thenAllowed) return false; |
| PsiExpression elseExpression = ((PsiConditionalExpression)expression).getElseExpression(); |
| return elseExpression == null || isAllowed(scope, elseExpression, allowedValues, manager, visited); |
| } |
| |
| if (isOneOf(expression, allowedValues, manager)) return true; |
| |
| if (allowedValues.canBeOred) { |
| PsiExpression zero = getLiteralExpression(expression, manager, "0"); |
| if (same(expression, zero, manager)) return true; |
| PsiExpression one = getLiteralExpression(expression, manager, "-1"); |
| if (same(expression, one, manager)) return true; |
| if (expression instanceof PsiPolyadicExpression) { |
| IElementType tokenType = ((PsiPolyadicExpression)expression).getOperationTokenType(); |
| if (JavaTokenType.OR.equals(tokenType) || JavaTokenType.AND.equals(tokenType) || JavaTokenType.PLUS.equals(tokenType)) { |
| for (PsiExpression operand : ((PsiPolyadicExpression)expression).getOperands()) { |
| if (!isAllowed(scope, operand, allowedValues, manager, visited)) return false; |
| } |
| return true; |
| } |
| } |
| if (expression instanceof PsiPrefixExpression && |
| JavaTokenType.TILDE.equals(((PsiPrefixExpression)expression).getOperationTokenType())) { |
| PsiExpression operand = ((PsiPrefixExpression)expression).getOperand(); |
| return operand == null || isAllowed(scope, operand, allowedValues, manager, visited); |
| } |
| } |
| |
| PsiElement resolved = null; |
| if (expression instanceof PsiReference) { |
| resolved = ((PsiReference)expression).resolve(); |
| } |
| else if (expression instanceof PsiCallExpression) { |
| resolved = ((PsiCallExpression)expression).resolveMethod(); |
| } |
| |
| Constraints allowedForRef; |
| if (resolved instanceof PsiModifierListOwner && |
| (allowedForRef = getAllowedValues((PsiModifierListOwner)resolved, getType((PsiModifierListOwner)resolved), null)) != null && |
| allowedForRef.isSubsetOf(allowedValues, manager)) return true; |
| |
| //noinspection ConstantConditions |
| return PsiType.NULL.equals(expression.getType()); |
| } |
| |
| private static long getLongValue(@Nullable PsiElement value, long defaultValue) { |
| if (value == null) { |
| return defaultValue; |
| } else if (value instanceof PsiLiteral) { |
| Object o = ((PsiLiteral)value).getValue(); |
| if (o instanceof Number) { |
| return ((Number)o).longValue(); |
| } |
| } else if (value instanceof PsiPrefixExpression) { |
| // negative number |
| PsiPrefixExpression exp = (PsiPrefixExpression)value; |
| if (exp.getOperationTokenType() == JavaTokenType.MINUS) { |
| PsiExpression operand = exp.getOperand(); |
| if (operand instanceof PsiLiteral) { |
| Object o = ((PsiLiteral)operand).getValue(); |
| if (o instanceof Number) { |
| return -((Number)o).longValue(); |
| } |
| } |
| } |
| } else if (value instanceof PsiReferenceExpression) { |
| PsiElement resolved = ((PsiReferenceExpression)value).resolve(); |
| if (resolved instanceof PsiField) { |
| return getLongValue(((PsiField)resolved).getInitializer(), defaultValue); |
| } |
| } // TODO: Allow inlined arithmetic here? If so look for operator nodes |
| |
| return defaultValue; |
| } |
| |
| private static double getDoubleValue(@Nullable PsiAnnotationMemberValue value, double defaultValue) { |
| if (value == null) { |
| return defaultValue; |
| } else if (value instanceof PsiLiteral) { |
| Object o = ((PsiLiteral)value).getValue(); |
| if (o instanceof Number) { |
| return ((Number)o).doubleValue(); |
| } |
| } else if (value instanceof PsiReferenceExpression) { |
| PsiElement resolved = ((PsiReferenceExpression)value).resolve(); |
| if (resolved instanceof PsiField) { |
| return getDoubleValue(((PsiField)resolved).getInitializer(), defaultValue); |
| } |
| } // TODO: Allow inlined arithmetic here? If so look for operator nodes |
| |
| return defaultValue; |
| } |
| |
| private static boolean getBooleanValue(@Nullable PsiAnnotationMemberValue value, boolean defaultValue) { |
| if (value == null) { |
| return defaultValue; |
| } else if (value instanceof PsiLiteral) { |
| Object o = ((PsiLiteral)value).getValue(); |
| if (o instanceof Boolean) { |
| return ((Boolean)o).booleanValue(); |
| } |
| } |
| |
| return defaultValue; |
| } |
| |
| /** Return value from {@link #isValidResourceTypeExpression} : the expression is valid */ |
| private static final int VALID = 1001; |
| /** Return value from {@link #isValidResourceTypeExpression} : the expression is not valid */ |
| private static final int INVALID = VALID + 1; |
| /** Return value from {@link #isValidResourceTypeExpression} : uncertain whether the resource type is valid */ |
| private static final int UNCERTAIN = INVALID + 1; |
| |
| private static boolean isResourceTypeAllowed(@NotNull final PsiElement scope, |
| @NotNull final PsiExpression argument, |
| @NotNull final ResourceTypeAllowedValues allowedValues, |
| @NotNull final PsiManager manager, |
| @Nullable final Set<PsiExpression> visited) { |
| int result = isValidResourceTypeExpression(argument, allowedValues, scope, manager, visited); |
| if (result == VALID) { |
| return true; |
| } else if (result == INVALID) { |
| return false; |
| } |
| assert result == UNCERTAIN; |
| |
| final AtomicInteger b = new AtomicInteger(); |
| processValuesFlownTo(argument, scope, manager, new Processor<PsiExpression>() { |
| @Override |
| public boolean process(PsiExpression expression) { |
| int goodExpression = isValidResourceTypeExpression(expression, allowedValues, scope, manager, visited); |
| b.set(goodExpression); |
| return goodExpression == UNCERTAIN; |
| } |
| }); |
| result = b.get(); |
| // Treat uncertain as allowed: this means that we were passed some integer whose origins |
| // we don't recognize; don't flag those. |
| return result != INVALID; |
| } |
| |
| private static int isValidResourceTypeExpression(@NotNull PsiExpression e, |
| @NotNull ResourceTypeAllowedValues allowedValues, |
| @NotNull PsiElement scope, |
| @NotNull PsiManager manager, |
| @Nullable Set<PsiExpression> visited) { |
| PsiExpression expression = PsiUtil.deparenthesizeExpression(e); |
| if (expression == null) return VALID; |
| if (visited == null) visited = new THashSet<PsiExpression>(); |
| if (!visited.add(expression)) return VALID; |
| if (expression instanceof PsiConditionalExpression) { |
| PsiExpression thenExpression = ((PsiConditionalExpression)expression).getThenExpression(); |
| boolean thenAllowed = thenExpression == null || isAllowed(scope, thenExpression, allowedValues, manager, visited); |
| if (!thenAllowed) return INVALID; |
| PsiExpression elseExpression = ((PsiConditionalExpression)expression).getElseExpression(); |
| return elseExpression == null || isAllowed(scope, elseExpression, allowedValues, manager, visited) ? VALID : UNCERTAIN; |
| } |
| |
| // Resource type check |
| if (expression instanceof PsiReferenceExpression) { |
| PsiReferenceExpression refExpression = (PsiReferenceExpression)expression; |
| PsiExpression qualifierExpression = refExpression.getQualifierExpression(); |
| if (qualifierExpression instanceof PsiReferenceExpression) { |
| PsiReferenceExpression typeDef = (PsiReferenceExpression)qualifierExpression; |
| PsiExpression r = typeDef.getQualifierExpression(); |
| if (r instanceof PsiReferenceExpression) { |
| if (R_CLASS.equals(((PsiReferenceExpression)r).getReferenceName())) { |
| String typeName = typeDef.getReferenceName(); |
| if (typeName != null) { |
| return allowedValues.isTypeAllowed(typeName) ? VALID : INVALID; |
| } |
| } |
| } |
| } |
| } |
| else if (expression instanceof PsiLiteral) { |
| if (expression instanceof PsiLiteralExpression) { |
| PsiElement parent = expression.getParent(); |
| if (parent instanceof PsiField) { |
| parent = parent.getParent(); |
| if (parent instanceof PsiClass) { |
| PsiElement outerMost = parent.getParent(); |
| if (outerMost instanceof PsiClass && R_CLASS.equals(((PsiClass)outerMost).getName())) { |
| PsiClass typeClass = (PsiClass)parent; |
| String typeClassName = typeClass.getName(); |
| return allowedValues.isTypeAllowed(typeClassName) ? VALID : INVALID; |
| } |
| } |
| } |
| |
| if (allowedValues.types.isEmpty() && expression.getType() == PsiType.INT) { |
| // Passing literal integer to a color |
| return VALID; |
| } |
| } |
| |
| // Allow a literal '0' or '-1' as the resource type; this is sometimes used to communicate that |
| // no id was specified (the support library does this in a few places for example) |
| Object value = ((PsiLiteral)expression).getValue(); |
| if (value instanceof Integer) { |
| return ((Integer)value).intValue() == 0 ? VALID : INVALID; |
| } |
| } else if (expression instanceof PsiPrefixExpression) { |
| // Allow a literal '-1' as the resource type; this is sometimes used to communicate that |
| // no id was specified |
| PsiPrefixExpression ppe = (PsiPrefixExpression)expression; |
| if (ppe.getOperationTokenType() == JavaTokenType.MINUS && |
| ppe.getOperand() instanceof PsiLiteral) { |
| Object value = ((PsiLiteral)ppe.getOperand()).getValue(); |
| if (value instanceof Integer) { |
| return ((Integer)value).intValue() == 1 ? VALID : INVALID; |
| } |
| } |
| } |
| |
| PsiElement resolved = null; |
| if (expression instanceof PsiReference) { |
| resolved = ((PsiReference)expression).resolve(); |
| if (resolved instanceof PsiField) { |
| PsiField field = (PsiField)resolved; |
| PsiClass containingClass = field.getContainingClass(); |
| if (containingClass != null) { |
| PsiClass r = containingClass.getContainingClass(); |
| if (r != null && R_CLASS.equals(r.getName())) { |
| ResourceType type = ResourceType.getEnum(containingClass.getName()); |
| if (type != null) { |
| for (ResourceType t : allowedValues.types) { |
| if (t == type) { |
| return VALID; |
| } |
| } |
| return INVALID; |
| } |
| } |
| } |
| } |
| } |
| else if (expression instanceof PsiCallExpression) { |
| resolved = ((PsiCallExpression)expression).resolveMethod(); |
| } |
| |
| Constraints allowedForRef; |
| |
| if (resolved instanceof PsiModifierListOwner) { |
| PsiType type = getType((PsiModifierListOwner)resolved); |
| allowedForRef = getAllowedValues((PsiModifierListOwner)resolved, type, null); |
| if (allowedForRef instanceof ResourceTypeAllowedValues) { |
| // Happy if *any* of the resource types on the annotation matches any of the |
| // annotations allowed for this API |
| return allowedValues.isCompatibleWith((ResourceTypeAllowedValues)allowedForRef) ? VALID : INVALID; |
| } |
| } |
| |
| return UNCERTAIN; |
| } |
| |
| private static boolean isInRange(@NotNull final PsiElement scope, |
| @NotNull final PsiExpression argument, |
| @NotNull final RangeAllowedValues allowedValues, |
| @NotNull final PsiManager manager, |
| @Nullable final Set<PsiExpression> visited) { |
| int result = isValidRangeExpression(argument, allowedValues, scope, manager, visited); |
| if (result == VALID) { |
| return true; |
| } else if (result == INVALID) { |
| return false; |
| } |
| assert result == UNCERTAIN; |
| |
| final AtomicInteger b = new AtomicInteger(); |
| processValuesFlownTo(argument, scope, manager, new Processor<PsiExpression>() { |
| @Override |
| public boolean process(PsiExpression expression) { |
| int goodExpression = isValidRangeExpression(expression, allowedValues, scope, manager, visited); |
| b.set(goodExpression); |
| return goodExpression == UNCERTAIN; |
| } |
| }); |
| result = b.get(); |
| // Treat uncertain as allowed: this means that we were passed some integer whose origins |
| // we don't recognize; don't flag those. |
| return result != INVALID; |
| } |
| |
| private static int isValidRangeExpression(@NotNull PsiExpression e, |
| @NotNull RangeAllowedValues allowedValues, |
| @NotNull PsiElement scope, |
| @NotNull PsiManager manager, |
| @Nullable Set<PsiExpression> visited) { |
| PsiExpression expression = PsiUtil.deparenthesizeExpression(e); |
| if (expression == null) return VALID; |
| if (visited == null) visited = new THashSet<PsiExpression>(); |
| if (!visited.add(expression)) return VALID; |
| if (expression instanceof PsiConditionalExpression) { |
| PsiExpression thenExpression = ((PsiConditionalExpression)expression).getThenExpression(); |
| boolean thenAllowed = thenExpression == null || isAllowed(scope, thenExpression, allowedValues, manager, visited); |
| if (!thenAllowed) return INVALID; |
| PsiExpression elseExpression = ((PsiConditionalExpression)expression).getElseExpression(); |
| return elseExpression == null || isAllowed(scope, elseExpression, allowedValues, manager, visited) ? VALID : UNCERTAIN; |
| } |
| |
| // Range check |
| int valid = allowedValues.isValid(expression); |
| if (valid != UNCERTAIN) { |
| return valid; |
| } |
| |
| PsiElement resolved = null; |
| if (expression instanceof PsiReference) { |
| resolved = ((PsiReference)expression).resolve(); |
| if (resolved instanceof PsiField) { |
| PsiField field = (PsiField)resolved; |
| if (field.getInitializer() != null) { |
| int fieldValid = allowedValues.isValid(field.getInitializer()); |
| if (fieldValid != UNCERTAIN) { |
| return fieldValid; |
| } |
| } |
| } |
| } |
| else if (expression instanceof PsiCallExpression) { |
| resolved = ((PsiCallExpression)expression).resolveMethod(); |
| } |
| |
| Constraints allowedForRef; |
| |
| if (resolved instanceof PsiModifierListOwner) { |
| PsiType type = getType((PsiModifierListOwner)resolved); |
| allowedForRef = getAllowedValues((PsiModifierListOwner)resolved, type, null); |
| if (allowedForRef instanceof RangeAllowedValues) { |
| return allowedValues.isCompatibleWith((RangeAllowedValues)allowedForRef); |
| } |
| } |
| |
| return UNCERTAIN; |
| } |
| |
| private static boolean isGrantedPermission(@NotNull PsiExpression argument, @NotNull IndirectPermission permission) { |
| PsiMethodCallExpression call = PsiTreeUtil.getParentOfType(argument, PsiMethodCallExpression.class); |
| if (call != null) { |
| String signature = permission.signature; |
| PermissionFinder.Operation operation; |
| if (signature.equals(PERMISSION_ANNOTATION_READ)) { |
| operation = READ; |
| } |
| else if (signature.equals(PERMISSION_ANNOTATION_WRITE)) { |
| operation = WRITE; |
| } |
| else { |
| PsiType type = argument.getType(); |
| if (type == null || !CLASS_INTENT.equals(type.getCanonicalText())) { |
| return true; |
| } |
| operation = ACTION; |
| } |
| permission.result = search(argument, operation); |
| if (permission.result != null) { |
| // Finish check in registerProblem |
| return false; |
| } |
| } |
| // No unsatisfied permission requirement found |
| return true; |
| } |
| |
| // Would be nice to reuse the MagicConstantInspection's cache for this, but it's not accessible |
| private static final Key<Map<String, PsiExpression>> LITERAL_EXPRESSION_CACHE = Key.create("TYPE_DEF_LITERAL_EXPRESSION"); |
| private static PsiExpression getLiteralExpression(@NotNull PsiExpression context, @NotNull PsiManager manager, @NotNull String text) { |
| Map<String, PsiExpression> cache = LITERAL_EXPRESSION_CACHE.get(manager); |
| if (cache == null) { |
| cache = ContainerUtil.createConcurrentSoftValueMap(); |
| cache = manager.putUserDataIfAbsent(LITERAL_EXPRESSION_CACHE, cache); |
| } |
| PsiExpression expression = cache.get(text); |
| if (expression == null) { |
| expression = JavaPsiFacade.getElementFactory(manager.getProject()).createExpressionFromText(text, context); |
| cache.put(text, expression); |
| } |
| return expression; |
| } |
| |
| private static boolean isOneOf(@NotNull PsiExpression expression, @NotNull AllowedValues allowedValues, @NotNull PsiManager manager) { |
| for (PsiAnnotationMemberValue allowedValue : allowedValues.values) { |
| if (same(allowedValue, expression, manager)) return true; |
| } |
| return false; |
| } |
| |
| private static boolean same(@Nullable PsiElement e1, @Nullable PsiElement e2, @NotNull PsiManager manager) { |
| if (e1 instanceof PsiLiteralExpression && e2 instanceof PsiLiteralExpression) { |
| return Comparing.equal(((PsiLiteralExpression)e1).getValue(), ((PsiLiteralExpression)e2).getValue()); |
| } |
| if (e1 instanceof PsiPrefixExpression && e2 instanceof PsiPrefixExpression && ((PsiPrefixExpression)e1).getOperationTokenType() == ((PsiPrefixExpression)e2).getOperationTokenType()) { |
| return same(((PsiPrefixExpression)e1).getOperand(), ((PsiPrefixExpression)e2).getOperand(), manager); |
| } |
| if (e1 instanceof PsiReference && e2 instanceof PsiReference) { |
| e1 = ((PsiReference)e1).resolve(); |
| e2 = ((PsiReference)e2).resolve(); |
| } |
| return manager.areElementsEquivalent(e2, e1); |
| } |
| |
| private static boolean processValuesFlownTo(@NotNull final PsiExpression argument, |
| @NotNull PsiElement scope, |
| @NotNull PsiManager manager, |
| @NotNull final Processor<PsiExpression> processor) { |
| SliceAnalysisParams params = new SliceAnalysisParams(); |
| params.dataFlowToThis = true; |
| params.scope = new AnalysisScope(new LocalSearchScope(scope), manager.getProject()); |
| |
| SliceRootNode rootNode = new SliceRootNode(manager.getProject(), new DuplicateMap(), SliceUsage.createRootUsage(argument, params)); |
| |
| @SuppressWarnings("unchecked") |
| Collection<? extends AbstractTreeNode> children = rootNode.getChildren().iterator().next().getChildren(); |
| for (AbstractTreeNode child : children) { |
| SliceUsage usage = (SliceUsage)child.getValue(); |
| if (usage == null) { |
| continue; |
| } |
| PsiElement element = usage.getElement(); |
| if (element instanceof PsiExpression && !processor.process((PsiExpression)element)) return false; |
| } |
| |
| return !children.isEmpty(); |
| } |
| } |