com.android.tools.idea.actions.annotations.InferSupportAnnotations.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.idea.actions.annotations.InferSupportAnnotations.java

Source

/*
 * Copyright (C) 2016 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.idea.actions.annotations;

import com.android.resources.ResourceType;
import com.android.tools.idea.AndroidPsiUtils;
import com.android.tools.lint.detector.api.ResourceEvaluator;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.intellij.analysis.AnalysisScope;
import com.intellij.codeInsight.AnnotationUtil;
import com.intellij.codeInsight.intention.AddAnnotationFix;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.impl.JavaConstantExpressionEvaluator;
import com.intellij.psi.javadoc.PsiDocComment;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.LocalSearchScope;
import com.intellij.psi.search.searches.OverridingMethodsSearch;
import com.intellij.psi.search.searches.ReferencesSearch;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.usageView.UsageInfo;
import com.intellij.util.ArrayUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;

import java.util.*;

import static com.android.SdkConstants.SUPPORT_ANNOTATIONS_PREFIX;
import static com.android.tools.lint.checks.SupportAnnotationDetector.*;
import static com.android.tools.lint.detector.api.ResourceEvaluator.*;

/**
 * Infer support annotations, e.g. if a method returns {@code R.drawable.something},
 * the method should be annotated with {@code @DrawableRes}.
 * <p>
 * TODO:
 * <ul>
 * <li>Control flow analysis on method calls</li>
 * <li>Check for resource type errors and warn if any are found, since they will lead to incorrect inferences!</li>
 * <li>Can I do a custom dialog UI? There I could let you choose things like whether to infer ranges, control whether to show a report, and explain issue with false positives</li>
 * <li>Look at reflection calls and proguard keep rules to add in @Keep</li>
 * <li>Make sure I flow all annotations not inferred (such as range annotations)</li>
 * <li>Check overridden methods: when doing resolve and hitting an interface I should check what implementations do</li>
 * <li>When analyzing overriding methods, also see if I find *conflicting* annotations. For the Nullable/Nonnull
 * scenario for example, it's possible for an overriding method to have a NonNull return value whereas that's
 * not true for its super implementation. Make sure I don't come up with false annotation inferences like that.</li>
 * <li>Look into inferring @IntDef. Approach: If we have a javadoc which lists multiple? Or what if we have a
 * getter or setter for a field and I can tell how the field is being used wrt bits? How do I name it?</li>
 * <li>Look at return statements to figure out more constraints</li>
 * <li>Look into inferring range restrictions</li>
 * <li>Setting for whether we should look at callsites for some annotations and use that for inference. E.g. if we
 * know nothing about foo(int) but somebody calls it with foo(R.string.name), then we have foo(@StringRes int)</li>
 * <li>Add setting for intdef inference (since it won't be accurate)</li>
 * </ul>
 * </p>
 */
@SuppressWarnings("ALL")
public class InferSupportAnnotations {
    static final boolean CREATE_INFERENCE_REPORT = true;
    /**
     * Whether to look for @hide markers in the javadocs and skip annotation generation from
     * hidden APIs. This is primarily used when this action is invoked on the framework itself.
     */
    static final boolean FILTER_HIDDEN = true;

    public static final String KEEP_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "Keep"; //$NON-NLS-1$

    private static final int MAX_PASSES = 10;
    private int numAnnotationsAdded;
    private final Map<SmartPsiElementPointer<? extends PsiModifierListOwner>, Constraints> myConstraints = Maps
            .newHashMapWithExpectedSize(400);
    private final boolean myAnnotateLocalVariables;
    private final SmartPointerManager myPointerManager;
    private final Project myProject;

    private static class Constraints {
        public List<String> inferences;
        public boolean readOnly;

        @Nullable
        public EnumSet<ResourceType> types;

        @Nullable
        public Set<Object> permissionReferences;
        public boolean requireAllPermissions;

        public boolean keep;

        public void addResourceType(@Nullable ResourceType type) {
            if (type != null) {
                if (types == null) {
                    types = EnumSet.of(type);
                } else {
                    types.add(type);
                }
            }
        }

        public void addResourceTypes(@Nullable EnumSet<ResourceType> types) {
            if (types != null) {
                if (this.types == null) {
                    this.types = EnumSet.copyOf(types);
                } else {
                    this.types.addAll(types);
                }
            }
        }

        public void addReport(PsiModifierListOwner annotated, String message) {
            if (CREATE_INFERENCE_REPORT) {
                PsiClass cls = null;
                PsiMember member = null;
                PsiParameter parameter = null;

                if (annotated instanceof PsiClass) {
                    cls = (PsiClass) annotated;
                } else if (annotated instanceof PsiMethod || annotated instanceof PsiField) {
                    member = (PsiMember) annotated;
                    cls = member.getContainingClass();
                } else if (annotated instanceof PsiParameter) {
                    parameter = (PsiParameter) annotated;
                    PsiMethod method = PsiTreeUtil.getParentOfType(parameter, PsiMethod.class, true);
                    if (method != null) {
                        member = method;
                        cls = method.getContainingClass();
                    }
                }

                StringBuilder sb = new StringBuilder();
                if (cls != null) {
                    sb.append("Class{").append(cls.getName());
                    if (isHidden(cls)) {
                        sb.append(" (Hidden)");
                    }
                    sb.append('}');
                }
                if (member instanceof PsiMethod) {
                    sb.append(" Method{").append(member.getName());
                    if (isHidden((PsiMethod) member)) {
                        sb.append(" (Hidden)");
                    }
                    sb.append('}');
                }
                if (member instanceof PsiField) {
                    sb.append("Field{").append(member.getName());
                    if (isHidden((PsiField) member)) {
                        sb.append(" (Hidden)");
                    }
                    sb.append('}');
                }
                if (parameter != null) {
                    sb.append("Parameter");
                    sb.append("{");
                    sb.append(parameter.getType().getCanonicalText()).append(" ").append(parameter.getName());
                    sb.append("}");
                }

                sb.append(":");
                sb.append(message);
                if (inferences == null) {
                    inferences = Lists.newArrayListWithCapacity(4);
                }
                inferences.add(sb.toString());
            }
        }

