/*
 * Copyright (C) 2011 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.lint.client.api;

import static com.android.SdkConstants.ANDROID_PKG;
import static com.android.SdkConstants.R_CLASS;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.tools.lint.client.api.JavaParser.ResolvedClass;
import com.android.tools.lint.client.api.JavaParser.ResolvedMethod;
import com.android.tools.lint.client.api.JavaParser.ResolvedNode;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Detector.JavaScanner;
import com.android.tools.lint.detector.api.Detector.XmlScanner;
import com.android.tools.lint.detector.api.JavaContext;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import lombok.ast.AlternateConstructorInvocation;
import lombok.ast.Annotation;
import lombok.ast.AnnotationDeclaration;
import lombok.ast.AnnotationElement;
import lombok.ast.AnnotationMethodDeclaration;
import lombok.ast.AnnotationValueArray;
import lombok.ast.ArrayAccess;
import lombok.ast.ArrayCreation;
import lombok.ast.ArrayDimension;
import lombok.ast.ArrayInitializer;
import lombok.ast.Assert;
import lombok.ast.AstVisitor;
import lombok.ast.BinaryExpression;
import lombok.ast.Block;
import lombok.ast.BooleanLiteral;
import lombok.ast.Break;
import lombok.ast.Case;
import lombok.ast.Cast;
import lombok.ast.Catch;
import lombok.ast.CharLiteral;
import lombok.ast.ClassDeclaration;
import lombok.ast.ClassLiteral;
import lombok.ast.Comment;
import lombok.ast.CompilationUnit;
import lombok.ast.ConstructorDeclaration;
import lombok.ast.ConstructorInvocation;
import lombok.ast.Continue;
import lombok.ast.Default;
import lombok.ast.DoWhile;
import lombok.ast.EmptyDeclaration;
import lombok.ast.EmptyStatement;
import lombok.ast.EnumConstant;
import lombok.ast.EnumDeclaration;
import lombok.ast.EnumTypeBody;
import lombok.ast.Expression;
import lombok.ast.ExpressionStatement;
import lombok.ast.FloatingPointLiteral;
import lombok.ast.For;
import lombok.ast.ForEach;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.Identifier;
import lombok.ast.If;
import lombok.ast.ImportDeclaration;
import lombok.ast.InlineIfExpression;
import lombok.ast.InstanceInitializer;
import lombok.ast.InstanceOf;
import lombok.ast.IntegralLiteral;
import lombok.ast.InterfaceDeclaration;
import lombok.ast.KeywordModifier;
import lombok.ast.LabelledStatement;
import lombok.ast.MethodDeclaration;
import lombok.ast.MethodInvocation;
import lombok.ast.Modifiers;
import lombok.ast.Node;
import lombok.ast.NormalTypeBody;
import lombok.ast.NullLiteral;
import lombok.ast.PackageDeclaration;
import lombok.ast.Return;
import lombok.ast.Select;
import lombok.ast.StaticInitializer;
import lombok.ast.StringLiteral;
import lombok.ast.Super;
import lombok.ast.SuperConstructorInvocation;
import lombok.ast.Switch;
import lombok.ast.Synchronized;
import lombok.ast.This;
import lombok.ast.Throw;
import lombok.ast.Try;
import lombok.ast.TypeReference;
import lombok.ast.TypeReferencePart;
import lombok.ast.TypeVariable;
import lombok.ast.UnaryExpression;
import lombok.ast.VariableDeclaration;
import lombok.ast.VariableDefinition;
import lombok.ast.VariableDefinitionEntry;
import lombok.ast.VariableReference;
import lombok.ast.While;

/**
 * Specialized visitor for running detectors on a Java AST.
 * It operates in three phases:
 * <ol>
 *   <li> First, it computes a set of maps where it generates a map from each
 *        significant AST attribute (such as method call names) to a list
 *        of detectors to consult whenever that attribute is encountered.
 *        Examples of "attributes" are method names, Android resource identifiers,
 *        and general AST node types such as "cast" nodes etc. These are
 *        defined on the {@link JavaScanner} interface.
 *   <li> Second, it iterates over the document a single time, delegating to
 *        the detectors found at each relevant AST attribute.
 *   <li> Finally, it calls the remaining visitors (those that need to process a
 *        whole document on their own).
 * </ol>
 * It also notifies all the detectors before and after the document is processed
 * such that they can do pre- and post-processing.
 */
