com.google.javascript.jscomp.parsing.NewIRFactory.java Source code

Java tutorial

Introduction

Here is the source code for com.google.javascript.jscomp.parsing.NewIRFactory.java

Source

/*
 * Copyright 2013 The Closure Compiler Authors.
 *
 * 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.google.javascript.jscomp.parsing;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.collect.UnmodifiableIterator;
import com.google.javascript.jscomp.parsing.Config.LanguageMode;
import com.google.javascript.jscomp.parsing.parser.IdentifierToken;
import com.google.javascript.jscomp.parsing.parser.LiteralToken;
import com.google.javascript.jscomp.parsing.parser.TokenType;
import com.google.javascript.jscomp.parsing.parser.trees.*;
import com.google.javascript.jscomp.parsing.parser.trees.Comment;
import com.google.javascript.jscomp.parsing.parser.util.SourcePosition;
import com.google.javascript.jscomp.parsing.parser.util.SourceRange;
import com.google.javascript.rhino.ErrorReporter;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import com.google.javascript.rhino.TokenStream;
import com.google.javascript.rhino.jstype.StaticSourceFile;

import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * IRFactory transforms the external AST to the internal AST.
 */
class NewIRFactory {

    static final String GETTER_ERROR_MESSAGE = "getters are not supported in older versions of JavaScript. "
            + "If you are targeting newer versions of JavaScript, " + "set the appropriate language_in option.";

    static final String SETTER_ERROR_MESSAGE = "setters are not supported in older versions of JavaScript. "
            + "If you are targeting newer versions of JavaScript, " + "set the appropriate language_in option.";

    static final String SUSPICIOUS_COMMENT_WARNING = "Non-JSDoc comment has annotations. "
            + "Did you mean to start it with '/**'?";

    static final String MISPLACED_TYPE_ANNOTATION = "Type annotations are not allowed here. Are you missing parentheses?";

    static final String INVALID_ES3_PROP_NAME = "Keywords and reserved words are not allowed as unquoted property "
            + "names in older versions of JavaScript. " + "If you are targeting newer versions of JavaScript, "
            + "set the appropriate language_in option.";

    static final String INVALID_ES5_STRICT_OCTAL = "Octal integer literals are not supported in Ecmascript 5 strict mode.";

    static final String INVALID_NUMBER_LITERAL = "Invalid number literal.";

    static final String STRING_CONTINUATION_WARNING = "String continuations are not supported in this language mode.";

    static final String BINARY_NUMBER_LITERAL_WARNING = "Binary integer literals are not supported in this language mode.";

    static final String OCTAL_NUMBER_LITERAL_WARNING = "Octal integer literals are not supported in this language mode.";

    static final String OCTAL_STRING_LITERAL_WARNING = "Octal literals in strings are not supported in this language mode.";

    static final String DUPLICATE_PARAMETER = "Duplicate parameter name \"%s\"";

    static final String UNLABELED_BREAK = "unlabelled break must be inside loop or switch";

    static final String UNEXPECTED_CONTINUE = "continue must be inside loop";

    static final String UNEXPECTED_LABLED_CONTINUE = "continue can only use labeles of iteration statements";

    static final String UNDEFINED_LABEL = "undefined label \"%s\"";

    private final String sourceString;
    private final List<Integer> newlines;
    private final StaticSourceFile sourceFile;
    private final String sourceName;
    private final Config config;
    private final ErrorReporter errorReporter;
    private final TransformDispatcher transformDispatcher;

    private static final ImmutableSet<String> ALLOWED_DIRECTIVES = ImmutableSet.of("use strict");

    private static final ImmutableSet<String> ES5_RESERVED_KEYWORDS = ImmutableSet.of(
            // From Section 7.6.1.2
            "class", "const", "enum", "export", "extends", "import", "super");
    private static final ImmutableSet<String> ES5_STRICT_RESERVED_KEYWORDS = ImmutableSet.of(
            // From Section 7.6.1.2
            "class", "const", "enum", "export", "extends", "import", "super", "implements", "interface", "let",
            "package", "private", "protected", "public", "static", "yield");

    private final Set<String> reservedKeywords;
    private final Set<Comment> parsedComments = Sets.newHashSet();

    // @license text gets appended onto the fileLevelJsDocBuilder as found,
    // and stored in JSDocInfo for placeholder node.
    Node rootNodeJsDocHolder = new Node(Token.SCRIPT);
    Node.FileLevelJsDocBuilder fileLevelJsDocBuilder = rootNodeJsDocHolder.getJsDocBuilderForNode();
    JSDocInfo fileOverviewInfo = null;

    // Use a template node for properties set on all nodes to minimize the
    // memory footprint associated with these.
    private final Node templateNode;

    private final UnmodifiableIterator<Comment> nextCommentIter;

    private Comment currentComment;

    private boolean currentFileIsExterns = false;

    private NewIRFactory(String sourceString, StaticSourceFile sourceFile, Config config,
            ErrorReporter errorReporter, ImmutableList<Comment> comments) {
        this.sourceString = sourceString;
        this.nextCommentIter = comments.iterator();
        this.currentComment = nextCommentIter.hasNext() ? nextCommentIter.next() : null;
        this.newlines = Lists.newArrayList();
        this.sourceFile = sourceFile;

        // Pre-generate all the newlines in the file.
        for (int charNo = 0; true; charNo++) {
            charNo = sourceString.indexOf('\n', charNo);
            if (charNo == -1) {
                break;
            }
            newlines.add(Integer.valueOf(charNo));
        }

        // Sometimes this will be null in tests.
        this.sourceName = sourceFile == null ? null : sourceFile.getName();

        this.config = config;
        this.errorReporter = errorReporter;
        this.transformDispatcher = new TransformDispatcher();
        // The template node properties are applied to all nodes in this transform.
        this.templateNode = createTemplateNode();

        switch (config.languageMode) {
        case ECMASCRIPT3:
            reservedKeywords = null; // use TokenStream.isKeyword instead
            break;
        case ECMASCRIPT5:
            reservedKeywords = ES5_RESERVED_KEYWORDS;
            break;
        case ECMASCRIPT5_STRICT:
            reservedKeywords = ES5_STRICT_RESERVED_KEYWORDS;
            break;
        case ECMASCRIPT6:
            reservedKeywords = ES5_RESERVED_KEYWORDS;
            break;
        case ECMASCRIPT6_STRICT:
            reservedKeywords = ES5_STRICT_RESERVED_KEYWORDS;
            break;
        default:
            throw new IllegalStateException("unknown language mode");
        }
    }

    // Create a template node to use as a source of common attributes, this allows
    // the prop structure to be shared among all the node from this source file.
    // This reduces the cost of these properties to O(nodes) to O(files).
    private Node createTemplateNode() {
        // The Node type choice is arbitrary.
        Node templateNode = new Node(Token.SCRIPT);
        templateNode.setStaticSourceFile(sourceFile);
        return templateNode;
    }

    public static Node transformTree(ProgramTree tree, StaticSourceFile sourceFile, String sourceString,
            Config config, ErrorReporter errorReporter) {
        NewIRFactory irFactory = new NewIRFactory(sourceString, sourceFile, config, errorReporter,
                tree.sourceComments);

        // don't call transform as we don't want standard jsdoc handling.
        Node n = irFactory.justTransform(tree);
        irFactory.setSourceInfo(n, tree);

        if (tree.sourceComments != null) {
            for (Comment comment : tree.sourceComments) {
                if (comment.type == Comment.Type.JSDOC && !irFactory.parsedComments.contains(comment)) {
                    irFactory.handlePossibleFileOverviewJsDoc(comment);
                } else if (comment.type == Comment.Type.BLOCK) {
                    irFactory.handleBlockComment(comment);
                }
            }
        }

        irFactory.setFileOverviewJsDoc(n);

        irFactory.validateAll(n);

        return n;
    }

    private void validateAll(Node n) {
        validate(n);
        for (Node c = n.getFirstChild(); c != null; c = c.getNext()) {
            validateAll(c);
        }
    }

    private void validate(Node n) {
        validateTypeAnnotations(n);
        validateParameters(n);
        validateBreakContinue(n);
    }

    private void validateBreakContinue(Node n) {
        if (n.isBreak() || n.isContinue()) {
            Node labelName = n.getFirstChild();
            if (labelName != null) {
                Node parent = n.getParent();
                while (!parent.isLabel() || !labelsMatch(parent, labelName)) {
                    if (parent.isFunction() || parent.isScript()) {
                        // report missing label
                        errorReporter.error(String.format(UNDEFINED_LABEL, labelName.getString()), sourceName,
                                n.getLineno(), n.getCharno());
                        break;
                    }
                    parent = parent.getParent();
                }
                if (parent.isLabel() && labelsMatch(parent, labelName)) {
                    if (n.isContinue() && !isContinueTarget(parent.getLastChild())) {
                        // report invalid continue target
                        errorReporter.error(UNEXPECTED_LABLED_CONTINUE, sourceName, n.getLineno(), n.getCharno());
                    }
                }
            } else {
                if (n.isContinue()) {
                    Node parent = n.getParent();
                    while (!isContinueTarget(parent)) {
                        if (parent.isFunction() || parent.isScript()) {
                            // report invalid continue
                            errorReporter.error(UNEXPECTED_CONTINUE, sourceName, n.getLineno(), n.getCharno());
                            break;
                        }
                        parent = parent.getParent();
                    }
                } else {
                    Node parent = n.getParent();
                    while (!isBreakTarget(parent)) {
                        if (parent.isFunction() || parent.isScript()) {
                            // report invalid break
                            errorReporter.error(UNLABELED_BREAK, sourceName, n.getLineno(), n.getCharno());
                            break;
                        }
                        parent = parent.getParent();
                    }
                }
            }
        }
    }