        public int merge(Constraints other) {
            int added = 0;
            if (other.types != null) {
                if (types == null) {
                    types = other.types;
                    added++;
                } else {
                    if (types.addAll(other.types)) {
                        added++;
                    }
                }
            }

            if (other.permissionReferences != null) {
                if (permissionReferences == null) {
                    permissionReferences = other.permissionReferences;
                    added++;
                } else {
                    if (permissionReferences.addAll(other.permissionReferences)) {
                        added++;
                    }
                }
                if (other.permissionReferences.size() > 1) {
                    requireAllPermissions = other.requireAllPermissions;
                }
            }

            if (!keep && other.keep) {
                keep = true;
                added++;
            }

            if (other.inferences != null) {
                if (inferences == null) {
                    inferences = other.inferences;
                } else {
                    for (String inference : other.inferences) {
                        if (!inferences.contains(inference)) {
                            inferences.add(inference);
                        }
                    }
                }
            }

            return added;
        }

        @NotNull
        public List<String> getResourceTypeAnnotations() {
            if (types != null && !types.isEmpty()) {
                List<String> annotations = Lists.newArrayList();
                for (ResourceType type : types) {
                    StringBuilder sb = new StringBuilder();
                    sb.append('@');
                    if (type == COLOR_INT_MARKER_TYPE) {
                        sb.append(COLOR_INT_ANNOTATION);
                    } else if (type == DIMENSION_MARKER_TYPE) {
                        sb.append(PX_ANNOTATION);
                    } else {
                        if (type == ResourceType.MIPMAP) {
                            type = ResourceType.DRAWABLE;
                        } else if (type == ResourceType.DECLARE_STYLEABLE) {
                            continue;
                        }
                        sb.append(SUPPORT_ANNOTATIONS_PREFIX);
                        sb.append(StringUtil.capitalize(type.getName()));
                        sb.append(ResourceEvaluator.RES_SUFFIX);
                    }
                    annotations.add(sb.toString());
                }

                return annotations;
            }

            return Collections.emptyList();
        }

        @NotNull
        public List<String> getPermissionAnnotations() {
            if (permissionReferences != null && !permissionReferences.isEmpty()) {
                if (permissionReferences.size() == 1) {
                    Object permission = permissionReferences.iterator().next();
                    StringBuilder sb = new StringBuilder();
                    sb.append('@').append(PERMISSION_ANNOTATION).append('(');
                    if (permission instanceof String) {
                        sb.append('"');
                        sb.append(permission);
                        sb.append('"');
                    } else if (permission instanceof PsiField) {
                        PsiField field = (PsiField) permission;
                        PsiClass containingClass = field.getContainingClass();
                        if (containingClass != null) {
                            String qualifiedName = containingClass.getQualifiedName();
                            if (qualifiedName != null) {
                                sb.append(qualifiedName);
                                sb.append('.');
                            }
                        }
                        sb.append(field.getName());
                    }
                    sb.append(')');
                    return Collections.singletonList(sb.toString());
                } else {
                    StringBuilder sb = new StringBuilder();
                    sb.append('@').append(PERMISSION_ANNOTATION).append('(');
                    if (requireAllPermissions) {
                        sb.append(ATTR_ALL_OF);
                    } else {
                        sb.append(ATTR_ANY_OF);
                    }
                    sb.append("={");

                    boolean first = true;
                    for (Object permission : permissionReferences) {
                        if (first) {
                            first = false;
                        } else {
                            sb.append(',');
                        }
                        if (permission instanceof String) {
                            sb.append('"');
                            sb.append(permission);
                            sb.append('"');
                        } else if (permission instanceof PsiField) {
                            PsiField field = (PsiField) permission;
                            PsiClass containingClass = field.getContainingClass();
                            if (containingClass != null) {
                                String qualifiedName = containingClass.getQualifiedName();
                                if (qualifiedName != null) {
                                    sb.append(qualifiedName);
                                    sb.append('.');
                                }
                            }
                            sb.append(field.getName());
                        }
                    }
                    sb.append("}");
                    sb.append(')');
                    return Collections.singletonList(sb.toString());
                }
            }

            return Collections.emptyList();
        }

        @NotNull
        public String getResourceTypeAnnotationsString() {
            List<String> annotations = getResourceTypeAnnotations();
            if (!annotations.isEmpty()) {
                return Joiner.on('\n').join(annotations).replace(SUPPORT_ANNOTATIONS_PREFIX, "");
            }

            return "";
        }

        @NotNull
        public String getPermissionAnnotationsString() {
            List<String> annotations = getPermissionAnnotations();
            if (!annotations.isEmpty()) {
                return Joiner.on('\n').join(annotations).replace(SUPPORT_ANNOTATIONS_PREFIX, "")
                        .replace("android.Manifest", "Manifest");
            }

            return "";
        }

        @NotNull
        public String getKeepAnnotationsString() {
            if (keep) {
                return "@" + KEEP_ANNOTATION;
            }

            return "";
        }

        //public boolean callSuper;
        //public boolean checkResult;
        // TODO ranges and sizes
        // TODO typedefs
        // TODO threads
    }

    static class ConstraintUsageInfo extends UsageInfo {
        private final Constraints myConstraints;

        private ConstraintUsageInfo(@NotNull PsiElement element, @NotNull Constraints constraints) {
            super(element);
            myConstraints = constraints;
        }