public class JavaVisitor {
    /** Default size of lists holding detectors of the same type for a given node type */
    private static final int SAME_TYPE_COUNT = 8;

    private final Map<String, List<VisitingDetector>> mMethodDetectors =
            Maps.newHashMapWithExpectedSize(40);
    private final Map<String, List<VisitingDetector>> mConstructorDetectors =
            Maps.newHashMapWithExpectedSize(12);
    private Set<String> mConstructorSimpleNames;
    private final List<VisitingDetector> mResourceFieldDetectors =
            new ArrayList<VisitingDetector>();
    private final List<VisitingDetector> mAllDetectors;
    private final List<VisitingDetector> mFullTreeDetectors;
    private final Map<Class<? extends Node>, List<VisitingDetector>> mNodeTypeDetectors =
            new HashMap<Class<? extends Node>, List<VisitingDetector>>(16);
    private final JavaParser mParser;
    private final Map<String, List<VisitingDetector>> mSuperClassDetectors =
            new HashMap<String, List<VisitingDetector>>();

    /**
     * Number of fatal exceptions (internal errors, usually from ECJ) we've
     * encountered; we don't log each and every one to avoid massive log spam
     * in code which triggers this condition
     */
    private static int sExceptionCount;
    /** Max number of logs to include */
    private static final int MAX_REPORTED_CRASHES = 20;

    JavaVisitor(@NonNull JavaParser parser, @NonNull List<Detector> detectors) {
        mParser = parser;
        mAllDetectors = new ArrayList<VisitingDetector>(detectors.size());
        mFullTreeDetectors = new ArrayList<VisitingDetector>(detectors.size());

        for (Detector detector : detectors) {
            VisitingDetector v = new VisitingDetector(detector, (JavaScanner) detector);
            mAllDetectors.add(v);

            List<String> applicableSuperClasses = detector.applicableSuperClasses();
            if (applicableSuperClasses != null) {
                for (String fqn : applicableSuperClasses) {
                    List<VisitingDetector> list = mSuperClassDetectors.get(fqn);
                    if (list == null) {
                        list = new ArrayList<VisitingDetector>(SAME_TYPE_COUNT);
                        mSuperClassDetectors.put(fqn, list);
                    }
                    list.add(v);
                }
                continue;
            }

            List<Class<? extends Node>> nodeTypes = detector.getApplicableNodeTypes();
            if (nodeTypes != null) {
                for (Class<? extends Node> type : nodeTypes) {
                    List<VisitingDetector> list = mNodeTypeDetectors.get(type);
                    if (list == null) {
                        list = new ArrayList<VisitingDetector>(SAME_TYPE_COUNT);
                        mNodeTypeDetectors.put(type, list);
                    }
                    list.add(v);
                }
            }

            List<String> names = detector.getApplicableMethodNames();
            if (names != null) {
                // not supported in Java visitors; adding a method invocation node is trivial
                // for that case.
                assert names != XmlScanner.ALL;

                for (String name : names) {
                    List<VisitingDetector> list = mMethodDetectors.get(name);
                    if (list == null) {
                        list = new ArrayList<VisitingDetector>(SAME_TYPE_COUNT);
                        mMethodDetectors.put(name, list);
                    }
                    list.add(v);
                }
            }

            List<String> types = detector.getApplicableConstructorTypes();
            if (types != null) {
                // not supported in Java visitors; adding a method invocation node is trivial
                // for that case.
                assert types != XmlScanner.ALL;
                if (mConstructorSimpleNames == null) {
                    mConstructorSimpleNames = Sets.newHashSet();
                }
                for (String type : types) {
                    List<VisitingDetector> list = mConstructorDetectors.get(type);
                    if (list == null) {
                        list = new ArrayList<VisitingDetector>(SAME_TYPE_COUNT);
                        mConstructorDetectors.put(type, list);
                        mConstructorSimpleNames.add(type.substring(type.lastIndexOf('.')+1));
                    }
                    list.add(v);
                }
            }

            if (detector.appliesToResourceRefs()) {
                mResourceFieldDetectors.add(v);
            } else if ((names == null || names.isEmpty())
                    && (nodeTypes == null || nodeTypes.isEmpty())
                    && (types == null || types.isEmpty())) {
                mFullTreeDetectors.add(v);
            }
        }
    }