    private static boolean isBreakTarget(Node n) {
        switch (n.getType()) {
        case Token.FOR:
        case Token.WHILE:
        case Token.DO:
        case Token.SWITCH:
            return true;
        }
        return false;
    }

    private static boolean isContinueTarget(Node n) {
        switch (n.getType()) {
        case Token.FOR:
        case Token.WHILE:
        case Token.DO:
            return true;
        }
        return false;
    }

    private static boolean labelsMatch(Node label, Node labelName) {
        return label.getFirstChild().getString().equals(labelName.getString());
    }

    private void validateParameters(Node n) {
        if (n.isParamList()) {
            Node c = n.getFirstChild();
            for (; c != null; c = c.getNext()) {
                if (!c.isName()) {
                    continue;
                }
                Node sibling = c.getNext();
                for (; sibling != null; sibling = sibling.getNext()) {
                    if (sibling.isName() && c.getString().equals(sibling.getString())) {
                        errorReporter.warning(String.format(DUPLICATE_PARAMETER, c.getString()), sourceName,
                                n.getLineno(), n.getCharno());
                    }
                }
            }
        }
    }

    @SuppressWarnings("incomplete-switch")
    private void validateTypeAnnotations(Node n) {
        JSDocInfo info = n.getJSDocInfo();
        if (info != null && info.hasType()) {
            boolean valid = false;
            switch (n.getType()) {
            // Casts are valid
            case Token.CAST:
                valid = true;
                break;
            // Variable declarations are valid
            case Token.VAR:
            case Token.LET:
            case Token.CONST:
                valid = true;
                break;
            // Function declarations are valid
            case Token.FUNCTION:
                valid = isFunctionDeclaration(n);
                break;
            // Object literal properties, catch declarations and variable
            // initializers are valid.
            case Token.NAME:
                Node parent = n.getParent();
                switch (parent.getType()) {
                case Token.STRING_KEY:
                case Token.GETTER_DEF:
                case Token.SETTER_DEF:
                case Token.CATCH:
                case Token.FUNCTION:
                case Token.VAR:
                case Token.LET:
                case Token.CONST:
                case Token.PARAM_LIST:
                    valid = true;
                    break;
                }
                break;
            // Object literal properties are valid
            case Token.STRING_KEY:
            case Token.GETTER_DEF:
            case Token.SETTER_DEF:
                valid = true;
                break;
            // Property assignments are valid, if at the root of an expression.
            case Token.ASSIGN:
                valid = n.getParent().isExprResult()
                        && (n.getFirstChild().isGetProp() || n.getFirstChild().isGetElem());
                break;
            case Token.GETPROP:
                valid = n.getParent().isExprResult() && n.isQualifiedName();
                break;
            case Token.CALL:
                valid = info.isDefine();
                break;
            }

            if (!valid) {
                errorReporter.warning(MISPLACED_TYPE_ANNOTATION, sourceName, n.getLineno(), n.getCharno());
            }
        }
    }

    private void setFileOverviewJsDoc(Node irNode) {
        // Only after we've seen all @fileoverview entries, attach the
        // last one to the root node, and copy the found license strings
        // to that node.
        JSDocInfo rootNodeJsDoc = rootNodeJsDocHolder.getJSDocInfo();
        if (rootNodeJsDoc != null) {
            irNode.setJSDocInfo(rootNodeJsDoc);
            rootNodeJsDoc.setAssociatedNode(irNode);
        }

        if (fileOverviewInfo != null) {
            if ((irNode.getJSDocInfo() != null) && (irNode.getJSDocInfo().getLicense() != null)) {
                fileOverviewInfo.setLicense(irNode.getJSDocInfo().getLicense());
            }
            irNode.setJSDocInfo(fileOverviewInfo);
            fileOverviewInfo.setAssociatedNode(irNode);
        }
    }

    private Node transformBlock(ParseTree node) {
        Node irNode = transform(node);
        if (!irNode.isBlock()) {
            if (irNode.isEmpty()) {
                irNode.setType(Token.BLOCK);
            } else {
                Node newBlock = newNode(Token.BLOCK, irNode);
                setSourceInfo(newBlock, irNode);
                irNode = newBlock;
            }
            irNode.setIsAddedBlock(true);
        }
        return irNode;
    }

    /**
     * Check to see if the given block comment looks like it should be JSDoc.
     */
    private void handleBlockComment(Comment comment) {
        Pattern p = Pattern.compile("(/|(\n[ \t]*))\\*[ \t]*@[a-zA-Z]+[ \t\n{]");
        if (p.matcher(comment.value).find()) {
            errorReporter.warning(SUSPICIOUS_COMMENT_WARNING, sourceName, lineno(comment.location.start),
                    charno(comment.location.start));
        }
    }

    /**
     * @return true if the jsDocParser represents a fileoverview.
     */
    private boolean handlePossibleFileOverviewJsDoc(JsDocInfoParser jsDocParser) {
        if (jsDocParser.getFileOverviewJSDocInfo() != fileOverviewInfo) {
            fileOverviewInfo = jsDocParser.getFileOverviewJSDocInfo();
            if (fileOverviewInfo.isExterns()) {
                this.currentFileIsExterns = true;
            }
            return true;
        }
        return false;
    }

    private void handlePossibleFileOverviewJsDoc(Comment comment) {
        JsDocInfoParser jsDocParser = createJsDocInfoParser(comment);
        parsedComments.add(comment);
        handlePossibleFileOverviewJsDoc(jsDocParser);
    }

    private Comment getJsDoc(SourceRange location) {
        Comment closestPreviousComment = null;
        while (currentComment != null && currentComment.location.end.offset <= location.start.offset) {
            if (currentComment.type == Comment.Type.JSDOC) {
                closestPreviousComment = currentComment;
            }
            if (this.nextCommentIter.hasNext()) {
                currentComment = this.nextCommentIter.next();
            } else {
                currentComment = null;
            }
        }

        return closestPreviousComment;
    }

    private Comment getJsDoc(ParseTree tree) {
        return getJsDoc(tree.location);
    }

    private Comment getJsDoc(com.google.javascript.jscomp.parsing.parser.Token token) {
        return getJsDoc(token.location);
    }

    private JSDocInfo handleJsDoc(Comment comment) {
        if (comment != null) {
            JsDocInfoParser jsDocParser = createJsDocInfoParser(comment);
            parsedComments.add(comment);
            if (!handlePossibleFileOverviewJsDoc(jsDocParser)) {
                return jsDocParser.retrieveAndResetParsedJSDocInfo();
            }
        }
        return null;
    }

    private JSDocInfo handleJsDoc(ParseTree node) {
        if (!shouldAttachJSDocHere(node)) {
            return null;
        }
        return handleJsDoc(getJsDoc(node));
    }

    private boolean shouldAttachJSDocHere(ParseTree tree) {
        switch (tree.type) {
        case EXPRESSION_STATEMENT:
            return false;
        case LABELLED_STATEMENT:
            return false;
        case CALL_EXPRESSION:
        case CONDITIONAL_EXPRESSION:
        case BINARY_OPERATOR:
        case MEMBER_EXPRESSION:
        case MEMBER_LOOKUP_EXPRESSION:
        case POSTFIX_EXPRESSION:
            ParseTree nearest = findNearestNode(tree);
            if (nearest.type == ParseTreeType.PAREN_EXPRESSION) {
                return false;
            }
            return true;
        default:
            return true;
        }
    }

    private static ParseTree findNearestNode(ParseTree tree) {
        while (true) {
            switch (tree.type) {
            case EXPRESSION_STATEMENT:
                tree = tree.asExpressionStatement().expression;
                continue;
            case CALL_EXPRESSION:
                tree = tree.asCallExpression().operand;
                continue;
            case BINARY_OPERATOR:
                tree = tree.asBinaryOperator().left;
                continue;
            case CONDITIONAL_EXPRESSION:
                tree = tree.asConditionalExpression().condition;
                continue;
            case MEMBER_EXPRESSION:
                tree = tree.asMemberExpression().operand;
                continue;
            case MEMBER_LOOKUP_EXPRESSION:
                tree = tree.asMemberLookupExpression().operand;
                continue;
            case POSTFIX_EXPRESSION:
                tree = tree.asPostfixExpression().operand;
                continue;
            default:
                return tree;
            }
        }
    }

    private JSDocInfo handleJsDoc(com.google.javascript.jscomp.parsing.parser.Token token) {
        return handleJsDoc(getJsDoc(token));
    }

    private boolean isFunctionDeclaration(Node n) {
        return n.isFunction() && isStmtContainer(n.getParent());
    }

    private static boolean isStmtContainer(Node n) {
        return n.isBlock() || n.isScript();
    }