        public Constraints getConstraints() {
            return myConstraints;
        }

        public void addInferenceExplanations(List<String> list) {
            if (CREATE_INFERENCE_REPORT && myConstraints.inferences != null) {
                list.addAll(myConstraints.inferences);
            }
        }
    }

    public InferSupportAnnotations(boolean annotateLocalVariables, Project project) {
        myProject = project;
        myAnnotateLocalVariables = annotateLocalVariables;
        myPointerManager = SmartPointerManager.getInstance(project);
    }

    public static void nothingFoundMessage(final Project project) {
        ApplicationManager.getApplication().invokeLater(new Runnable() {
            @Override
            public void run() {
                Messages.showInfoMessage(project, "Did not infer any new annotations",
                        "Infer Support Annotation Results");
            }
        });
    }

    @TestOnly
    public void apply(final Project project) {
        for (Map.Entry<SmartPsiElementPointer<? extends PsiModifierListOwner>, Constraints> entry : myConstraints
                .entrySet()) {
            SmartPsiElementPointer<? extends PsiModifierListOwner> owner = entry.getKey();
            Constraints value = entry.getValue();
            PsiModifierListOwner element = owner.getElement();
            if (element != null) {
                annotateConstraints(project, value, element);
            }
        }

        if (myConstraints.isEmpty()) {
            throw new RuntimeException("Nothing found to infer");
        }
    }

    public static void apply(Project project, UsageInfo info) {
        if (info instanceof ConstraintUsageInfo) {
            annotateConstraints(project, ((ConstraintUsageInfo) info).getConstraints(),
                    (PsiModifierListOwner) info.getElement());
        }
    }

    private static boolean isHidden(@NotNull PsiDocCommentOwner owner) {
        if (FILTER_HIDDEN) {
            while (owner != null) {
                PsiDocComment docComment = owner.getDocComment();
                if (docComment != null) {
                    // We cna't just look for a PsiDocTag with name "hide" from docComment.getTags()
                    // because that method only works for "@hide", not "{@hide}" which is used in a bunch
                    // of places; we'd need to search for PsiInlineDocTags too
                    String text = docComment.getText();
                    return text.contains("@hide");
                }
                owner = PsiTreeUtil.getParentOfType(owner, PsiDocCommentOwner.class, true);
            }
        }

        return false;
    }

    private static void annotateConstraints(Project project, Constraints constraints,
            PsiModifierListOwner element) {
        // TODO: Add some option for only annotating public/protected API methods, not private etc
        if (element == null) {
            return;
        }

        if (constraints.readOnly || ModuleUtilCore.findModuleForPsiElement(element) == null) {
            return;
        }

        if (FILTER_HIDDEN) {
            PsiDocCommentOwner doc = PsiTreeUtil.getParentOfType(element, PsiDocCommentOwner.class, false);
            if (doc != null && isHidden(doc)) {
                return;
            }
        }

        for (String code : constraints.getResourceTypeAnnotations()) {
            insertAnnotation(project, element, code);
        }

        for (String code : constraints.getPermissionAnnotations()) {
            insertAnnotation(project, element, code);
        }

        if (constraints.keep) {
            insertAnnotation(project, element, constraints.getKeepAnnotationsString());
        }
    }

    private static void insertAnnotation(@NotNull final Project project,
            @NotNull final PsiModifierListOwner element, @NotNull final String code) {
        PsiElementFactory elementFactory = JavaPsiFacade.getInstance(project).getElementFactory();
        PsiAnnotation newAnnotation = elementFactory.createAnnotationFromText(code, element);
        PsiNameValuePair[] attributes = newAnnotation.getParameterList().getAttributes();
        int end = code.indexOf('(');
        if (end == -1) {
            end = code.length();
        }
        assert code.startsWith("@") : code;
        String fqn = code.substring(1, end);
        insertAnnotation(project, element, fqn, null, attributes);
    }

    private static void insertAnnotation(@NotNull final Project project,
            @NotNull final PsiModifierListOwner element, @NotNull final String fqn, @Nullable final String toRemove,
            @NotNull final PsiNameValuePair[] values) {
        WriteCommandAction.runWriteCommandAction(project, new Runnable() {
            @Override
            public void run() {
                String[] toRemoveArray = toRemove != null ? new String[] { toRemove }
                        : ArrayUtil.EMPTY_STRING_ARRAY;
                new AddAnnotationFix(fqn, element, values, toRemoveArray).invoke(project, null,
                        element.getContainingFile());
            }
        });
    }

    public void collect(List<UsageInfo> usages, AnalysisScope scope) {
        for (Map.Entry<SmartPsiElementPointer<? extends PsiModifierListOwner>, Constraints> entry : myConstraints
                .entrySet()) {
            SmartPsiElementPointer<? extends PsiModifierListOwner> pointer = entry.getKey();
            PsiModifierListOwner element = pointer.getElement();
            if (element != null && scope.contains(element) && !shouldIgnore(element)) {
                Constraints constraints = entry.getValue();
                usages.add(new ConstraintUsageInfo(element, constraints));
            }
        }
    }

    private boolean shouldIgnore(PsiModifierListOwner element) {
        if (!myAnnotateLocalVariables) {
            if (element instanceof PsiLocalVariable)
                return true;
            if (element instanceof PsiParameter
                    && ((PsiParameter) element).getDeclarationScope() instanceof PsiForeachStatement)
                return true;
        }
        return false;
    }

