UnparserTest.java

/*
 * Janino - An embedded Java[TM] compiler
 *
 * Copyright (c) 2001-2010 Arno Unkrig. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
 * following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
 *       following disclaimer.
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
 *       following disclaimer in the documentation and/or other materials provided with the distribution.
 *    3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
 *       products derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.codehaus.janino.tests;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.codehaus.commons.compiler.Location;
import org.codehaus.commons.nullanalysis.Nullable;
import org.codehaus.janino.Java;
import org.codehaus.janino.Java.AbstractCompilationUnit;
import org.codehaus.janino.Java.AbstractTypeDeclaration;
import org.codehaus.janino.Java.AmbiguousName;
import org.codehaus.janino.Java.ArrayAccessExpression;
import org.codehaus.janino.Java.ArrayCreationReference;
import org.codehaus.janino.Java.ArrayLength;
import org.codehaus.janino.Java.Assignment;
import org.codehaus.janino.Java.Atom;
import org.codehaus.janino.Java.BinaryOperation;
import org.codehaus.janino.Java.Block;
import org.codehaus.janino.Java.BooleanLiteral;
import org.codehaus.janino.Java.Cast;
import org.codehaus.janino.Java.CharacterLiteral;
import org.codehaus.janino.Java.ClassInstanceCreationReference;
import org.codehaus.janino.Java.ClassLiteral;
import org.codehaus.janino.Java.ConditionalExpression;
import org.codehaus.janino.Java.Crement;
import org.codehaus.janino.Java.FieldAccess;
import org.codehaus.janino.Java.FieldAccessExpression;
import org.codehaus.janino.Java.FloatingPointLiteral;
import org.codehaus.janino.Java.Instanceof;
import org.codehaus.janino.Java.IntegerLiteral;
import org.codehaus.janino.Java.LambdaExpression;
import org.codehaus.janino.Java.LocalVariableAccess;
import org.codehaus.janino.Java.Locatable;
import org.codehaus.janino.Java.Located;
import org.codehaus.janino.Java.Lvalue;
import org.codehaus.janino.Java.MethodInvocation;
import org.codehaus.janino.Java.MethodReference;
import org.codehaus.janino.Java.Modifier;
import org.codehaus.janino.Java.NewAnonymousClassInstance;
import org.codehaus.janino.Java.NewArray;
import org.codehaus.janino.Java.NewClassInstance;
import org.codehaus.janino.Java.NewInitializedArray;
import org.codehaus.janino.Java.NullLiteral;
import org.codehaus.janino.Java.PackageMemberClassDeclaration;
import org.codehaus.janino.Java.ParameterAccess;
import org.codehaus.janino.Java.ParenthesizedExpression;
import org.codehaus.janino.Java.QualifiedThisReference;
import org.codehaus.janino.Java.Rvalue;
import org.codehaus.janino.Java.SimpleConstant;
import org.codehaus.janino.Java.StringLiteral;
import org.codehaus.janino.Java.SuperclassFieldAccessExpression;
import org.codehaus.janino.Java.SuperclassMethodInvocation;
import org.codehaus.janino.Java.TextBlock;
import org.codehaus.janino.Java.ThisReference;
import org.codehaus.janino.Java.Type;
import org.codehaus.janino.Java.UnaryOperation;
import org.codehaus.janino.Parser;
import org.codehaus.janino.Scanner;
import org.codehaus.janino.TokenType;
import org.codehaus.janino.Unparser;
import org.codehaus.janino.Visitor.LvalueVisitor;
import org.codehaus.janino.Visitor.RvalueVisitor;
import org.codehaus.janino.util.AbstractTraverser;
import org.junit.Assert;
import org.junit.Test;

// SUPPRESS CHECKSTYLE JavadocMethod:9999

/**
 * Unit tests for the {@link Unparser}.
 */
public
class UnparserTest {