    private Node transform(ParseTree tree) {
        JSDocInfo info = handleJsDoc(tree);
        Node node = justTransform(tree);
        if (info != null) {
            node = maybeInjectCastNode(tree, info, node);
            attachJSDoc(info, node);
        }
        setSourceInfo(node, tree);
        return node;
    }

    private static void attachJSDoc(JSDocInfo info, Node n) {
        info.setAssociatedNode(n);
        n.setJSDocInfo(info);
    }

    private Node maybeInjectCastNode(ParseTree node, JSDocInfo info, Node irNode) {
        if (node.type == ParseTreeType.PAREN_EXPRESSION && info.hasType()) {
            irNode = newNode(Token.CAST, irNode);
        }
        return irNode;
    }

    /**
     * NAMEs in parameters or variable declarations are special, because they can
     * have inline type docs attached.
     *
     * function f(/** string &#42;/ x) {}
     * annotates 'x' as a string.
     *
     * @see <a href="http://code.google.com/p/jsdoc-toolkit/wiki/InlineDocs">
     *   Using Inline Doc Comments</a>
     */
    private Node transformNodeWithInlineJsDoc(ParseTree node, boolean optionalInline) {
        JSDocInfo info = handleInlineJsDoc(node, optionalInline);
        Node irNode = justTransform(node);
        if (info != null) {
            irNode.setJSDocInfo(info);
        }
        setSourceInfo(irNode, node);
        return irNode;
    }

    private JSDocInfo handleInlineJsDoc(ParseTree node, boolean optional) {
        return handleInlineJsDoc(node.location, optional);
    }

    private JSDocInfo handleInlineJsDoc(com.google.javascript.jscomp.parsing.parser.Token token, boolean optional) {
        return handleInlineJsDoc(token.location, optional);
    }

    private JSDocInfo handleInlineJsDoc(SourceRange location, boolean optional) {
        Comment comment = getJsDoc(location);
        if (comment != null && (!optional || !comment.value.contains("@"))) {
            return parseInlineTypeDoc(comment);
        } else {
            return handleJsDoc(comment);
        }
    }

    private Node transformNumberAsString(LiteralToken token) {
        double value = normalizeNumber(token);
        Node irNode = newStringNode(getStringValue(value));
        JSDocInfo jsDocInfo = handleJsDoc(token);
        if (jsDocInfo != null) {
            irNode.setJSDocInfo(jsDocInfo);
        }
        setSourceInfo(irNode, token);
        return irNode;
    }

    private static String getStringValue(double value) {
        long longValue = (long) value;

        // Return "1" instead of "1.0"
        if (longValue == value) {
            return Long.toString(longValue);
        } else {
            return Double.toString(value);
        }
    }

    private static int lineno(ParseTree node) {
        // location lines start at zero, our AST starts at 1.
        return lineno(node.location.start);
    }

    private static int charno(ParseTree node) {
        return charno(node.location.start);
    }

    private static int lineno(SourcePosition location) {
        // location lines start at zero, our AST starts at 1.
        return location.line + 1;
    }

    private static int charno(SourcePosition location) {
        return location.column;
    }

    private void setSourceInfo(Node node, Node ref) {
        node.setLineno(ref.getLineno());
        node.setCharno(ref.getCharno());
        maybeSetLengthFrom(node, ref);
    }

    private void setSourceInfo(Node irNode, ParseTree node) {
        if (irNode.getLineno() == -1) {
            setSourceInfo(irNode, node.location.start, node.location.end);
        }
    }

    private void setSourceInfo(Node irNode, com.google.javascript.jscomp.parsing.parser.Token token) {
        setSourceInfo(irNode, token.location.start, token.location.end);
    }

    private void setSourceInfo(Node node, SourcePosition start, SourcePosition end) {
        if (node.getLineno() == -1) {
            // If we didn't already set the line, then set it now. This avoids
            // cases like ParenthesizedExpression where we just return a previous
            // node, but don't want the new node to get its parent's line number.
            int lineno = lineno(start);
            node.setLineno(lineno);
            int charno = charno(start);
            node.setCharno(charno);
            maybeSetLength(node, start, end);
        }
    }

    /**
     * Creates a JsDocInfoParser and parses the JsDoc string.
     *
     * Used both for handling individual JSDoc comments and for handling
     * file-level JSDoc comments (@fileoverview and @license).
     *
     * @param node The JsDoc Comment node to parse.
     * @return A JsDocInfoParser. Will contain either fileoverview JsDoc, or
     *     normal JsDoc, or no JsDoc (if the method parses to the wrong level).
     */
    private JsDocInfoParser createJsDocInfoParser(Comment node) {
        String comment = node.value;
        int lineno = lineno(node.location.start);
        int charno = charno(node.location.start);
        int position = node.location.start.offset;

        // The JsDocInfoParser expects the comment without the initial '/**'.
        int numOpeningChars = 3;
        JsDocInfoParser jsdocParser = new JsDocInfoParser(
                new JsDocTokenStream(comment.substring(numOpeningChars), lineno, charno + numOpeningChars), comment,
                position, null, sourceFile, config, errorReporter);
        jsdocParser.setFileLevelJsDocBuilder(fileLevelJsDocBuilder);
        jsdocParser.setFileOverviewJSDocInfo(fileOverviewInfo);
        jsdocParser.parse();
        return jsdocParser;
    }

    /**
     * Parses inline type info.
     */
    private JSDocInfo parseInlineTypeDoc(Comment node) {
        String comment = node.value;
        int lineno = lineno(node.location.start);
        int charno = charno(node.location.start);

        // The JsDocInfoParser expects the comment without the initial '/**'.
        int numOpeningChars = 3;
        JsDocInfoParser parser = new JsDocInfoParser(
                new JsDocTokenStream(comment.substring(numOpeningChars), lineno, charno + numOpeningChars), comment,
                node.location.start.offset, null, sourceFile, config, errorReporter);
        return parser.parseInlineTypeDoc();
    }

    // Set the length on the node if we're in IDE mode.
    private void maybeSetLength(Node node, SourcePosition start, SourcePosition end) {
        if (config.isIdeMode) {
            node.setLength(end.offset - start.offset);
        }
    }

    private void maybeSetLengthFrom(Node node, Node ref) {
        if (config.isIdeMode) {
            node.setLength(ref.getLength());
        }
    }

    private void maybeSetLength(Node node, int length) {
        if (config.isIdeMode) {
            node.setLength(length);
        }
    }

    private Node justTransform(ParseTree node) {
        return transformDispatcher.process(node);
    }

    private class TransformDispatcher extends NewTypeSafeDispatcher<Node> {
        /**
         * Transforms the given node and then sets its type to Token.STRING if it
         * was Token.NAME. If its type was already Token.STRING, then quotes it.
         * Used for properties, as the old AST uses String tokens, while the new one
         * uses Name tokens for unquoted strings. For example, in
         * var o = {'a' : 1, b: 2};
         * the string 'a' is quoted, while the name b is turned into a string, but
         * unquoted.
         */
        private Node processObjectLitKeyAsString(com.google.javascript.jscomp.parsing.parser.Token token) {
            Node ret;
            if (token == null) {
                return createMissingExpressionNode();
            } else if (token.type == TokenType.IDENTIFIER) {
                ret = processName(token.asIdentifier(), true);
            } else if (token.type == TokenType.NUMBER) {
                ret = transformNumberAsString(token.asLiteral());
                ret.putBooleanProp(Node.QUOTED_PROP, true);
            } else {
                ret = processString(token.asLiteral());
                ret.putBooleanProp(Node.QUOTED_PROP, true);
            }
            Preconditions.checkState(ret.isString());
            return ret;
        }

        @Override
        Node processComprehension(ComprehensionTree tree) {
            maybeWarnEs6Feature(tree, "array/generator comprehensions");

            int type;
            switch (tree.type) {
            case ARRAY:
                type = Token.ARRAY_COMP;
                break;
            case GENERATOR:
                type = Token.GENERATOR_COMP;
                break;
            default:
                throw new IllegalStateException("unreachable");
            }
            Node node = newNode(type);
            for (ParseTree child : tree.children) {
                node.addChildToBack(transform(child));
            }
            node.addChildToBack(transform(tree.tailExpression));
            return node;
        }

        @Override
        Node processComprehensionFor(ComprehensionForTree tree) {
            return newNode(Token.FOR_OF, transform(tree.initializer), transform(tree.collection));
        }

        @Override
        Node processComprehensionIf(ComprehensionIfTree tree) {
            return newNode(Token.IF, transform(tree.expression));
        }

        @Override
        Node processArrayLiteral(ArrayLiteralExpressionTree tree) {
            Node node = newNode(Token.ARRAYLIT);
            for (ParseTree child : tree.elements) {
                Node c = transform(child);
                node.addChildToBack(c);
            }
            return node;
        }

        @Override
        Node processArrayPattern(ArrayPatternTree tree) {
            maybeWarnEs6Feature(tree, "destructuring");

            Node node = newNode(Token.ARRAY_PATTERN);
            for (ParseTree child : tree.elements) {
                node.addChildToBack(transform(child));
            }
            return node;
        }

        @Override
        Node processObjectPattern(ObjectPatternTree tree) {
            maybeWarnEs6Feature(tree, "destructuring");

            Node node = newNode(Token.OBJECT_PATTERN);
            for (ParseTree child : tree.fields) {
                node.addChildToBack(transform(child));
            }
            return node;
        }