    void visitFile(@NonNull JavaContext context) {
        Node compilationUnit = null;
        try {
            compilationUnit = mParser.parseJava(context);
            if (compilationUnit == null) {
                // No need to log this; the parser should be reporting
                // a full warning (such as IssueRegistry#PARSER_ERROR)
                // with details, location, etc.
                return;
            }
            context.setCompilationUnit(compilationUnit);

            for (VisitingDetector v : mAllDetectors) {
                v.setContext(context);
                v.getDetector().beforeCheckFile(context);
            }

            if (!mSuperClassDetectors.isEmpty()) {
                SuperclassVisitor visitor = new SuperclassVisitor(context);
                compilationUnit.accept(visitor);
            }

            for (VisitingDetector v : mFullTreeDetectors) {
                AstVisitor visitor = v.getVisitor();
                compilationUnit.accept(visitor);
            }

            if (!mMethodDetectors.isEmpty() || !mResourceFieldDetectors.isEmpty() ||
                    !mConstructorDetectors.isEmpty()) {
                AstVisitor visitor = new DelegatingJavaVisitor(context);
                compilationUnit.accept(visitor);
            } else if (!mNodeTypeDetectors.isEmpty()) {
                AstVisitor visitor = new DispatchVisitor();
                compilationUnit.accept(visitor);
            }

            for (VisitingDetector v : mAllDetectors) {
                v.getDetector().afterCheckFile(context);
            }
        } catch (RuntimeException e) {
            if (sExceptionCount++ > MAX_REPORTED_CRASHES) {
                // No need to keep spamming the user that a lot of the files
                // are tripping up ECJ, they get the picture.
                return;
            }

            if (e.getClass().getSimpleName().equals("IndexNotReadyException")) {
                // Attempting to access PSI during startup before indices are ready; ignore these.
                // See http://b.android.com/176644 for an example.
                return;
            } else if (e.getClass().getSimpleName().equals("ProcessCanceledException")) {
                // Cancelling inspections in the IDE
                context.getDriver().cancel();
                return;
            }

            // Work around ECJ bugs; see https://code.google.com/p/android/issues/detail?id=172268
            // Don't allow lint bugs to take down the whole build. TRY to log this as a
            // lint error instead!
            StringBuilder sb = new StringBuilder(100);
            sb.append("Unexpected failure during lint analysis of ");
            sb.append(context.file.getName());
            sb.append(" (this is a bug in lint or one of the libraries it depends on)\n");

            sb.append(e.getClass().getSimpleName());
            sb.append(':');
            StackTraceElement[] stackTrace = e.getStackTrace();
            int count = 0;
            for (StackTraceElement frame : stackTrace) {
                if (count > 0) {
                    sb.append("<-");
                }

                String className = frame.getClassName();
                sb.append(className.substring(className.lastIndexOf('.') + 1));
                sb.append('.').append(frame.getMethodName());
                sb.append('(');
                sb.append(frame.getFileName()).append(':').append(frame.getLineNumber());
                sb.append(')');
                count++;
                // Only print the top 3-4 frames such that we can identify the bug
                if (count == 4) {
                    break;
                }
            }
            Throwable throwable = null; // NOT e: this makes for very noisy logs
            //noinspection ConstantConditions
            context.log(throwable, sb.toString());
        } finally {
            if (compilationUnit != null) {
                mParser.dispose(context, compilationUnit);
            }
        }
    }

    /**
     * For testing only: returns the number of exceptions thrown during Java AST analysis
     *
     * @return the number of internal errors found
     */
    @VisibleForTesting
    public static int getCrashCount() {
        return sExceptionCount;
    }

    /**
     * For testing only: clears the crash counter
     */
    @VisibleForTesting
    public static void clearCrashCount() {
        sExceptionCount = 0;
    }

