/*
 * Copyright (C) 2013 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;

import static com.android.tools.lint.EcjParser.equalsCompound;
import static com.android.tools.lint.EcjParser.startsWithCompound;
import static com.android.tools.lint.client.api.JavaParser.ResolvedClass;
import static com.android.tools.lint.client.api.JavaParser.ResolvedField;
import static com.android.tools.lint.client.api.JavaParser.ResolvedMethod;
import static com.android.tools.lint.client.api.JavaParser.ResolvedNode;
import static com.android.tools.lint.client.api.JavaParser.ResolvedVariable;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import com.android.annotations.NonNull;
import com.android.tools.lint.checks.AbstractCheckTest;
import com.android.tools.lint.checks.SdCardDetector;
import com.android.tools.lint.client.api.JavaParser;
import com.android.tools.lint.client.api.JavaParser.ResolvedAnnotation;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.LintUtilsTest;
import com.android.tools.lint.detector.api.Project;
import com.google.common.collect.Lists;
import com.intellij.psi.JavaRecursiveElementVisitor;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.PsiJavaFile;

import org.intellij.lang.annotations.Language;
import org.junit.Assert;

import java.io.File;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import lombok.ast.AnnotationElement;
import lombok.ast.BinaryExpression;
import lombok.ast.Block;
import lombok.ast.ClassDeclaration;
import lombok.ast.DescribedNode;
import lombok.ast.ExpressionStatement;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.Identifier;
import lombok.ast.KeywordModifier;
import lombok.ast.MethodInvocation;
import lombok.ast.Modifiers;
import lombok.ast.Node;
import lombok.ast.NormalTypeBody;
import lombok.ast.Select;
import lombok.ast.TypeReferencePart;
import lombok.ast.VariableDeclaration;
import lombok.ast.VariableDefinition;
import lombok.ast.VariableReference;
import lombok.ast.printer.SourceFormatter;
import lombok.ast.printer.SourcePrinter;
import lombok.ast.printer.TextFormatter;

// Disable code warnings that are applied to injected languages in ECJ sample code: these
// are deliberately doing dodgy things to test parser scenarios
@SuppressWarnings({"ClassNameDiffersFromFileName", "MethodMayBeStatic", "ImplicitArrayToString",
        "UnnecessaryLocalVariable", "UnnecessarySemicolon"})
public class EcjParserTest extends AbstractCheckTest {
    public void testTryCatchHang() throws Exception {
        // Ensure that we're really using this parser
        JavaParser javaParser = createClient().getJavaParser(null);
        assertNotNull(javaParser);
        assertTrue(javaParser.getClass().getName(), javaParser instanceof EcjParser);

        // See https://code.google.com/p/projectlombok/issues/detail?id=573#c6
        // With lombok.ast 0.2.1 and the parboiled-based Java parser this test will hang forever.
        assertEquals(
                "No warnings.",

                lintProject("src/test/pkg/TryCatchHang.java.txt=>src/test/pkg/TryCatchHang.java"));
    }

    public void testKitKatLanguageFeatures() throws Exception {
        @Language("JAVA")
        String testClass = "" +
                "package test.pkg;\n" +
                "\n" +
                "import java.io.BufferedReader;\n" +
                "import java.io.FileReader;\n" +
                "import java.io.IOException;\n" +
                "import java.lang.reflect.InvocationTargetException;\n" +
                "import java.util.List;\n" +
                "import java.util.Map;\n" +
                "import java.util.TreeMap;\n" +
                "\n" +
                "public class Java7LanguageFeatureTest {\n" +
                "    public void testDiamondOperator() {\n" +
                "        Map<String, List<Integer>> map = new TreeMap<>();\n" +
                "    }\n" +
                "\n" +
                "    public int testStringSwitches(String value) {\n" +
                "        final String first = \"first\";\n" +
                "        final String second = \"second\";\n" +
                "\n" +
                "        switch (value) {\n" +
                "            case first:\n" +
                "                return 41;\n" +
                "            case second:\n" +
                "                return 42;\n" +
                "            default:\n" +
                "                return 0;\n" +
                "        }\n" +
                "    }\n" +
                "\n" +
                "    public String testTryWithResources(String path) throws IOException {\n" +
                "        try (BufferedReader br = new BufferedReader(new FileReader(path))) {\n" +
                "            return br.readLine();\n" +
                "        }\n" +
                "    }\n" +
                "\n" +
                "    public void testNumericLiterals() {\n" +
                "        int thousand = 1_000;\n" +
                "        int million = 1_000_000;\n" +
                "        int binary = 0B01010101;\n" +
                "    }\n" +
                "\n" +
                "    public void testMultiCatch() {\n" +
                "\n" +
                "        try {\n" +
                "            Class.forName(\"java.lang.Integer\").getMethod(\"toString\").invoke(null);\n" +
                "        } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {\n" +
                "            e.printStackTrace();\n" +
                "        } catch (ClassNotFoundException e) {\n" +
                "            // TODO: Logging here\n" +
                "        }\n" +
                "    }\n" +
                "}\n";

        // Make sure PSI parsing works here too
        assertNotNull(LintUtilsTest.parsePsi(testClass).getJavaFile());
        JavaContext context = LintUtilsTest.parse(testClass);
        assertNotNull(context);
        Node unit = context.getCompilationUnit();

        // Now print the AST back and make sure that it contains at least the essence of the AST
        TextFormatter formatter = new TextFormatter();
        unit.accept(new SourcePrinter(formatter));
        String actual = formatter.finish();
        assertEquals(""
                + "package test.pkg;\n"
                + "\n"
                + "import java.io.BufferedReader;\n"
                + "import java.io.FileReader;\n"
                + "import java.io.IOException;\n"
                + "import java.lang.reflect.InvocationTargetException;\n"
                + "import java.util.List;\n"
                + "import java.util.Map;\n"
                + "import java.util.TreeMap;\n"
                + "\n"
                + "public class Java7LanguageFeatureTest {\n"
                + "    public void testDiamondOperator() {\n"
                + "        Map<String, List<Integer>> map = new TreeMap();\n" // missing types on rhs
                + "    }\n"
                + "    \n"
                + "    public int testStringSwitches(String value) {\n"
                + "        final String first = \"first\";\n"
                + "        final String second = \"second\";\n"
                + "        switch (value) {\n"
                + "        case first:\n"
                + "            return 41;\n"
                + "        case second:\n"
                + "            return 42;\n"
                + "        default:\n"
                + "            return 0;\n"
                + "        }\n"
                + "    }\n"
                + "    \n"
                + "    public String testTryWithResources(String path) throws IOException {\n"
                + "        try {\n" // Note how the initialization clause is gone here
                + "            return br.readLine();\n"
                + "        }\n"
                + "    }\n"
                + "    \n"
                + "    public void testNumericLiterals() {\n"
                + "        int thousand = 1_000;\n"
                + "        int million = 1_000_000;\n"
                + "        int binary = 0B01010101;\n"
                + "    }\n"
                + "    \n"
                + "    public void testMultiCatch() {\n"
                + "        try {\n"
                + "            Class.forName(\"java.lang.Integer\").getMethod(\"toString\").invoke(null);\n"
                + "        } catch (IllegalAccessException e) {\n" // Note: missing other union types
                + "            e.printStackTrace();\n"
                + "        } catch (ClassNotFoundException e) {\n"
                + "        }\n"
                + "    }\n"
                + "}",
                actual);
    }

    @SuppressWarnings("ClassNameDiffersFromFileName")
    public void testGetFields() throws Exception {
        @Language("JAVA")
        String source = ""
                + "public class FieldTest {\n"
                + "    public int field1 = 1;\n"
                + "    public int field2 = 3;\n"
                + "    public int field3 = 5;\n"
                + "    \n"
                + "    public static class Inner extends FieldTest {\n"
                + "        public int field2 = 5;\n"
                + "    }\n"
                + "}\n";

        final JavaContext context = LintUtilsTest.parse(source,
                new File("src/test/pkg/FieldTest.java"));
        assertNotNull(context);

        Node compilationUnit = context.getCompilationUnit();
        assertNotNull(compilationUnit);
        final AtomicBoolean found = new AtomicBoolean();
        compilationUnit.accept(new ForwardingAstVisitor() {
            @Override
            public boolean visitClassDeclaration(ClassDeclaration node) {
                if (node.astName().astValue().equals("Inner")) {
                    found.set(true);
                    ResolvedNode resolved = context.resolve(node);
                    assertNotNull(resolved);
                    ResolvedClass cls = (ResolvedClass) resolved;
                    List<ResolvedField> declaredFields = Lists.newArrayList(cls.getFields(false));
                    assertEquals(1, declaredFields.size());
                    assertEquals("field2", declaredFields.get(0).getName());

                    declaredFields = Lists.newArrayList(cls.getFields(true));
                    assertEquals(3, declaredFields.size());
                    assertEquals("field2", declaredFields.get(0).getName());
                    assertEquals("FieldTest.Inner", declaredFields.get(0).getContainingClassName());
                    assertEquals("field1", declaredFields.get(1).getName());
                    assertEquals("FieldTest", declaredFields.get(1).getContainingClassName());
                    assertEquals("field3", declaredFields.get(2).getName());
                    assertEquals("FieldTest", declaredFields.get(2).getContainingClassName());
                }

                return super.visitClassDeclaration(node);
            }
        });
        assertTrue(found.get());
    }

    @SuppressWarnings("ClassNameDiffersFromFileName")
    public void testRemoveDuplicates() throws Exception {
        @SuppressWarnings("override")
        @Language("JAVA")
        String source = ""
                + "package test.pkg;\n"
                + "import java.lang.annotation.Retention;\n"
                + "import java.lang.annotation.RetentionPolicy;\n"
                + "\n"
                + "@SuppressWarnings(\"unused\")\n"
                + "public class AnnotationTest {\n"
                + "    @Retention(RetentionPolicy.CLASS)\n"
                + "    public @interface Annotation1 {};\n"
                + "    @Retention(RetentionPolicy.CLASS)\n"
                + "    public @interface Annotation2 {};\n"
                + "    @Retention(RetentionPolicy.CLASS)\n"
                + "    public @interface Annotation3 {};\n"
                + "\n"
                + "    public static void method(ChildClass child) {\n"
                + "        child.method(0xff0000);\n"
                + "    }\n"
                + "\n"
                + "    public static class ParentClass {\n"
                + "        @Annotation1\n"
                + "        void method(@Annotation2 int rgb) {\n"
                + "        }\n"
                + "    }\n"
                + "\n"
                + "    public static class ChildClass extends ParentClass {\n"
                + "        @Annotation1\n"
                + "        void method(@Annotation2 @Annotation3 int rgb) {\n"
                + "        }\n"
                + "    }\n"
                + "}\n";

        final JavaContext context = LintUtilsTest.parse(source,
                new File("src/test/pkg/AnnotationTest.java"));
        assertNotNull(context);

        Node compilationUnit = context.getCompilationUnit();
        assertNotNull(compilationUnit);
        final AtomicBoolean found = new AtomicBoolean();
        compilationUnit.accept(new ForwardingAstVisitor() {
            @Override
            public boolean visitMethodInvocation(MethodInvocation node) {
                if (node.astName().astValue().equals("method")) {
                    found.set(true);
                    ResolvedNode resolved = context.resolve(node);
                    assertNotNull(resolved);
                    ResolvedMethod method = (ResolvedMethod) resolved;
                    List<ResolvedAnnotation> methodAnnotations =
                            Lists.newArrayList(method.getAnnotations());
                    // This is the point of this test: make sure there is only *one* occurrence
                    // of this annotation (prior to this fix, we'd accumulate up the hierarchy)
                    assertEquals(1, methodAnnotations.size());
                    assertEquals("test.pkg.AnnotationTest.Annotation1",
                            methodAnnotations.get(0).getName());
                    assertEquals(methodAnnotations.get(0).getName(),
                            methodAnnotations.get(0).getSignature());

                    List<ResolvedAnnotation> annotations =
                            Lists.newArrayList(method.getParameterAnnotations(0));
                    assertEquals(2, annotations.size());
                    String first = annotations.get(0).getName();
                    String second = annotations.get(1).getName();
                    if (first.compareTo(second) > 0) {
                        String temp = first;
                        first = second;
                        second = temp;
                    }
                    assertEquals("test.pkg.AnnotationTest.Annotation2", first);
                    assertEquals("test.pkg.AnnotationTest.Annotation3", second);
                }

                return super.visitMethodInvocation(node);
            }
        });
        assertTrue(found.get());
    }

    public void testResolution() throws Exception {
        @Language("JAVA")
        String source =
                "package test.pkg;\n" +
                "\n" +
                "import java.io.File;\n" +
                "\n" +
                "public class TypeResolutionTest {\n" +
                "    public static class Inner extends File {\n" +
                "        public float myField = 5f;\n" +
                "        public int[] myInts;\n" +
                "\n" +
                "        public Inner(File dir, String name) {\n" +
                "            super(dir, name);\n" +
                "        }\n" +
                "\n" +
                "        public void call(int arg1, double arg2) {\n" +
                "            boolean x = super.canRead();\n" +
                "            System.out.println(x);\n" +
                "        }\n" +
                "    }\n" +
                "\n" +
                "    @SuppressWarnings(\"all\")\n" +
                "    public static class Other {\n" +
                "         private void client(int z) {\n" +
                "             int x = z;\n" +
                "             int y = x + 5;\n" +
                "             {\n" +
                "               int w = 5;\n" +
                "             }\n" +
                "             Inner inner = new Inner(null, null);\n" +
                "             inner.myField = 6;\n" +
                "             System.out.println(inner.myInts);\n" +
                "         }\n" +
                "    }\n" +
                "}\n";

        Node unit = LintUtilsTest.getCompilationUnit(source,
                new File("src/test/pkg/TypeResolutionTest.java"));

        // Visit all nodes and assert nativeNode != null unless I expect it!
        unit.accept(new ForwardingAstVisitor() {
            @SuppressWarnings("Contract")
            @Override
            public boolean visitNode(Node node) {
                if (node.getNativeNode() == null && requiresNativeNode(node)) {
                    fail("Expected native node on node of type " +
                                    node.getClass().getSimpleName());
                }
                return super.visitNode(node);
            }

            private boolean requiresNativeNode(Node node) {
                if (node instanceof TypeReferencePart &&
                        node.getParent().getNativeNode() != null) {
                    return false;
                }

                if (node instanceof Identifier
                        || node instanceof NormalTypeBody
                        || node instanceof Block
                        || node instanceof VariableDeclaration
                        || node instanceof VariableDefinition
                        || node instanceof AnnotationElement
                        || node instanceof BinaryExpression
                        || node instanceof Modifiers
                        || node instanceof KeywordModifier) {
                    return false;
                }

                if (node instanceof VariableReference) {
                    VariableReference reference = (VariableReference)node;
                    if (reference.getParent() instanceof Select) {
                        return false;
                    }
                } else if (node instanceof MethodInvocation) {
                    Node parent = node.getParent();
                    if (parent instanceof ExpressionStatement &&
                            parent.getNativeNode() != null) {
                        return false;
                    }
                }

                return true;
            }
        });

        JavaParser parser = new EcjParser(new LintCliClient(), null);
        AstPrettyPrinter astPrettyPrinter = new AstPrettyPrinter(parser);
        unit.accept(new SourcePrinter(astPrettyPrinter));
        String actual = astPrettyPrinter.finish();
        assertEquals(
                "[CompilationUnit]\n" +
                "  [PackageDeclaration]\n" +
                "    [Identifier test]\n" +
                "      PROPERTY: name = test\n" +
                "    [Identifier pkg]\n" +
                "      PROPERTY: name = pkg\n" +
                "  [ImportDeclaration]\n" +
                "    PROPERTY: static = false\n" +
                "    PROPERTY: star = false\n" +
                "    [Identifier java]\n" +
                "      PROPERTY: name = java\n" +
                "    [Identifier io]\n" +
                "      PROPERTY: name = io\n" +
                "    [Identifier File]\n" +
                "      PROPERTY: name = File\n" +
                "  [ClassDeclaration TypeResolutionTest], type: test.pkg.TypeResolutionTest, resolved class: test.pkg.TypeResolutionTest \n" +
                "    [Modifiers], type: test.pkg.TypeResolutionTest, resolved class: test.pkg.TypeResolutionTest \n" +
                "      [KeywordModifier public]\n" +
                "        PROPERTY: modifier = public\n" +
                "    typeName: [Identifier TypeResolutionTest], type: test.pkg.TypeResolutionTest, resolved class: test.pkg.TypeResolutionTest \n" +
                "      PROPERTY: name = TypeResolutionTest\n" +
                "    [NormalTypeBody], type: test.pkg.TypeResolutionTest, resolved class: test.pkg.TypeResolutionTest \n" +
                "        [ClassDeclaration Inner], type: test.pkg.TypeResolutionTest.Inner, resolved class: test.pkg.TypeResolutionTest.Inner \n" +
                "          [Modifiers], type: test.pkg.TypeResolutionTest.Inner, resolved class: test.pkg.TypeResolutionTest.Inner \n" +
                "            [KeywordModifier public]\n" +
                "              PROPERTY: modifier = public\n" +
                "            [KeywordModifier static]\n" +
                "              PROPERTY: modifier = static\n" +
                "          typeName: [Identifier Inner], type: test.pkg.TypeResolutionTest.Inner, resolved class: test.pkg.TypeResolutionTest.Inner \n" +
                "            PROPERTY: name = Inner\n" +
                "          extends: [TypeReference File], type: java.io.File, resolved class: java.io.File \n" +
                "            PROPERTY: WildcardKind = NONE\n" +
                "            PROPERTY: arrayDimensions = 0\n" +
                "            [TypeReferencePart], type: java.io.File, resolved class: java.io.File \n" +
                "              [Identifier File]\n" +
                "                PROPERTY: name = File\n" +
                "          [NormalTypeBody], type: test.pkg.TypeResolutionTest.Inner, resolved class: test.pkg.TypeResolutionTest.Inner \n" +
                "              [VariableDeclaration], type: float, resolved field: myField test.pkg.TypeResolutionTest.Inner\n" +
                "                [VariableDefinition], type: float, resolved field: myField test.pkg.TypeResolutionTest.Inner\n" +
                "                  PROPERTY: varargs = false\n" +
                "                  [Modifiers]\n" +
                "                    [KeywordModifier public]\n" +
                "                      PROPERTY: modifier = public\n" +
                "                  type: [TypeReference float], type: float, resolved class: float \n" +
                "                    PROPERTY: WildcardKind = NONE\n" +
                "                    PROPERTY: arrayDimensions = 0\n" +
                "                    [TypeReferencePart], type: float, resolved class: float \n" +
                "                      [Identifier float]\n" +
                "                        PROPERTY: name = float\n" +
                "                  [VariableDefinitionEntry], type: float, resolved field: myField test.pkg.TypeResolutionTest.Inner\n" +
                "                    PROPERTY: arrayDimensions = 0\n" +
                "                    varName: [Identifier myField], type: float, resolved field: myField test.pkg.TypeResolutionTest.Inner\n" +
                "                      PROPERTY: name = myField\n" +
                "                    [FloatingPointLiteral 5.0], type: float\n" +
                "                      PROPERTY: value = 5f\n" +
                "              [VariableDeclaration], type: int[], resolved field: myInts test.pkg.TypeResolutionTest.Inner\n" +
                "                [VariableDefinition], type: int[], resolved field: myInts test.pkg.TypeResolutionTest.Inner\n" +
                "                  PROPERTY: varargs = false\n" +
                "                  [Modifiers]\n" +
                "                    [KeywordModifier public]\n" +
                "                      PROPERTY: modifier = public\n" +
                "                  type: [TypeReference int[]], type: int[], resolved class: int[] \n" +
                "                    PROPERTY: WildcardKind = NONE\n" +
                "                    PROPERTY: arrayDimensions = 1\n" +
                "                    [TypeReferencePart], type: int[], resolved class: int[] \n" +
                "                      [Identifier int]\n" +
                "                        PROPERTY: name = int\n" +
                "                  [VariableDefinitionEntry], type: int[], resolved field: myInts test.pkg.TypeResolutionTest.Inner\n" +
                "                    PROPERTY: arrayDimensions = 0\n" +
                "                    varName: [Identifier myInts], type: int[], resolved field: myInts test.pkg.TypeResolutionTest.Inner\n" +
                "                      PROPERTY: name = myInts\n" +
                "              [ConstructorDeclaration], type: void, resolved method: test.pkg.TypeResolutionTest.Inner test.pkg.TypeResolutionTest.Inner\n" +
                "                [Modifiers], type: void, resolved method: test.pkg.TypeResolutionTest.Inner test.pkg.TypeResolutionTest.Inner\n" +
                "                  [KeywordModifier public]\n" +
                "                    PROPERTY: modifier = public\n" +
                "                typeName: [Identifier Inner], type: void, resolved method: test.pkg.TypeResolutionTest.Inner test.pkg.TypeResolutionTest.Inner\n" +
                "                  PROPERTY: name = Inner\n" +
                "                parameter: [VariableDefinition], type: java.io.File, resolved variable: dir java.io.File\n" +
                "                  PROPERTY: varargs = false\n" +
                "                  [Modifiers]\n" +
                "                  type: [TypeReference File], type: java.io.File, resolved class: java.io.File \n" +
                "                    PROPERTY: WildcardKind = NONE\n" +
                "                    PROPERTY: arrayDimensions = 0\n" +
                "                    [TypeReferencePart], type: java.io.File, resolved class: java.io.File \n" +
                "                      [Identifier File]\n" +
                "                        PROPERTY: name = File\n" +
                "                  [VariableDefinitionEntry], type: java.io.File, resolved variable: dir java.io.File\n" +
                "                    PROPERTY: arrayDimensions = 0\n" +
                "                    varName: [Identifier dir], type: java.io.File, resolved variable: dir java.io.File\n" +
                "                      PROPERTY: name = dir\n" +
                "                parameter: [VariableDefinition], type: java.lang.String, resolved variable: name java.lang.String\n" +
                "                  PROPERTY: varargs = false\n" +
                "                  [Modifiers]\n" +
                "                  type: [TypeReference String], type: java.lang.String, resolved class: java.lang.String \n" +
                "                    PROPERTY: WildcardKind = NONE\n" +
                "                    PROPERTY: arrayDimensions = 0\n" +
                "                    [TypeReferencePart], type: java.lang.String, resolved class: java.lang.String \n" +
                "                      [Identifier String]\n" +
                "                        PROPERTY: name = String\n" +
                "                  [VariableDefinitionEntry], type: java.lang.String, resolved variable: name java.lang.String\n" +
                "                    PROPERTY: arrayDimensions = 0\n" +
                "                    varName: [Identifier name], type: java.lang.String, resolved variable: name java.lang.String\n" +
                "                      PROPERTY: name = name\n" +
                "                [Block], type: void, resolved method: test.pkg.TypeResolutionTest.Inner test.pkg.TypeResolutionTest.Inner\n" +
                "                    [SuperConstructorInvocation], resolved method: java.io.File java.io.File\n" +
                "                      [VariableReference], type: java.io.File, resolved variable: dir java.io.File\n" +
                "                        [Identifier dir], type: java.io.File, resolved variable: dir java.io.File\n" +
                "                          PROPERTY: name = dir\n" +
                "                      [VariableReference], type: java.lang.String, resolved variable: name java.lang.String\n" +
                "                        [Identifier name], type: java.lang.String, resolved variable: name java.lang.String\n" +
                "                          PROPERTY: name = name\n" +
                "              [MethodDeclaration call], type: void, resolved method: call test.pkg.TypeResolutionTest.Inner\n" +
                "                [Modifiers], type: void, resolved method: call test.pkg.TypeResolutionTest.Inner\n" +
                "                  [KeywordModifier public]\n" +
                "                    PROPERTY: modifier = public\n" +
                "                returnType: [TypeReference void], type: void, resolved class: void \n" +
                "                  PROPERTY: WildcardKind = NONE\n" +
                "                  PROPERTY: arrayDimensions = 0\n" +
                "                  [TypeReferencePart], type: void, resolved class: void \n" +
                "                    [Identifier void]\n" +
                "                      PROPERTY: name = void\n" +
                "                methodName: [Identifier call], type: void, resolved method: call test.pkg.TypeResolutionTest.Inner\n" +
                "                  PROPERTY: name = call\n" +
                "                parameter: [VariableDefinition], type: int, resolved variable: arg1 int\n" +
                "                  PROPERTY: varargs = false\n" +
                "                  [Modifiers]\n" +
                "                  type: [TypeReference int], type: int, resolved class: int \n" +
                "                    PROPERTY: WildcardKind = NONE\n" +
                "                    PROPERTY: arrayDimensions = 0\n" +
                "                    [TypeReferencePart], type: int, resolved class: int \n" +
                "                      [Identifier int]\n" +
                "                        PROPERTY: name = int\n" +
                "                  [VariableDefinitionEntry], type: int, resolved variable: arg1 int\n" +
                "                    PROPERTY: arrayDimensions = 0\n" +
                "                    varName: [Identifier arg1], type: int, resolved variable: arg1 int\n" +
                "                      PROPERTY: name = arg1\n" +
                "                parameter: [VariableDefinition], type: double, resolved variable: arg2 double\n" +
                "                  PROPERTY: varargs = false\n" +
                "                  [Modifiers]\n" +
                "                  type: [TypeReference double], type: double, resolved class: double \n" +
                "                    PROPERTY: WildcardKind = NONE\n" +
                "                    PROPERTY: arrayDimensions = 0\n" +
                "                    [TypeReferencePart], type: double, resolved class: double \n" +
                "                      [Identifier double]\n" +
                "                        PROPERTY: name = double\n" +
                "                  [VariableDefinitionEntry], type: double, resolved variable: arg2 double\n" +
                "                    PROPERTY: arrayDimensions = 0\n" +
                "                    varName: [Identifier arg2], type: double, resolved variable: arg2 double\n" +
                "                      PROPERTY: name = arg2\n" +
                "                [Block], type: void, resolved method: call test.pkg.TypeResolutionTest.Inner\n" +
                "                    [VariableDeclaration], type: boolean, resolved variable: x boolean\n" +
                "                      [VariableDefinition], type: boolean, resolved variable: x boolean\n" +
                "                        PROPERTY: varargs = false\n" +
                "                        [Modifiers]\n" +
                "                        type: [TypeReference boolean], type: boolean, resolved class: boolean \n" +
                "                          PROPERTY: WildcardKind = NONE\n" +
                "                          PROPERTY: arrayDimensions = 0\n" +
                "                          [TypeReferencePart], type: boolean, resolved class: boolean \n" +
                "                            [Identifier boolean]\n" +
                "                              PROPERTY: name = boolean\n" +
                "                        [VariableDefinitionEntry], type: boolean, resolved variable: x boolean\n" +
                "                          PROPERTY: arrayDimensions = 0\n" +
                "                          varName: [Identifier x], type: boolean, resolved variable: x boolean\n" +
                "                            PROPERTY: name = x\n" +
                "                          [MethodInvocation canRead], type: boolean, resolved method: canRead java.io.File\n" +
                "                            operand: [Super], type: java.io.File\n" +
                "                            methodName: [Identifier canRead], type: boolean, resolved method: canRead java.io.File\n" +
                "                              PROPERTY: name = canRead\n" +
                "                    [ExpressionStatement], type: void, resolved method: println java.io.PrintStream\n" +
                "                      [MethodInvocation println], type: void, resolved method: println java.io.PrintStream\n" +
                "                        operand: [Select], type: java.io.PrintStream, resolved field: out java.lang.System\n" +
                "                          operand: [VariableReference], type: java.io.PrintStream, resolved field: out java.lang.System\n" +
                "                            [Identifier System]\n" +
                "                              PROPERTY: name = System\n" +
                "                          selected: [Identifier out], type: java.io.PrintStream, resolved field: out java.lang.System\n" +
                "                            PROPERTY: name = out\n" +
                "                        methodName: [Identifier println]\n" +
                "                          PROPERTY: name = println\n" +
                "                        [VariableReference], type: boolean, resolved variable: x boolean\n" +
                "                          [Identifier x], type: boolean, resolved variable: x boolean\n" +
                "                            PROPERTY: name = x\n" +
                "        [ClassDeclaration Other], type: test.pkg.TypeResolutionTest.Other, resolved class: test.pkg.TypeResolutionTest.Other \n" +
                "          [Modifiers], type: test.pkg.TypeResolutionTest.Other, resolved class: test.pkg.TypeResolutionTest.Other \n" +
                "            [Annotation SuppressWarnings], type: java.lang.SuppressWarnings, resolved annotation: java.lang.SuppressWarnings \n" +
                "              [TypeReference SuppressWarnings], type: java.lang.SuppressWarnings, resolved class: java.lang.SuppressWarnings \n" +
                "                PROPERTY: WildcardKind = NONE\n" +
                "                PROPERTY: arrayDimensions = 0\n" +
                "                [TypeReferencePart], type: java.lang.SuppressWarnings, resolved class: java.lang.SuppressWarnings \n" +
                "                  [Identifier SuppressWarnings]\n" +
                "                    PROPERTY: name = SuppressWarnings\n" +
                "              [AnnotationElement null], type: java.lang.SuppressWarnings, resolved annotation: java.lang.SuppressWarnings \n" +
                "                [StringLiteral all], type: java.lang.String\n" +
                "                  PROPERTY: value = \"all\"\n" +
                "            [KeywordModifier public]\n" +
                "              PROPERTY: modifier = public\n" +
                "            [KeywordModifier static]\n" +
                "              PROPERTY: modifier = static\n" +
                "          typeName: [Identifier Other], type: test.pkg.TypeResolutionTest.Other, resolved class: test.pkg.TypeResolutionTest.Other \n" +
                "            PROPERTY: name = Other\n" +
                "          [NormalTypeBody], type: test.pkg.TypeResolutionTest.Other, resolved class: test.pkg.TypeResolutionTest.Other \n" +
                "              [MethodDeclaration client], type: void, resolved method: client test.pkg.TypeResolutionTest.Other\n" +
                "                [Modifiers], type: void, resolved method: client test.pkg.TypeResolutionTest.Other\n" +
                "                  [KeywordModifier private]\n" +
                "                    PROPERTY: modifier = private\n" +
                "                returnType: [TypeReference void], type: void, resolved class: void \n" +
                "                  PROPERTY: WildcardKind = NONE\n" +
                "                  PROPERTY: arrayDimensions = 0\n" +
                "                  [TypeReferencePart], type: void, resolved class: void \n" +
                "                    [Identifier void]\n" +
                "                      PROPERTY: name = void\n" +
                "                methodName: [Identifier client], type: void, resolved method: client test.pkg.TypeResolutionTest.Other\n" +
                "                  PROPERTY: name = client\n" +
                "                parameter: [VariableDefinition], type: int, resolved variable: z int\n" +
                "                  PROPERTY: varargs = false\n" +
                "                  [Modifiers]\n" +
                "                  type: [TypeReference int], type: int, resolved class: int \n" +
                "                    PROPERTY: WildcardKind = NONE\n" +
                "                    PROPERTY: arrayDimensions = 0\n" +
                "                    [TypeReferencePart], type: int, resolved class: int \n" +
                "                      [Identifier int]\n" +
                "                        PROPERTY: name = int\n" +
                "                  [VariableDefinitionEntry], type: int, resolved variable: z int\n" +
                "                    PROPERTY: arrayDimensions = 0\n" +
                "                    varName: [Identifier z], type: int, resolved variable: z int\n" +
                "                      PROPERTY: name = z\n" +
                "                [Block], type: void, resolved method: client test.pkg.TypeResolutionTest.Other\n" +
                "                    [VariableDeclaration], type: int, resolved variable: x int\n" +
                "                      [VariableDefinition], type: int, resolved variable: x int\n" +
                "                        PROPERTY: varargs = false\n" +
                "                        [Modifiers]\n" +
                "                        type: [TypeReference int], type: int, resolved class: int \n" +
                "                          PROPERTY: WildcardKind = NONE\n" +
                "                          PROPERTY: arrayDimensions = 0\n" +
                "                          [TypeReferencePart], type: int, resolved class: int \n" +
                "                            [Identifier int]\n" +
                "                              PROPERTY: name = int\n" +
                "                        [VariableDefinitionEntry], type: int, resolved variable: x int\n" +
                "                          PROPERTY: arrayDimensions = 0\n" +
                "                          varName: [Identifier x], type: int, resolved variable: x int\n" +
                "                            PROPERTY: name = x\n" +
                "                          [VariableReference], type: int, resolved variable: z int\n" +
                "                            [Identifier z], type: int, resolved variable: z int\n" +
                "                              PROPERTY: name = z\n" +
                "                    [VariableDeclaration], type: int, resolved variable: y int\n" +
                "                      [VariableDefinition], type: int, resolved variable: y int\n" +
                "                        PROPERTY: varargs = false\n" +
                "                        [Modifiers]\n" +
                "                        type: [TypeReference int], type: int, resolved class: int \n" +
                "                          PROPERTY: WildcardKind = NONE\n" +
                "                          PROPERTY: arrayDimensions = 0\n" +
                "                          [TypeReferencePart], type: int, resolved class: int \n" +
                "                            [Identifier int]\n" +
                "                              PROPERTY: name = int\n" +
                "                        [VariableDefinitionEntry], type: int, resolved variable: y int\n" +
                "                          PROPERTY: arrayDimensions = 0\n" +
                "                          varName: [Identifier y], type: int, resolved variable: y int\n" +
                "                            PROPERTY: name = y\n" +
                "                          [BinaryExpression +], type: int\n" +
                "                            PROPERTY: operator = +\n" +
                "                            left: [VariableReference], type: int, resolved variable: x int\n" +
                "                              [Identifier x], type: int, resolved variable: x int\n" +
                "                                PROPERTY: name = x\n" +
                "                            right: [IntegralLiteral 5], type: int\n" +
                "                              PROPERTY: value = 5\n" +
                "                    [Block]\n" +
                "                        [VariableDeclaration], type: int, resolved variable: w int\n" +
                "                          [VariableDefinition], type: int, resolved variable: w int\n" +
                "                            PROPERTY: varargs = false\n" +
                "                            [Modifiers]\n" +
                "                            type: [TypeReference int], type: int, resolved class: int \n" +
                "                              PROPERTY: WildcardKind = NONE\n" +
                "                              PROPERTY: arrayDimensions = 0\n" +
                "                              [TypeReferencePart], type: int, resolved class: int \n" +
                "                                [Identifier int]\n" +
                "                                  PROPERTY: name = int\n" +
                "                            [VariableDefinitionEntry], type: int, resolved variable: w int\n" +
                "                              PROPERTY: arrayDimensions = 0\n" +
                "                              varName: [Identifier w], type: int, resolved variable: w int\n" +
                "                                PROPERTY: name = w\n" +
                "                              [IntegralLiteral 5], type: int\n" +
                "                                PROPERTY: value = 5\n" +
                "                    [VariableDeclaration], type: test.pkg.TypeResolutionTest.Inner, resolved variable: inner test.pkg.TypeResolutionTest.Inner\n" +
                "                      [VariableDefinition], type: test.pkg.TypeResolutionTest.Inner, resolved variable: inner test.pkg.TypeResolutionTest.Inner\n" +
                "                        PROPERTY: varargs = false\n" +
                "                        [Modifiers]\n" +
                "                        type: [TypeReference Inner], type: test.pkg.TypeResolutionTest.Inner, resolved class: test.pkg.TypeResolutionTest.Inner \n" +
                "                          PROPERTY: WildcardKind = NONE\n" +
                "                          PROPERTY: arrayDimensions = 0\n" +
                "                          [TypeReferencePart], type: test.pkg.TypeResolutionTest.Inner, resolved class: test.pkg.TypeResolutionTest.Inner \n" +
                "                            [Identifier Inner]\n" +
                "                              PROPERTY: name = Inner\n" +
                "                        [VariableDefinitionEntry], type: test.pkg.TypeResolutionTest.Inner, resolved variable: inner test.pkg.TypeResolutionTest.Inner\n" +
                "                          PROPERTY: arrayDimensions = 0\n" +
                "                          varName: [Identifier inner], type: test.pkg.TypeResolutionTest.Inner, resolved variable: inner test.pkg.TypeResolutionTest.Inner\n" +
                "                            PROPERTY: name = inner\n" +
                "                          [ConstructorInvocation Inner], type: test.pkg.TypeResolutionTest.Inner, resolved method: test.pkg.TypeResolutionTest.Inner test.pkg.TypeResolutionTest.Inner\n" +
                "                            type: [TypeReference Inner], type: test.pkg.TypeResolutionTest.Inner, resolved class: test.pkg.TypeResolutionTest.Inner \n" +
                "                              PROPERTY: WildcardKind = NONE\n" +
                "                              PROPERTY: arrayDimensions = 0\n" +
                "                              [TypeReferencePart], type: test.pkg.TypeResolutionTest.Inner, resolved class: test.pkg.TypeResolutionTest.Inner \n" +
                "                                [Identifier Inner]\n" +
                "                                  PROPERTY: name = Inner\n" +
                "                            [NullLiteral], type: null\n" +
                "                            [NullLiteral], type: null\n" +
                "                    [ExpressionStatement], type: float\n" +
                "                      [BinaryExpression =], type: float\n" +
                "                        PROPERTY: operator = =\n" +
                "                        left: [Select], type: float, resolved variable: inner test.pkg.TypeResolutionTest.Inner\n" +
                "                          operand: [VariableReference], type: float, resolved variable: inner test.pkg.TypeResolutionTest.Inner\n" +
                "                            [Identifier inner]\n" +
                "                              PROPERTY: name = inner\n" +
                "                          selected: [Identifier myField], type: float, resolved variable: inner test.pkg.TypeResolutionTest.Inner\n" +
                "                            PROPERTY: name = myField\n" +
                "                        right: [IntegralLiteral 6], type: int\n" +
                "                          PROPERTY: value = 6\n" +
                "                    [ExpressionStatement], type: void, resolved method: println java.io.PrintStream\n" +
                "                      [MethodInvocation println], type: void, resolved method: println java.io.PrintStream\n" +
                "                        operand: [Select], type: java.io.PrintStream, resolved field: out java.lang.System\n" +
                "                          operand: [VariableReference], type: java.io.PrintStream, resolved field: out java.lang.System\n" +
                "                            [Identifier System]\n" +
                "                              PROPERTY: name = System\n" +
                "                          selected: [Identifier out], type: java.io.PrintStream, resolved field: out java.lang.System\n" +
                "                            PROPERTY: name = out\n" +
                "                        methodName: [Identifier println]\n" +
                "                          PROPERTY: name = println\n" +
                "                        [Select], type: int[], resolved variable: inner test.pkg.TypeResolutionTest.Inner\n" +
                "                          operand: [VariableReference], type: int[], resolved variable: inner test.pkg.TypeResolutionTest.Inner\n" +
                "                            [Identifier inner]\n" +
                "                              PROPERTY: name = inner\n" +
                "                          selected: [Identifier myInts], type: int[], resolved variable: inner test.pkg.TypeResolutionTest.Inner\n" +
                "                            PROPERTY: name = myInts\n",
                actual);
    }

    public void testStartsWithCompound() throws Exception {
        assertTrue(startsWithCompound("test.pkg",
                new char[][]{
                        "test".toCharArray(),
                        "pkg".toCharArray()
                }));

        assertTrue(startsWithCompound("test.pkg",
                new char[][]{
                        "test".toCharArray(),
                        "pkg".toCharArray(),
                        "other".toCharArray(),
                }));

        assertFalse(startsWithCompound("test.pkg",
                new char[][]{
                        "test".toCharArray(),
                        "other".toCharArray()
                }));

        // Corner cases
        assertFalse(startsWithCompound("test.pk",
                new char[][]{
                        "test".toCharArray(),
                        "pkg".toCharArray(),
                }));
        assertFalse(startsWithCompound("test.pkg",
                new char[][]{
                        "test".toCharArray()
                }));
        assertTrue(startsWithCompound("test.",
                new char[][]{
                        "test".toCharArray(),
                        "pkg".toCharArray()
                }));
        assertFalse(startsWithCompound("test.pkg",
                new char[][]{
                        "test".toCharArray(),
                        "pk".toCharArray()
                }));
    }

    public void testEqualsWithCompound() throws Exception {
        assertTrue(equalsCompound("test.pkg",
                new char[][]{
                        "test".toCharArray(),
                        "pkg".toCharArray()
                }));

        assertFalse(equalsCompound("test.pkg",
                new char[][]{
                        "test".toCharArray(),
                        "pkg".toCharArray(),
                        "other".toCharArray(),
                }));

        assertFalse(equalsCompound("test.pkg",
                new char[][]{
                        "test".toCharArray(),
                        "other".toCharArray()
                }));

        // Corner cases
        assertFalse(equalsCompound("test.pk",
                new char[][]{
                        "test".toCharArray(),
                        "pkg".toCharArray(),
                }));
        assertFalse(equalsCompound("test.pkg",
                new char[][]{
                        "test".toCharArray()
                }));
        assertFalse(equalsCompound("test.",
                new char[][]{
                        "test".toCharArray(),
                        "pkg".toCharArray()
                }));
        assertFalse(equalsCompound("test.pkg",
                new char[][]{
                        "test".toCharArray(),
                        "pk".toCharArray()
                }));
    }

    // Regression test for https://code.google.com/p/android/issues/detail?id=172268
    // This is a reduced version of the in-the-wild occurrence that happened when lint processed
    // Chromium for Android sources.
    public void testTypeInformationIsNotClearedAfterCompilation() throws Exception {
        // We need to generate lots of classes to overflow default Compiler's TypeSystem internal array
        // of types to trigger ArrayIndexOutOfBoundsException. This array initially has 256 entries.
        int innerClassesNumber = 256;
        String innerClassWithFieldTemplate = ""
                + "  private static class Inner%1$d {}\n"
                + "  private final HashMap<Inner%1$d, Integer> mMap%1$d;";

        StringBuilder innerClasses = new StringBuilder();
        for (int i = 0; i < innerClassesNumber; ++i) {
            innerClasses.append(String.format(Locale.US, innerClassWithFieldTemplate, i));
            innerClasses.append('\n');
        }

        String accessorTemplate = ""
                + "    for (Map.Entry<Inner%1$d, Integer> entry : mMap%1$d.entrySet()) {}";
        StringBuilder accessors = new StringBuilder();
        for (int i = 0; i < innerClassesNumber; ++i) {
            accessors.append(String.format(Locale.US, accessorTemplate, i)).append('\n');
        }
        @SuppressWarnings({"OnDemandImport", "ClassNameDiffersFromFileName"})
        @Language("JAVA")
        String source = ""
                + "import java.util.*;\n"
                + "public class TypeInfoTest {\n"
                + innerClasses.toString()
                + "  public void doSomething() {\n"
                + accessors.toString()
                + "  }"
                + "}\n";

        final JavaContext context = LintUtilsTest.parse(source,
                new File("src/test/pkg/TypeInfoTest.java"));
        assertNotNull(context);

        Node compilationUnit = context.getCompilationUnit();
        assertNotNull(compilationUnit);

        // null means OK
        final AtomicReference<String> result = new AtomicReference<String>();

        compilationUnit.accept(new ForwardingAstVisitor() {
            @Override
            public boolean visitMethodInvocation(MethodInvocation call) {
                // Snapshot of SupportAnnotationDetector
                try {
                    ResolvedNode resolved = context.resolve(call);
                    if (resolved instanceof ResolvedMethod) {
                        ResolvedMethod method = (ResolvedMethod) resolved;
                        method.getAnnotations();
                        method.getContainingClass().getAnnotations();
                    } else {
                        // Most likely target SDK isn't supplied (via ANDROID_HOME or by some other
                        // means). The test is meaningless then.
                        result.compareAndSet(null,
                                "Cannot get resolved method, is test setup correct?");
                    }
                } catch (RuntimeException e) {
                    // Exception can be swallowed by some upper-level code so we cannot rely on
                    // compilationUnit.accept to fail test
                    result.compareAndSet(null, "Got an exception " + e);
                }
                return false;
            }
        });

        String failMessage = result.get();
        assertNull(failMessage, result.get());
    }

    public void testGetClassNamesWithoutSignatures() {
        @SuppressWarnings("TypeParameterExplicitlyExtendsObject")
        @Language("JAVA")
        String source = ""
                + "package test.pkg;\n"
                + "public abstract class Adapter<VH extends Object> {\n"
                + "    public abstract void onBindViewHolder(VH holder, int position);\n"
                + "}\n";
        final JavaContext context = LintUtilsTest.parse(source,
                new File("src/test/pkg/Adapter.java"));
        assertNotNull(context);

        Node compilationUnit = context.getCompilationUnit();
        assertNotNull(compilationUnit);
        final AtomicBoolean found = new AtomicBoolean();
        compilationUnit.accept(new ForwardingAstVisitor() {
            @Override
            public boolean visitClassDeclaration(ClassDeclaration node) {
                if (node.astName().astValue().startsWith("Adapter")) {
                    found.set(true);
                    ResolvedNode resolved = context.resolve(node);
                    assertNotNull(resolved);
                    ResolvedClass cls = (ResolvedClass) resolved;
                    assertEquals("test.pkg.Adapter", cls.getName());
                    assertEquals("Adapter", cls.getSimpleName());
                }
                return super.visitClassDeclaration(node);
            }
        });
        assertTrue(found.get());
    }

    @Override
    protected Detector getDetector() {
        return new SdCardDetector();
    }

    @Override
    protected TestLintClient createClient() {
        return new TestLintClient() {
            @NonNull
            @Override
            protected ClassPathInfo getClassPath(@NonNull Project project) {
                ClassPathInfo classPath = super.getClassPath(project);
                // Insert fake classpath entries (non existent directories) to
                // make sure the parser handles that gracefully. See issue 87740.
                classPath.getLibraries(true).add(new File("nonexistent path"));
                return classPath;
            }
        };
    }

    public void testEnsureUnique() {
        ResolvedAnnotation foo1 = createTestAnnotation("foo");
        ResolvedAnnotation foo2 = createTestAnnotation("foo");
        ResolvedAnnotation foo3 = createTestAnnotation("foo");
        ResolvedAnnotation bar1 = createTestAnnotation("bar");
        ResolvedAnnotation bar2 = createTestAnnotation("bar");

        assertEquals("[]",
                EcjParser.ensureUnique(Lists.<ResolvedAnnotation>newArrayList()).toString());
        assertEquals("[foo, bar]",
                EcjParser.ensureUnique(Lists.newArrayList(foo1, foo2, foo3, bar1)).toString());
        assertEquals("[foo]",
                EcjParser.ensureUnique(Lists.newArrayList(foo1, foo2)).toString());
        assertEquals("[foo]",
                EcjParser.ensureUnique(Lists.newArrayList(foo1, foo2, foo3)).toString());
        assertEquals("[foo, bar]",
                EcjParser.ensureUnique(Lists.newArrayList(foo1, foo2, foo3, bar1)).toString());
        assertEquals("[bar, foo]",
                EcjParser.ensureUnique(Lists.newArrayList(bar1, foo2, foo3)).toString());
        assertEquals("[bar, foo]",
                EcjParser.ensureUnique(Lists.newArrayList(bar1, bar2, foo2, foo3)).toString());
    }

    private ResolvedAnnotation createTestAnnotation(String name) {
        ResolvedAnnotation annotation = mock(ResolvedAnnotation.class);
        when(annotation.getName()).thenReturn(name);
        when(annotation.toString()).thenReturn(name);
        return annotation;
    }

    public static class AstPrettyPrinter implements SourceFormatter {

        @SuppressWarnings("StringBufferField") // Don't care about performance here
        private final StringBuilder mOutput = new StringBuilder(1000);

        private final JavaParser mResolver;

        private int mIndent;

        private String mName;

        public AstPrettyPrinter(JavaParser resolver) {
            mResolver = resolver;
        }

        private void add(String in, Object... args) {
            for (int i = 0; i < mIndent; i++) {
                mOutput.append("  ");
            }
            if (mName != null) {
                mOutput.append(mName).append(": ");
                mName = null;
            }
            if (args.length == 0) {
                mOutput.append(in);
            } else {
                mOutput.append(String.format(in, args));
            }
        }

        @Override
        public void buildInline(Node node) {
            buildNode(node);
        }

        @Override
        public void buildBlock(Node node) {
            buildNode(node);
        }

        private void buildNode(Node node) {
            if (node == null) {
                mIndent++;
                return;
            }
            String name = node.getClass().getSimpleName();
            String description = "";
            if (node instanceof DescribedNode) {
                description = " " + ((DescribedNode) node).getDescription();
            }

            String typeDescription = "";
            String resolutionDescription = "";
            @SuppressWarnings("ConstantConditions") // Hack for test pretty printer only
            JavaParser.TypeDescriptor t = mResolver.getType(null, node);
            if (t != null) {
                typeDescription = ", type: " + t.getName();
            }
            @SuppressWarnings("ConstantConditions") // Hack for test pretty printer only
            ResolvedNode resolved = mResolver.resolve(null, node);
            if (resolved != null) {
                String c = "unknown";
                String extra = "";
                if (resolved instanceof ResolvedClass) {
                    c = "class";
                } else if (resolved instanceof ResolvedMethod) {
                    c = "method";
                    ResolvedMethod method = (ResolvedMethod) resolved;
                    extra = method.getContainingClass().getName();
                } else if (resolved instanceof ResolvedField) {
                    c = "field";
                    ResolvedField field = (ResolvedField) resolved;
                    ResolvedClass containingClass = field.getContainingClass();
                    if (containingClass != null) {
                        extra = containingClass.getName();
                    }
                } else if (resolved instanceof ResolvedVariable) {
                    c = "variable";
                    ResolvedVariable variable = (ResolvedVariable) resolved;
                    extra = variable.getType().getName();
                } else if (resolved instanceof ResolvedAnnotation) {
                    c = "annotation";
                }
                resolutionDescription = String.format(", resolved %1$s: %2$s %3$s",
                        c, resolved.getName(), extra);
            }

            add("[%1$s%2$s]%3$s%4$s\n", name, description, typeDescription, resolutionDescription);

            mIndent++;
        }

        @Override
        public void fail(String fail) {
            Assert.fail(fail);
        }

        @Override
        public void property(String name, Object value) {
            add("PROPERTY: %s = %s\n", name, value);
        }

        @Override
        public void keyword(String text) {
        }

        @Override
        public void operator(String text) {
        }

        @Override
        public void verticalSpace() {
        }

        @Override
        public void space() {
        }

        @Override
        public void append(String text) {
        }

        @Override
        public void startSuppressBlock() {
        }

        @Override
        public void endSuppressBlock() {
        }

        @Override
        public void startSuppressIndent() {
        }

        @Override
        public void endSuppressIndent() {
        }

        @Override
        public void closeInline() {
            mIndent--;
        }

        @Override
        public void closeBlock() {
            mIndent--;
        }

        @Override
        public void addError(int start, int end, String message) {
            fail(message);
        }

        @Override
        public String finish() {
            return mOutput.toString();
        }

        @Override
        public void setTimeTaken(long taken) {
        }

        @Override
        public void nameNextElement(String name) {
            mName = name;
        }
    }

    public void testPrivateSuperMethod() {
        @Language("JAVA")
        String source = ""
                + "package test.pkg;\n"
                + "\n"
                + "public class Parent {\n"
                + "    public int mPublicField = 1;\n"
                + "    private int mPrivateField = 2;\n"
                + "    @SuppressWarnings(\"all\")\n"
                + "    private void foo(int x, float y) {\n"
                + "    }\n"
                + "    private void foo2(int x, float y) {\n"
                + "    }\n"
                + "\n"
                + "    public static class Child extends Parent {\n"
                + "        public void foo(int x, float y) {\n"
                + "        }\n"
                + "    }\n"
                + "}\n";

        final JavaContext context = LintUtilsTest.parse(source,
                new File("src/test/pkg/Parent.java"));
        assertNotNull(context);

        Node compilationUnit = context.getCompilationUnit();
        assertNotNull(compilationUnit);
        final AtomicBoolean found = new AtomicBoolean();
        compilationUnit.accept(new ForwardingAstVisitor() {
            @Override
            public boolean visitClassDeclaration(ClassDeclaration node) {
                if (node.astName().astValue().equals("Child")) {
                    found.set(true);
                    ResolvedNode resolved = context.resolve(node);
                    assertNotNull(resolved);
                    ResolvedClass cls = (ResolvedClass) resolved;
                    List<ResolvedField> declaredFields = Lists.newArrayList(cls.getFields(true));
                    assertEquals(1, declaredFields.size());
                    assertEquals("mPublicField", declaredFields.get(0).getName());

                    List<ResolvedMethod> methods = Lists.newArrayList(cls.getMethods(true));
                    ResolvedMethod resolvedMethod = methods.get(0);
                    assertEquals("foo", resolvedMethod.getName());
                    assertEquals(cls, resolvedMethod.getContainingClass());
                    ResolvedMethod superMethod = resolvedMethod.getSuperMethod();
                    assertNotNull(superMethod); // compiler inserts trampoline to private method
                    assertTrue((superMethod.getModifiers() & Modifier.PRIVATE) != 0);
                    // However, it doesn't inherit annotations; it's not an actual override
                    assertNotNull(superMethod.getAnnotation("java.lang.SuppressWarnings"));
                    assertNull(resolvedMethod.getAnnotation("java.lang.SuppressWarnings"));
                }

                return super.visitClassDeclaration(node);
            }
        });
        assertTrue(found.get());
    }
}