    private static void
    helpTestExpr(String input, String expected, boolean simplify) throws Exception {
        Parser p    = new Parser(new Scanner(null, new StringReader(input)));
        Rvalue expr = p.parseExpression();
        if (simplify) {
            expr = UnparserTest.stripUnnecessaryParenExprs(expr);
        }

        StringWriter sw = new StringWriter();
        Unparser     u  = new Unparser(sw);
        u.unparseRvalue(expr);
        u.close();
        String s = sw.toString();
        s = UnparserTest.replace(s, "((( ", "(");
        s = UnparserTest.replace(s, " )))", ")");
        Assert.assertEquals(input, expected, s);
    }

    private static void
    helpTestScript(String input) throws Exception {
        UnparserTest.helpTestScript(input, input);
    }

    private static void
    helpTestScript(String expected, String input) throws Exception {

        Parser p = new Parser(new Scanner(null, new StringReader(input)));

        Block block = new Block(Location.NOWHERE);
        block.addStatements(p.parseBlockStatements());

        String s;
        {
            StringWriter sw = new StringWriter();
            Unparser     u  = new Unparser(sw);
            u.unparseStatements(block.statements);
            u.close();
            s = sw.toString();
        }

        Assert.assertEquals(input, UnparserTest.normalizeWhitespace(expected), UnparserTest.normalizeWhitespace(s));
    }

    private static void
    helpTestClassBody(String input) throws Exception {
        UnparserTest.helpTestClassBody(input, input);
    }

    private static void
    helpTestClassBody(String expected, String input) throws Exception {

        Parser p = new Parser(new Scanner(null, new StringReader(input)));

        PackageMemberClassDeclaration pmcd = new PackageMemberClassDeclaration(
            Location.NOWHERE,
            null,             // docComment
            new Modifier[0],  // modifiers
            "SC",             // name
            null,             // typeParameters
            null,             // extendedType
            new Type[0]       // implementedTypes
        );
        while (!p.peek(TokenType.END_OF_INPUT)) p.parseClassBodyDeclaration(pmcd);

        String s;
        {
            StringWriter sw = new StringWriter();
            Unparser     u  = new Unparser(sw);
            u.unparseClassDeclarationBody(pmcd);
            u.close();
            s = sw.toString();
        }

        Assert.assertEquals(input, UnparserTest.normalizeWhitespace(expected), UnparserTest.normalizeWhitespace(s));
    }

    private static void
    helpTestCu(String cu) throws Exception { UnparserTest.helpTestCu(cu, cu); }

    private static void
    helpTestCu(String input, String expected) throws Exception {

        Parser                  p   = new Parser(new Scanner(null, new StringReader(input)));
        AbstractCompilationUnit acu = p.parseAbstractCompilationUnit();

        StringWriter sw = new StringWriter();
        Unparser     u  = new Unparser(sw);
        u.unparseAbstractCompilationUnit(acu);
        u.close();

        String actual = UnparserTest.normalizeWhitespace(sw.toString());
        Assert.assertEquals(expected, actual);
    }

    public static String
    normalizeWhitespace(String input) { return input.replaceAll("\\s+", " ").trim(); }

    private static String
    replace(String s, String from, String to) {
        for (;;) {
            int idx = s.indexOf(from);
            if (idx == -1) break;
            s = s.substring(0, idx) + to + s.substring(idx + from.length());
        }
        return s;
    }

    private static Java.Rvalue[]
    stripUnnecessaryParenExprs(Java.Rvalue[] rvalues) {
        Java.Rvalue[] res = new Java.Rvalue[rvalues.length];
        for (int i = 0; i < res.length; ++i) {
            res[i] = UnparserTest.stripUnnecessaryParenExprs(rvalues[i]);
        }
        return res;
    }

    @Nullable private static Java.Atom
    stripUnnecessaryParenExprsOpt(@Nullable Java.Atom atom) {
        return atom == null ? null : UnparserTest.stripUnnecessaryParenExprs(atom);
    }