    public void prepare(@NonNull List<JavaContext> contexts) {
        mParser.prepareJavaParse(contexts);
    }

    public void dispose() {
        mParser.dispose();
    }

    @Nullable
    private static Set<String> getInterfaceNames(
            @Nullable Set<String> addTo,
            @NonNull ResolvedClass cls) {
        Iterable<ResolvedClass> interfaces = cls.getInterfaces();
        for (ResolvedClass resolvedInterface : interfaces) {
            String name = resolvedInterface.getName();
            if (addTo == null) {
                addTo = Sets.newHashSet();
            } else if (addTo.contains(name)) {
                // Superclasses can explicitly implement the same interface,
                // so keep track of visited interfaces as we traverse up the
                // super class chain to avoid checking the same interface
                // more than once.
                continue;
            }
            addTo.add(name);
            getInterfaceNames(addTo, resolvedInterface);
        }

        return addTo;
    }

    private static class VisitingDetector {
        private AstVisitor mVisitor; // construct lazily, and clear out on context switch!
        private JavaContext mContext;
        public final Detector mDetector;
        public final JavaScanner mJavaScanner;

        public VisitingDetector(@NonNull Detector detector, @NonNull JavaScanner javaScanner) {
            mDetector = detector;
            mJavaScanner = javaScanner;
        }

        @NonNull
        public Detector getDetector() {
            return mDetector;
        }

        @NonNull
        public JavaScanner getJavaScanner() {
            return mJavaScanner;
        }

        public void setContext(@NonNull JavaContext context) {
            mContext = context;

            // The visitors are one-per-context, so clear them out here and construct
            // lazily only if needed
            mVisitor = null;
        }

        @NonNull
        AstVisitor getVisitor() {
            if (mVisitor == null) {
                mVisitor = mDetector.createJavaVisitor(mContext);
                if (mVisitor == null) {
                    mVisitor = new ForwardingAstVisitor() {
                    };
                }
            }
            return mVisitor;
        }
    }

    private class SuperclassVisitor extends ForwardingAstVisitor {
        private JavaContext mContext;

        public SuperclassVisitor(@NonNull JavaContext context) {
            mContext = context;
        }

        @Override
        public boolean visitClassDeclaration(ClassDeclaration node) {
            ResolvedNode resolved = mContext.resolve(node);
            if (!(resolved instanceof ResolvedClass)) {
                return true;
            }

            ResolvedClass resolvedClass = (ResolvedClass) resolved;
            ResolvedClass cls = resolvedClass;
            int depth = 0;
            while (cls != null) {
                List<VisitingDetector> list = mSuperClassDetectors.get(cls.getName());
                if (list != null) {
                    for (VisitingDetector v : list) {
                        v.getJavaScanner().checkClass(mContext, node, node, resolvedClass);
                    }
                }

                // Check interfaces too
                Set<String> interfaceNames = getInterfaceNames(null, cls);
                if (interfaceNames != null) {
                    for (String name : interfaceNames) {
                        list = mSuperClassDetectors.get(name);
                        if (list != null) {
                            for (VisitingDetector v : list) {
                                v.getJavaScanner().checkClass(mContext, node, node,
                                        resolvedClass);
                            }
                        }
                    }
                }

                cls = cls.getSuperClass();
                depth++;
                if (depth == 500) {
                    // Shouldn't happen in practice; this prevents the IDE from
                    // hanging if the user has accidentally typed in an incorrect
                    // super class which creates a cycle.
                    break;
                }
            }

            return false;
        }