        @Override
        Node processObjectPatternField(ObjectPatternFieldTree tree) {
            Node node = processObjectLitKeyAsString(tree.identifier);
            node.setType(Token.STRING_KEY);
            if (tree.element != null) {
                node.addChildToBack(transform(tree.element));
            }
            return node;
        }

        @Override
        Node processAssignmentRestElement(AssignmentRestElementTree tree) {
            return newStringNode(Token.REST, tree.identifier.value);
        }

        @Override
        Node processAstRoot(ProgramTree rootNode) {
            Node node = newNode(Token.SCRIPT);
            for (ParseTree child : rootNode.sourceElements) {
                node.addChildToBack(transform(child));
            }
            parseDirectives(node);
            return node;
        }

        /**
         * Parse the directives, encode them in the AST, and remove their nodes.
         *
         * For information on ES5 directives, see section 14.1 of
         * ECMA-262, Edition 5.
         *
         * It would be nice if Rhino would eventually take care of this for
         * us, but right now their directive-processing is a one-off.
         */
        private void parseDirectives(Node node) {
            // Remove all the directives, and encode them in the AST.
            Set<String> directives = null;
            while (isDirective(node.getFirstChild())) {
                String directive = node.removeFirstChild().getFirstChild().getString();
                if (directives == null) {
                    directives = Sets.newHashSet(directive);
                } else {
                    directives.add(directive);
                }
            }

            if (directives != null) {
                node.setDirectives(directives);
            }
        }

        private boolean isDirective(Node n) {
            if (n == null) {
                return false;
            }
            int nType = n.getType();
            return nType == Token.EXPR_RESULT && n.getFirstChild().isString()
                    && ALLOWED_DIRECTIVES.contains(n.getFirstChild().getString());
        }

        @Override
        Node processBlock(BlockTree blockNode) {
            Node node = newNode(Token.BLOCK);
            for (ParseTree child : blockNode.statements) {
                node.addChildToBack(transform(child));
            }
            return node;
        }

        @Override
        Node processBreakStatement(BreakStatementTree statementNode) {
            Node node = newNode(Token.BREAK);
            if (statementNode.getLabel() != null) {
                Node labelName = transformLabelName(statementNode.name);
                node.addChildToBack(labelName);
            }
            return node;
        }

        Node transformLabelName(IdentifierToken token) {
            Node label = newStringNode(Token.LABEL_NAME, token.value);
            setSourceInfo(label, token);
            return label;
        }

        @Override
        Node processConditionalExpression(ConditionalExpressionTree exprNode) {
            return newNode(Token.HOOK, transform(exprNode.condition), transform(exprNode.left),
                    transform(exprNode.right));
        }

        @Override
        Node processContinueStatement(ContinueStatementTree statementNode) {
            Node node = newNode(Token.CONTINUE);
            if (statementNode.getLabel() != null) {
                Node labelName = transformLabelName(statementNode.name);
                node.addChildToBack(labelName);
            }
            return node;
        }

        @Override
        Node processDoLoop(DoWhileStatementTree loopNode) {
            return newNode(Token.DO, transformBlock(loopNode.body), transform(loopNode.condition));
        }

        @Override
        Node processElementGet(MemberLookupExpressionTree getNode) {
            return newNode(Token.GETELEM, transform(getNode.operand), transform(getNode.memberExpression));
        }

        @Override
        Node processEmptyStatement(EmptyStatementTree exprNode) {
            return newNode(Token.EMPTY);
        }

        @Override
        Node processExpressionStatement(ExpressionStatementTree statementNode) {
            Node node = newNode(Token.EXPR_RESULT);
            node.addChildToBack(transform(statementNode.expression));
            return node;
        }

        @Override
        Node processForInLoop(ForInStatementTree loopNode) {
            return newNode(Token.FOR, transform(loopNode.initializer), transform(loopNode.collection),
                    transformBlock(loopNode.body));
        }

        @Override
        Node processForOf(ForOfStatementTree loopNode) {
            return newNode(Token.FOR_OF, transform(loopNode.initializer), transform(loopNode.collection),
                    transformBlock(loopNode.body));
        }

        @Override
        Node processForLoop(ForStatementTree loopNode) {
            Node node = newNode(Token.FOR, transformOrEmpty(loopNode.initializer, loopNode),
                    transformOrEmpty(loopNode.condition, loopNode), transformOrEmpty(loopNode.increment, loopNode));
            node.addChildToBack(transformBlock(loopNode.body));
            return node;
        }

        Node transformOrEmpty(ParseTree tree, ParseTree parent) {
            if (tree == null) {
                Node n = newNode(Token.EMPTY);
                setSourceInfo(n, parent);
                return n;
            }
            return transform(tree);
        }

        Node transformOrEmpty(IdentifierToken token, ParseTree parent) {
            if (token == null) {
                Node n = newNode(Token.EMPTY);
                setSourceInfo(n, parent);
                return n;
            }
            return processName(token);
        }

        @Override
        Node processFunctionCall(CallExpressionTree callNode) {
            Node node = newNode(Token.CALL, transform(callNode.operand));
            for (ParseTree child : callNode.arguments.arguments) {
                node.addChildToBack(transform(child));
            }
            return node;
        }

        @Override
        Node processFunction(FunctionDeclarationTree functionTree) {
            boolean isDeclaration = (functionTree.kind == FunctionDeclarationTree.Kind.DECLARATION);
            boolean isMember = (functionTree.kind == FunctionDeclarationTree.Kind.MEMBER);
            boolean isArrow = (functionTree.kind == FunctionDeclarationTree.Kind.ARROW);
            boolean isGenerator = functionTree.isGenerator;

            if (!isEs6Mode()) {
                if (isGenerator) {
                    maybeWarnEs6Feature(functionTree, "generators");
                }

                if (isMember) {
                    maybeWarnEs6Feature(functionTree, "member declarations");
                }

                if (isArrow) {
                    maybeWarnEs6Feature(functionTree, "short function syntax");
                }
            }

            IdentifierToken name = functionTree.name;
            Node newName;
            if (name != null) {
                newName = processNameWithInlineJSDoc(name);
            } else {
                if (isDeclaration || isMember) {
                    errorReporter.error("unnamed function statement", sourceName, lineno(functionTree),
                            charno(functionTree));

                    // Return the bare minimum to put the AST in a valid state.
                    newName = createMissingNameNode();
                } else {
                    newName = newStringNode(Token.NAME, "");
                }

                // Old Rhino tagged the empty name node with the line number of the
                // declaration.
                newName.setLineno(lineno(functionTree));
                newName.setCharno(charno(functionTree));
                maybeSetLength(newName, 0);
            }

            Node node = newNode(Token.FUNCTION);
            if (!isMember) {
                node.addChildToBack(newName);
            } else {
                Node emptyName = newStringNode(Token.NAME, "");
                emptyName.setLineno(lineno(functionTree));
                emptyName.setCharno(charno(functionTree));
                maybeSetLength(emptyName, 0);
                node.addChildToBack(emptyName);
            }
            node.addChildToBack(transform(functionTree.formalParameterList));

            Node bodyNode = transform(functionTree.functionBody);
            if (!isArrow && !bodyNode.isBlock()) {
                // When in ideMode the parser tries to parse some constructs the
                // compiler doesn't support, repair it here.
                Preconditions.checkState(config.isIdeMode);
                bodyNode = IR.block();
            }
            parseDirectives(bodyNode);
            node.addChildToBack(bodyNode);

            Node result;

            if (functionTree.kind == FunctionDeclarationTree.Kind.MEMBER) {
                setSourceInfo(node, functionTree);
                Node member = newStringNode(Token.MEMBER_DEF, name.value);
                member.addChildToBack(node);
                member.setStaticMember(functionTree.isStatic);
                result = member;
            } else {
                result = node;
            }

            result.setIsGeneratorFunction(isGenerator);
            result.setIsArrowFunction(isArrow);

            return result;
        }

        @Override
        Node processFormalParameterList(FormalParameterListTree tree) {
            Node params = newNode(Token.PARAM_LIST);
            for (ParseTree param : tree.parameters) {
                Node paramNode = transformNodeWithInlineJsDoc(param, false);
                // Children must be simple names, default parameters, rest
                // parameters, or destructuring patterns.
                Preconditions.checkState(paramNode.isName() || paramNode.isRest() || paramNode.isArrayPattern()
                        || paramNode.isObjectPattern() || paramNode.isDefaultValue());
                params.addChildToBack(paramNode);
            }
            return params;
        }

        @Override
        Node processDefaultParameter(DefaultParameterTree tree) {
            maybeWarnEs6Feature(tree, "default parameters");
            return newNode(Token.DEFAULT_VALUE, transform(tree.lhs), transform(tree.defaultValue));
        }

        @Override
        Node processRestParameter(RestParameterTree tree) {
            maybeWarnEs6Feature(tree, "rest parameters");

            return newStringNode(Token.REST, tree.identifier.value);
        }

        @Override
        Node processSpreadExpression(SpreadExpressionTree tree) {
            maybeWarnEs6Feature(tree, "spread expression");

            return newNode(Token.SPREAD, transform(tree.expression));
        }