    @Nullable
    private Constraints registerPermissionRequirement(@NotNull PsiModifierListOwner owner, boolean all,
            Object... permissions) {
        final SmartPsiElementPointer<PsiModifierListOwner> pointer = myPointerManager
                .createSmartPsiElementPointer(owner);

        Constraints constraints = myConstraints.get(pointer);
        if (constraints == null) {
            constraints = new Constraints();
            constraints.permissionReferences = Sets.newHashSet(permissions);
            constraints.requireAllPermissions = all;
            storeConstraint(owner, pointer, constraints);
            numAnnotationsAdded++;
        } else if (constraints.permissionReferences == null) {
            constraints.permissionReferences = Sets.newHashSet(permissions);
            constraints.requireAllPermissions = all;
            numAnnotationsAdded++;
        } else {
            Set<Object> set = constraints.permissionReferences;
            if (Collections.addAll(set, permissions)) {
                if (set.size() > 1) {
                    constraints.requireAllPermissions = all;
                }
                numAnnotationsAdded++;
            } else {
                return null;
            }
        }
        return constraints;
    }

    private void storeConstraint(@NotNull PsiModifierListOwner owner,
            SmartPsiElementPointer<PsiModifierListOwner> pointer, Constraints constraints) {
        constraints.readOnly = ModuleUtilCore.findModuleForPsiElement(owner) == null;
        if (ApplicationManager.getApplication().isUnitTestMode()) {
            constraints.readOnly = false;
        }
        myConstraints.put(pointer, constraints);
    }

    public void collect(@NotNull PsiFile file) {
        // This isn't quite right; this does iteration for a single file, but
        // really newly added annotations can change previously visited files'
        // inferred data too. We should do it in a more global way.
        int prevNumAnnotationsAdded;
        int pass = 0;
        do {
            final InferenceVisitor visitor = new InferenceVisitor();
            prevNumAnnotationsAdded = numAnnotationsAdded;
            file.accept(visitor);
            pass++;
        } while (prevNumAnnotationsAdded < numAnnotationsAdded && pass < MAX_PASSES);
    }

    @Nullable
    private Constraints getResourceTypeConstraints(PsiModifierListOwner owner, boolean inHierarchy) {
        Constraints constraints = null;
        for (PsiAnnotation annotation : AnnotationUtil.getAllAnnotations(owner, inHierarchy, null)) {
            String qualifiedName = annotation.getQualifiedName();
            if (qualifiedName == null) {
                continue;
            }
            ResourceType type = null;
            if (qualifiedName.startsWith(SUPPORT_ANNOTATIONS_PREFIX) && qualifiedName.endsWith(RES_SUFFIX)) {
                String name = qualifiedName.substring(SUPPORT_ANNOTATIONS_PREFIX.length(),
                        qualifiedName.length() - RES_SUFFIX.length());
                type = ResourceType.getEnum(name.toLowerCase(Locale.US));
            } else if (qualifiedName.equals(COLOR_INT_ANNOTATION)) {
                type = COLOR_INT_MARKER_TYPE;
            } else if (qualifiedName.equals(PX_ANNOTATION)) {
                type = DIMENSION_MARKER_TYPE;
            }
            if (type != null) {
                if (constraints == null) {
                    constraints = new Constraints();
                }
                constraints.addResourceType(type);
            }
        }

        final SmartPsiElementPointer<PsiModifierListOwner> pointer = myPointerManager
                .createSmartPsiElementPointer(owner);
        Constraints existing = myConstraints.get(pointer);
        if (existing != null) {
            if (constraints != null) {
                constraints.merge(existing);
                return constraints;
            }
            return existing;
        }

        return constraints;
    }

    @Nullable
    private PsiModifierListOwner findReflectiveReference(PsiMethodCallExpression call) {
        PsiReferenceExpression methodExpression = call.getMethodExpression();
        if (!"invoke".equals(methodExpression.getReferenceName())) {
            return null;
        }

        PsiElement qualifier = methodExpression.getQualifier();

        PsiMethodCallExpression methodCall = null;
        if (qualifier instanceof PsiMethodCallExpression) {
            methodCall = (PsiMethodCallExpression) qualifier;
        } else if (qualifier instanceof PsiReferenceExpression) {
            PsiElement methodVar = ((PsiReferenceExpression) qualifier).resolve();
            if (methodVar == null) {
                return null;
            }
            // Now find the assignment of the method --
            // TODO: make this smarter to handle assignment separate from declaration of variable etc
            if (methodVar instanceof PsiLocalVariable) {
                PsiLocalVariable var = (PsiLocalVariable) methodVar;
                PsiExpression initializer = var.getInitializer();
                if (initializer instanceof PsiMethodCallExpression) {
                    methodCall = (PsiMethodCallExpression) initializer;
                }
            }
        } else {
            return null;
        }

        if (methodCall != null) {
            PsiReferenceExpression methodCallMethodExpression = methodCall.getMethodExpression();
            String declarationName = methodCallMethodExpression.getReferenceName();
            if (!"getDeclaredMethod".equals(declarationName) && !"getMethod".equals(declarationName)) {
                return null;
            }

            PsiExpression[] arguments = methodCall.getArgumentList().getExpressions();
            if (arguments.length < 1) {
                return null;
            }
            Object o = JavaConstantExpressionEvaluator.computeConstantExpression(arguments[0], false);
            if (!(o instanceof String)) {
                return null;
            }
            String methodName = (String) o;
            String className = null;

            qualifier = methodCallMethodExpression.getQualifier();

            if (qualifier instanceof PsiReferenceExpression) {
                PsiElement clsVar = ((PsiReferenceExpression) qualifier).resolve();
                if (clsVar == null) {
                    return null;
                }

                if (clsVar instanceof PsiLocalVariable) {
                    PsiLocalVariable var = (PsiLocalVariable) clsVar;
                    qualifier = var.getInitializer();
                }
            }

            if (qualifier instanceof PsiMethodCallExpression) {
                methodCall = (PsiMethodCallExpression) qualifier;
                methodCallMethodExpression = methodCall.getMethodExpression();
                declarationName = methodCallMethodExpression.getReferenceName();
                if (!"loadClass".equals(declarationName) && !"forName".equals(declarationName)) {
                    return null;
                }

                PsiExpression[] arguments2 = methodCall.getArgumentList().getExpressions();
                if (arguments2.length < 1) {
                    return null;
                }
                o = JavaConstantExpressionEvaluator.computeConstantExpression(arguments2[0], false);
                if (!(o instanceof String)) {
                    return null;
                }

                className = (String) o;
            } else if (qualifier instanceof PsiClassObjectAccessExpression) {
                PsiClassObjectAccessExpression accessExpression = (PsiClassObjectAccessExpression) qualifier;
                PsiTypeElement operand = accessExpression.getOperand();
                if (operand != null) {
                    className = operand.getType().getCanonicalText();
                }
            } else {
                return null;
            }

            if (className != null) {
                PsiClass psiClass = JavaPsiFacade.getInstance(myProject).findClass(className,
                        GlobalSearchScope.allScope(myProject));
                if (psiClass == null) {
                    return null;
                }
                PsiMethod[] methods = psiClass.findMethodsByName(methodName, true);
                if (methods.length == 1) {
                    return methods[0];
                } else if (methods.length == 0) {
                    return null;
                }

                for (PsiMethod method : methods) {
                    // Try to match parameters
                    PsiParameter[] parameters = method.getParameterList().getParameters();
                    if (arguments.length == parameters.length + 1) {
                        boolean allMatch = true;
                        for (int i = 0; i < parameters.length; i++) {
                            PsiParameter parameter = parameters[i];
                            PsiExpression argument = arguments[i + 1];
                            PsiType parameterType = parameter.getType();
                            PsiType argumentType = argument.getType();
                            if (!typesMatch(argumentType, parameterType)) {
                                allMatch = false;
                                break;
                            }
                        }
                        if (allMatch) {
                            return method;
                        }
                    }
                }

                return null;
            }
        }

        // Also consider reflection libraries
        return null;
    }