        @Override
        public boolean visitConstructorInvocation(ConstructorInvocation node) {
            NormalTypeBody anonymous = node.astAnonymousClassBody();
            if (anonymous != null) {
                ResolvedNode resolved = mContext.resolve(node.astTypeReference());
                if (!(resolved instanceof ResolvedClass)) {
                    return true;
                }

                ResolvedClass resolvedClass = (ResolvedClass) resolved;
                ResolvedClass cls = resolvedClass;
                while (cls != null) {
                    List<VisitingDetector> list = mSuperClassDetectors.get(cls.getName());
                    if (list != null) {
                        for (VisitingDetector v : list) {
                            v.getJavaScanner().checkClass(mContext, null, anonymous,
                                    resolvedClass);
                        }
                    }

                    // Check interfaces too
                    Set<String> interfaceNames = getInterfaceNames(null, cls);
                    if (interfaceNames != null) {
                        for (String name : interfaceNames) {
                            list = mSuperClassDetectors.get(name);
                            if (list != null) {
                                for (VisitingDetector v : list) {
                                    v.getJavaScanner().checkClass(mContext, null, anonymous,
                                            resolvedClass);
                                }
                            }
                        }
                    }

                    cls = cls.getSuperClass();
                }
            }

            return true;
        }

        @Override
        public boolean visitImportDeclaration(ImportDeclaration node) {
            return true;
        }
    }

    /**
     * Generic dispatcher which visits all nodes (once) and dispatches to
     * specific visitors for each node. Each visitor typically only wants to
     * look at a small part of a tree, such as a method call or a class
     * declaration, so this means we avoid visiting all "uninteresting" nodes in
     * the tree repeatedly.
     */
    private class DispatchVisitor extends ForwardingAstVisitor {
        @Override
        public void endVisit(Node node) {
            for (VisitingDetector v : mAllDetectors) {
                v.getVisitor().endVisit(node);
            }
        }