    private static Java.Atom
    stripUnnecessaryParenExprs(Java.Atom atom) {
        if (atom instanceof Java.Rvalue) {
            return UnparserTest.stripUnnecessaryParenExprs((Java.Rvalue) atom);
        }
        return atom;
    }

    private static Java.Lvalue
    stripUnnecessaryParenExprs(Java.Lvalue lvalue) {
        return (Java.Lvalue) UnparserTest.stripUnnecessaryParenExprs((Java.Rvalue) lvalue);
    }

    @Nullable private static Java.Rvalue
    stripUnnecessaryParenExprsOpt(@Nullable Java.Rvalue rvalue) {
        return rvalue == null ? null : UnparserTest.stripUnnecessaryParenExprs(rvalue);
    }

    private static Java.Rvalue
    stripUnnecessaryParenExprs(Java.Rvalue rvalue) {

        Java.Rvalue result = rvalue.accept(new RvalueVisitor<Rvalue, RuntimeException>() {

            @Override @Nullable public Rvalue
            visitLvalue(Lvalue lv) {
                return lv.accept(new LvalueVisitor<Rvalue, RuntimeException>() {

                    @Override public Rvalue
                    visitAmbiguousName(AmbiguousName an) { return an; }

                    @Override public Rvalue
                    visitArrayAccessExpression(ArrayAccessExpression aae) {
                        return new Java.ArrayAccessExpression(
                            aae.getLocation(),
                            UnparserTest.stripUnnecessaryParenExprs(aae.lhs),
                            UnparserTest.stripUnnecessaryParenExprs(aae.index)
                        );
                    }

                    @Override public Rvalue
                    visitFieldAccess(FieldAccess fa) {
                        return new Java.FieldAccess(
                            fa.getLocation(),
                            UnparserTest.stripUnnecessaryParenExprs(fa.lhs),
                            fa.field
                        );
                    }

                    @Override public Rvalue
                    visitFieldAccessExpression(FieldAccessExpression fae) {
                        return new Java.FieldAccessExpression(
                            fae.getLocation(),
                            UnparserTest.stripUnnecessaryParenExprs(fae.lhs),
                            fae.fieldName
                        );
                    }

                    @Override public Rvalue
                    visitLocalVariableAccess(LocalVariableAccess lva) { return lva; }

                    @Override public Rvalue
                    visitParenthesizedExpression(ParenthesizedExpression pe) {
                        return UnparserTest.stripUnnecessaryParenExprs(pe.value);
                    }

                    @Override public Rvalue
                    visitSuperclassFieldAccessExpression(SuperclassFieldAccessExpression scfae) { return scfae; }
                });
            }

            @Override public Rvalue
            visitArrayLength(ArrayLength al) {
                return new Java.ArrayLength(
                    al.getLocation(),
                    UnparserTest.stripUnnecessaryParenExprs(al.lhs)
                );
            }

            @Override public Rvalue
            visitAssignment(Assignment a) {
                return new Java.Assignment(
                    a.getLocation(),
                    UnparserTest.stripUnnecessaryParenExprs(a.lhs),
                    a.operator,
                    UnparserTest.stripUnnecessaryParenExprs(a.rhs)
                );
            }

            @Override public Rvalue
            visitBinaryOperation(BinaryOperation bo) {
                return new Java.BinaryOperation(
                    bo.getLocation(),
                    UnparserTest.stripUnnecessaryParenExprs(bo.lhs),
                    bo.operator,
                    UnparserTest.stripUnnecessaryParenExprs(bo.rhs)
                );
            }

            @Override public Rvalue
            visitCast(Cast c) {
                return new Java.Cast(
                    c.getLocation(),
                    c.targetType,
                    UnparserTest.stripUnnecessaryParenExprs(c.value)
                );
            }

            @Override public Rvalue
            visitClassLiteral(ClassLiteral cl) {
                return cl; //too much effort
            }

            @Override public Rvalue
            visitConditionalExpression(ConditionalExpression ce) {
                return new Java.ConditionalExpression(
                    ce.getLocation(),
                    UnparserTest.stripUnnecessaryParenExprs(ce.lhs),
                    UnparserTest.stripUnnecessaryParenExprs(ce.mhs),
                    UnparserTest.stripUnnecessaryParenExprs(ce.rhs)
                );
            }

            @Override public Rvalue
            visitCrement(Crement c) {
                if (c.pre) {
                    return new Java.Crement(
                        c.getLocation(),
                        c.operator,
                        UnparserTest.stripUnnecessaryParenExprs(c.operand)
                    );
                } else {
                    return new Java.Crement(
                        c.getLocation(),
                        UnparserTest.stripUnnecessaryParenExprs(c.operand),
                        c.operator
                    );
                }
            }

            @Override public Rvalue
            visitInstanceof(Instanceof io) {
                return new Java.Instanceof(
                    io.getLocation(),
                    UnparserTest.stripUnnecessaryParenExprs(io.lhs),
                    io.rhs
                );
            }

            @Override public Rvalue visitIntegerLiteral(IntegerLiteral il)              { return il;  }
            @Override public Rvalue visitFloatingPointLiteral(FloatingPointLiteral fpl) { return fpl; }
            @Override public Rvalue visitBooleanLiteral(BooleanLiteral bl)              { return bl;  }
            @Override public Rvalue visitCharacterLiteral(CharacterLiteral cl)          { return cl;  }
            @Override public Rvalue visitStringLiteral(StringLiteral sl)                { return sl;  }
            @Override public Rvalue visitTextBlock(TextBlock tb)                        { return tb;  }
            @Override public Rvalue visitNullLiteral(NullLiteral nl)                    { return nl;  }

            @Override public Rvalue visitSimpleConstant(SimpleConstant sl) { return sl; }

            @Override public Rvalue
            visitMethodInvocation(MethodInvocation mi) {
                return new Java.MethodInvocation(
                    mi.getLocation(),
                    UnparserTest.stripUnnecessaryParenExprsOpt(mi.target),
                    mi.methodName,
                    UnparserTest.stripUnnecessaryParenExprs(mi.arguments)
                );
            }

            @Override public Rvalue
            visitNewAnonymousClassInstance(NewAnonymousClassInstance naci) {
                return naci; //too much effort
            }

            @Override public Rvalue
            visitNewArray(NewArray na) {
                return new Java.NewArray(
                    na.getLocation(),
                    na.type,
                    UnparserTest.stripUnnecessaryParenExprs(na.dimExprs),
                    na.dims
                );
            }

            @Override public Rvalue
            visitNewClassInstance(NewClassInstance nci) {
                Type type = nci.type;
                assert type != null;
                return new Java.NewClassInstance(
                    nci.getLocation(),
                    UnparserTest.stripUnnecessaryParenExprsOpt(nci.qualification),
                    type,
                    UnparserTest.stripUnnecessaryParenExprs(nci.arguments)
                );
            }

            @Override public Rvalue
            visitNewInitializedArray(NewInitializedArray nia) {
                return nia; //too much effort
            }

            @Override public Rvalue
            visitParameterAccess(ParameterAccess pa) { return pa; }

            @Override public Rvalue
            visitQualifiedThisReference(QualifiedThisReference qtr) { return qtr; }

            @Override public Rvalue
            visitSuperclassMethodInvocation(SuperclassMethodInvocation smi) {
                return new Java.SuperclassMethodInvocation(
                    smi.getLocation(),
                    smi.methodName,
                    UnparserTest.stripUnnecessaryParenExprs(smi.arguments)
                );
            }

            @Override public Rvalue
            visitThisReference(ThisReference tr) { return tr; }

            @Override @Nullable public Rvalue
            visitLambdaExpression(LambdaExpression lr) { return lr; }

            @Override @Nullable public Rvalue
            visitMethodReference(MethodReference mr) { return mr; }

            @Override @Nullable public Rvalue
            visitInstanceCreationReference(ClassInstanceCreationReference cicr) { return cicr; }

            @Override @Nullable public Rvalue
            visitArrayCreationReference(ArrayCreationReference acr) { return acr; }

            @Override public Rvalue
            visitUnaryOperation(UnaryOperation uo) {
                return new Java.UnaryOperation(
                    uo.getLocation(),
                    uo.operator,
                    UnparserTest.stripUnnecessaryParenExprs(uo.operand)
                );
            }
        });

        assert result != null;
        return result;
    }