    // Checks that a class type matches a given parameter type, e.g.
    //     Class<Integer> matches int
    private static boolean typesMatch(PsiType argumentType, PsiType parameterType) {
        if (argumentType instanceof PsiClassType) {
            PsiClassType type = (PsiClassType) argumentType;
            PsiType[] typeParameters = type.getParameters();
            if (typeParameters.length != 1) {
                return false;
            }
            PsiPrimitiveType unboxed = PsiPrimitiveType.getUnboxedType(parameterType);
            if (unboxed != null) {
                parameterType = unboxed;
            }

            argumentType = typeParameters[0];
            unboxed = PsiPrimitiveType.getUnboxedType(argumentType);
            if (unboxed != null) {
                argumentType = unboxed;
            }

            return parameterType.equals(argumentType);
        } else {
            return false;
        }
    }

    @Nullable
    private Constraints computeRequiredPermissions(PsiModifierListOwner owner) {
        Constraints constraints = null;
        for (PsiAnnotation annotation : AnnotationUtil.getAllAnnotations(owner, true, null)) {
            String qualifiedName = annotation.getQualifiedName();
            if (qualifiedName == null) {
                continue;
            }
            if (qualifiedName.startsWith(PERMISSION_ANNOTATION)) {
                if (constraints == null) {
                    constraints = new Constraints();
                }
                List<Object> permissions = Lists.newArrayList();

                PsiAnnotationMemberValue value = annotation.findAttributeValue(null); // TODO: Or "value" ?
                addPermissions(value, permissions);
                if (!permissions.isEmpty()) {
                    constraints.permissionReferences = Sets.<Object>newHashSet(permissions);
                } else {
                    PsiAnnotationMemberValue anyOf = annotation.findAttributeValue(ATTR_ANY_OF);
                    addPermissions(anyOf, permissions);
                    if (!permissions.isEmpty()) {
                        constraints.permissionReferences = Sets.<Object>newHashSet(permissions);
                    } else {
                        PsiAnnotationMemberValue allOf = annotation.findAttributeValue(ATTR_ALL_OF);
                        addPermissions(allOf, permissions);
                        if (!permissions.isEmpty()) {
                            constraints.permissionReferences = Sets.<Object>newHashSet(permissions);
                            constraints.requireAllPermissions = true;
                        }
                    }
                }
            } else if (qualifiedName.equals(UI_THREAD_ANNOTATION) || qualifiedName.equals(MAIN_THREAD_ANNOTATION)
                    || qualifiedName.equals(BINDER_THREAD_ANNOTATION)
                    || qualifiedName.equals(WORKER_THREAD_ANNOTATION)) {
                // TODO: Record thread here to pass to caller, BUT ONLY IF CONDITIONAL
            }
        }

        final SmartPsiElementPointer<PsiModifierListOwner> pointer = myPointerManager
                .createSmartPsiElementPointer(owner);
        Constraints existing = myConstraints.get(pointer);
        if (existing != null) {
            if (constraints != null) {
                constraints.merge(existing);
                return constraints;
            }
            return existing;
        }

        return constraints;
    }

    private static void addPermissions(@Nullable PsiAnnotationMemberValue value, @NotNull List<Object> names) {
        if (value == null) {
            return;
        }
        if (value instanceof PsiLiteral) {
            String name = (String) ((PsiLiteral) value).getValue();
            if (name != null && !name.isEmpty()) {
                names.add(name);
            }
            // empty is just the default: means not specified
        } else if (value instanceof PsiReferenceExpression) {
            PsiReferenceExpression referenceExpression = (PsiReferenceExpression) value;
            PsiElement resolved = referenceExpression.resolve();
            if (resolved instanceof PsiField) {
                names.add(resolved);
            }
        } else if (value instanceof PsiArrayInitializerMemberValue) {
            PsiArrayInitializerMemberValue array = (PsiArrayInitializerMemberValue) value;
            for (PsiAnnotationMemberValue memberValue : array.getInitializers()) {
                addPermissions(memberValue, names);
            }
        }
    }