        @Override
        Node processIfStatement(IfStatementTree statementNode) {
            Node node = newNode(Token.IF);
            node.addChildToBack(transform(statementNode.condition));
            node.addChildToBack(transformBlock(statementNode.ifClause));
            if (statementNode.elseClause != null) {
                node.addChildToBack(transformBlock(statementNode.elseClause));
            }
            return node;
        }

        @Override
        Node processBinaryExpression(BinaryOperatorTree exprNode) {
            Node n = newNode(transformBinaryTokenType(exprNode.operator.type), transform(exprNode.left),
                    transform(exprNode.right));

            if (isAssignmentOp(n)) {
                Node target = n.getFirstChild();
                if (!validAssignmentTarget(target)) {
                    errorReporter.error("invalid assignment target: " + target, sourceName, target.getLineno(), 0);
                }
            }

            return n;
        }

        // Move this to a more maintainable location.
        boolean isAssignmentOp(Node n) {
            switch (n.getType()) {
            case Token.ASSIGN:
            case Token.ASSIGN_BITOR:
            case Token.ASSIGN_BITXOR:
            case Token.ASSIGN_BITAND:
            case Token.ASSIGN_LSH:
            case Token.ASSIGN_RSH:
            case Token.ASSIGN_URSH:
            case Token.ASSIGN_ADD:
            case Token.ASSIGN_SUB:
            case Token.ASSIGN_MUL:
            case Token.ASSIGN_DIV:
            case Token.ASSIGN_MOD:
                return true;
            }
            return false;
        }

        @Override
        Node processDebuggerStatement(DebuggerStatementTree node) {
            return newNode(Token.DEBUGGER);
        }

        @Override
        Node processThisExpression(ThisExpressionTree node) {
            return newNode(Token.THIS);
        }

        @Override
        Node processLabeledStatement(LabelledStatementTree labelTree) {
            return newNode(Token.LABEL, transformLabelName(labelTree.name), transform(labelTree.statement));
        }

        @Override
        Node processName(IdentifierExpressionTree nameNode) {
            return processName(nameNode, false);
        }

        Node processName(IdentifierExpressionTree nameNode, boolean asString) {
            return processName(nameNode.identifierToken, asString);
        }

        Node processName(IdentifierToken identifierToken) {
            return processName(identifierToken, false);
        }

        Node processName(IdentifierToken identifierToken, boolean asString) {
            Node node;
            if (asString) {
                node = newStringNode(Token.STRING, identifierToken.value);
            } else {
                JSDocInfo info = handleJsDoc(identifierToken);
                if (isReservedKeyword(identifierToken.toString())) {
                    errorReporter.error("identifier is a reserved word", sourceName,
                            lineno(identifierToken.location.start), charno(identifierToken.location.start));
                }
                node = newStringNode(Token.NAME, identifierToken.value);
                if (info != null) {
                    attachJSDoc(info, node);
                }
            }
            setSourceInfo(node, identifierToken);
            return node;
        }

        Node processString(LiteralToken token) {
            Preconditions.checkArgument(token.type == TokenType.STRING);
            Node node = newStringNode(Token.STRING, normalizeString(token));
            setSourceInfo(node, token);
            return node;
        }

        Node processTemplateString(LiteralToken token) {
            Preconditions.checkArgument(
                    token.type == TokenType.NO_SUBSTITUTION_TEMPLATE || token.type == TokenType.TEMPLATE_HEAD
                            || token.type == TokenType.TEMPLATE_MIDDLE || token.type == TokenType.TEMPLATE_TAIL);
            // <CR><LF> and <CR> are normalized as <LF> for raw string value
            Node node = newStringNode(token.value.replaceAll("\\r(\\n)?", "\n"));
            setSourceInfo(node, token);
            return node;
        }

        Node processNameWithInlineJSDoc(IdentifierToken identifierToken) {
            JSDocInfo info = handleInlineJsDoc(identifierToken, true);
            if (isReservedKeyword(identifierToken.toString())) {
                errorReporter.error("identifier is a reserved word", sourceName,
                        lineno(identifierToken.location.start), charno(identifierToken.location.start));
            }
            Node node = newStringNode(Token.NAME, identifierToken.toString());
            if (info != null) {
                attachJSDoc(info, node);
            }
            setSourceInfo(node, identifierToken);
            return node;
        }

        private boolean isAllowedProp(String identifier) {
            if (config.languageMode == LanguageMode.ECMASCRIPT3) {
                return !TokenStream.isKeyword(identifier);
            }
            return true;
        }

        private boolean isReservedKeyword(String identifier) {
            if (config.languageMode == LanguageMode.ECMASCRIPT3) {
                return TokenStream.isKeyword(identifier);
            }
            return reservedKeywords != null && reservedKeywords.contains(identifier);
        }

        @Override
        Node processNewExpression(NewExpressionTree exprNode) {
            Node node = newNode(Token.NEW, transform(exprNode.operand));
            if (exprNode.arguments != null) {
                for (ParseTree arg : exprNode.arguments.arguments) {
                    node.addChildToBack(transform(arg));
                }
            }
            return node;
        }

        @Override
        Node processNumberLiteral(LiteralExpressionTree literalNode) {
            double value = normalizeNumber(literalNode.literalToken.asLiteral());
            return newNumberNode(value);
        }

        @Override
        Node processObjectLiteral(ObjectLiteralExpressionTree objTree) {
            Node node = newNode(Token.OBJECTLIT);
            boolean maybeWarn = false;
            for (ParseTree el : objTree.propertyNameAndValues) {
                if (config.languageMode == LanguageMode.ECMASCRIPT3) {
                    if (el.type == ParseTreeType.GET_ACCESSOR) {
                        reportGetter(el);
                        continue;
                    } else if (el.type == ParseTreeType.SET_ACCESSOR) {
                        reportSetter(el);
                        continue;
                    }
                }

                Node key = transform(el);
                if (!key.isComputedProp() && !key.isQuotedString() && !currentFileIsExterns
                        && !isAllowedProp(key.getString())) {
                    errorReporter.warning(INVALID_ES3_PROP_NAME, sourceName, key.getLineno(), key.getCharno());
                }
                if (key.getFirstChild() == null) {
                    maybeWarn = true;
                }

                node.addChildToBack(key);
            }
            if (maybeWarn) {
                maybeWarnEs6Feature(objTree, "extended object literals");
            }
            return node;
        }

        @Override
        Node processComputedProperty(ComputedPropertyAssignmentTree tree) {
            maybeWarnEs6Feature(tree, "computed property");

            return newNode(Token.COMPUTED_PROP, transform(tree.property), transform(tree.value));
        }

        @Override
        Node processGetAccessor(GetAccessorTree tree) {
            Node key = processObjectLitKeyAsString(tree.propertyName);
            key.setType(Token.GETTER_DEF);
            Node body = transform(tree.body);
            Node dummyName = IR.name("");
            setSourceInfo(dummyName, tree.body);
            Node paramList = IR.paramList();
            setSourceInfo(paramList, tree.body);
            Node value = IR.function(dummyName, paramList, body);
            setSourceInfo(value, tree.body);
            key.addChildToFront(value);
            key.setStaticMember(tree.isStatic);
            return key;
        }

        @Override
        Node processSetAccessor(SetAccessorTree tree) {
            Node key = processObjectLitKeyAsString(tree.propertyName);
            key.setType(Token.SETTER_DEF);
            Node body = transform(tree.body);
            Node dummyName = IR.name("");
            setSourceInfo(dummyName, tree.propertyName);
            Node paramList = IR.paramList(safeProcessName(tree.parameter));
            setSourceInfo(paramList, tree.parameter);
            Node value = IR.function(dummyName, paramList, body);
            setSourceInfo(value, tree.body);
            key.addChildToFront(value);
            key.setStaticMember(tree.isStatic);
            return key;
        }

        @Override
        Node processPropertyNameAssignment(PropertyNameAssignmentTree tree) {
            Node key = processObjectLitKeyAsString(tree.name);
            key.setType(Token.STRING_KEY);
            if (tree.value != null) {
                key.addChildToFront(transform(tree.value));
            }
            return key;
        }

        private Node safeProcessName(IdentifierToken identifierToken) {
            if (identifierToken == null) {
                return createMissingExpressionNode();
            } else {
                return processName(identifierToken);
            }
        }

        @Override
        Node processParenthesizedExpression(ParenExpressionTree exprNode) {
            return transform(exprNode.expression);
        }

        @Override
        Node processPropertyGet(MemberExpressionTree getNode) {
            Node leftChild = transform(getNode.operand);
            IdentifierToken nodeProp = getNode.memberName;
            Node rightChild = processObjectLitKeyAsString(nodeProp);
            if (!rightChild.isQuotedString() && !currentFileIsExterns && !isAllowedProp(rightChild.getString())) {
                errorReporter.warning(INVALID_ES3_PROP_NAME, sourceName, rightChild.getLineno(),
                        rightChild.getCharno());
            }
            return newNode(Token.GETPROP, leftChild, rightChild);
        }