    @Test public void
    testInterface() throws Exception {
        UnparserTest.testInterfaceHelper(false);
        UnparserTest.testInterfaceHelper(true);
    }

    private static void
    testInterfaceHelper(boolean interfaceMod) {
        Java.PackageMemberInterfaceDeclaration decl = new Java.PackageMemberInterfaceDeclaration(
            Location.NOWHERE,                                                            // location
            "foo",                                                                       // docComment
            new Java.Modifier[] { new Java.AccessModifier("public", Location.NOWHERE) }, // modifiers
            "Foo",                                                                       // name
            null,                                                                        // typeParameters
            new Type[0]                                                                  // extendedTypes
        );
        StringWriter sw = new StringWriter();
        Unparser     u  = new Unparser(sw);
        u.unparseTypeDeclaration(decl);
        u.close();
        String s             = sw.toString();
        String correctString = "/**foo */ public interface Foo { }";
        Assert.assertEquals(correctString, UnparserTest.normalizeWhitespace(s));
    }

    @Test public void
    testLiterals() throws Exception {
        Object[][] tests = {
            { new FloatingPointLiteral(Location.NOWHERE, "-0.0D"), "-0.0D" },
            { new FloatingPointLiteral(Location.NOWHERE, "-0.0F"), "-0.0F" },
            { new TextBlock(Location.NOWHERE, "\"\"\"  \r   Line 1\r    Line 2\r\n   \t Line 3 \"\"\""), "\"\"\"  \r   Line 1\r    Line 2\r\n   \t Line 3 \"\"\"" },
        };
        for (Object[] test : tests) {
            assert test.length == 2;
            final Atom   expr     = (Atom)   test[0];
            final String expected = (String) test[1];

            StringWriter sw = new StringWriter();
            Unparser     u  = new Unparser(sw);
            u.unparseAtom(expr);
            u.close();
            Assert.assertEquals(expected, sw.toString());
        }
    }

