Java tutorial
/* * 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 */ 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); } }