        @Override
        Node processRegExpLiteral(LiteralExpressionTree literalTree) {
            LiteralToken token = literalTree.literalToken.asLiteral();
            Node literalStringNode = newStringNode(normalizeRegex(token));
            // TODO(johnlenz): fix the source location.
            setSourceInfo(literalStringNode, token);
            Node node = newNode(Token.REGEXP, literalStringNode);

            String rawRegex = token.value;
            int lastSlash = rawRegex.lastIndexOf('/');
            String flags = "";
            if (lastSlash < rawRegex.length()) {
                flags = rawRegex.substring(lastSlash + 1);
            }
            validateRegExpFlags(literalTree, flags);

            if (!flags.isEmpty()) {
                Node flagsNode = newStringNode(flags);
                // TODO(johnlenz): fix the source location.
                setSourceInfo(flagsNode, token);
                node.addChildToBack(flagsNode);
            }
            return node;
        }

        private void validateRegExpFlags(LiteralExpressionTree tree, String flags) {
            for (char flag : Lists.charactersOf(flags)) {
                switch (flag) {
                case 'g':
                case 'i':
                case 'm':
                    break;
                case 'u':
                case 'y':
                    maybeWarnEs6Feature(tree, "new RegExp flag '" + flag + "'");
                    break;
                default:
                    errorReporter.error("Invalid RegExp flag '" + flag + "'", sourceName, lineno(tree),
                            charno(tree));
                }
            }
        }

        @Override
        Node processReturnStatement(ReturnStatementTree statementNode) {
            Node node = newNode(Token.RETURN);
            if (statementNode.expression != null) {
                node.addChildToBack(transform(statementNode.expression));
            }
            return node;
        }

        @Override
        Node processStringLiteral(LiteralExpressionTree literalTree) {
            LiteralToken token = literalTree.literalToken.asLiteral();

            Node n = processString(token);
            String value = n.getString();
            if (value.indexOf('\u000B') != -1) {
                // NOTE(nicksantos): In JavaScript, there are 3 ways to
                // represent a vertical tab: \v, \x0B, \u000B.
                // The \v notation was added later, and is not understood
                // on IE. So we need to preserve it as-is. This is really
                // obnoxious, because we do not have a good way to represent
                // how the original string was encoded without making the
                // representation of strings much more complicated.
                //
                // To handle this, we look at the original source test, and
                // mark the string as \v-encoded or not. If a string is
                // \v encoded, then all the vertical tabs in that string
                // will be encoded with a \v.
                int start = token.location.start.offset;
                int end = token.location.end.offset;
                if (start < sourceString.length()
                        && (sourceString.substring(start, Math.min(sourceString.length(), end)).contains("\\v"))) {
                    n.putBooleanProp(Node.SLASH_V, true);
                }
            }
            return n;
        }

        @Override
        Node processTemplateLiteral(TemplateLiteralExpressionTree tree) {
            maybeWarnEs6Feature(tree, "template strings");
            Node node = tree.operand == null ? newNode(Token.TEMPLATELIT)
                    : newNode(Token.TEMPLATELIT, transform(tree.operand));
            for (ParseTree child : tree.elements) {
                node.addChildToBack(transform(child));
            }
            return node;
        }

        @Override
        Node processTemplateLiteralPortion(TemplateLiteralPortionTree tree) {
            return processTemplateString(tree.value.asLiteral());
        }

        @Override
        Node processTemplateSubstitution(TemplateSubstitutionTree tree) {
            return newNode(Token.TEMPLATELIT_SUB, transform(tree.expression));
        }

        @Override
        Node processSwitchCase(CaseClauseTree caseNode) {
            ParseTree expr = caseNode.expression;
            Node node = newNode(Token.CASE, transform(expr));
            Node block = newNode(Token.BLOCK);
            block.putBooleanProp(Node.SYNTHETIC_BLOCK_PROP, true);
            setSourceInfo(block, caseNode);
            if (caseNode.statements != null) {
                for (ParseTree child : caseNode.statements) {
                    block.addChildToBack(transform(child));
                }
            }
            node.addChildToBack(block);
            return node;
        }

        @Override
        Node processSwitchDefault(DefaultClauseTree caseNode) {
            Node node = newNode(Token.DEFAULT_CASE);
            Node block = newNode(Token.BLOCK);
            block.putBooleanProp(Node.SYNTHETIC_BLOCK_PROP, true);
            setSourceInfo(block, caseNode);
            if (caseNode.statements != null) {
                for (ParseTree child : caseNode.statements) {
                    block.addChildToBack(transform(child));
                }
            }
            node.addChildToBack(block);
            return node;
        }

        @Override
        Node processSwitchStatement(SwitchStatementTree statementNode) {
            Node node = newNode(Token.SWITCH, transform(statementNode.expression));
            for (ParseTree child : statementNode.caseClauses) {
                node.addChildToBack(transform(child));
            }
            return node;
        }

        @Override
        Node processThrowStatement(ThrowStatementTree statementNode) {
            return newNode(Token.THROW, transform(statementNode.value));
        }

        @Override
        Node processTryStatement(TryStatementTree statementNode) {
            Node node = newNode(Token.TRY, transformBlock(statementNode.body));
            Node block = newNode(Token.BLOCK);
            boolean lineSet = false;

            ParseTree cc = statementNode.catchBlock;
            if (cc != null) {
                // Mark the enclosing block at the same line as the first catch
                // clause.
                setSourceInfo(block, cc);
                lineSet = true;
                block.addChildToBack(transform(cc));
            }

            node.addChildToBack(block);

            ParseTree finallyBlock = statementNode.finallyBlock;
            if (finallyBlock != null) {
                node.addChildToBack(transformBlock(finallyBlock));
            }

            // If we didn't set the line on the catch clause, then
            // we've got an empty catch clause.  Set its line to be the same
            // as the finally block (to match Old Rhino's behavior.)
            if (!lineSet && (finallyBlock != null)) {
                setSourceInfo(block, finallyBlock);
            }

            return node;
        }

        @Override
        Node processCatchClause(CatchTree clauseNode) {
            IdentifierToken catchVar = clauseNode.exceptionName;
            Node node = newNode(Token.CATCH, processName(catchVar));
            node.addChildToBack(transformBlock(clauseNode.catchBody));
            return node;
        }

        @Override
        Node processFinally(FinallyTree finallyNode) {
            return transformBlock(finallyNode.block);
        }

        @Override
        Node processUnaryExpression(UnaryExpressionTree exprNode) {
            int type = transformUniaryTokenType(exprNode.operator.type);
            Node operand = transform(exprNode.operand);
            if (type == Token.NEG && operand.isNumber()) {
                operand.setDouble(-operand.getDouble());
                return operand;
            } else {
                if (type == Token.DELPROP && !(operand.isGetProp() || operand.isGetElem() || operand.isName())) {
                    String msg = "Invalid delete operand. Only properties can be deleted.";
                    errorReporter.error(msg, sourceName, operand.getLineno(), 0);
                } else if (type == Token.INC || type == Token.DEC) {
                    if (!validAssignmentTarget(operand)) {
                        String msg = (type == Token.INC) ? "invalid increment target" : "invalid decrement target";
                        errorReporter.error(msg, sourceName, operand.getLineno(), 0);
                    }
                }

                return newNode(type, operand);
            }
        }

        @Override
        Node processPostfixExpression(PostfixExpressionTree exprNode) {
            int type = transformPostfixTokenType(exprNode.operator.type);
            Node operand = transform(exprNode.operand);
            // Only INC and DEC
            if (!validAssignmentTarget(operand)) {
                String msg = (type == Token.INC) ? "invalid increment target" : "invalid decrement target";
                errorReporter.error(msg, sourceName, operand.getLineno(), 0);
            }

            Node node = newNode(type, operand);
            node.putBooleanProp(Node.INCRDECR_PROP, true);
            return node;
        }

        private boolean validAssignmentTarget(Node target) {
            switch (target.getType()) {
            case Token.CAST: // CAST is a bit weird, but syntactically valid.
            case Token.NAME:
            case Token.GETPROP:
            case Token.GETELEM:
            case Token.ARRAY_PATTERN:
            case Token.OBJECT_PATTERN:
                return true;
            }
            return false;
        }

        @Override
        Node processVariableStatement(VariableStatementTree stmt) {
            // skip the special handling so the doc is attached in the right place.
            return justTransform(stmt.declarations);
        }

        @Override
        Node processVariableDeclarationList(VariableDeclarationListTree decl) {
            int declType;
            switch (decl.declarationType) {
            case CONST:
                if (!config.acceptConstKeyword) {
                    maybeWarnEs6Feature(decl, "const declarations");
                }

                if (isEs6Mode()) {
                    declType = Token.CONST;
                } else {
                    // Code uses the 'const' keyword which is non-standard in ES5 and
                    // below. Just treat it as though it was a 'var'.
                    // TODO(tbreisacher): Treat this node as though it had an @const
                    // annotation.
                    declType = Token.VAR;
                }
                break;
            case LET:
                maybeWarnEs6Feature(decl, "let declarations");
                declType = Token.LET;
                break;
            case VAR:
                declType = Token.VAR;
                break;
            default:
                throw new IllegalStateException();
            }

            Node node = newNode(declType);
            for (VariableDeclarationTree child : decl.declarations) {
                node.addChildToBack(transformNodeWithInlineJsDoc(child, true));
            }
            return node;
        }

        @Override
        Node processVariableDeclaration(VariableDeclarationTree decl) {
            Node node = transformNodeWithInlineJsDoc(decl.lvalue, true);
            if (decl.initializer != null) {
                Node initalizer = transform(decl.initializer);
                node.addChildToBack(initalizer);
            }
            return node;
        }