    @Test public void
    testSimple() throws Exception {
        UnparserTest.helpTestExpr("1 + 2*3", "1 + 2 * 3", false);
        UnparserTest.helpTestExpr("1 + 2*3", "1 + 2 * 3", true);
    }

    @Test public void
    testParens() throws Exception {
        UnparserTest.helpTestExpr("(1 + 2)*3", "(1 + 2) * 3", false);
        UnparserTest.helpTestExpr("(1 + 2)*3", "(1 + 2) * 3", true);
    }

    @Test public void
    testMany() throws Exception {
        final String[][] exprs = {
            //input                                    expected simplified                    expect non-simplified
            { "((1)+2)",                               "1 + 2",                               "((1) + 2)"            },
            { "1 - 2 + 3",                             null,                                  null                   },
            { "(1 - 2) + 3",                           "1 - 2 + 3",                           null                   },
            { "1 - (2 + 3)",                           "1 - (2 + 3)",                         null                   },
            { "1 + 2 * 3",                             null,                                  null                   },
            { "1 + (2 * 3)",                           "1 + 2 * 3",                           null                   },
            { "3 - (2 - 1)",                           null,                                  null                   },
            { "true ? 1 : 2",                          null,                                  null                   },
            { "(true ? false : true) ? 1 : 2",         null,                                  null                   },
            { "true ? false : (true ? false : true)",  "true ? false : true ? false : true",  null                   },
            { "-(-(2))",                               "-(-2)",                               "-(-(2))"              },
            { "- - 2",                                 "-(-2)",                               "-(-2)"                },
            { "x && (y || z)",                         null,                                  null                   },
            { "(x && y) || z",                         "x && y || z",                         null                   },
            { "x = (y = z)",                           "x = y = z",                           null                   },
            { "x *= (y *= z)",                         "x *= y *= z",                         null                   },
            { "(--x) + 3",                             "--x + 3",                             null                   },
            { "(baz.bar).foo(x, (3 + 4) * 5)",         "baz.bar.foo(x, (3 + 4) * 5)",         null                   },
            { "!(bar instanceof Integer)",             null,                                  null                   },
            { "(true ? foo : bar).baz()",              null,                                  null                   },
            { "((String) foo).length()",               null,                                  null                   },
            { "-~2",                                   "-(~2)",                               "-(~2)"                },
            { "(new String[1])[0]",                    null,                                  null                   },
            { "(new String()).length()",               "new String().length()",               null                   },
            { "(new int[] { 1, 2 })[0]",               "new int[] { 1, 2 }[0]",               null                   },
            { "(\"asdf\" + \"qwer\").length()",        null,                                  null                   },
            { "-(a++)",                                "-a++",                                null                   },
            { "-1",                                    null,                                  null                   },
            { "-0x1",                                  "-0x1",                                "-0x1"                 },
            { "-0x7fffffff",                           "-0x7fffffff",                         "-0x7fffffff"          },
            { "-0x80000000",                           "-0x80000000",                         "-0x80000000"          },
            { "-0x80000001",                           "-0x80000001",                         "-0x80000001"          },
            { "-0x1l",                                 "-0x1l",                               "-0x1l",               },
            { "-0x7fffffffffffffffl",                  "-0x7fffffffffffffffl",                "-0x7fffffffffffffffl" },
            { "-0x8000000000000000l",                  "-0x8000000000000000l",                "-0x8000000000000000l" },
        };

        for (String[] expr : exprs) {
            String input          = expr[0];
            String expectSimplify = expr[1];
            if (expectSimplify == null) {
                expectSimplify = input;
            }

            String expectNoSimplify = expr[2];
            if (expectNoSimplify == null) {
                expectNoSimplify = input;
            }

            UnparserTest.helpTestExpr(input, expectSimplify, true);
            UnparserTest.helpTestExpr(input, expectNoSimplify, false);
        }
    }