    @Nullable
    private Constraints storeConstraints(@NotNull PsiModifierListOwner owner, @NotNull Constraints constraints) {
        final SmartPsiElementPointer<PsiModifierListOwner> pointer = myPointerManager
                .createSmartPsiElementPointer(owner);

        Constraints existing = myConstraints.get(pointer);
        if (existing == null) {
            existing = getResourceTypeConstraints(owner, false);
            if (existing != null) {
                storeConstraint(owner, pointer, existing);
            }
        }

        if (existing == null) {
            storeConstraint(owner, pointer, constraints);
            numAnnotationsAdded++;
            return constraints;
        } else {
            // Merge
            int added = existing.merge(constraints);
            numAnnotationsAdded += added;
            return added > 0 ? existing : null;
        }
    }

    private class InferenceVisitor extends JavaRecursiveElementWalkingVisitor {

        @Override
        public void visitMethod(@NotNull final PsiMethod method) {
            super.visitMethod(method);

            Constraints constraints = getResourceTypeConstraints(method, true);
            Collection<PsiMethod> overridingMethods = OverridingMethodsSearch.search(method).findAll();
            for (final PsiMethod overridingMethod : overridingMethods) {
                Constraints additional = getResourceTypeConstraints(overridingMethod, true);
                if (additional != null) {
                    if (constraints == null) {
                        constraints = additional;
                    } else {
                        constraints.addResourceTypes(additional.types);
                    }
                }
            }
            if (constraints != null) {
                constraints = storeConstraints(method, constraints);
                if (CREATE_INFERENCE_REPORT && constraints != null && !constraints.readOnly) {
                    constraints.addReport(method, constraints.getResourceTypeAnnotationsString()
                            + " because it extends or is overridden by an annotated method");
                }
            }

            final PsiCodeBlock body = method.getBody();
            if (body != null) {
                body.accept(new JavaRecursiveElementWalkingVisitor() {
                    private boolean myReturnedFromMethod = false;

                    @Override
                    public void visitClass(PsiClass aClass) {
                    }

                    @Override
                    public void visitThrowStatement(PsiThrowStatement statement) {
                        myReturnedFromMethod = true;
                        super.visitThrowStatement(statement);
                    }

                    @Override
                    public void visitLambdaExpression(PsiLambdaExpression expression) {
                    }

                    @Override
                    public void visitReturnStatement(PsiReturnStatement statement) {
                        PsiExpression expression = statement.getReturnValue();
                        if (expression instanceof PsiReferenceExpression) {
                            PsiElement resolved = ((PsiReferenceExpression) expression).resolve();
                            if (resolved instanceof PsiModifierListOwner) {
                                // TODO: Look up annotations on this method; here we're for example
                                // returning a value that must have the same type as this method
                                // e.g.
                                //   int unknownReturnType() {
                                //       return getKnownReturnType();
                                //   }
                                //   @DimenRes int getKnownReturnType() { ... }
                            }
                        }

                        // TODO: Resolve expression: if's a resource type, use that
                        myReturnedFromMethod = true;
                        super.visitReturnStatement(statement);
                    }

                    @Override
                    public void visitMethodCallExpression(PsiMethodCallExpression expression) {
                        super.visitMethodCallExpression(expression);
                        PsiMethod calledMethod = expression.resolveMethod();
                        if (calledMethod != null) {
                            Constraints constraints = computeRequiredPermissions(calledMethod);
                            if (constraints != null && constraints.permissionReferences != null
                                    && isUnconditionallyReachable(method, expression)) {

                                Constraints inferred = constraints;
                                constraints = storeConstraints(method, constraints);
                                if (CREATE_INFERENCE_REPORT && constraints != null && !constraints.readOnly) {
                                    PsiClass containingClass = calledMethod.getContainingClass();
                                    String signature = (containingClass != null ? (containingClass.getName() + "#")
                                            : "") + calledMethod.getName();
                                    String message = inferred.getPermissionAnnotationsString()
                                            + " because it calls " + signature;
                                    constraints.addReport(method, message);
                                }
                            }
                        }

                        PsiModifierListOwner reflectiveReference = findReflectiveReference(expression);
                        if (reflectiveReference != null) {
                            Constraints constraints = new Constraints();
                            constraints.keep = true;
                            constraints = storeConstraints(reflectiveReference, constraints);
                            if (CREATE_INFERENCE_REPORT && constraints != null && !constraints.readOnly) {
                                PsiClass containingClass = method.getContainingClass();
                                String signature = (containingClass != null ? (containingClass.getName() + "#")
                                        : "") + method.getName();
                                String message = constraints.getKeepAnnotationsString()
                                        + " because it is called reflectively from " + signature;
                                constraints.addReport(reflectiveReference, message);
                            }
                        }

                        String name = expression.getMethodExpression().getReferenceName();
                        if (name != null && name.startsWith("enforce")
                                && ("enforceCallingOrSelfPermission".equals(name)
                                        || "enforceCallingOrSelfUriPermission".equals(name)
                                        || "enforceCallingPermission".equals(name)
                                        || "enforceCallingUriPermission".equals(name)
                                        || "enforcePermission".equals(name)
                                        || "enforceUriPermission".equals(name))) {
                            // TODO: Determine whether this method is reached *unconditionally*
                            // and use that to merge multiple requirements in the method as well as
                            // the permission conditional flag
                            PsiExpression[] args = expression.getArgumentList().getExpressions();
                            if (args.length > 0) {
                                PsiExpression first = args[0];
                                PsiReference reference = first.getReference();
                                if (reference != null) {
                                    PsiElement resolved = reference.resolve();
                                    if (resolved instanceof PsiField) {
                                        PsiField field = (PsiField) resolved;
                                        if (field.hasModifierProperty(PsiModifier.FINAL)
                                                && field.hasModifierProperty(PsiModifier.STATIC)) {
                                            Constraints constraints = registerPermissionRequirement(method, true,
                                                    field);
                                            if (CREATE_INFERENCE_REPORT && constraints != null
                                                    && !constraints.readOnly) {
                                                constraints.addReport(method,
                                                        constraints.getPermissionAnnotationsString()
                                                                + " because it calls " + name);
                                            }
                                            return;
                                        }
                                    }
                                }
                                Object v = JavaConstantExpressionEvaluator.computeConstantExpression(first, false);
                                if (v instanceof String) {
                                    String permission = (String) v;
                                    Constraints constraints = registerPermissionRequirement(method, true,
                                            permission);
                                    if (CREATE_INFERENCE_REPORT && constraints != null && !constraints.readOnly) {
                                        constraints.addReport(method, constraints.getPermissionAnnotationsString()
                                                + " because it calls " + name);
                                    }
                                }
                            }
                        }
                    }

                    private boolean isUnconditionallyReachable(PsiMethod method, PsiElement expression) {
                        if (myReturnedFromMethod) {
                            return false;
                        }

                        PsiElement curr = expression.getParent();
                        PsiElement prev = curr;
                        while (curr != null) {
                            if (curr == method) {
                                return true;
                            }
                            if (curr instanceof PsiIfStatement || curr instanceof PsiConditionalExpression
                                    || curr instanceof PsiSwitchStatement) {
                                return false;
                            }
                            if (curr instanceof PsiBinaryExpression) {
                                // Check for short circuit evaluation:  A && B && C -- here A is unconditional, B and C is not
                                PsiBinaryExpression binaryExpression = (PsiBinaryExpression) curr;
                                if (prev != binaryExpression.getLOperand()
                                        && binaryExpression.getOperationTokenType() == JavaTokenType.ANDAND) {
                                    return false;
                                }
                            }

                            prev = curr;
                            curr = curr.getParent();
                        }

                        return true;
                    }
                });
            }
        }