        @Override
        Node processWhileLoop(WhileStatementTree stmt) {
            return newNode(Token.WHILE, transform(stmt.condition), transformBlock(stmt.body));
        }

        @Override
        Node processWithStatement(WithStatementTree stmt) {
            return newNode(Token.WITH, transform(stmt.expression), transformBlock(stmt.body));
        }

        @Override
        Node processMissingExpression(MissingPrimaryExpressionTree tree) {
            // This will already have been reported as an error by the parser.
            // Try to create something valid that ide mode might be able to
            // continue with.
            return createMissingExpressionNode();
        }

        private Node createMissingNameNode() {
            return newStringNode(Token.NAME, "__missing_name__");
        }

        private Node createMissingExpressionNode() {
            return newStringNode(Token.NAME, "__missing_expression__");
        }

        @Override
        Node processIllegalToken(ParseTree node) {
            errorReporter.error("Unsupported syntax: " + node.type, sourceName, lineno(node), 0);
            return newNode(Token.EMPTY);
        }

        void reportDestructuringAssign(ParseTree node) {
            errorReporter.error("destructuring assignment forbidden", sourceName, lineno(node), 0);
        }

        void reportGetter(ParseTree node) {
            errorReporter.error(GETTER_ERROR_MESSAGE, sourceName, lineno(node), 0);
        }

        void reportSetter(ParseTree node) {
            errorReporter.error(SETTER_ERROR_MESSAGE, sourceName, lineno(node), 0);
        }

        @Override
        Node processBooleanLiteral(LiteralExpressionTree literal) {
            return newNode(transformBooleanTokenType(literal.literalToken.type));
        }

        @Override
        Node processNullLiteral(LiteralExpressionTree literal) {
            return newNode(Token.NULL);
        }

        @Override
        Node processNull(NullTree literal) {
            // NOTE: This is not a NULL literal but a placeholder node such as in
            // an array with "holes".
            return newNode(Token.EMPTY);
        }

        @Override
        Node processCommaExpression(CommaExpressionTree tree) {
            Node root = newNode(Token.COMMA);
            SourcePosition start = tree.expressions.get(0).location.start;
            SourcePosition end = tree.expressions.get(1).location.end;
            setSourceInfo(root, start, end);
            for (ParseTree expr : tree.expressions) {
                int count = root.getChildCount();
                if (count < 2) {
                    root.addChildrenToBack(transform(expr));
                } else {
                    end = expr.location.end;
                    root = newNode(Token.COMMA, root, transform(expr));
                    setSourceInfo(root, start, end);
                }
            }
            return root;
        }

        @Override
        Node processClassDeclaration(ClassDeclarationTree tree) {
            maybeWarnEs6Feature(tree, "class");

            Node name = transformOrEmpty(tree.name, tree);
            Node superClass = transformOrEmpty(tree.superClass, tree);

            Node body = newNode(Token.CLASS_MEMBERS);
            setSourceInfo(body, tree);
            for (ParseTree child : tree.elements) {
                body.addChildToBack(transform(child));
            }

            return newNode(Token.CLASS, name, superClass, body);
        }

        @Override
        Node processSuper(SuperExpressionTree tree) {
            maybeWarnEs6Feature(tree, "super");
            return newNode(Token.SUPER);
        }

        @Override
        Node processYield(YieldExpressionTree tree) {
            Node yield = new Node(Token.YIELD);
            if (tree.expression != null) {
                yield.addChildToBack(transform(tree.expression));
            }
            yield.setYieldFor(tree.isYieldFor);
            return yield;
        }

        @Override
        Node processExportDecl(ExportDeclarationTree tree) {
            maybeWarnEs6Feature(tree, "modules");
            Node decls = null;
            if (tree.isExportAll) {
                Preconditions.checkState(tree.declaration == null && tree.exportSpecifierList == null);
            } else if (tree.declaration != null) {
                Preconditions.checkState(tree.exportSpecifierList == null);
                decls = transform(tree.declaration);
            } else {
                decls = transformList(Token.EXPORT_SPECS, tree.exportSpecifierList);
            }
            if (decls == null) {
                decls = newNode(Token.EMPTY);
            }
            Node export = newNode(Token.EXPORT, decls);
            if (tree.from != null) {
                Node from = processString(tree.from);
                export.addChildToBack(from);
            }

            export.putBooleanProp(Node.EXPORT_ALL_FROM, tree.isExportAll);
            export.putBooleanProp(Node.EXPORT_DEFAULT, tree.isDefault);
            return export;
        }

        @Override
        Node processExportSpec(ExportSpecifierTree tree) {
            Node importSpec = newNode(Token.EXPORT_SPEC, processName(tree.importedName));
            if (tree.destinationName != null) {
                importSpec.addChildToBack(processName(tree.destinationName));
            }
            return importSpec;
        }

        @Override
        Node processImportDecl(ImportDeclarationTree tree) {
            maybeWarnEs6Feature(tree, "modules");
            Node export = newNode(Token.IMPORT, transformOrEmpty(tree.defaultBindingIndentifier, tree),
                    transformListOrEmpty(Token.IMPORT_SPECS, tree.importSpecifierList),
                    processString(tree.moduleSpecifier));
            return export;
        }

        @Override
        Node processImportSpec(ImportSpecifierTree tree) {
            Node importSpec = newNode(Token.IMPORT_SPEC, processName(tree.importedName));
            if (tree.destinationName != null) {
                importSpec.addChildToBack(processName(tree.destinationName));
            }
            return importSpec;
        }

        @Override
        Node processModuleImport(ModuleImportTree tree) {
            maybeWarnEs6Feature(tree, "modules");
            Node module = newNode(Token.MODULE, processName(tree.name), processString(tree.from));
            return module;
        }

        private Node transformList(int type, ImmutableList<ParseTree> list) {
            Node n = newNode(type);
            for (ParseTree tree : list) {
                n.addChildToBack(transform(tree));
            }
            return n;
        }

        private Node transformListOrEmpty(int type, ImmutableList<ParseTree> list) {
            if (list != null) {
                return transformList(type, list);
            } else {
                return newNode(Token.EMPTY);
            }
        }

        void maybeWarnEs6Feature(ParseTree node, String feature) {
            if (!isEs6Mode()) {
                errorReporter.warning("this language feature is only supported in es6 mode: " + feature, sourceName,
                        lineno(node), charno(node));
            }
        }

        @Override
        Node unsupportedLanguageFeature(ParseTree node, String feature) {
            errorReporter.error("unsupported language feature: " + feature, sourceName, lineno(node), charno(node));
            return createMissingExpressionNode();
        }
    }

    private String normalizeRegex(LiteralToken token) {
        String value = token.value;
        int lastSlash = value.lastIndexOf('/');
        return value.substring(1, lastSlash);
    }

    private String normalizeString(LiteralToken token) {
        String value = token.value;
        int start = 1; // skip the leading quote
        int cur = value.indexOf('\\');
        if (cur == -1) {
            // short circuit no escapes.
            return value.substring(1, value.length() - 1);
        }
        StringBuilder result = new StringBuilder();
        while (cur != -1) {
            if (cur - start > 0) {
                result.append(value, start, cur);
            }
            cur += 1; // skip the escape char.
            char c = value.charAt(cur);
            switch (c) {
            case '\'':
            case '"':
            case '\\':
                result.append(c);
                break;
            case 'b':
                result.append('\b');
                break;
            case 'f':
                result.append('\f');
                break;
            case 'n':
                result.append('\n');
                break;
            case 'r':
                result.append('\r');
                break;
            case 't':
                result.append('\t');
                break;
            case 'v':
                result.append('\u000B');
                break;
            case '\n':
                if (!isEs5OrBetterMode()) {
                    errorReporter.warning(STRING_CONTINUATION_WARNING, sourceName, lineno(token.location.start),
                            charno(token.location.start));
                }
                // line continuation, skip the line break
                break;
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
                char next1 = value.charAt(cur + 1);

                if (inStrictContext()) {
                    if (c == '0' && !isOctalDigit(next1)) {
                        // No warning: "\0" followed by a character which is not an octal digit
                        // is allowed in strict mode.
                    } else {
                        errorReporter.warning(OCTAL_STRING_LITERAL_WARNING, sourceName,
                                lineno(token.location.start), charno(token.location.start));
                    }
                }

                if (!isOctalDigit(next1)) {
                    result.append((char) octaldigit(c));
                } else {
                    char next2 = value.charAt(cur + 2);
                    if (!isOctalDigit(next2)) {
                        result.append((char) (8 * octaldigit(c) + octaldigit(next1)));
                        cur += 1;
                    } else {
                        result.append((char) (8 * 8 * octaldigit(c) + 8 * octaldigit(next1) + octaldigit(next2)));
                        cur += 2;
                    }
                }

                break;
            case 'x':
                result.append((char) (hexdigit(value.charAt(cur + 1)) * 0x10 + hexdigit(value.charAt(cur + 2))));
                cur += 2;
                break;
            case 'u':
                int escapeEnd;
                String hexDigits;
                if (value.charAt(cur + 1) != '{') {
                    // Simple escape with exactly four hex digits: \\uXXXX
                    escapeEnd = cur + 5;
                    hexDigits = value.substring(cur + 1, escapeEnd);
                } else {
                    // Escape with braces can have any number of hex digits: \\u{XXXXXXX}
                    escapeEnd = cur + 2;
                    while (Character.digit(value.charAt(escapeEnd), 0x10) >= 0) {
                        escapeEnd++;
                    }
                    hexDigits = value.substring(cur + 2, escapeEnd);
                    escapeEnd++;
                }
                result.append(Character.toChars(Integer.parseInt(hexDigits, 0x10)));
                cur = escapeEnd - 1;
                break;
            default:
                // TODO(tbreisacher): Add a warning because the user probably
                // intended to type an escape sequence.
                result.append(c);
                break;
            }
            start = cur + 1;
            cur = value.indexOf('\\', start);
        }
        // skip the trailing quote.
        result.append(value, start, value.length() - 1);

        return result.toString();
    }