    @Test public void
    testParseUnparseEnumDeclaration() throws Exception {
        final String[][] cus = {
            //input:                                                      expected output (if different from input):
            { "enum Foo { A, B ; }",                                      null },
            { "public enum Foo { A, B ; }",                               null },
            { "public enum Foo implements Serializable { A, B ; }",       null },
            { "enum Foo { A, B ; int meth() { return 0; } }",             null },
            { "enum Foo { @Deprecated A ; }",                             null },
            { "enum Foo { @Bar(foo = \"bar\", cls = String.class) A ; }", null },
            { "enum Foo { A(1, 2, 3) ; }",                                null },
            { "enum Foo { A { void meth() {} } ; }",                      null },
        };

        for (String[] expr : cus) {

            String input = expr[0];

            String expect = expr.length >= 2 && expr[1] != null ? expr[1] : input;

            UnparserTest.helpTestCu(input, expect);
        }
    }

    @Test public void
    testParseUnparseAnnotationTypeDeclaration() throws Exception {
        final String[][] cus = {
            //input:                                        expected output (if different from input):
            { "@interface Foo { }",                         null },
            { "public @interface Foo { }",                  null },
            { "@Deprecated @interface Foo { }",             null },
            { "@interface Foo { int value(); }",            null },
            { "@interface Foo { int[] value(); }",          null },
//            { "@interface Foo { int value() default 99; }", null },  NYI
        };

        for (String[] expr : cus) {

            String input = expr[0];

            String expect = expr.length >= 2 && expr[1] != null ? expr[1] : input;

            UnparserTest.helpTestCu(input, expect);
        }
    }