        @Override
        public void visitMethodCallExpression(PsiMethodCallExpression expression) {
            super.visitMethodCallExpression(expression);
            PsiMethod method = expression.resolveMethod();
            if (method != null) {
                PsiParameter[] parameters = method.getParameterList().getParameters();
                PsiExpression[] arguments = expression.getArgumentList().getExpressions();
                if (parameters.length > 0 && arguments.length >= parameters.length) { // >: varargs
                    for (int i = 0; i < arguments.length; i++) {
                        PsiExpression argument = arguments[i];
                        ResourceType resourceType = AndroidPsiUtils.getResourceType(argument);
                        if (resourceType != null) {
                            PsiParameter parameter = parameters[i];
                            // If we see a call to some generic method, such as
                            //    prettyPrint(R.id.foo)
                            // or
                            //    intent.putExtra(key, R.id.foo)
                            // we shouldn't conclude that ALL calls to that method must also
                            // use the same resource type! In other words, if we
                            // see a method that takes non-integers, or an actual put method
                            // (2 parameter method where our target is the second parameter and
                            // the name begins with put) we ignore it.
                            if (!PsiType.INT.equals(parameter.getType())
                                    || i == 1 && parameters.length == 2 && method.getName().startsWith("put")) {
                                continue;
                            }

                            Constraints newConstraint = new Constraints();
                            newConstraint.addResourceType(resourceType);
                            Constraints constraints = storeConstraints(parameter, newConstraint);
                            if (CREATE_INFERENCE_REPORT && constraints != null && !constraints.readOnly) {
                                constraints.addReport(parameter, newConstraint.getResourceTypeAnnotationsString()
                                        + " because it's passed " + argument.getText() + " in a call");
                            }
                        }
                    }
                }
            }
        }

        @Override
        public void visitReturnStatement(PsiReturnStatement statement) {
            super.visitReturnStatement(statement);

            PsiExpression returnValue = statement.getReturnValue();
            if (returnValue == null) {
                return;
            }

            ResourceType resourceType = AndroidPsiUtils.getResourceType(returnValue);
            if (resourceType != null) {
                Constraints newConstraint = new Constraints();
                newConstraint.addResourceType(resourceType);
                PsiMethod method = PsiTreeUtil.getParentOfType(statement, PsiMethod.class);
                Constraints constraints = storeConstraints(method, newConstraint);
                if (CREATE_INFERENCE_REPORT && constraints != null && !constraints.readOnly) {
                    constraints.addReport(method, newConstraint.getResourceTypeAnnotationsString()
                            + " because it returns " + returnValue.getText());
                }
            } else if (returnValue instanceof PsiReferenceExpression) {
                PsiElement resolved = ((PsiReferenceExpression) returnValue).resolve();
                if (resolved instanceof PsiModifierListOwner) {
                    PsiModifierListOwner owner = (PsiModifierListOwner) resolved;
                    Constraints newConstraint = getResourceTypeConstraints(owner, true);
                    if (newConstraint != null) {
                        PsiMethod method = PsiTreeUtil.getParentOfType(statement, PsiMethod.class);
                        Constraints constraints = storeConstraints(method, newConstraint);
                        if (CREATE_INFERENCE_REPORT && constraints != null && !constraints.readOnly) {
                            constraints.addReport(method, newConstraint.getResourceTypeAnnotationsString()
                                    + " because it returns " + returnValue.getText());
                        }
                    }
                }
            }
        }