    boolean isEs6Mode() {
        return config.languageMode == LanguageMode.ECMASCRIPT6
                || config.languageMode == LanguageMode.ECMASCRIPT6_STRICT;
    }

    boolean isEs5OrBetterMode() {
        return config.languageMode != LanguageMode.ECMASCRIPT3;
    }

    private boolean inStrictContext() {
        // TODO(johnlenz): in ECMASCRIPT5/6 is a "mixed" mode and we should track the context
        // that we are in, if we want to support it.
        return config.languageMode == LanguageMode.ECMASCRIPT5_STRICT
                || config.languageMode == LanguageMode.ECMASCRIPT6_STRICT;
    }

    double normalizeNumber(LiteralToken token) {
        String value = token.value;
        SourceRange location = token.location;
        int length = value.length();
        Preconditions.checkState(length > 0);
        Preconditions.checkState(value.charAt(0) != '-' && value.charAt(0) != '+');
        if (value.charAt(0) == '.') {
            return Double.valueOf('0' + value);
        } else if (value.charAt(0) == '0' && length > 1) {
            // TODO(johnlenz): accept octal numbers in es3 etc.
            switch (value.charAt(1)) {
            case '.':
            case 'e':
            case 'E':
                return Double.valueOf(value);
            case 'b':
            case 'B': {
                if (!isEs6Mode()) {
                    errorReporter.warning(BINARY_NUMBER_LITERAL_WARNING, sourceName, lineno(token.location.start),
                            charno(token.location.start));
                }
                long v = 0;
                int c = 1;
                while (++c < length) {
                    v = (v * 2) + binarydigit(value.charAt(c));
                }
                return Double.valueOf(v);
            }
            case 'o':
            case 'O': {
                if (!isEs6Mode()) {
                    errorReporter.warning(OCTAL_NUMBER_LITERAL_WARNING, sourceName, lineno(token.location.start),
                            charno(token.location.start));
                }
                long v = 0;
                int c = 1;
                while (++c < length) {
                    v = (v * 8) + octaldigit(value.charAt(c));
                }
                return Double.valueOf(v);
            }
            case 'x':
            case 'X': {
                long v = 0;
                int c = 1;
                while (++c < length) {
                    v = (v * 0x10) + hexdigit(value.charAt(c));
                }
                return Double.valueOf(v);
            }
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
                errorReporter.warning(INVALID_ES5_STRICT_OCTAL, sourceName, lineno(location.start),
                        charno(location.start));
                if (!inStrictContext()) {
                    long v = 0;
                    int c = 0;
                    while (++c < length) {
                        v = (v * 8) + octaldigit(value.charAt(c));
                    }
                    return Double.valueOf(v);
                } else {
                    return Double.valueOf(value);
                }
            default:
                errorReporter.error(INVALID_NUMBER_LITERAL, sourceName, lineno(location.start),
                        charno(location.start));
                return 0;
            }
        } else {
            return Double.valueOf(value);
        }
    }

    private static int binarydigit(char c) {
        if (c >= '0' && c <= '1') {
            return (c - '0');
        }
        throw new IllegalStateException("unexpected: " + c);
    }

    private static boolean isOctalDigit(char c) {
        return c >= '0' && c <= '7';
    }

    private static int octaldigit(char c) {
        if (isOctalDigit(c)) {
            return (c - '0');
        }
        throw new IllegalStateException("unexpected: " + c);
    }

    private static int hexdigit(char c) {
        switch (c) {
        case '0':
            return 0;
        case '1':
            return 1;
        case '2':
            return 2;
        case '3':
            return 3;
        case '4':
            return 4;
        case '5':
            return 5;
        case '6':
            return 6;
        case '7':
            return 7;
        case '8':
            return 8;
        case '9':
            return 9;
        case 'a':
        case 'A':
            return 10;
        case 'b':
        case 'B':
            return 11;
        case 'c':
        case 'C':
            return 12;
        case 'd':
        case 'D':
            return 13;
        case 'e':
        case 'E':
            return 14;
        case 'f':
        case 'F':
            return 15;
        }
        throw new IllegalStateException("unexpected: " + c);
    }

    private static int transformBooleanTokenType(TokenType token) {
        switch (token) {
        case TRUE:
            return Token.TRUE;
        case FALSE:
            return Token.FALSE;

        default:
            throw new IllegalStateException(String.valueOf(token));
        }
    }

    private static int transformPostfixTokenType(TokenType token) {
        switch (token) {
        case PLUS_PLUS:
            return Token.INC;
        case MINUS_MINUS:
            return Token.DEC;

        default:
            throw new IllegalStateException(String.valueOf(token));
        }
    }

    private static int transformUniaryTokenType(TokenType token) {
        switch (token) {
        case BANG:
            return Token.NOT;
        case TILDE:
            return Token.BITNOT;
        case PLUS:
            return Token.POS;
        case MINUS:
            return Token.NEG;
        case DELETE:
            return Token.DELPROP;
        case TYPEOF:
            return Token.TYPEOF;

        case PLUS_PLUS:
            return Token.INC;
        case MINUS_MINUS:
            return Token.DEC;

        case VOID:
            return Token.VOID;

        default:
            throw new IllegalStateException(String.valueOf(token));
        }
    }

    private static int transformBinaryTokenType(TokenType token) {
        switch (token) {
        case BAR:
            return Token.BITOR;
        case CARET:
            return Token.BITXOR;
        case AMPERSAND:
            return Token.BITAND;
        case EQUAL_EQUAL:
            return Token.EQ;
        case NOT_EQUAL:
            return Token.NE;
        case OPEN_ANGLE:
            return Token.LT;
        case LESS_EQUAL:
            return Token.LE;
        case CLOSE_ANGLE:
            return Token.GT;
        case GREATER_EQUAL:
            return Token.GE;
        case LEFT_SHIFT:
            return Token.LSH;
        case RIGHT_SHIFT:
            return Token.RSH;
        case UNSIGNED_RIGHT_SHIFT:
            return Token.URSH;
        case PLUS:
            return Token.ADD;
        case MINUS:
            return Token.SUB;
        case STAR:
            return Token.MUL;
        case SLASH:
            return Token.DIV;
        case PERCENT:
            return Token.MOD;

        case EQUAL_EQUAL_EQUAL:
            return Token.SHEQ;
        case NOT_EQUAL_EQUAL:
            return Token.SHNE;

        case IN:
            return Token.IN;
        case INSTANCEOF:
            return Token.INSTANCEOF;
        case COMMA:
            return Token.COMMA;

        case EQUAL:
            return Token.ASSIGN;
        case BAR_EQUAL:
            return Token.ASSIGN_BITOR;
        case CARET_EQUAL:
            return Token.ASSIGN_BITXOR;
        case AMPERSAND_EQUAL:
            return Token.ASSIGN_BITAND;
        case LEFT_SHIFT_EQUAL:
            return Token.ASSIGN_LSH;
        case RIGHT_SHIFT_EQUAL:
            return Token.ASSIGN_RSH;
        case UNSIGNED_RIGHT_SHIFT_EQUAL:
            return Token.ASSIGN_URSH;
        case PLUS_EQUAL:
            return Token.ASSIGN_ADD;
        case MINUS_EQUAL:
            return Token.ASSIGN_SUB;
        case STAR_EQUAL:
            return Token.ASSIGN_MUL;
        case SLASH_EQUAL:
            return Token.ASSIGN_DIV;
        case PERCENT_EQUAL:
            return Token.ASSIGN_MOD;

        case OR:
            return Token.OR;
        case AND:
            return Token.AND;

        default:
            throw new IllegalStateException(String.valueOf(token));
        }
    }

    // Simple helper to create nodes and set the initial node properties.
    private Node newNode(int type) {
        return new Node(type).clonePropsFrom(templateNode);
    }

    private Node newNode(int type, Node child1) {
        return new Node(type, child1).clonePropsFrom(templateNode);
    }

    private Node newNode(int type, Node child1, Node child2) {
        return new Node(type, child1, child2).clonePropsFrom(templateNode);
    }

    private Node newNode(int type, Node child1, Node child2, Node child3) {
        return new Node(type, child1, child2, child3).clonePropsFrom(templateNode);
    }

    private Node newStringNode(String value) {
        return IR.string(value).clonePropsFrom(templateNode);
    }

    private Node newStringNode(int type, String value) {
        return Node.newString(type, value).clonePropsFrom(templateNode);
    }

    private Node newNumberNode(Double value) {
        return IR.number(value).clonePropsFrom(templateNode);
    }
}