    @Test public void
    testParseUnparseJava5() throws Exception {

        // Type arguments.
        UnparserTest.helpTestScript("{ java.util.Set<String> set = new java.util.HashSet<String>(); }");
    }

    @Test public void
    testParseUnparseJava7() throws Exception {

        // Catching and rethrowing multiple exception types.
        UnparserTest.helpTestScript("{ try {} catch (java.io.IOException | RuntimeException e) { throw e; } }");

        // Type inference for generic instance creation.
        UnparserTest.helpTestScript("{ java.util.Map<String, Integer> map = new java.util.HashMap<>(); }");
    }

    @Test public void
    testParseUnparseJava8() throws Exception {

        // Lambda expressions.
        UnparserTest.helpTestScript("{ new Thread(()         -> {});   }");
        UnparserTest.helpTestScript("{ return ()             -> {};    }");
        UnparserTest.helpTestScript("{ return a              -> {};    }");
        UnparserTest.helpTestScript("{ return (int a, int b) -> {};    }");
        UnparserTest.helpTestScript("{ return ()             -> 3 + 7; }");

        // Annotated wildcards (JLS8, 4.5.1).
        UnparserTest.helpTestScript(
            "{ java.util.Map<?, ?> map = new java.util.HashMap<@WildcardAnnotation ? extends String, ? extends @TypeAnnotation Integer>(); }"
        );
    }

    @Test public void
    testParseUnparseJava9() throws Exception {

        // Method references.
        UnparserTest.helpTestScript("Runnable r = new Runnable() { @Override public void run() {} }; Runnable s = r::run;");
        UnparserTest.helpTestScript("Runnable r = new Runnable() { @Override public void run() {} }; Runnable s = (r)::run;");
        UnparserTest.helpTestScript("Runnable r = new Runnable() { @Override public void run() {} }; Runnable t = java.util.Collections::emptySet;");
        UnparserTest.helpTestScript("x.map(this.productConverter::convert);");
        // TODO: 'super' '::' [ TypeArguments ] Identifier
        // TODO: TypeName '.' 'super' '::' [ TypeArguments ] Identifier
        UnparserTest.helpTestScript("Runnable r4 = java.util.HashMap::new;");
        UnparserTest.helpTestScript("java.util.function.Consumer<Integer> c1 = int[]::new;");

        // Modular compilation unit.
        UnparserTest.helpTestCu("import java.io.*; @Deprecated open module a.b.c()");
        UnparserTest.helpTestCu("module a(requires transitive static a.b;)");
        UnparserTest.helpTestCu("module a( requires a; requires b; )");
        UnparserTest.helpTestCu("module a(exports a.b to c.d, e.f;)");
        UnparserTest.helpTestCu("module a(opens a.b to c.d, e.f;)");
        UnparserTest.helpTestCu("module a(uses java.lang.String;)");
        UnparserTest.helpTestCu("module a(provides java.lang.String with a.b, c.d;)");

        // Default methods.
        UnparserTest.helpTestCu(
            ""
            + "interface Foo {"
            +    " default <T> T convertJsonToObject(String jsonFile, Class<T> classType) throws ServiceException {} "
            + "}"
        );

        // Type annotations.
        UnparserTest.helpTestClassBody(
            ""
            + "private Map<@notblank String, @notblank String> extensions;\n"
            + "private List<@NotNull AssetRelationship>        sources = new ArrayList<>();\n"
        );
        UnparserTest.helpTestScript("@Foo int a;");
    }

