// Copyright (C) 2005 Google Inc.
//
// 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.caja.parser.js;
import com.google.caja.SomethingWidgyHappenedError;
import com.google.caja.lexer.FilePosition;
import com.google.caja.lexer.JsTokenQueue;
import com.google.caja.lexer.JsTokenType;
import com.google.caja.lexer.Keyword;
import com.google.caja.lexer.ParseException;
import com.google.caja.lexer.Punctuation;
import com.google.caja.lexer.Token;
import com.google.caja.lexer.TokenQueue.Mark;
import com.google.caja.parser.AbstractParseTreeNode;
import com.google.caja.parser.ParseTreeNode;
import com.google.caja.parser.ParserBase;
import com.google.caja.reporting.Message;
import com.google.caja.reporting.MessageLevel;
import com.google.caja.reporting.MessagePart;
import com.google.caja.reporting.MessageQueue;
import com.google.caja.reporting.MessageType;
import com.google.caja.reporting.RenderContext;
import com.google.caja.util.Lists;
import com.google.caja.util.Pair;
import com.google.caja.util.Sets;
import com.google.javascript.jscomp.jsonml.JsonML;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Collections;
import java.util.List;
import java.util.Set;
/**
* Parses javascript source returning a javascript parse tree.
*
* <p>
* The grammar below is a context-free representation of the grammar this
* parser parses. It disagrees with EcmaScript 262 Edition 3 (ES3) where
* implementations disagree with ES3. The rules for semicolon insertion and
* the possible backtracking in expressions needed to properly handle
* backtracking are commented thoroughly in code, since semicolon insertion
* requires information from both the lexer and parser and is not determinable
* with finite lookahead.
* <p>
* Noteworthy features
* <ul>
* <li>Reports warnings on a queue where an error doesn't prevent any further
* errors, so that we can report multiple errors in a single compile pass
* instead of forcing developers to play whack-a-mole.
* <li>Does not parse Firefox style {@code catch (<Identifier> if <Expression>)}
* since those don't work on IE and many other interpreters.
* <li>Recognizes {@code const} since many interpreters do (not IE) but warns.
* <li>Allows, but warns, on trailing commas in {@code Array} and {@code Object}
* constructors.
* <li>Allows keywords as identifier names but warns since different
* interpreters have different keyword sets. This allows us to use an
* expansive keyword set.</li>
* </ul>
* To parse strict code, pass in a {@code PedanticWarningMessageQueue} that
* converts {@link MessageLevel#WARNING} and above to
* {@link MessageLevel#FATAL_ERROR}.
*
* <xmp>
* // Terminal productions
* Program => <DirectivePrologue>? <TerminatedStatement>*
* Statement => <TerminalStatement>
* | <NonTerminalStatement>
* Expression => <CommaOperator>
*
* // Non-terminal productions. Indentation indicates that a nonterminal is
* // used only by the unindented nonterminal above it.
* DirectivePrologue => <Directive> <DirectivePrologue>?
* Directive => <StringLiteral> ';'
* TerminatedStatement => <BlockStatement>
* | <SimpleStatement> <StatementTerminator>
* StatementTerminator => ';'
* | <InsertedSemicolon>
* BlockStatement => <StatementLabel>? <BlockStatementAtom>
* SimpleStatement => <StatementLabel>? <SimpleStatementAtom>
* InsertedSemicolon => ε // abort if disallowed in context
* StatementLabel => <Identifier> ':'
* BlockStatementAtom => <Block>
* | <Conditional>
* | <For>
* // Noop is a block statement atom since it's
* // guaranteed to end with a semicolon. If it were
* // a simple statement epsilon, then it could be
* // inserted anywhere that semicolon insertion is
* // allowed.
* | <Noop>
* | <Switch>
* | <Try>
* | <While>
* | <With>
* SimpleStatementAtom => <Break>
* | <Continue>
* | <Debugger>
* // Do-While loops are simple because, unlike other
* // loops their body isn't last, so they aren't
* // guaranteed to end with a right-curly or semi.
* | <Do>
* | <ExprStatement>
* | <FunctionDeclaration>
* | <Declaration>
* | <Throw>
* | <Return>
*
* Identifier != <Keyword>
* | <Word>
* Body => <TerminatedStatement>
*
* Block => '{' <TerminatedStatement>* '}'
* Conditional => 'if' '(' <Expression> ')' <Body> <Else>
* Else => 'else' <Body>
* | ε
* For => 'for' '(' ';' <ExpressionOrNoop> <Expression>? ')'
* <Body>
* | 'for' '(' <DeclarationStart> 'in' <Expression> ')'
* <Body>
* | 'for' '(' <LValue> 'in' <Expression> ')'
* <Body>
* | 'for' '(' <Declaration> ';' <ExpressionOrNoop>
* <Expression>? ')' <Body>
* | 'for' '(' <Expression> ';' <ExpressionOrNoop>
* <Expression>? ')' <Body>
* Noop => ';'
* Switch => 'switch' '(' <Expression> ')' '{' <Cases> '}'
* Cases => <Case>* <DefaultAndCases>?
* DefaultAndCases => <Default> <Case>*
* Case => 'case' <Expression> ':' <TerminatedStatement>*
* Default => 'default' ':' <TerminatedStatement>*
* Try => 'try' <Body> <TryClauses>
* TryClauses => <Catch> <Finally>?
* | <Finally>
* Catch => 'catch' '(' <Identifier> ')' <Body>
* Finally => 'finally' <Body>
* While => 'while' '(' <Expression> ')' <Body>
* With => 'with' '(' <Expression> ')' <Body>
* Do => 'do' <Body> 'while' '(' <Expression> ')'
* Break => 'break' [no LineTerminator] <StatementLabel>?
* Continue => 'continue' [no LineTerminator] <StatementLabel>?
* Debugger => 'debugger'
* Return => 'return' [no LineTerminator] <Expression>?
* Throw => 'throw' [no LineTerminator] <Expression>
* ExprStatement => <Expression>
*
* ExpressionOrNoop => <Expression>?
* DeclarationStart => <DeclarationKeyword> <Identifier>
* Declaration => <DeclarationKeyword> <DeclarationBodyList>
* DeclarationKeyword => 'var' | 'const'
* DeclarationBodyList => <DeclarationBody> <DeclarationBodyTail>
* DeclarationBodyTail => ',' <DeclarationBodyList>
* | ε
* DeclarationBody => <Identifier> <InitialValue>?
* InitialValue => '=' <Expression>
*
* FunctionDeclaration => <NamedFunction>
* FunctionConstructor => <NamedFunction>
* | <AnonymousFunction>
* AnonymousFunction => 'function' <Formals> <FunctionBody>
* NamedFunction => 'function' <Identifier> <Formals> <FunctionBody>
* FunctionBody => '{' <Program> '}'
* Formals => '(' <IdentifierList>? ')'
* IdentifierList => <Identifier> <IdentifierListTail>
* IdentifierListTail => ',' <IdentifierList>
*
* // All of the operators have an associated precedence defined in the Operator
* // enum. The p variable on the right of the productions is the precedence
* // of the most recently consumed operator.
* CommaOperator => <Operator(MAX)>
* | <CommaOperator> ',' <Operator(MAX)>
* Operator(prec) => <OperatorHead(prec)> <OperatorTail(prec)>
* OperatorHead(prec) => <PrefixOperator(p < prec)> <Operator(p + 1)>
* | <ExpressionAtom>
* OperatorTail(prec) => [no LineTerminator] <PostfixOperator(p < prec)>
* | <RightAssocInfixOperator(p <= prec)> <Operator(p)>
* | <LeftAssocInfixOperator(p < prec)> <Operator(p)>
* // below only match if p < prec
* | '[' <Expression> ']'
* | '(' <ActualList>? ')'
* | '?' <Operator(TERNARY)> ':' <Operator(TERNARY)>
* | <ExpressionAtom>
* // ActualList uses Operator(MAX) to prevent parsing actuals from being
* // treated as operands to the comma operator. Since the parentheses
* // recurse to Expression under ExpressionAtom, the result of a comma
* // operator can be passed to a function by nesting in parens.
* ActualList => <Operator(MAX)> <ActualListTail>?
* ActualListTail => ',' <ActualList>
* | ε
* ExpressionAtom => <StringLiteral>
* | <NumberLiteral>
* | <RegexpLiteral>
* | 'null' | 'true' | 'false' | 'this'
* | <FunctionConstructor>
* | <Keyword> // error and treat as identifier
* | <Identifier>
* | '(' <Expression> ')'
* | <ArrayConstructor>
* | <ObjectConstructor>
* | ε // in error-tolerant mode
* ArrayConstructor => '[' <ArrayElements> <TrailingComma>? ']'
* ArrayElements => <ArrayElementsHead>* <Expression>
* | ε
* ArrayElementsHead => <Expression>? ','
* ObjectConstructor => '{' <ObjPropertyList> '}'
* ObjPropertyList => <ObjPropery> <ObjProperties>* <TrailingComma>?
* ObjProperties => ',' <ObjectPreperty>
* ObjProperty => <ValueProperty>
* | <GetterProperty>
* | <SetterProperty>
* ValueProperty => <PropName> ':' <Expression>
* GetterProperty => 'get' <PropName> <Formals> <FunctionBody>
* SetterProperty => 'set' <PropName> <Formals> <FunctionBody>
* ObjPropertyKey => <Identifier>
* | <StringLiteral>
* | <NumberLiteral>
* | <Keyword> // set allowed interpreter dependent
* TrailingComma => ',' // warn
* </xmp>
* See also {@link Keyword} for definitions of keywords, {@link Operator} for
* operator precedences and associativity, and {@link JsTokenType} for
* definitions of Word, NumberLiteral, and StringLiteral.
*
* <p>Questions as to whether a semicolon should be inserted can only be
* answered at the lexical level. A lexer can emit a comment-like-token where
* semicolons can be inserted though this is not the approach taken in this
* implementation.</p>
*
* @author mikesamuel@gmail.com
*/
public final class Parser extends ParserBase {
private boolean recoverFromFailure;
public Parser(JsTokenQueue tq, MessageQueue mq) {
this(tq, mq, false);
}
public Parser(JsTokenQueue tq, MessageQueue mq, boolean isQuasiliteral) {
super(tq, mq, isQuasiliteral);
}
/**
* True iff the parser will attempt to recover from errors that can be
* attributed to a missing run of parenthetically balanced tokens by inserting
* a placeholder reference "_".
*/
public boolean getRecoverFromFailure() {
return recoverFromFailure;
}
/**
* Setter corresponding to {@link #getRecoverFromFailure}.
*/
public void setRecoverFromFailure(boolean shouldRecover) {
this.recoverFromFailure = shouldRecover;
}
/** Parses a top level block. */
public Block parse() throws ParseException {
Block program = parseProgram();
tq.expectEmpty();
return program;
}
/** Parses and returns a Statement. */
public Statement parseStatement() throws ParseException {
// look for any labels preceding statement
Mark m = tq.mark();
Token<JsTokenType> t = tq.peek();
if (JsTokenType.WORD == t.type) {
String label = parseIdentifier(false);
FilePosition labelPos = t.pos;
if (tq.checkToken(Punctuation.COLON)) {
t = tq.peek();
AbstractStatement s = null;
if (JsTokenType.KEYWORD == t.type) {
switch (Keyword.fromString(t.text)) {
case FOR: case DO: case WHILE: case SWITCH:
s = parseLoopOrSwitch(labelPos, label);
break;
default:
break;
}
}
if (null == s) {
Statement labelless = parseStatementWithoutLabel();
s = new LabeledStmtWrapper(posFrom(m), label, labelless);
}
finish(s, m);
return s;
}
tq.rewind(m);
}
return parseStatementWithoutLabel();
}
private Block parseFunctionBody() throws ParseException {
return parseProgramOrFunctionBody(true);
}
private Block parseProgram() throws ParseException {
return parseProgramOrFunctionBody(false);
}
private Block parseProgramOrFunctionBody(boolean requireBrackets)
throws ParseException {
Mark m = tq.mark();
if (requireBrackets) { tq.expectToken(Punctuation.LCURLY); }
List<Statement> stmts = Lists.newArrayList();
DirectivePrologue prologue = parseOptionalDirectivePrologue();
if (prologue != null) { stmts.add(prologue); }
while (!tq.isEmpty() && !tq.lookaheadToken(Punctuation.RCURLY)) {
stmts.add(parseTerminatedStatement());
}
if (requireBrackets) { tq.expectToken(Punctuation.RCURLY); }
Block b = new Block(posFrom(m), stmts);
finish(b, m);
return b;
}
private DirectivePrologue parseOptionalDirectivePrologue()
throws ParseException {
// Quick return if we are sure we will not accumulate anything
if (tq.isEmpty() || tq.peek().type != JsTokenType.STRING) { return null; }
Mark startOfPrologue = tq.mark();
List<Directive> directives = Lists.newArrayList();
while (!tq.isEmpty() && tq.peek().type == JsTokenType.STRING) {
Mark startOfDirective = tq.mark();
Token<JsTokenType> quotedString = tq.pop();
if (!tq.checkToken(Punctuation.SEMI)) {
Token<JsTokenType> t = !tq.isEmpty() ? tq.peek() : null;
if ((t == null || !continuesExpr(t.text)) && semicolonInserted()) {
FilePosition semiPoint = FilePosition.endOf(tq.lastPosition());
MessageLevel lvl = tq.isEmpty()
|| tq.lookaheadToken(Punctuation.RCURLY)
? MessageLevel.LOG : MessageLevel.LINT;
mq.addMessage(MessageType.SEMICOLON_INSERTED, lvl, semiPoint);
} else {
tq.rewind(startOfDirective);
break;
}
}
String unquoted = quotedString.text.substring(
1, quotedString.text.length() - 1);
String decoded = StringLiteral.getUnquotedValueOf(quotedString.text);
if (!unquoted.equals(decoded)
|| !Directive.RecognizedValue.isDirectiveStringRecognized(unquoted)) {
mq.addMessage(
MessageType.UNRECOGNIZED_DIRECTIVE_IN_PROLOGUE,
quotedString.pos, MessagePart.Factory.valueOf(unquoted));
}
Directive d = new Directive(posFrom(quotedString.pos), unquoted);
finish(d, startOfDirective);
directives.add(d);
}
if (directives.isEmpty()) { return null; }
DirectivePrologue prologue = new DirectivePrologue(
posFrom(startOfPrologue), directives);
finish(prologue, startOfPrologue);
return prologue;
}
private static boolean continuesExpr(String tokenText) {
return Operator.lookupOperation(tokenText, OperatorType.INFIX) != null
|| Operator.lookupOperation(tokenText, OperatorType.BRACKET) != null
|| Operator.lookupOperation(tokenText, OperatorType.TERNARY) != null;
}
private LabeledStatement parseLoopOrSwitch(FilePosition start, String label)
throws ParseException {
Token<JsTokenType> t = tq.peek();
LabeledStatement s;
switch (Keyword.fromString(t.text)) {
case FOR:
{
tq.advance();
tq.expectToken(Punctuation.LPAREN);
if (tq.checkToken(Punctuation.SEMI)) {
Statement initializer = noop(tq.lastPosition());
Expression condition = parseExpressionOrNoop(new BooleanLiteral(
FilePosition.startOf(tq.currentPosition()), true), true);
Statement increment;
if (!tq.checkToken(Punctuation.RPAREN)) {
increment = parseExpressionStmt(true);
tq.expectToken(Punctuation.RPAREN);
} else {
increment = noop(tq.lastPosition());
}
Statement body = parseBody(true);
s = new ForLoop(
posFrom(start), label, initializer, condition, increment, body);
} else {
Statement initializer = parseDeclarationsOrExpression(true);
Expression initializerExpr = null;
if ((initializer instanceof Declaration // no multi-decls
&& null == ((Declaration) initializer).getInitializer()
&& tq.checkToken(Keyword.IN))
|| (!tq.lookaheadToken(Punctuation.SEMI)
&& (initializerExpr = checkInExprWithLhs(initializer)) != null
)) {
Expression iterable;
Expression lvalue;
if (null == initializerExpr) {
iterable = parseExpressionInt(true);
lvalue = null;
} else {
Operation op = (Operation) initializerExpr;
lvalue = op.children().get(0);
iterable = op.children().get(1);
}
tq.expectToken(Punctuation.RPAREN);
Statement body = parseBody(true);
if (null == lvalue) {
s = new ForEachLoop(
posFrom(start),
label, (Declaration) initializer, iterable, body);
} else {
s = new ForEachLoop(
posFrom(start), label, lvalue, iterable, body);
}
} else {
Mark m = tq.mark();
tq.expectToken(Punctuation.SEMI);
Expression condition = parseExpressionOrNoop(
new BooleanLiteral(posFrom(m), true), true);
Statement increment;
if (!tq.checkToken(Punctuation.RPAREN)) {
increment = parseExpressionStmt(true);
tq.expectToken(Punctuation.RPAREN);
} else {
increment = noop(tq.lastPosition());
}
Statement body = parseBody(true);
s = new ForLoop(
posFrom(start), label, initializer, condition, increment, body);
}
}
break;
}
case WHILE:
{
tq.advance();
tq.expectToken(Punctuation.LPAREN);
Expression cond = parseExpressionInt(true);
tq.expectToken(Punctuation.RPAREN);
Statement body = parseBody(true);
s = new WhileLoop(posFrom(start), label, cond, body);
break;
}
case DO:
{
tq.advance();
Statement body = parseBody(false);
tq.expectToken(Keyword.WHILE);
tq.expectToken(Punctuation.LPAREN);
Expression cond = parseExpressionInt(true);
tq.expectToken(Punctuation.RPAREN);
s = new DoWhileLoop(posFrom(start), label, body, cond);
// http://code.google.com/p/google-caja/issues/detail?id=1316
// ES[35] requires ; after do-while, but browsers are ok without it.
// Here we either eat a ; or warn if it's missing.
if (!tq.checkToken(Punctuation.SEMI)) {
FilePosition pos = FilePosition.endOf(tq.lastPosition());
mq.addMessage(
MessageType.SEMICOLON_INSERTED, MessageLevel.LINT, pos);
}
break;
}
case SWITCH:
{
tq.advance();
tq.expectToken(Punctuation.LPAREN);
Expression switchValue = parseExpressionInt(true);
tq.expectToken(Punctuation.RPAREN);
tq.expectToken(Punctuation.LCURLY);
List<SwitchCase> cases = Lists.newArrayList();
while (!tq.checkToken(Punctuation.RCURLY)) {
Mark caseMark = tq.mark();
Expression caseValue;
if (tq.checkToken(Keyword.DEFAULT)) {
caseValue = null;
} else {
tq.expectToken(Keyword.CASE);
caseValue = parseExpressionInt(false);
}
tq.expectToken(Punctuation.COLON);
FilePosition colonPos = tq.lastPosition();
Mark caseBodyStart = tq.mark();
List<Statement> caseBodyContents = Lists.newArrayList();
while (!(tq.lookaheadToken(Keyword.DEFAULT)
|| tq.lookaheadToken(Keyword.CASE)
|| tq.lookaheadToken(Punctuation.RCURLY))) {
caseBodyContents.add(parseTerminatedStatement());
}
FilePosition caseBodyPos = caseBodyContents.isEmpty()
? FilePosition.endOf(colonPos) : posFrom(caseBodyStart);
Block caseBody = new Block(caseBodyPos, caseBodyContents);
finish(caseBody, caseBodyStart);
SwitchCase caseStmt = (null != caseValue)
? new CaseStmt(posFrom(caseMark), caseValue, caseBody)
: new DefaultCaseStmt(posFrom(caseMark), caseBody);
finish(caseStmt, caseMark);
cases.add(caseStmt);
}
s = new SwitchStmt(posFrom(start), label, switchValue, cases);
break;
}
default:
throw new SomethingWidgyHappenedError(t.text);
}
return s;
}
private AbstractStatement parseStatementWithoutLabel() throws ParseException {
Mark m = tq.mark();
Token<JsTokenType> t = tq.peek();
if (JsTokenType.KEYWORD == t.type) {
AbstractStatement s;
switch (Keyword.fromString(t.text)) {
case FOR: case DO: case WHILE: case SWITCH:
s = parseLoopOrSwitch(t.pos, "");
break;
case IF:
{
tq.advance();
List<Pair<Expression, Statement>> clauses = Lists.newArrayList();
Statement elseClause = null;
boolean sawElse;
do {
tq.expectToken(Punctuation.LPAREN);
Expression cond = parseExpressionInt(true);
tq.expectToken(Punctuation.RPAREN);
Statement body = parseBody(false);
sawElse = tq.checkToken(Keyword.ELSE);
clauses.add(new Pair<Expression, Statement>(cond, body));
} while (sawElse && tq.checkToken(Keyword.IF));
if (sawElse) {
elseClause = parseBody(true);
}
s = new Conditional(posFrom(m), clauses, elseClause);
break;
}
case VAR:
case CONST:
return parseDeclarationsOrExpression(false);
case FUNCTION:
{
Mark fs = tq.mark();
tq.advance();
if (tq.lookaheadToken(Punctuation.LPAREN)) {
// If no name, then treat it as an expression
tq.rewind(fs);
return parseExpressionStmt(false);
} else { // a function declaration
Identifier identifier = parseIdentifierNode(false);
tq.expectToken(Punctuation.LPAREN);
FormalParamList params = parseFormalParams();
tq.expectToken(Punctuation.RPAREN);
Block body = parseFunctionBody();
FunctionConstructor fc = new FunctionConstructor(
posFrom(m), identifier, params.params, body);
finish(fc, m);
s = new FunctionDeclaration(fc);
finish(s, m);
}
break;
}
case RETURN:
{
tq.advance();
AbstractExpression value;
// Check for semicolon insertion without lookahead since return is a
// restricted production. See the grammar above and ES3 or ES5 S7.9.1
if (semicolonInserted() || tq.lookaheadToken(Punctuation.SEMI)) {
value = null;
} else {
value = parseExpressionInt(false);
}
s = new ReturnStmt(posFrom(m), value);
break;
}
case BREAK:
{
tq.advance();
String targetLabel = "";
if (!semicolonInserted() && JsTokenType.WORD == tq.peek().type) {
targetLabel = parseIdentifier(false);
}
s = new BreakStmt(posFrom(m), targetLabel);
break;
}
case CONTINUE:
{
tq.advance();
String targetLabel = "";
if (!semicolonInserted() && JsTokenType.WORD == tq.peek().type) {
targetLabel = parseIdentifier(false);
}
s = new ContinueStmt(posFrom(m), targetLabel);
break;
}
case DEBUGGER:
{
tq.advance();
s = new DebuggerStmt(posFrom(m));
break;
}
case THROW:
{
tq.advance();
if (semicolonInserted()) {
throw new ParseException(new Message(
MessageType.EXPECTED_TOKEN,
FilePosition.endOf(tq.lastPosition()),
MessagePart.Factory.valueOf("<expression>"),
MessagePart.Factory.valueOf("<newline>")));
}
Expression ex = parseExpressionInt(false);
s = new ThrowStmt(posFrom(m), ex);
break;
}
case TRY:
{
tq.advance();
Block body = parseBodyBlock();
CatchStmt handler;
FinallyStmt finallyBlock;
Mark m2 = tq.mark();
boolean sawFinally = tq.checkToken(Keyword.FINALLY);
if (sawFinally) {
handler = null;
} else {
tq.expectToken(Keyword.CATCH);
tq.expectToken(Punctuation.LPAREN);
Identifier idNode = parseIdentifierNode(false);
Declaration exvar = new Declaration(
idNode.getFilePosition(), idNode, (Expression)null);
exvar.setComments(idNode.getComments());
tq.expectToken(Punctuation.RPAREN);
Block catchBody = parseBodyBlock();
handler = new CatchStmt(posFrom(m2), exvar, catchBody);
finish(handler, m2);
m2 = tq.mark();
sawFinally = tq.checkToken(Keyword.FINALLY);
}
if (sawFinally) {
Block st = parseBodyBlock();
finallyBlock = new FinallyStmt(posFrom(m2), st);
finish(finallyBlock, m2);
} else {
finallyBlock = null;
}
s = new TryStmt(posFrom(m), body, handler, finallyBlock);
break;
}
case WITH:
{
tq.advance();
tq.expectToken(Punctuation.LPAREN);
Expression scopeObject = parseExpressionInt(true);
tq.expectToken(Punctuation.RPAREN);
Statement body = parseBody(true);
s = new WithStmt(posFrom(m), scopeObject, body);
break;
}
default:
return parseExpressionStmt(false);
}
finish(s, m);
return s;
} else if (tq.checkToken(Punctuation.LCURLY)) {
// In a statement a curly block opens a block.
// Blocks don't have a scope associated, so are effectively useless,
// except to group statements in a loop.
List<Statement> blockParts = Lists.newArrayList();
while (!tq.checkToken(Punctuation.RCURLY)) {
blockParts.add(parseTerminatedStatement());
}
Block b = new Block(posFrom(m), blockParts);
finish(b, m);
return b;
} else if (tq.checkToken(Punctuation.SEMI)) {
return noop(tq.lastPosition());
} else {
return parseExpressionStmt(false);
}
}
/**
* Parses an expression.
* @param insertionProtected true iff the expression appears directly inside
* parentheses or square brackets or in some other context where
* semicolons cannot be inserted. For example, the x in {@code f(x);}
* appears in an insertionProtected contexts, but the x in {@code x = 1;}
* does not.
* @return non null.
*/
public Expression parseExpression(boolean insertionProtected)
throws ParseException {
return parseExpressionInt(insertionProtected);
}
private AbstractExpression parseExpressionInt(boolean insertionProtected)
throws ParseException {
Mark m = tq.mark();
AbstractExpression e = parseOp(Integer.MAX_VALUE, insertionProtected);
// Handle comma operator
while (tq.checkToken(Punctuation.COMMA)) {
// The comma operator is left-associative so parse expression part in loop
// instead of recursing
Expression right = parseExpressionPart(insertionProtected);
e = Operation.create(posFrom(m), Operator.COMMA, e, right);
finish(e, m);
}
return e;
}
/**
* Parses an expression part -- one that can appear inside a comma separated
* list. This differs from {@link #parseExpression} in that it will not parse
* <code>a, b</code> as the comma will be interpreted as a terminator,
* although it will parse <code>(a, b)</code>.
* @return non null.
*/
public Expression parseExpressionPart(boolean insertionProtected)
throws ParseException {
return parseOp(Integer.MAX_VALUE, insertionProtected);
}
private AbstractExpression parseOp(
int precedence, boolean insertionProtected)
throws ParseException {
AbstractExpression left = null;
// Handle prefix operations
{
Token<JsTokenType> t = tq.peek();
Operator op = Operator.lookupOperation(t.text, OperatorType.PREFIX);
if (null != op) {
Mark m = tq.mark();
tq.advance();
int opprec = op.getPrecedence();
if (opprec < precedence) {
// The opprec + 1 may look a bit odd but it allows binary operators
// to associate when they have the same precedence as the prefix
// op preceding them. This is the desired behavior:
// new Foo[4] should parenthesize as new (Foo[4]) as verified by
// the fact that new Object['toString'] fails to parse in FF.
// It introduces no problem since there are no right-associative
// binary operators with precedence 2 or 5.
left = parseOp(opprec + 1, insertionProtected);
} else {
throw new ParseException(
new Message(MessageType.UNEXPECTED_TOKEN, t.pos,
MessagePart.Factory.valueOf(t.text)));
}
if (op == Operator.CONSTRUCTOR && tq.checkToken(Punctuation.LPAREN)) {
List<Expression> operands = Lists.newArrayList();
operands.add(left);
if (!tq.checkToken(Punctuation.RPAREN)) {
do {
operands.add(parseExpressionPart(true));
} while (tq.checkToken(Punctuation.COMMA));
tq.expectToken(Punctuation.RPAREN);
}
left = new SpecialOperation(posFrom(m), op, operands);
} else {
try {
left = Operation.create(posFrom(m), op, left);
} catch (IllegalArgumentException e) {
throw new ParseException(
new Message(MessageType.ASSIGN_TO_NON_LVALUE, t.pos,
MessagePart.Factory.valueOf(t.text)));
}
}
finish(left, m);
// Not pulling multiple operators off the stack means that
// some prefix operator nestings are impossible. This is intended.
// This prevents such things as (new (++i)).
// This only affects the new operator though since it is the only
// prefix operator with a precedence != 4.
}
if (null == left) {
left = parseExpressionAtom();
}
}
// Parse binary operators, except comma.
while (!tq.isEmpty()) {
Token<JsTokenType> t = tq.peek();
// If it is a binary op then we should consider using it
Operator op = Operator.lookupOperation(t.text, OperatorType.INFIX);
if (null == op) {
op = Operator.lookupOperation(t.text, OperatorType.BRACKET);
if (null == op) {
op = Operator.lookupOperation(t.text, OperatorType.TERNARY);
// Check for semicolon insertion since postfix operators are
// "restricted productions" according to ES3 or ES5 S7.9.1.
if (null == op) {
if (!semicolonInserted()) {
op = Operator.lookupOperation(t.text, OperatorType.POSTFIX);
}
if (null == op) { break; }
}
}
} else if (Operator.COMMA == op) {
break;
}
int opprec = op.getPrecedence();
if (!(opprec < precedence
|| (opprec == precedence
&& Associativity.RIGHT == op.getAssociativity()))) {
break;
}
if (op.getType() == OperatorType.BRACKET) {
checkForMissingSemicolon();
}
Mark opStart = tq.mark();
int nMessages = mq.getMessages().size();
tq.advance(); // Consume the operator token
Expression right;
try {
// Recurse to parse operator arguments.
if (OperatorType.BRACKET == op.getType()) {
if (Operator.FUNCTION_CALL == op) {
List<Expression> actuals;
if (tq.checkToken(op.getClosingSymbol())) {
actuals = Collections.<Expression>emptyList();
} else {
actuals = Lists.newArrayList();
do {
actuals.add(parseExpressionPart(true));
} while (tq.checkToken(Punctuation.COMMA));
tq.expectToken(op.getClosingSymbol());
}
right = new ActualList(actuals);
} else {
right = parseExpressionInt(true);
tq.expectToken(op.getClosingSymbol());
}
} else if (OperatorType.POSTFIX == op.getType()) {
right = null;
} else if (OperatorType.TERNARY == op.getType()) {
right = parseExpressionPart(insertionProtected);
} else if (Operator.MEMBER_ACCESS != op) {
right = parseOp(opprec, insertionProtected);
} else {
// The . operator only accepts a reference on the right.
// No a.b.4 or a.b.(c.d)
right = parseReference(true);
}
} catch (ParseException ex) {
// According to
// http://www.mozilla.org/js/language/js20/rationale/syntax.html
// semicolon insertion requires that we reconsider the decision to
// treat op as a binary op if it could be a prefix op.
// Line-Break Semicolon Insertion
// If the first through the nth tokens of a JavaScript program form
// are grammatically valid but the first through the n+1st tokens
// are not and there is a line break between the nth tokens and the
// n+1st tokens, then the parser tries to parse the program again
// after inserting a VirtualSemicolon token between the nth and the
// n+1st tokens.
if ((Operator.FUNCTION_CALL == op
|| null != Operator.lookupOperation(
op.getOpeningSymbol(), OperatorType.PREFIX))
&& !insertionProtected) {
Mark m3 = tq.mark();
tq.rewind(opStart);
if (semicolonInserted()) {
List<Message> messages = mq.getMessages();
if (nMessages < messages.size()) {
messages.subList(nMessages, messages.size()).clear();
}
FilePosition semiPoint = FilePosition.endOf(tq.lastPosition());
messages.add(new Message(
MessageType.SEMICOLON_INSERTED, semiPoint));
return left;
} else {
tq.rewind(m3);
}
}
throw ex;
}
switch (op.getType()) {
case TERNARY:
{
tq.expectToken(op.getClosingSymbol());
Expression farRight = parseExpressionPart(insertionProtected);
left = Operation.create(posFrom(left), op, left, right, farRight);
}
break;
case BRACKET:
if (Operator.FUNCTION_CALL == op) {
// Function calls can take nothing or multiple on the right, so
// we wrap function calls up in an ActualList.
ActualList actuals = (ActualList) right;
List<? extends Expression> params = actuals.children();
Expression[] operands = new Expression[params.size() + 1];
operands[0] = left;
for (int i = 1; i < operands.length; ++i) {
operands[i] = params.get(i - 1);
}
left = Operation.create(posFrom(left), op, operands);
} else {
left = Operation.create(posFrom(left), op, left, right);
}
break;
case INFIX:
if (op.getCategory() == OperatorCategory.ASSIGNMENT
&& !left.isLeftHandSide()) {
throw new ParseException(
new Message(MessageType.ASSIGN_TO_NON_LVALUE,
t.pos, MessagePart.Factory.valueOf(t.text)));
}
left = Operation.create(posFrom(left), op, left, right);
break;
case POSTFIX:
if (op.getCategory() == OperatorCategory.ASSIGNMENT
&& !left.isLeftHandSide()) {
throw new ParseException(
new Message(MessageType.ASSIGN_TO_NON_LVALUE,
t.pos, MessagePart.Factory.valueOf(t.text)));
}
left = Operation.create(posFrom(left), op, left);
break;
default:
throw new SomethingWidgyHappenedError();
}
}
return left;
}
private boolean semicolonInserted() throws ParseException {
if (tq.isEmpty() || tq.lookaheadToken(Punctuation.RCURLY)) { return true; }
FilePosition last = tq.lastPosition(),
current = tq.currentPosition();
if (last == null) { return true; } // Can insert at beginning
if (current.startLineNo() == last.endLineNo()) { return false; }
for (Token<JsTokenType> filtered : tq.filteredTokens()) {
if (filtered.type == JsTokenType.LINE_CONTINUATION) { return false; }
}
return true;
}
private void checkForMissingSemicolon() throws ParseException {
FilePosition current = tq.currentPosition();
FilePosition last = tq.lastPosition();
if (current.source().equals(last.source())
&& current.startLineNo() > last.endLineNo()) {
mq.addMessage(MessageType.MAYBE_MISSING_SEMI, FilePosition.endOf(last));
}
}
private double toNumber(Token<JsTokenType> t) {
// Double.parseDouble is not locale dependent.
return Double.parseDouble(t.text);
}
private String floatToString(Token<JsTokenType> t) throws ParseException {
try {
return NumberLiteral.numberToString(new BigDecimal(t.text));
} catch (NumberFormatException e) {
throw new ParseException(
new Message(
MessageType.MALFORMED_NUMBER, t.pos,
MessagePart.Factory.valueOf(t.text)));
}
}
private NumberLiteral toNumberLiteral(Token<JsTokenType> t) {
return new RealLiteral(t.pos, toNumber(t));
}
private static BigInteger MAX_REPRESENTABLE =
BigInteger.valueOf((1L << 51) -1);
private static BigInteger MIN_REPRESENTABLE =
BigInteger.valueOf(-(1L << 51));
private strictfp long toInteger(Token<JsTokenType> t) {
try {
Number longValue = Long.decode(t.text);
// Make sure that the number fits in a 51 bit mantissa
long lv = longValue.longValue();
if (0 != ((lv < 0 ? ~lv : lv) & ~((1L << 51) - 1))) {
mq.addMessage(MessageType.UNREPRESENTABLE_INTEGER_LITERAL,
t.pos, MessagePart.Factory.valueOf(t.text));
double dv = lv; // strictfp affects this.
return (long) dv;
}
return lv;
} catch (NumberFormatException e) {
Pair<String, Integer> p = breakOutRadix(t.text);
BigInteger bi = new BigInteger(p.a, p.b);
if (bi.compareTo(MIN_REPRESENTABLE) < 0 ||
bi.compareTo(MAX_REPRESENTABLE) > 0) {
mq.addMessage(MessageType.UNREPRESENTABLE_INTEGER_LITERAL,
t.pos, MessagePart.Factory.valueOf(t.text));
}
return bi.longValue();
}
}
private Pair<String, Integer> breakOutRadix(String n) {
if (n.startsWith("0X") || n.startsWith("0x")) {
return Pair.pair(n.substring(2), 16);
} else if (n.startsWith("0")) {
return Pair.pair(n.substring(1), 8);
} else {
return Pair.pair(n, 10);
}
}
private NumberLiteral toIntegerLiteral(Token<JsTokenType> t) {
try {
Number longValue = Long.decode(t.text);
// Make sure that the number fits in a 51 bit mantissa
long lv = longValue.longValue();
if (0 != ((lv < 0 ? ~lv : lv) & ~((1L << 51) - 1))) {
mq.addMessage(MessageType.UNREPRESENTABLE_INTEGER_LITERAL,
t.pos, MessagePart.Factory.valueOf(t.text));
return new RealLiteral(t.pos, lv);
}
return new IntegerLiteral(t.pos, lv);
} catch (NumberFormatException e) {
Pair<String, Integer> p = breakOutRadix(t.text);
BigInteger bi = new BigInteger(p.a, p.b);
if (bi.compareTo(MIN_REPRESENTABLE) < 0 ||
bi.compareTo(MAX_REPRESENTABLE) > 0) {
mq.addMessage(MessageType.UNREPRESENTABLE_INTEGER_LITERAL,
t.pos, MessagePart.Factory.valueOf(t.text));
return new RealLiteral(t.pos, bi.longValue());
}
return new IntegerLiteral(t.pos, bi.longValue());
}
}
@SuppressWarnings("fallthrough")
private AbstractExpression parseExpressionAtom() throws ParseException {
AbstractExpression e;
Mark m = tq.mark();
Token<JsTokenType> t = tq.pop();
typeswitch: switch (t.type) {
case STRING:
issueLintWarningsForProblematicEscapes(t, mq);
e = new StringLiteral(t.pos, t.text);
break;
case INTEGER:
if (integerPartIsOctal(t.text)) {
mq.addMessage(
MessageType.OCTAL_LITERAL, MessageLevel.LINT,
t.pos, MessagePart.Factory.valueOf(t.text));
}
e = toIntegerLiteral(t);
break;
case FLOAT:
if (integerPartIsOctal(t.text)) {
mq.addMessage(
MessageType.OCTAL_LITERAL, MessageLevel.ERROR,
t.pos, MessagePart.Factory.valueOf(t.text));
}
e = toNumberLiteral(t);
break;
case REGEXP:
{
e = new RegexpLiteral(t.pos, t.text);
// Check letters. Warn on s suffix character as non-FF.
String modifiers = t.text.substring(t.text.lastIndexOf("/") + 1);
if (!RegexpLiteral.areRegexpModifiersValid(modifiers)) {
mq.addMessage(
MessageType.UNRECOGNIZED_REGEX_MODIFIERS, t.pos,
MessagePart.Factory.valueOf(modifiers));
}
break;
}
case KEYWORD:
{
Keyword k = Keyword.fromString(t.text);
if (null != k) {
switch (k) {
case NULL:
e = new NullLiteral(t.pos);
break typeswitch;
case TRUE:
e = new BooleanLiteral(t.pos, true);
break typeswitch;
case FALSE:
e = new BooleanLiteral(t.pos, false);
break typeswitch;
case FUNCTION:
{
Identifier identifier = null;
if (!tq.isEmpty() && JsTokenType.WORD == tq.peek().type) {
identifier = parseIdentifierNode(false);
} else {
identifier = new Identifier(
FilePosition.endOf(tq.lastPosition()), null);
}
tq.expectToken(Punctuation.LPAREN);
FormalParamList params = parseFormalParams();
tq.expectToken(Punctuation.RPAREN);
Block body = parseFunctionBody();
e = new FunctionConstructor(
posFrom(m), identifier, params.params, body);
break typeswitch;
}
default:
break; // Will be handled by the word handler below
}
}
// fall through
}
case WORD:
{
String identifier;
if (Keyword.THIS.toString().equals(t.text)) {
// this is allowed, but not t\u0068is as per the grammar
identifier = Keyword.THIS.toString();
} else {
tq.rewind(m);
identifier = parseIdentifier(false);
}
Identifier idNode = new Identifier(posFrom(m), identifier);
finish(idNode, m);
e = new Reference(idNode);
break;
}
case PUNCTUATION:
switch (Punctuation.fromString(t.text)) {
case LPAREN:
e = parseExpressionInt(true);
tq.expectToken(Punctuation.RPAREN);
return e; // Don't pull comments outside parens inside
case LSQUARE:
{
List<Expression> elements = Lists.newArrayList();
if (!tq.checkToken(Punctuation.RSQUARE)) {
Mark comma = null;
do {
// Handle adjacent commas that specify undefined values.
// E.g. [1,,2]
for (Mark cm = tq.mark(); tq.checkToken(Punctuation.COMMA);
cm = tq.mark()) {
comma = cm;
Elision vl = new Elision(posFrom(cm));
finish(vl, cm);
elements.add(vl);
}
if (tq.lookaheadToken(Punctuation.RSQUARE)) { break; }
comma = null;
elements.add(parseExpressionPart(true));
} while (tq.checkToken(Punctuation.COMMA));
if (comma != null) {
// On IE, [1,] has length 2 unlike on other browsers.
mq.addMessage(MessageType.NOT_IE, comma.getFilePosition());
}
tq.expectToken(Punctuation.RSQUARE);
}
e = new ArrayConstructor(posFrom(m), elements);
break;
}
case LCURLY:
{
List<ObjProperty> properties = Lists.newArrayList();
if (!tq.checkToken(Punctuation.RCURLY)) {
boolean sawComma;
do {
Mark km = tq.mark();
Token<JsTokenType> keyToken = tq.peek();
String propertyType = null;
Mark beforeProperty = km;
if (keyToken.type == JsTokenType.WORD) {
if ("get".equals(keyToken.text)
|| "set".equals(keyToken.text)) {
tq.advance();
Mark afterWord = tq.mark();
if (!tq.checkToken(Punctuation.COLON)) {
propertyType = keyToken.text;
beforeProperty = afterWord;
keyToken = tq.peek();
}
tq.rewind(beforeProperty);
}
}
StringLiteral key;
switch (keyToken.type) {
case STRING:
tq.advance();
key = new StringLiteral(posFrom(km), keyToken.text);
break;
case FLOAT:
tq.advance();
key = StringLiteral.valueOf(
posFrom(km), floatToString(keyToken));
break;
case INTEGER:
tq.advance();
key = StringLiteral.valueOf(
posFrom(km), "" + toInteger(keyToken));
break;
default:
String ident = parseIdentifier(true);
key = new StringLiteral(posFrom(km), ident);
break;
}
finish(key, beforeProperty);
ObjProperty prop;
if (propertyType == null) {
tq.expectToken(Punctuation.COLON);
Expression value = parseExpressionPart(true);
prop = new ValueProperty(posFrom(km), key, value);
} else {
Mark beforeFormals = tq.mark();
Identifier ident = new Identifier(tq.currentPosition(), null);
tq.expectToken(Punctuation.LPAREN);
FormalParamList params = parseFormalParams();
tq.expectToken(Punctuation.RPAREN);
Block body = parseFunctionBody();
FunctionConstructor fn = new FunctionConstructor(
posFrom(beforeFormals), ident, params.params, body);
if ("get".equals(propertyType)) {
prop = new GetterProperty(posFrom(km), key, fn);
} else {
assert "set".equals(propertyType);
prop = new SetterProperty(posFrom(km), key, fn);
}
}
finish(prop, km);
properties.add(prop);
Mark cm = tq.mark();
sawComma = tq.checkToken(Punctuation.COMMA);
if (sawComma && tq.lookaheadToken(Punctuation.RCURLY)) {
tq.rewind(cm);
mq.addMessage(MessageType.NOT_IE, tq.currentPosition());
tq.advance();
break;
}
} while (sawComma);
tq.expectToken(Punctuation.RCURLY);
}
e = new ObjectConstructor(posFrom(m), properties);
break;
}
default:
e = null;
break;
}
break;
default:
e = null;
break;
}
if (null == e) {
if (recoverFromFailure) {
tq.rewind(m);
// create a placeholder expression
FilePosition pos = FilePosition.span(
tq.lastPosition(), tq.currentPosition());
mq.addMessage(MessageType.PLACEHOLDER_INSERTED, pos);
Identifier idNode = new Identifier(pos, "_");
e = new Reference(idNode);
} else {
throw new ParseException(
new Message(
MessageType.UNEXPECTED_TOKEN, t.pos,
MessagePart.Factory.valueOf(t.text)));
}
}
finish(e, m);
return e;
}
private Reference parseReference(boolean allowReservedWords)
throws ParseException {
Mark m = tq.mark();
Identifier idNode = parseIdentifierNode(allowReservedWords);
Reference r = new Reference(idNode);
finish(r, m);
return r;
}
private Identifier parseIdentifierNode(boolean allowReservedWords)
throws ParseException {
Mark m = tq.mark();
String identifierName = parseIdentifier(allowReservedWords);
Identifier ident = new Identifier(posFrom(m), identifierName);
finish(ident, m);
return ident;
}
private ExpressionStmt parseExpressionStmt(boolean insertionProtected)
throws ParseException {
Mark m = tq.mark();
Expression e = parseExpressionInt(insertionProtected);
ExpressionStmt es = new ExpressionStmt(posFrom(m), e);
finish(es, m);
return es;
}
private Expression parseExpressionOrNoop(
AbstractExpression def, boolean insertionProtected)
throws ParseException {
Mark m = tq.mark();
if (tq.checkToken(Punctuation.SEMI)) {
finish(def, m);
return def;
}
Expression e = parseExpressionInt(insertionProtected);
tq.expectToken(Punctuation.SEMI);
return e;
}
private static boolean isTerminal(Statement s) {
if (s instanceof LabeledStmtWrapper) {
return isTerminal(((LabeledStmtWrapper) s).getBody());
}
// http://code.google.com/p/google-caja/issues/detail?id=1316
// Note since DoWhileLoop is a Loop, we're allowing do-while to omit
// the semicolon, which deviates from ES[35] but is allowed by browsers.
return (s instanceof Loop
|| s instanceof Conditional || s instanceof FunctionDeclaration
|| s instanceof Block || s instanceof TryStmt
|| s instanceof ForEachLoop || s instanceof SwitchStmt)
|| s instanceof Noop || s instanceof WithStmt;
}
private Statement parseTerminatedStatement() throws ParseException {
Statement s = parseStatement();
if (!isTerminal(s)) { checkSemicolon(); }
return s;
}
private void checkSemicolon() throws ParseException {
// Look for a semicolon
if (tq.checkToken(Punctuation.SEMI)) { return; }
// None found, so maybe do insertion.
if (tq.isEmpty()) { return; }
if (semicolonInserted()) {
FilePosition semiPoint = FilePosition.endOf(tq.lastPosition());
MessageLevel lvl = tq.isEmpty() || tq.lookaheadToken(Punctuation.RCURLY)
? MessageLevel.LOG : MessageLevel.LINT;
mq.addMessage(MessageType.SEMICOLON_INSERTED, lvl, semiPoint);
} else {
tq.expectToken(Punctuation.SEMI); // Just used to throw an exception
}
}
// Visible for testing.
static boolean integerPartIsOctal(String numberLiteral) {
for (int i = 0, n = numberLiteral.length(); i < n; ++i) {
char ch = numberLiteral.charAt(i);
if (ch == '.') { return false; }
if (ch != '0') { return i != 0 && ch >= '1' && ch <= '9'; }
}
return false;
}
private AbstractStatement parseDeclarationsOrExpression(
boolean insertionProtected)
throws ParseException {
Mark m = tq.mark();
boolean isDeclaration;
if (tq.checkToken(Keyword.VAR)) {
isDeclaration = true;
} else if (tq.checkToken(Keyword.CONST)) {
isDeclaration = true;
mq.addMessage(MessageType.NOT_IE, posFrom(m));
} else {
isDeclaration = false;
}
if (isDeclaration) {
AbstractStatement s;
Declaration d;
{
Identifier idNode = parseIdentifierNode(false);
Expression initializer = null;
if (tq.checkToken(Punctuation.EQ)) {
initializer = parseExpressionPart(insertionProtected);
}
d = new Declaration(posFrom(m), idNode, initializer);
finish(d, m);
}
if (tq.checkToken(Punctuation.COMMA)) {
List<Declaration> decls = Lists.newArrayList();
decls.add(d);
do {
Mark m2 = tq.mark();
Identifier idNode = parseIdentifierNode(false);
Expression initializer = null;
if (tq.checkToken(Punctuation.EQ)) {
initializer = parseExpressionPart(insertionProtected);
}
Declaration d2 = new Declaration(posFrom(m2), idNode, initializer);
finish(d2, m2);
decls.add(d2);
} while (tq.checkToken(Punctuation.COMMA));
MultiDeclaration md = new MultiDeclaration(posFrom(m), decls);
finish(md, m);
s = md;
} else {
s = d;
}
return s;
} else {
return parseExpressionStmt(insertionProtected);
}
}
private Statement parseBody(boolean terminal) throws ParseException {
if (terminal || tq.lookaheadToken(Punctuation.LCURLY)) {
return parseTerminatedStatement();
} else {
Statement s = parseStatement();
tq.checkToken(Punctuation.SEMI);
return s;
}
}
private Block parseBodyBlock() throws ParseException {
if (!tq.lookaheadToken(Punctuation.LCURLY)) {
tq.expectToken(Punctuation.LCURLY);
}
return (Block) parseTerminatedStatement();
}
private FormalParamList parseFormalParams() throws ParseException {
List<FormalParam> params = Lists.newArrayList();
if (!tq.lookaheadToken(Punctuation.RPAREN)) {
do {
Mark m = tq.mark();
FormalParam param = new FormalParam(parseIdentifierNode(false));
finish(param, m);
params.add(param);
} while (tq.checkToken(Punctuation.COMMA));
}
return new FormalParamList(params, mq);
}
private static Noop noop(FilePosition fp) {
Noop n = new Noop(fp);
n.setComments(Collections.<Token<?>>emptyList());
return n;
}
/** The file position that spans from startMark to the current position. */
private FilePosition posFrom(Mark startMark) throws ParseException {
return posFrom(startMark.getFilePosition());
}
private FilePosition posFrom(ParseTreeNode childFlushWithStart)
throws ParseException {
return posFrom(childFlushWithStart.getFilePosition());
}
private FilePosition posFrom(FilePosition start) throws ParseException {
return (tq.isEmpty() || tq.currentPosition() != start)
? FilePosition.span(start, tq.lastPosition())
: FilePosition.startOf(start);
}
/**
* Attaches to the parse tree filtered tokens,
* such as type annotation carrying comments.
*/
private void finish(AbstractParseTreeNode n, Mark startMark)
throws ParseException {
Mark endMark = tq.mark();
tq.rewind(startMark);
try {
n.setComments(tq.filteredTokens());
} finally {
tq.rewind(endMark);
}
}
private static class FormalParamList {
public List<FormalParam> params;
public FormalParamList(List<FormalParam> params, MessageQueue mq) {
Set<String> paramNames = Sets.newHashSet();
paramNames.add("arguments");
paramNames.add("this");
for (FormalParam p : params) {
if (!paramNames.add(p.getIdentifierName())) {
mq.addMessage(
MessageType.DUPLICATE_FORMAL_PARAM,
p.getFilePosition(),
MessagePart.Factory.valueOf(p.getIdentifierName()));
}
}
this.params = params;
}
}
/**
* Placeholder node for the actuals in a function call. Never appears in the
* final tree.
*/
private static class ActualList extends AbstractExpression {
private static final long serialVersionUID = 4826666277822756805L;
ActualList(List<Expression> actuals) {
super(FilePosition.UNKNOWN, Expression.class);
createMutation().appendChildren(actuals).execute();
}
@Override
public List<? extends Expression> children() {
return childrenAs(Expression.class);
}
@Override
public Object getValue() { return null; }
public void render(RenderContext rc) {
throw new UnsupportedOperationException();
}
public String typeOf() { return null; }
public JsonML toJsonML() { throw new UnsupportedOperationException(); }
}
private static void issueLintWarningsForProblematicEscapes(
Token<JsTokenType> t, MessageQueue mq) {
String body = t.text.substring(1, t.text.length() - 1);
for (int i = -1; (i = body.indexOf('\\', i + 1)) >= 0; ++i) {
char next = body.charAt(i + 1);
switch (next) {
// control character escapes
// \b is problematic since it has a different meaning in a regexp than
// in a string literal.
case 'b': case 'f': case 'n': case 'r': case 't':
// numeric escape prefixes
case 'u': case 'x': case 'X':
case '0': case '1': case '2': case '3':
case '4': case '5': case '6': case '7':
// special characters that can appear in strings.
// The / is used since it is often escaped to prevent close tags
// in strings from appearing to end a script block.
case '\\': case '/': case '\'': case '"':
break;
// specified in ES3 but not implemented consistently.
case 'v':
mq.addMessage(
MessageType.AMBIGUOUS_ESCAPE_SEQUENCE,
t.pos, MessagePart.Factory.valueOf("\\" + next));
break;
case 's': case 'w': case 'S': case 'W':
case '+': case '?': case '*': case '.': case '-': case '|':
case '^': case '$':
case '[': case ']': case '(': case ')': case '{': case '}':
mq.addMessage(
MessageType.REDUNDANT_ESCAPE_SEQUENCE,
t.pos, MessagePart.Factory.valueOf("\\" + next));
break;
}
}
}
private static Operation checkInExprWithLhs(Statement s) {
if (!(s instanceof ExpressionStmt)) { return null; }
Expression e = ((ExpressionStmt) s).getExpression();
if (!(e instanceof Operation)) { return null; }
Operation op = (Operation) e;
if (Operator.IN != op.getOperator()) { return null; }
return op.children().get(0).isLeftHandSide() ? op : null;
}
}