        @Override
        public void visitParameter(@NotNull PsiParameter parameter) {
            super.visitParameter(parameter);

            Constraints resourceTypeConstraints = getResourceTypeConstraints(parameter, true);

            if (resourceTypeConstraints != null && resourceTypeConstraints.types != null
                    && !resourceTypeConstraints.types.isEmpty()) {
                Constraints constraints = storeConstraints(parameter, resourceTypeConstraints);
                if (CREATE_INFERENCE_REPORT && constraints != null && !constraints.readOnly) {
                    constraints.addReport(parameter, constraints.getResourceTypeAnnotationsString()
                            + " because it extends a method with that parameter annotated or inferred");
                }
            }

            PsiElement grandParent = parameter.getDeclarationScope();
            if (grandParent instanceof PsiMethod) {
                final PsiMethod method = (PsiMethod) grandParent;
                if (method.getBody() != null) {

                    for (PsiReference reference : ReferencesSearch.search(parameter,
                            new LocalSearchScope(method))) {
                        final PsiElement place = reference.getElement();
                        if (place instanceof PsiReferenceExpression) {
                            final PsiReferenceExpression expr = (PsiReferenceExpression) place;
                            final PsiElement parent = PsiTreeUtil.skipParentsOfType(expr,
                                    PsiParenthesizedExpression.class, PsiTypeCastExpression.class);

                            if (processParameter(parameter, expr, parent)) {
                                return; //  TODO: return? Shouldn't it be break?
                            }
                        }
                    }
                }
            }
        }

        private boolean processParameter(PsiParameter parameter, PsiReferenceExpression expr, PsiElement parent) {
            if (PsiUtil.isAccessedForWriting(expr)) {
                return true; // TODO: Move into super class
            }

            PsiCall call = PsiTreeUtil.getParentOfType(expr, PsiCall.class);
            if (call != null) {
                final PsiExpressionList argumentList = call.getArgumentList();
                if (argumentList != null) {
                    final PsiExpression[] args = argumentList.getExpressions();
                    int idx = ArrayUtil.find(args, expr);
                    if (idx >= 0) {
                        final PsiMethod resolvedMethod = call.resolveMethod();
                        if (resolvedMethod != null) {
                            final PsiParameter[] parameters = resolvedMethod.getParameterList().getParameters();
                            if (idx < parameters.length) { //not vararg
                                final PsiParameter resolvedToParam = parameters[idx];
                                Constraints constraints = getResourceTypeConstraints(resolvedToParam, true);
                                if (constraints != null && constraints.types != null && !constraints.types.isEmpty()
                                        && !resolvedToParam.isVarArgs()) {
                                    constraints = storeConstraints(parameter, constraints);
                                    if (CREATE_INFERENCE_REPORT && constraints != null && !constraints.readOnly) {
                                        constraints.addReport(parameter, constraints
                                                .getResourceTypeAnnotationsString()
                                                + " because it calls "
                                                + (resolvedMethod.getContainingClass() != null
                                                        ? (resolvedMethod.getContainingClass().getName() + "#")
                                                        : "")
                                                + resolvedMethod.getName());
                                    }
                                    return true;
                                }
                            }
                        }
                    }
                }
            }
            return false;
        }
    }

    @NotNull
    public static String generateReport(@NotNull UsageInfo[] infos) {
        if (CREATE_INFERENCE_REPORT) {
            StringBuilder sb = new StringBuilder(1000);
            sb.append("INFER SUPPORT ANNOTATIONS REPORT\n");
            sb.append("================================\n\n");

            List<String> list = Lists.newArrayList();
            for (UsageInfo info : infos) {
                ((InferSupportAnnotations.ConstraintUsageInfo) info).addInferenceExplanations(list);
            }
            Collections.sort(list);

            String lastClass = null;
            String lastMethod = null;
            String lastLine = null;
            for (String s : list) {
                if (s.equals(lastLine)) {
                    // Some inferences are duplicated
                    continue;
                }
                lastLine = s;

                String cls = null;
                String method = null;
                String field = null;
                String parameter = null;
                int index;

                index = s.indexOf("Class{");
                if (index != -1) {
                    cls = s.substring(index + "Class{".length(), s.indexOf('}', index));
                }

                index = s.indexOf("Method{");
                if (index != -1) {
                    method = s.substring(index + "Method{".length(), s.indexOf('}', index));
                    index = s.indexOf("Parameter{");
                    if (index != -1) {
                        parameter = s.substring(index + "Parameter{".length(), s.indexOf('}', index));
                    }
                } else {
                    index = s.indexOf("Field{");
                    if (index != -1) {
                        field = s.substring(index + "Field{".length(), s.indexOf('}', index));
                    }
                }

                boolean printedMethod = false;
                if (cls != null && !cls.equals(lastClass)) {
                    lastClass = cls;
                    lastMethod = null;
                    sb.append("\n");
                    sb.append("Class ").append(cls).append(":\n");
                }

                if (method != null && !method.equals(lastMethod)) {
                    lastMethod = method;
                    sb.append("  Method ").append(method).append(":\n");
                    printedMethod = true;
                } else if (field != null) {
                    sb.append("  Field ").append(field).append(":\n");
                }

                if (parameter != null) {
                    if (!printedMethod) {
                        sb.append("  Method ").append(method).append(":\n");
                    }
                    sb.append("    Parameter ");
                    sb.append(parameter).append(":\n");
                }

                String message = s.substring(s.indexOf(':') + 1);
                sb.append("      ").append(message).append("\n");
            }

            if (list.isEmpty()) {
                sb.append("Nothing found.");
            }
            return sb.toString();
        } else {
            return "";
        }
    }
}