        @Override
        public boolean visitAlternateConstructorInvocation(AlternateConstructorInvocation node) {
            List<VisitingDetector> list =
                    mNodeTypeDetectors.get(AlternateConstructorInvocation.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitAlternateConstructorInvocation(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitAnnotation(Annotation node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(Annotation.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitAnnotation(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitAnnotationDeclaration(AnnotationDeclaration node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(AnnotationDeclaration.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitAnnotationDeclaration(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitAnnotationElement(AnnotationElement node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(AnnotationElement.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitAnnotationElement(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitAnnotationMethodDeclaration(AnnotationMethodDeclaration node) {
            List<VisitingDetector> list =
                    mNodeTypeDetectors.get(AnnotationMethodDeclaration.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitAnnotationMethodDeclaration(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitAnnotationValueArray(AnnotationValueArray node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(AnnotationValueArray.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitAnnotationValueArray(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitArrayAccess(ArrayAccess node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(ArrayAccess.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitArrayAccess(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitArrayCreation(ArrayCreation node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(ArrayCreation.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitArrayCreation(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitArrayDimension(ArrayDimension node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(ArrayDimension.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitArrayDimension(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitArrayInitializer(ArrayInitializer node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(ArrayInitializer.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitArrayInitializer(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitAssert(Assert node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(Assert.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitAssert(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitBinaryExpression(BinaryExpression node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(BinaryExpression.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitBinaryExpression(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitBlock(Block node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(Block.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitBlock(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitBooleanLiteral(BooleanLiteral node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(BooleanLiteral.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitBooleanLiteral(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitBreak(Break node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(Break.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitBreak(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitCase(Case node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(Case.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitCase(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitCast(Cast node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(Cast.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitCast(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitCatch(Catch node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(Catch.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitCatch(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitCharLiteral(CharLiteral node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(CharLiteral.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitCharLiteral(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitClassDeclaration(ClassDeclaration node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(ClassDeclaration.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitClassDeclaration(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitClassLiteral(ClassLiteral node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(ClassLiteral.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitClassLiteral(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitComment(Comment node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(Comment.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitComment(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitCompilationUnit(CompilationUnit node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(CompilationUnit.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitCompilationUnit(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitConstructorDeclaration(ConstructorDeclaration node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(ConstructorDeclaration.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitConstructorDeclaration(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitConstructorInvocation(ConstructorInvocation node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(ConstructorInvocation.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitConstructorInvocation(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitContinue(Continue node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(Continue.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitContinue(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitDefault(Default node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(Default.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitDefault(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitDoWhile(DoWhile node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(DoWhile.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitDoWhile(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitEmptyDeclaration(EmptyDeclaration node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(EmptyDeclaration.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitEmptyDeclaration(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitEmptyStatement(EmptyStatement node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(EmptyStatement.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitEmptyStatement(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitEnumConstant(EnumConstant node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(EnumConstant.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitEnumConstant(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitEnumDeclaration(EnumDeclaration node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(EnumDeclaration.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitEnumDeclaration(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitEnumTypeBody(EnumTypeBody node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(EnumTypeBody.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitEnumTypeBody(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitExpressionStatement(ExpressionStatement node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(ExpressionStatement.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitExpressionStatement(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitFloatingPointLiteral(FloatingPointLiteral node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(FloatingPointLiteral.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitFloatingPointLiteral(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitFor(For node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(For.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitFor(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitForEach(ForEach node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(ForEach.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitForEach(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitIdentifier(Identifier node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(Identifier.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitIdentifier(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitIf(If node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(If.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitIf(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitImportDeclaration(ImportDeclaration node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(ImportDeclaration.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitImportDeclaration(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitInlineIfExpression(InlineIfExpression node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(InlineIfExpression.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitInlineIfExpression(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitInstanceInitializer(InstanceInitializer node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(InstanceInitializer.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitInstanceInitializer(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitInstanceOf(InstanceOf node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(InstanceOf.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitInstanceOf(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitIntegralLiteral(IntegralLiteral node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(IntegralLiteral.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitIntegralLiteral(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitInterfaceDeclaration(InterfaceDeclaration node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(InterfaceDeclaration.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitInterfaceDeclaration(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitKeywordModifier(KeywordModifier node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(KeywordModifier.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitKeywordModifier(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitLabelledStatement(LabelledStatement node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(LabelledStatement.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitLabelledStatement(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitMethodDeclaration(MethodDeclaration node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(MethodDeclaration.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitMethodDeclaration(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitMethodInvocation(MethodInvocation node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(MethodInvocation.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitMethodInvocation(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitModifiers(Modifiers node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(Modifiers.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitModifiers(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitNormalTypeBody(NormalTypeBody node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(NormalTypeBody.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitNormalTypeBody(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitNullLiteral(NullLiteral node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(NullLiteral.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitNullLiteral(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitPackageDeclaration(PackageDeclaration node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(PackageDeclaration.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitPackageDeclaration(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitParseArtefact(Node node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(Node.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitParseArtefact(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitReturn(Return node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(Return.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitReturn(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitSelect(Select node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(Select.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitSelect(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitStaticInitializer(StaticInitializer node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(StaticInitializer.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitStaticInitializer(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitStringLiteral(StringLiteral node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(StringLiteral.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitStringLiteral(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitSuper(Super node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(Super.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitSuper(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitSuperConstructorInvocation(SuperConstructorInvocation node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(SuperConstructorInvocation.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitSuperConstructorInvocation(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitSwitch(Switch node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(Switch.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitSwitch(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitSynchronized(Synchronized node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(Synchronized.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitSynchronized(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitThis(This node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(This.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitThis(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitThrow(Throw node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(Throw.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitThrow(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitTry(Try node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(Try.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitTry(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitTypeReference(TypeReference node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(TypeReference.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitTypeReference(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitTypeReferencePart(TypeReferencePart node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(TypeReferencePart.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitTypeReferencePart(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitTypeVariable(TypeVariable node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(TypeVariable.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitTypeVariable(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitUnaryExpression(UnaryExpression node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(UnaryExpression.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitUnaryExpression(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitVariableDeclaration(VariableDeclaration node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(VariableDeclaration.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitVariableDeclaration(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitVariableDefinition(VariableDefinition node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(VariableDefinition.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitVariableDefinition(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitVariableDefinitionEntry(VariableDefinitionEntry node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(VariableDefinitionEntry.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitVariableDefinitionEntry(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitVariableReference(VariableReference node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(VariableReference.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitVariableReference(node);
                }
            }
            return false;
        }

        @Override
        public boolean visitWhile(While node) {
            List<VisitingDetector> list = mNodeTypeDetectors.get(While.class);
            if (list != null) {
                for (VisitingDetector v : list) {
                    v.getVisitor().visitWhile(node);
                }
            }
            return false;
        }
    }

    /** Performs common AST searches for method calls and R-type-field references.
     * Note that this is a specialized form of the {@link DispatchVisitor}. */
    private class DelegatingJavaVisitor extends DispatchVisitor {
        private final JavaContext mContext;
        private final boolean mVisitResources;
        private final boolean mVisitMethods;
        private final boolean mVisitConstructors;

        public DelegatingJavaVisitor(JavaContext context) {
            mContext = context;

            mVisitMethods = !mMethodDetectors.isEmpty();
            mVisitConstructors = !mConstructorDetectors.isEmpty();
            mVisitResources = !mResourceFieldDetectors.isEmpty();
        }

        @Override
        public boolean visitSelect(Select node) {
            if (mVisitResources) {
                // R.type.name
                if (node.astOperand() instanceof Select) {
                    Select select = (Select) node.astOperand();
                    if (select.astOperand() instanceof VariableReference) {
                        VariableReference reference = (VariableReference) select.astOperand();
                        if (reference.astIdentifier().astValue().equals(R_CLASS)) {
                            String type = select.astIdentifier().astValue();
                            String name = node.astIdentifier().astValue();

                            // R -could- be referenced locally and really have been
                            // imported as "import android.R;" in the import statements,
                            // but this is not recommended (and in fact there's a specific
                            // lint rule warning against it)
                            boolean isFramework = false;

                            for (VisitingDetector v : mResourceFieldDetectors) {
                                JavaScanner detector = v.getJavaScanner();
                                //noinspection ConstantConditions
                                detector.visitResourceReference(mContext, v.getVisitor(),
                                        node, type, name, isFramework);
                            }

                            return super.visitSelect(node);
                        }
                    }
                }

                // Arbitrary packages -- android.R.type.name, foo.bar.R.type.name
                if (node.astIdentifier().astValue().equals(R_CLASS)) {
                    Node parent = node.getParent();
                    if (parent instanceof Select) {
                        Node grandParent = parent.getParent();
                        if (grandParent instanceof Select) {
                            Select select = (Select) grandParent;
                            String name = select.astIdentifier().astValue();
                            Expression typeOperand = select.astOperand();
                            if (typeOperand instanceof Select) {
                                Select typeSelect = (Select) typeOperand;
                                String type = typeSelect.astIdentifier().astValue();
                                boolean isFramework = node.astOperand().toString().equals(
                                        ANDROID_PKG);
                                for (VisitingDetector v : mResourceFieldDetectors) {
                                    JavaScanner detector = v.getJavaScanner();
                                    detector.visitResourceReference(mContext, v.getVisitor(),
                                            node, type, name, isFramework);
                                }
                            }
                        }
                    }
                }
            }

            return super.visitSelect(node);
        }

        @Override
        public boolean visitMethodInvocation(MethodInvocation node) {
            if (mVisitMethods) {
                String methodName = node.astName().astValue();
                List<VisitingDetector> list = mMethodDetectors.get(methodName);
                if (list != null) {
                    for (VisitingDetector v : list) {
                        v.getJavaScanner().visitMethod(mContext, v.getVisitor(), node);
                    }
                }
            }

            return super.visitMethodInvocation(node);
        }

        @Override
        public boolean visitConstructorInvocation(ConstructorInvocation node) {
            if (mVisitConstructors) {
                TypeReference typeReference = node.astTypeReference();
                if (typeReference != null) {
                    TypeReferencePart last = typeReference.astParts().last();
                    if (last != null) {
                        String name = last.astIdentifier().astValue();
                        if (mConstructorSimpleNames.contains(name)) {
                            ResolvedNode resolved = mContext.resolve(node);
                            if (resolved instanceof ResolvedMethod) {
                                ResolvedMethod method = (ResolvedMethod) resolved;
                                String type = method.getContainingClass().getName();
                                List<VisitingDetector> list = mConstructorDetectors.get(type);
                                if (list != null) {
                                    for (VisitingDetector v : list) {
                                        v.getJavaScanner().visitConstructor(mContext,
                                                v.getVisitor(), node, method);
                                    }
                                }

                            }
                        }
                    }
                }
            }

            return super.visitConstructorInvocation(node);
        }
    }
}