    @Test public void
    testParseUnparseJava15() throws Exception {

        // Text blocks.
        UnparserTest.helpTestExpr(
            (                        // input
                ""
                + "   \"\"\" \r"
                + "Line 1\n"
                + "Line 2\r\n"
                + "Line3   \"\"\"  "
            ),
            (                        // expected
                ""
                + "\"\"\" \r"
                + "Line 1\n"
                + "Line 2\r\n"
                + "Line3   \"\"\""
            ),
            false
        );
    }

    @Test public void
    testParseUnparseParseJanino() throws Exception {

        for (File f : UnparserTest.findJaninoJavaFiles()) {

            try {

                // Parse the source file once.
                AbstractCompilationUnit acu1;
                {
                    InputStream is = new FileInputStream(f);
                    acu1 = new Parser(new Scanner(f.toString(), is)).parseAbstractCompilationUnit();
                    is.close();
                }

                // Unparse the compilation unit.
                String text;
                {
                    StringWriter sw = new StringWriter();
                    Unparser.unparse(acu1, sw);
                    text = sw.toString();
                }

                // Then parse again.
                AbstractCompilationUnit acu2  = new Parser(
                    new Scanner(f.toString(), new StringReader(text))
                ).parseAbstractCompilationUnit();

                // Compare the two ASTs.
                Java.Locatable[] elements1 = UnparserTest.listSyntaxElements(acu1);
                Java.Locatable[] elements2 = UnparserTest.listSyntaxElements(acu2);
                for (int i = 0;; ++i) {
                    if (i == elements1.length) {
                        if (i == elements2.length) break;
                        Assert.fail("Extra element " + elements2[i]);
                    }
                    Locatable locatable1 = elements1[i];

                    if (i == elements2.length) {
                        Assert.fail("Element missing: " + locatable1);
                    }
                    Locatable locatable2 = elements2[i];

                    String s1 = locatable1.toString();
                    String s2 = locatable2.toString();
                    if (!s1.equals(s2)) {
                        Assert.fail(
                            locatable1.getLocation().toString()
                            + ": Expected \""
                            + s1
                            + "\", was \""
                            + s2
                            + "\""
                        );
                    }
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static Collection<File>
    findJaninoJavaFiles() {
        final Collection<File> result = new ArrayList<>();

        // Process all "*.java" files in the JANINO source tree.
        // Must use the "janino" project directory, because that is pre-Java 5.
        UnparserTest.find(new File("../janino/src/main/java"), new FileFilter() {

            @Override public boolean
            accept(@Nullable File f) {
                assert f != null;

                if (f.isDirectory()) return true;

                if (f.getName().endsWith(".java") && f.isFile()) result.add(f);

                return false;
            }
        });

        return result;
    }

    /**
     * Traverses the given {@link AbstractCompilationUnit} and collect a list of all its syntactical elements.
     */
    private static Locatable[]
    listSyntaxElements(AbstractCompilationUnit cu) {

        final List<Locatable> locatables = new ArrayList<>();
        new AbstractTraverser<RuntimeException>() {

            // Two implementations of "Locatable": "Located" and "AbstractTypeDeclaration".
            @Override public void
            traverseLocated(Located l) {
                locatables.add(l);
                super.traverseLocated(l);
            }

            @Override public void
            traverseAbstractTypeDeclaration(AbstractTypeDeclaration atd) {
                locatables.add(atd);
                super.traverseAbstractTypeDeclaration(atd);
            }
        }.visitAbstractCompilationUnit(cu);

        return locatables.toArray(new Java.Locatable[locatables.size()]);
    }

    /**
     * Invokes <var>fileFilter</var> for all files and subdirectories in the given <var>directory</var>. If {@link
     * FileFilter#accept(File)} returns {@code true}, recurse with that file/directory.
     */
    private static void
    find(File directory, FileFilter fileFilter) {
        File[] subDirectories = directory.listFiles(fileFilter);
        if (subDirectories == null) throw new AssertionError(directory + " is not a directory");
        for (File subDirectorie : subDirectories) UnparserTest.find(subDirectorie, fileFilter);
    }
}