mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
843 lines
29 KiB
JavaScript
843 lines
29 KiB
JavaScript
///// JAVASCRIPT PARSER
|
|
|
|
// What we don't support from ECMA-262 5.1:
|
|
// - object literal trailing comma
|
|
// - object literal get/set
|
|
|
|
(function () {
|
|
|
|
var expecting = Parser.expecting;
|
|
|
|
var assertion = Parsers.assertion;
|
|
var node = Parsers.node;
|
|
var or = Parsers.or;
|
|
var and = Parsers.and;
|
|
var not = Parsers.not;
|
|
var list = Parsers.list;
|
|
var seq = Parsers.seq;
|
|
var opt = Parsers.opt;
|
|
var constant = Parsers.constant;
|
|
var mapResult = Parsers.mapResult;
|
|
|
|
|
|
var makeSet = function (array) {
|
|
var s = {};
|
|
for (var i = 0, N = array.length; i < N; i++)
|
|
s[array[i]] = true;
|
|
return s;
|
|
};
|
|
|
|
|
|
JSParser = function (code, options) {
|
|
this.lexer = new JSLexer(code);
|
|
this.oldToken = null;
|
|
this.newToken = null;
|
|
this.pos = 0;
|
|
this.isLineTerminatorHere = false;
|
|
this.includeComments = false;
|
|
// the last COMMENT lexeme between oldToken and newToken
|
|
// that we've consumed, if any.
|
|
this.lastCommentConsumed = null;
|
|
|
|
options = options || {};
|
|
// pass {tokens:'strings'} to get strings for
|
|
// tokens instead of token objects
|
|
if (options.tokens === 'strings') {
|
|
this.tokenFunc = function (tok) {
|
|
return tok.text();
|
|
};
|
|
} else {
|
|
this.tokenFunc = function (tok) {
|
|
return tok;
|
|
};
|
|
}
|
|
|
|
// pass {includeComments: true} to include comments in the AST. For
|
|
// a comment to be included, it must occur where a series of
|
|
// statements could occur, and it must be preceded by only comments
|
|
// and whitespace on the same line.
|
|
if (options.includeComments) {
|
|
this.includeComments = true;
|
|
}
|
|
};
|
|
|
|
JSParser.prototype.consumeNewToken = function () {
|
|
var self = this;
|
|
var lexer = self.lexer;
|
|
self.oldToken = self.newToken;
|
|
self.isLineTerminatorHere = false;
|
|
var lex;
|
|
do {
|
|
lex = lexer.next();
|
|
if (lex.isError())
|
|
throw new Error("Bad token at " +
|
|
JSLexer.prettyOffset(lexer.code, lex.startPos()) +
|
|
", text `" + lex.text() + "`");
|
|
else if (lex.type() === "NEWLINE")
|
|
self.isLineTerminatorHere = true;
|
|
else if (lex.type() === "COMMENT" && ! /^.*$/.test(lex.text()))
|
|
// multiline comments containing line terminators count
|
|
// as line terminators.
|
|
self.isLineTerminatorHere = true;
|
|
} while (! lex.isEOF() && ! lex.isToken());
|
|
self.newToken = lex;
|
|
self.pos = lex.startPos();
|
|
self.lastCommentConsumed = null;
|
|
};
|
|
|
|
JSParser.prototype.getParseError = function (expecting, found) {
|
|
var msg = (expecting ? "Expected " + expecting : "Unexpected token");
|
|
if (this.oldToken)
|
|
msg += " after " + this.oldToken;
|
|
var pos = this.pos;
|
|
msg += " at " + JSLexer.prettyOffset(this.lexer.code, pos);
|
|
msg += ", found " + (found || this.newToken);
|
|
return new Error(msg);
|
|
};
|
|
|
|
JSParser.prototype.getSyntaxTree = function () {
|
|
var self = this;
|
|
|
|
self.consumeNewToken();
|
|
|
|
var NIL = new ParseNode('nil', []);
|
|
|
|
var booleanFlaggedParser = function (parserConstructFunc) {
|
|
return {
|
|
'false': parserConstructFunc(false),
|
|
'true': parserConstructFunc(true)
|
|
};
|
|
};
|
|
|
|
// Takes a space-separated list of either punctuation or keyword tokens
|
|
var lookAheadToken = function (tokens) {
|
|
var type = (/\w/.test(tokens) ? 'KEYWORD' : 'PUNCTUATION');
|
|
var textSet = makeSet(tokens.split(' '));
|
|
return expecting(
|
|
tokens.split(' ').join(', '),
|
|
assertion(function (t) {
|
|
return (t.newToken.type() === type && textSet[t.newToken.text()]);
|
|
}));
|
|
};
|
|
|
|
var lookAheadTokenType = function (type) {
|
|
return expecting(type, assertion(function (t) {
|
|
return t.newToken.type() === type;
|
|
}));
|
|
};
|
|
|
|
// Takes a space-separated list of either punctuation or keyword tokens
|
|
var token = function (tokens) {
|
|
var type = (/\w/.test(tokens) ? 'KEYWORD' : 'PUNCTUATION');
|
|
var textSet = makeSet(tokens.split(' '));
|
|
return new Parser(
|
|
tokens.split(' ').join(', '),
|
|
function (t) {
|
|
if (t.newToken.type() === type && textSet[t.newToken.text()]) {
|
|
t.consumeNewToken();
|
|
return self.tokenFunc(t.oldToken);
|
|
}
|
|
return null;
|
|
});
|
|
};
|
|
|
|
var tokenType = function (type) {
|
|
return new Parser(type, function (t) {
|
|
if (t.newToken.type() === type) {
|
|
t.consumeNewToken();
|
|
return self.tokenFunc(t.oldToken);
|
|
}
|
|
return null;
|
|
});
|
|
};
|
|
|
|
var noLineTerminatorHere = expecting(
|
|
'noLineTerminator', assertion(function (t) {
|
|
return ! t.isLineTerminatorHere;
|
|
}));
|
|
|
|
var nonLHSExpressionNames = makeSet(
|
|
'unary binary postfix ternary assignment comma'.split(' '));
|
|
var isExpressionLHS = function (exprNode) {
|
|
return ! nonLHSExpressionNames[exprNode.name];
|
|
};
|
|
|
|
// Like token, but marks tokens that need to defy the lexer's
|
|
// heuristic about whether the next '/' is a division or
|
|
// starts a regex.
|
|
var preSlashToken = function (text, divisionNotRegex) {
|
|
var inner = token(text);
|
|
return new Parser(
|
|
inner.expecting,
|
|
function (t) {
|
|
// temporarily set divisionPermitted,
|
|
// restoring it if we don't match.
|
|
var oldValue = t.lexer.divisionPermitted;
|
|
var result;
|
|
try {
|
|
t.lexer.divisionPermitted = divisionNotRegex;
|
|
result = inner.parse(t);
|
|
return result;
|
|
} finally {
|
|
if (! result)
|
|
t.lexer.divisionPermitted = oldValue;
|
|
}
|
|
});
|
|
};
|
|
|
|
// Mark some productions "lazy" to allow grammar circularity, i.e. accessing
|
|
// later parsers from earlier ones.
|
|
// These lazy versions will be replaced with real ones, which they will
|
|
// access when run.
|
|
var expressionMaybeNoIn = {
|
|
'false': Parsers.lazy(
|
|
'expression',
|
|
function () { return expressionMaybeNoIn[false]; }),
|
|
'true': Parsers.lazy(
|
|
'expression',
|
|
function () { return expressionMaybeNoIn[true]; })
|
|
};
|
|
var expression = expressionMaybeNoIn[false];
|
|
|
|
var assignmentExpressionMaybeNoIn = {
|
|
'false': Parsers.lazy(
|
|
'expression',
|
|
function () { return assignmentExpressionMaybeNoIn[false]; }),
|
|
'true': Parsers.lazy(
|
|
'expression',
|
|
function () { return assignmentExpressionMaybeNoIn[true]; })
|
|
};
|
|
var assignmentExpression = assignmentExpressionMaybeNoIn[false];
|
|
|
|
var functionBody = Parsers.lazy(
|
|
'statement', function () { return functionBody; });
|
|
var statement = Parsers.lazy(
|
|
'statement', function () { return statement; });
|
|
////
|
|
|
|
var arrayLiteral =
|
|
node('array',
|
|
seq(token('['),
|
|
opt(list(token(','))),
|
|
or(
|
|
lookAheadToken(']'),
|
|
list(
|
|
expecting(
|
|
'expression',
|
|
or(assignmentExpression,
|
|
// count a peeked-at ']' as an expression
|
|
// to support elisions at end, e.g.
|
|
// `[1,2,3,,,,,,]`.
|
|
lookAheadToken(']'))),
|
|
// list seperator is one or more commas
|
|
// to support elision
|
|
list(token(',')))),
|
|
token(']')));
|
|
|
|
// "IdentifierName" in ES5 allows reserved words, like in a property access
|
|
// or a key of an object literal.
|
|
// Put IDENTIFIER last so it shows up in the error message.
|
|
var identifierName = or(tokenType('NULL'), tokenType('BOOLEAN'),
|
|
tokenType('KEYWORD'), tokenType('IDENTIFIER'));
|
|
|
|
var propertyName = expecting('propertyName', or(
|
|
node('idPropName', identifierName),
|
|
node('numPropName', tokenType('NUMBER')),
|
|
node('strPropName', tokenType('STRING'))));
|
|
var nameColonValue = expecting(
|
|
'propertyName',
|
|
node('prop', seq(propertyName, token(':'), assignmentExpression)));
|
|
|
|
// Allow trailing comma in object literal, per ES5. Trailing comma
|
|
// must follow a `name:value`, that is, `{,}` is invalid.
|
|
//
|
|
// We can't just use a normal comma list(), because it will seize
|
|
// on the comma as a sign that the list continues. Instead,
|
|
// we specify a list of either ',' or nameColonValue, using positive
|
|
// and negative lookAheads to constrain the sequence. The grammar
|
|
// is ordered so that error messages will always say
|
|
// "Expected propertyName" or "Expected ," as appropriate, not
|
|
// "Expected ," when the look-ahead is negative or "Expected }".
|
|
var objectLiteral =
|
|
node('object',
|
|
seq(token('{'),
|
|
or(lookAheadToken('}'),
|
|
and(not(lookAheadToken(',')),
|
|
list(or(seq(token(','),
|
|
expecting('propertyName',
|
|
not(lookAheadToken(',')))),
|
|
seq(nameColonValue,
|
|
or(lookAheadToken('}'),
|
|
lookAheadToken(','))))))),
|
|
expecting('propertyName', token('}'))));
|
|
|
|
var functionMaybeNameRequired = booleanFlaggedParser(
|
|
function (nameRequired) {
|
|
return seq(token('function'),
|
|
(nameRequired ? tokenType('IDENTIFIER') :
|
|
or(tokenType('IDENTIFIER'),
|
|
and(lookAheadToken('('), constant(NIL)))),
|
|
token('('),
|
|
or(lookAheadToken(')'),
|
|
list(tokenType('IDENTIFIER'), token(','))),
|
|
token(')'),
|
|
token('{'),
|
|
functionBody,
|
|
token('}'));
|
|
});
|
|
var functionExpression = node('functionExpr',
|
|
functionMaybeNameRequired[false]);
|
|
|
|
var primaryOrFunctionExpression =
|
|
expecting('expression',
|
|
or(node('this', token('this')),
|
|
node('identifier', tokenType('IDENTIFIER')),
|
|
node('number', tokenType('NUMBER')),
|
|
node('boolean', tokenType('BOOLEAN')),
|
|
node('null', tokenType('NULL')),
|
|
node('regex', tokenType('REGEX')),
|
|
node('string', tokenType('STRING')),
|
|
node('parens',
|
|
seq(token('('), expression, token(')'))),
|
|
arrayLiteral,
|
|
objectLiteral,
|
|
functionExpression));
|
|
|
|
|
|
var dotEnding = seq(token('.'), identifierName);
|
|
var bracketEnding = seq(token('['), expression, token(']'));
|
|
var callArgs = seq(token('('),
|
|
or(lookAheadToken(')'),
|
|
list(assignmentExpression,
|
|
token(','))),
|
|
token(')'));
|
|
|
|
var newKeyword = token('new');
|
|
|
|
// This is a completely equivalent refactor of the spec's production
|
|
// for a LeftHandSideExpression.
|
|
//
|
|
// An lhsExpression is basically an expression that can serve as
|
|
// the left-hand-side of an assignment, though function calls and
|
|
// "new" invocation are included because they have the same
|
|
// precedence. Actually, the spec technically allows a function
|
|
// call to "return" a valid l-value, as in `foo(bar) = baz`,
|
|
// though no built-in or user-specifiable call has this property
|
|
// (it would have to be defined by a browser or other "host").
|
|
var lhsExpression = new Parser(
|
|
'expression',
|
|
function (t) {
|
|
// Accumulate all initial "new" keywords, not yet knowing
|
|
// if they have a corresponding argument list later.
|
|
var news = [];
|
|
var n;
|
|
while ((n = newKeyword.parse(t)))
|
|
news.push(n);
|
|
|
|
// Read the primaryOrFunctionExpression that will be the "core"
|
|
// of this lhsExpression. It is preceded by zero or more `new`
|
|
// keywords, and followed by any sequence of (...), [...],
|
|
// and .foo add-ons.
|
|
// if we have 'new' keywords, we are committed and must
|
|
// match an expression or error.
|
|
var result = primaryOrFunctionExpression.parseRequiredIf(t, news.length);
|
|
if (! result)
|
|
return null;
|
|
|
|
// Our plan of attack is to apply each dot, bracket, or call
|
|
// as we come across it. Whether a call is a `new` call depends
|
|
// on whether there are `new` keywords we haven't used. If so,
|
|
// we pop one off the stack.
|
|
var done = false;
|
|
while (! done) {
|
|
var r;
|
|
if ((r = dotEnding.parse(t))) {
|
|
result = new ParseNode('dot', [result].concat(r));
|
|
} else if ((r = bracketEnding.parse(t))) {
|
|
result = new ParseNode('bracket', [result].concat(r));
|
|
} else if ((r = callArgs.parse(t))) {
|
|
if (news.length)
|
|
result = new ParseNode('newcall', [news.pop(), result].concat(r));
|
|
else
|
|
result = new ParseNode('call', [result].concat(r));
|
|
} else {
|
|
done = true;
|
|
}
|
|
}
|
|
|
|
// There may be more `new` keywords than calls, which is how
|
|
// paren-less constructions (`new Date`) are parsed. We've
|
|
// already handled `new foo().bar()`, now handle `new new foo().bar`.
|
|
while (news.length)
|
|
result = new ParseNode('new', [news.pop(), result]);
|
|
|
|
return result;
|
|
});
|
|
|
|
var postfixToken = token('++ --');
|
|
var postfixLookahead = lookAheadToken('++ --');
|
|
var postfixExpression = expecting(
|
|
'expression',
|
|
mapResult(seq(lhsExpression,
|
|
opt(and(noLineTerminatorHere,
|
|
postfixLookahead,
|
|
postfixToken))),
|
|
function (v) {
|
|
if (v.length === 1)
|
|
return v[0];
|
|
return new ParseNode('postfix', v);
|
|
}));
|
|
|
|
var unaryExpression = Parsers.unary(
|
|
'unary', postfixExpression,
|
|
or(token('delete void typeof'),
|
|
preSlashToken('++ -- + - ~ !', false)));
|
|
|
|
// The "noIn" business is all to facilitate parsing
|
|
// of for-in constructs, though the cases that make
|
|
// this required are quite obscure.
|
|
// The `for(var x in y)` form is allowed to take
|
|
// an initializer for `x` (which is only useful for
|
|
// its side effects, or if `y` has no properties).
|
|
// So an example might be:
|
|
// `for(var x = a().b in c);`
|
|
// In this example, `var x = a().b` is parsed without
|
|
// the `in`, which would otherwise be part of the
|
|
// varDecl, using varDeclNoIn.
|
|
|
|
// Our binaryExpression is the spec's LogicalORExpression,
|
|
// which includes all the higher-precendence operators.
|
|
var binaryExpressionMaybeNoIn = booleanFlaggedParser(
|
|
function (noIn) {
|
|
// high to low precedence
|
|
var binaryOps = [token('* / %'),
|
|
token('+ -'),
|
|
token('<< >> >>>'),
|
|
or(token('< > <= >='),
|
|
noIn ? token('instanceof') :
|
|
token('instanceof in')),
|
|
token('== != === !=='),
|
|
token('&'),
|
|
token('^'),
|
|
token('|'),
|
|
token('&&'),
|
|
token('||')];
|
|
return expecting(
|
|
'expression',
|
|
Parsers.binaryLeft('binary', unaryExpression, binaryOps));
|
|
});
|
|
var binaryExpression = binaryExpressionMaybeNoIn[false];
|
|
|
|
var conditionalExpressionMaybeNoIn = booleanFlaggedParser(
|
|
function (noIn) {
|
|
return expecting(
|
|
'expression',
|
|
mapResult(
|
|
seq(binaryExpressionMaybeNoIn[noIn],
|
|
opt(seq(
|
|
token('?'),
|
|
assignmentExpression, token(':'),
|
|
assignmentExpressionMaybeNoIn[noIn]))),
|
|
function (v) {
|
|
if (v.length === 1)
|
|
return v[0];
|
|
return new ParseNode('ternary', v);
|
|
}));
|
|
});
|
|
var conditionalExpression = conditionalExpressionMaybeNoIn[false];
|
|
|
|
var assignOp = token('= *= /= %= += -= <<= >>= >>>= &= ^= |=');
|
|
|
|
assignmentExpressionMaybeNoIn = booleanFlaggedParser(
|
|
function (noIn) {
|
|
return new Parser(
|
|
'expression',
|
|
function (t) {
|
|
var r = conditionalExpressionMaybeNoIn[noIn].parse(t);
|
|
if (! r)
|
|
return null;
|
|
|
|
// Assignment is right-associative.
|
|
// Plan of attack: make a list of all the parts
|
|
// [expression, op, expression, op, ... expression]
|
|
// and then fold them up at the end.
|
|
var parts = [r];
|
|
var op;
|
|
while (isExpressionLHS(r) &&(op = assignOp.parse(t)))
|
|
parts.push(op,
|
|
conditionalExpressionMaybeNoIn[noIn].parseRequired(t));
|
|
|
|
var result = parts.pop();
|
|
while (parts.length) {
|
|
op = parts.pop();
|
|
var lhs = parts.pop();
|
|
result = new ParseNode('assignment', [lhs, op, result]);
|
|
}
|
|
return result;
|
|
});
|
|
});
|
|
assignmentExpression = assignmentExpressionMaybeNoIn[false];
|
|
|
|
expressionMaybeNoIn = booleanFlaggedParser(
|
|
function (noIn) {
|
|
return expecting(
|
|
'expression',
|
|
mapResult(
|
|
list(assignmentExpressionMaybeNoIn[noIn], token(',')),
|
|
function (v) {
|
|
if (v.length === 1)
|
|
return v[0];
|
|
return new ParseNode('comma', v);
|
|
}));
|
|
});
|
|
expression = expressionMaybeNoIn[false];
|
|
|
|
// STATEMENTS
|
|
|
|
var comment = node('comment', new Parser(null, function (t) {
|
|
if (! t.includeComments)
|
|
return null;
|
|
|
|
// Match a COMMENT lexeme between oldToken and newToken.
|
|
//
|
|
// This is an unusual Parser because it doesn't match and consume
|
|
// newToken, but instead uses the next()/prev() API on lexemes.
|
|
// It assumes it can walk the linked list backwards from newToken
|
|
// (though not necessarily forwards).
|
|
//
|
|
// We start at the last comment we've visited for this
|
|
// oldToken/newToken pair, if any, or else oldToken, or else the
|
|
// beginning of the token stream. We ignore comments that are
|
|
// preceded by any non-comment source code on the same line.
|
|
var lexeme = (t.lastCommentConsumed || t.oldToken || null);
|
|
if (! lexeme) {
|
|
// no oldToken, must be on first token. walk backwards
|
|
// to start with first lexeme (which may be a comment
|
|
// or whitespace)
|
|
lexeme = t.newToken;
|
|
while (lexeme.prev())
|
|
lexeme = lexeme.prev();
|
|
} else {
|
|
// start with lexeme after last token or comment consumed
|
|
lexeme = lexeme.next();
|
|
}
|
|
var seenNewline = ((! t.oldToken) || t.lastCommentConsumed || false);
|
|
while (lexeme !== t.newToken) {
|
|
var type = lexeme.type();
|
|
if (type === "NEWLINE") {
|
|
seenNewline = true;
|
|
} else if (type === "COMMENT") {
|
|
t.lastCommentConsumed = lexeme;
|
|
if (seenNewline)
|
|
return lexeme;
|
|
}
|
|
lexeme = lexeme.next();
|
|
}
|
|
return null;
|
|
}));
|
|
|
|
var statements = list(or(comment, statement));
|
|
|
|
// implements JavaScript's semicolon "insertion" rules
|
|
var maybeSemicolon = expecting(
|
|
'semicolon',
|
|
or(token(';'),
|
|
and(
|
|
or(
|
|
lookAheadToken('}'),
|
|
lookAheadTokenType('EOF'),
|
|
assertion(function (t) {
|
|
return t.isLineTerminatorHere;
|
|
})),
|
|
constant(new ParseNode(';', [])))));
|
|
|
|
var expressionStatement = node(
|
|
'expressionStmnt',
|
|
and(
|
|
not(or(lookAheadToken('{'), lookAheadToken('function'))),
|
|
seq(expression,
|
|
expecting('semicolon',
|
|
or(maybeSemicolon,
|
|
// allow presence of colon to terminate
|
|
// statement legally, for the benefit of
|
|
// expressionOrLabelStatement. Basically assume
|
|
// an implicit semicolon. This
|
|
// is safe because a colon can never legally
|
|
// follow a semicolon anyway.
|
|
and(lookAheadToken(':'),
|
|
constant(new ParseNode(';', []))))))));
|
|
|
|
// it's hard to parse statement labels, as in
|
|
// `foo: x = 1`, because we can't tell from the
|
|
// first token whether we are looking at an expression
|
|
// statement or a label statement. To work around this,
|
|
// expressionOrLabelStatement parses the expression and
|
|
// then rewrites the result if it is an identifier
|
|
// followed by a colon.
|
|
var labelColonAndStatement = seq(token(':'), statement);
|
|
var noColon = expecting(
|
|
'semicolon', not(lookAheadToken(':')));
|
|
var expressionOrLabelStatement = new Parser(
|
|
null,
|
|
function (t) {
|
|
var exprStmnt = expressionStatement.parse(t);
|
|
if (! exprStmnt)
|
|
return null;
|
|
|
|
var expr = exprStmnt.children[0];
|
|
var maybeSemi = exprStmnt.children[1];
|
|
if (expr.name !== 'identifier' ||
|
|
! (maybeSemi instanceof ParseNode)) {
|
|
// We either have a non-identifier expression or a present
|
|
// semicolon. This is not a label.
|
|
//
|
|
// Fail now if we are looking at a colon, causing an
|
|
// error message on input like `1+1:` of the same kind
|
|
// you'd get without statement label parsing.
|
|
noColon.parseRequired(t);
|
|
return exprStmnt;
|
|
}
|
|
|
|
var rest = labelColonAndStatement.parse(t);
|
|
if (! rest)
|
|
return exprStmnt;
|
|
|
|
return new ParseNode('labelStmnt',
|
|
[expr.children[0]].concat(rest));
|
|
});
|
|
|
|
var emptyStatement = node('emptyStmnt', token(';')); // required semicolon
|
|
|
|
var blockStatement = expecting('block', node('blockStmnt', seq(
|
|
token('{'), or(lookAheadToken('}'), statements),
|
|
token('}'))));
|
|
|
|
var varDeclMaybeNoIn = booleanFlaggedParser(function (noIn) {
|
|
return node(
|
|
'varDecl',
|
|
seq(tokenType('IDENTIFIER'),
|
|
opt(seq(token('='),
|
|
assignmentExpressionMaybeNoIn[noIn]))));
|
|
});
|
|
var varDecl = varDeclMaybeNoIn[false];
|
|
|
|
var variableStatement = node(
|
|
'varStmnt',
|
|
seq(token('var'), list(varDecl, token(',')),
|
|
maybeSemicolon));
|
|
|
|
// A paren that may be followed by a statement
|
|
// beginning with a regex literal.
|
|
var closeParenBeforeStatement = preSlashToken(')', false);
|
|
|
|
var ifStatement = node(
|
|
'ifStmnt',
|
|
seq(token('if'), token('('), expression,
|
|
closeParenBeforeStatement, statement,
|
|
opt(seq(token('else'), statement))));
|
|
|
|
var secondThirdClauses = expecting(
|
|
'semicolon',
|
|
and(lookAheadToken(';'),
|
|
seq(
|
|
expecting('semicolon', token(';')),
|
|
or(and(lookAheadToken(';'),
|
|
constant(NIL)),
|
|
expression),
|
|
expecting('semicolon', token(';')),
|
|
or(and(lookAheadToken(')'),
|
|
constant(NIL)),
|
|
expression))));
|
|
var inExpr = seq(token('in'), expression);
|
|
var inExprExpectingSemi = expecting('semicolon',
|
|
seq(token('in'), expression));
|
|
var forSpec = mapResult(node(
|
|
'forSpec',
|
|
or(seq(token('var'),
|
|
varDeclMaybeNoIn[true],
|
|
expecting(
|
|
'commaOrIn',
|
|
or(inExpr,
|
|
seq(
|
|
or(
|
|
lookAheadToken(';'),
|
|
seq(token(','),
|
|
list(varDeclMaybeNoIn[true], token(',')))),
|
|
secondThirdClauses)))),
|
|
// get the case where the first clause is empty out of the way.
|
|
// the lookAhead's return value is the empty placeholder for the
|
|
// missing expression.
|
|
seq(and(lookAheadToken(';'),
|
|
constant(NIL)), secondThirdClauses),
|
|
// custom parser the non-var case because we have to
|
|
// read the first expression before we know if there's
|
|
// an "in".
|
|
new Parser(
|
|
null,
|
|
function (t) {
|
|
var firstExpr = expressionMaybeNoIn[true].parse(t);
|
|
if (! firstExpr)
|
|
return null;
|
|
var rest = secondThirdClauses.parse(t);
|
|
if (! rest) {
|
|
// we need a left-hand-side expression for a
|
|
// `for (x in y)` loop.
|
|
if (! isExpressionLHS(firstExpr))
|
|
throw t.getParseError("semicolon");
|
|
// if we don't see 'in' at this point, it's probably
|
|
// a missing semicolon
|
|
rest = inExprExpectingSemi.parseRequired(t);
|
|
}
|
|
|
|
return [firstExpr].concat(rest);
|
|
}))),
|
|
function (clauses) {
|
|
// There are four kinds of for-loop, and we call the
|
|
// part between the parens one of forSpec, forVarSpec,
|
|
// forInSpec, and forVarInSpec. Having parsed it
|
|
// already, we rewrite the node name based on how
|
|
// many items came out. forIn and forVarIn always
|
|
// have 3 and 4 items respectively. for has 5
|
|
// (the optional expressions are present as nils).
|
|
// forVar has 6 or more, because `for(var x;;);`
|
|
// produces [`var` `x` `;` nil `;` nil].
|
|
var numChildren = clauses.children.length;
|
|
if (numChildren === 3)
|
|
return new ParseNode('forInSpec', clauses.children);
|
|
else if (numChildren === 4)
|
|
return new ParseNode('forVarInSpec', clauses.children);
|
|
else if (numChildren >= 6)
|
|
return new ParseNode('forVarSpec', clauses.children);
|
|
return clauses;
|
|
});
|
|
|
|
var iterationStatement = or(
|
|
node('doStmnt', seq(token('do'), statement, token('while'),
|
|
token('('), expression, token(')'),
|
|
maybeSemicolon)),
|
|
node('whileStmnt', seq(token('while'), token('('), expression,
|
|
closeParenBeforeStatement, statement)),
|
|
// semicolons must be real, not maybeSemicolons
|
|
node('forStmnt', seq(
|
|
token('for'), token('('), forSpec, closeParenBeforeStatement,
|
|
statement)));
|
|
|
|
var returnStatement = node(
|
|
'returnStmnt',
|
|
seq(token('return'), or(
|
|
and(noLineTerminatorHere, expression), constant(NIL)),
|
|
maybeSemicolon));
|
|
var continueStatement = node(
|
|
'continueStmnt',
|
|
seq(token('continue'), or(
|
|
and(noLineTerminatorHere, tokenType('IDENTIFIER')), constant(NIL)),
|
|
maybeSemicolon));
|
|
var breakStatement = node(
|
|
'breakStmnt',
|
|
seq(token('break'), or(
|
|
and(noLineTerminatorHere, tokenType('IDENTIFIER')), constant(NIL)),
|
|
maybeSemicolon));
|
|
var throwStatement = node(
|
|
'throwStmnt',
|
|
seq(token('throw'),
|
|
and(or(noLineTerminatorHere,
|
|
// If there is a line break here and more tokens after,
|
|
// we want to error appropriately. `throw \n e` should
|
|
// complain about the "end of line", not the `e`.
|
|
and(not(lookAheadTokenType("EOF")),
|
|
new Parser(null,
|
|
function (t) {
|
|
throw t.getParseError('expression', 'end of line');
|
|
}))),
|
|
expression),
|
|
maybeSemicolon));
|
|
|
|
var withStatement = node(
|
|
'withStmnt',
|
|
seq(token('with'), token('('), expression, closeParenBeforeStatement,
|
|
statement));
|
|
|
|
var switchCase = node(
|
|
'case',
|
|
seq(token('case'), expression, token(':'),
|
|
or(lookAheadToken('}'),
|
|
lookAheadToken('case default'),
|
|
statements)));
|
|
var switchDefault = node(
|
|
'default',
|
|
seq(token('default'), token(':'),
|
|
or(lookAheadToken('}'),
|
|
lookAheadToken('case'),
|
|
statements)));
|
|
|
|
var switchStatement = node(
|
|
'switchStmnt',
|
|
seq(token('switch'), token('('), expression, token(')'),
|
|
token('{'),
|
|
or(lookAheadToken('}'),
|
|
lookAheadToken('default'),
|
|
list(switchCase)),
|
|
opt(seq(switchDefault,
|
|
opt(list(switchCase)))),
|
|
token('}')));
|
|
|
|
var catchFinally = expecting(
|
|
'catch',
|
|
and(lookAheadToken('catch finally'),
|
|
seq(
|
|
or(node(
|
|
'catch',
|
|
seq(token('catch'), token('('), tokenType('IDENTIFIER'),
|
|
token(')'), blockStatement)),
|
|
constant(NIL)),
|
|
or(node(
|
|
'finally',
|
|
seq(token('finally'), blockStatement)),
|
|
constant(NIL)))));
|
|
var tryStatement = node(
|
|
'tryStmnt',
|
|
seq(token('try'), blockStatement, catchFinally));
|
|
var debuggerStatement = node(
|
|
'debuggerStmnt', seq(token('debugger'), maybeSemicolon));
|
|
|
|
statement = expecting('statement',
|
|
or(expressionOrLabelStatement,
|
|
emptyStatement,
|
|
blockStatement,
|
|
variableStatement,
|
|
ifStatement,
|
|
iterationStatement,
|
|
returnStatement,
|
|
continueStatement,
|
|
breakStatement,
|
|
withStatement,
|
|
switchStatement,
|
|
throwStatement,
|
|
tryStatement,
|
|
debuggerStatement));
|
|
|
|
// PROGRAM
|
|
|
|
var functionDecl = node(
|
|
'functionDecl', functionMaybeNameRequired[true]);
|
|
|
|
// Look for statement before functionDecl, to catch comments in
|
|
// includeComments mode. A statement can't start with 'function'
|
|
// anyway, so the order doesn't matter otherwise.
|
|
var sourceElement = or(statement, functionDecl);
|
|
var sourceElements = list(or(comment, sourceElement));
|
|
|
|
functionBody = expecting(
|
|
'functionBody', or(lookAheadToken('}'), sourceElements));
|
|
|
|
var program = node(
|
|
'program',
|
|
seq(opt(sourceElements),
|
|
// If not at EOF, complain "expecting statement"
|
|
expecting('statement', lookAheadTokenType("EOF"))));
|
|
|
|
return program.parse(this);
|
|
};
|
|
|
|
})();
|