blob: 6217e98ad3b4cb4fa64c07553197d48962fd5c7c [file] [log] [blame]
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// ignore_for_file: slash_for_doc_comments
// Utilities for building JS ASTs at runtime. Contains a builder class
// and a parser that parses part of the language.
part of js_ast;
/**
* Global template manager. We should aim to have a fixed number of
* templates. This implies that we do not use js('xxx') to parse text that is
* constructed from values that depend on names in the Dart program.
*
* TODO(sra): Find the remaining places where js('xxx') used to parse an
* unbounded number of expression, or institute a cache policy.
*/
TemplateManager templateManager = TemplateManager();
/**
[js] is a singleton instance of JsBuilder. JsBuilder is a set of conveniences
for constructing JavaScript ASTs.
[string] and [number] are used to create leaf AST nodes:
var s = js.string('hello'); // s = new LiteralString('"hello"')
var n = js.number(123); // n = new LiteralNumber(123)
In the line above `a --> b` means Dart expression `a` evaluates to a JavaScript
AST that would pretty-print as `b`.
The [call] method constructs an Expression AST.
No argument
js('window.alert("hello")') --> window.alert("hello")
The input text can contain placeholders `#` that are replaced with provided
arguments. A single argument can be passed directly:
js('window.alert(#)', s) --> window.alert("hello")
Multiple arguments are passed as a list:
js('# + #', [s, s]) --> "hello" + "hello"
The [statement] method constructs a Statement AST, but is otherwise like the
[call] method. This constructs a Return AST:
var ret = js.statement('return #;', n); --> return 123;
A placeholder in a Statement context must be followed by a semicolon ';'. You
can think of a statement placeholder as being `#;` to explain why the output
still has one semicolon:
js.statement('if (happy) #;', ret)
-->
if (happy)
return 123;
If the placeholder is not followed by a semicolon, it is part of an expression.
Here the placeholder is in the position of the function in a function call:
var vFoo = new Identifier('foo');
js.statement('if (happy) #("Happy!")', vFoo)
-->
if (happy)
foo("Happy!");
Generally, a placeholder in an expression position requires an Expression AST as
an argument and a placeholder in a statement position requires a Statement AST.
An expression will be converted to a Statement if needed by creating an
ExpressionStatement. A String argument will be converted into a Identifier and
requires that the string is a JavaScript identifier.
js('# + 1', vFoo) --> foo + 1
js('# + 1', 'foo') --> foo + 1
js('# + 1', 'foo.bar') --> assertion failure
Some placeholder positions are _splicing contexts_. A function argument list is
a splicing expression context. A placeholder in a splicing expression context
can take a single Expression (or String, converted to Identifier) or an
Iterable of Expressions (and/or Strings).
// non-splicing argument:
js('#(#)', ['say', s]) --> say("hello")
// splicing arguments:
js('#(#)', ['say', []]) --> say()
js('#(#)', ['say', [s]]) --> say("hello")
js('#(#)', ['say', [s, n]]) --> say("hello", 123)
A splicing context can be used to append 'lists' and add extra elements:
js('foo(#, #, 1)', [ ['a', n], s]) --> foo(a, 123, "hello", 1)
js('foo(#, #, 1)', [ ['a', n], [s, n]]) --> foo(a, 123, "hello", 123, 1)
js('foo(#, #, 1)', [ [], [s, n]]) --> foo("hello", 123, 1)
js('foo(#, #, 1)', [ [], [] ]) --> foo(1)
The generation of a compile-time optional argument expression can be chosen by
providing an empty or singleton list.
In addition to Expressions and Statements, there are Parameters, which occur
only in the parameter list of a function expression or declaration.
Placeholders in parameter positions behave like placeholders in Expression
positions, except only Parameter AST nodes are permitted. String arguments for
parameter placeholders are converted to Parameter AST nodes.
var pFoo = new Parameter('foo')
js('function(#) { return #; }', [pFoo, vFoo])
-->
function(foo) { return foo; }
Expressions and Parameters are not compatible with each other's context:
js('function(#) { return #; }', [vFoo, vFoo]) --> error
js('function(#) { return #; }', [pFoo, pFoo]) --> error
The parameter context is a splicing context. When combined with the
context-sensitive conversion of Strings, this simplifies the construction of
trampoline-like functions:
var args = ['a', 'b'];
js('function(#) { return f(this, #); }', [args, args])
-->
function(a, b) { return f(this, a, b); }
A statement placeholder in a Block is also in a splicing context. In addition
to splicing Iterables, statement placeholders in a Block will also splice a
Block or an EmptyStatement. This flattens nested blocks and allows blocks to be
appended.
var b1 = js.statement('{ 1; 2; }');
var sEmpty = new Emptystatement();
js.statement('{ #; #; #; #; }', [sEmpty, b1, b1, sEmpty])
-->
{ 1; 2; 1; 2; }
A placeholder in the context of an if-statement condition also accepts a Dart
bool argument, which selects the then-part or else-part of the if-statement:
js.statement('if (#) return;', vFoo) --> if (foo) return;
js.statement('if (#) return;', true) --> return;
js.statement('if (#) return;', false) --> ; // empty statement
var eTrue = new LiteralBool(true);
js.statement('if (#) return;', eTrue) --> if (true) return;
Combined with block splicing, if-statement condition context placeholders allows
the creation of templates that select code depending on variables.
js.statement('{ 1; if (#) 2; else { 3; 4; } 5;}', true)
--> { 1; 2; 5; }
js.statement('{ 1; if (#) 2; else { 3; 4; } 5;}', false)
--> { 1; 3; 4; 5; }
A placeholder following a period in a property access is in a property access
context. This is just like an expression context, except String arguments are
converted to JavaScript property accesses. In JavaScript, `a.b` is short-hand
for `a["b"]`:
js('a[#]', vFoo) --> a[foo]
js('a[#]', s) --> a.hello (i.e. a["hello"]).
js('a[#]', 'x') --> a[x]
js('a.#', vFoo) --> a[foo]
js('a.#', s) --> a.hello (i.e. a["hello"])
js('a.#', 'x') --> a.x (i.e. a["x"])
(Question - should `.#` be restricted to permit only String arguments? The
template should probably be written with `[]` if non-strings are accepted.)
Object initializers allow placeholders in the key property name position:
js('{#:1, #:2}', [s, 'bye']) --> {hello: 1, bye: 2}
What is not implemented:
- Array initializers and object initializers could support splicing. In the
array case, we would need some way to know if an ArrayInitializer argument
should be splice or is intended as a single value.
- There are no placeholders in definition contexts:
function #(){}
var # = 1;
*/
const JsBuilder js = JsBuilder();
class JsBuilder {
const JsBuilder();
/**
* Parses a bit of JavaScript, and returns an expression.
*
* See the MiniJsParser class.
*
* [arguments] can be a single [Node] (e.g. an [Expression] or [Statement]) or
* a list of [Node]s, which will be interpolated into the source at the '#'
* signs.
*/
Expression call(String source, [arguments]) {
Template template = _findExpressionTemplate(source);
if (arguments == null) return template.instantiate([]) as Expression;
// We allow a single argument to be given directly.
if (arguments is! List && arguments is! Map) arguments = [arguments];
return template.instantiate(arguments) as Expression;
}
/**
* Parses a JavaScript Statement, otherwise just like [call].
*/
Statement statement(String source, [arguments]) {
Template template = _findStatementTemplate(source);
if (arguments == null) return template.instantiate([]) as Statement;
// We allow a single argument to be given directly.
if (arguments is! List && arguments is! Map) arguments = [arguments];
return template.instantiate(arguments) as Statement;
}
Block block(String source, [arguments]) =>
statement(source, arguments) as Block;
Fun fun(String source, [arguments]) => call(source, arguments) as Fun;
/**
* Parses JavaScript written in the `JS` foreign instruction.
*
* The [source] must be a JavaScript expression or a JavaScript throw
* statement.
*/
Template parseForeignJS(String source) {
// TODO(sra): Parse with extra validation to forbid `#` interpolation in
// functions, as this leads to unanticipated capture of temporaries that are
// reused after capture.
if (source.startsWith("throw ")) {
return _findStatementTemplate(source);
} else {
return _findExpressionTemplate(source);
}
}
Template _findExpressionTemplate(String source) {
Template template = templateManager.lookupExpressionTemplate(source);
if (template == null) {
MiniJsParser parser = MiniJsParser(source);
Expression expression = parser.expression();
template = templateManager.defineExpressionTemplate(source, expression);
}
return template;
}
Template _findStatementTemplate(String source) {
Template template = templateManager.lookupStatementTemplate(source);
if (template == null) {
MiniJsParser parser = MiniJsParser(source);
Statement statement = parser.statement();
template = templateManager.defineStatementTemplate(source, statement);
}
return template;
}
/**
* Creates an Expression template without caching the result.
*/
Template uncachedExpressionTemplate(String source) {
MiniJsParser parser = MiniJsParser(source);
Expression expression = parser.expression();
return Template(source, expression, isExpression: true, forceCopy: false);
}
/**
* Creates a Statement template without caching the result.
*/
Template uncachedStatementTemplate(String source) {
MiniJsParser parser = MiniJsParser(source);
Statement statement = parser.statement();
return Template(source, statement, isExpression: false, forceCopy: false);
}
/**
* Create an Expression template which has [ast] as the result. This is used
* to wrap a generated AST in a zero-argument Template so it can be passed to
* context that expects a template.
*/
Template expressionTemplateYielding(Node ast) {
return Template.withExpressionResult(ast);
}
Template statementTemplateYielding(Node ast) {
return Template.withStatementResult(ast);
}
/// Creates a literal js string from [value], escaped for use in a UTF-8
/// output.
LiteralString escapedString(String value, [String quote = '"']) {
int otherEscapes = 0;
int unpairedSurrogates = 0;
int quoteRune = quote.codeUnitAt(0);
for (int rune in value.runes) {
if (rune == charCodes.$BACKSLASH) {
++otherEscapes;
} else if (rune == charCodes.$LF ||
rune == charCodes.$CR ||
rune == charCodes.$LS ||
rune == charCodes.$PS) {
// Line terminators.
++otherEscapes;
} else if (rune == charCodes.$BS ||
rune == charCodes.$TAB ||
rune == charCodes.$VTAB ||
rune == charCodes.$FF) {
++otherEscapes;
} else if (rune == quoteRune ||
rune == charCodes.$$ && quoteRune == charCodes.$BACKPING) {
++otherEscapes;
} else if (_isUnpairedSurrogate(rune)) {
++unpairedSurrogates;
}
}
if (otherEscapes == 0 && unpairedSurrogates == 0) {
return string(value, quote);
}
var sb = new StringBuffer();
for (int rune in value.runes) {
String escape = _irregularEscape(rune, quote);
if (escape != null) {
sb.write(escape);
continue;
}
if (rune == charCodes.$LS ||
rune == charCodes.$PS ||
_isUnpairedSurrogate(rune)) {
if (rune < 0x100) {
sb.write(r'\x');
sb.write(rune.toRadixString(16).padLeft(2, '0'));
} else if (rune < 0x10000) {
sb.write(r'\u');
sb.write(rune.toRadixString(16).padLeft(4, '0'));
} else {
sb.write(r'\u{');
sb.write(rune.toRadixString(16));
sb.write('}');
}
} else {
sb.writeCharCode(rune);
}
}
return string(sb.toString(), quote);
}
static bool _isUnpairedSurrogate(int code) => (code & 0xFFFFF800) == 0xD800;
static String _irregularEscape(int code, String quote) {
switch (code) {
case charCodes.$SQ:
return quote == "'" ? r"\'" : "'";
case charCodes.$DQ:
return quote == '"' ? r'\"' : '"';
case charCodes.$BACKPING:
return quote == '`' ? r'\`' : '`';
case charCodes.$$:
// Escape $ inside of template strings.
return quote == '`' ? r'\$' : r'$';
case charCodes.$BACKSLASH:
return r'\\';
case charCodes.$BS:
return r'\b';
case charCodes.$TAB:
return r'\t';
case charCodes.$LF:
return r'\n';
case charCodes.$VTAB:
return r'\v';
case charCodes.$FF:
return r'\f';
case charCodes.$CR:
return r'\r';
}
return null;
}
/// Creates a literal js string from [value].
///
/// Note that this function only puts quotes around [value]. It does not do
/// any escaping, so use only when you can guarantee that [value] does not
/// contain newlines or backslashes. For escaping the string use
/// [escapedString].
LiteralString string(String value, [String quote = '"']) =>
LiteralString('$quote$value$quote');
LiteralNumber number(num value) => LiteralNumber('$value');
LiteralNumber uint64(int value) {
BigInt uint64Value = new BigInt.from(value).toUnsigned(64);
return LiteralNumber('$uint64Value');
}
LiteralBool boolean(bool value) => LiteralBool(value);
ArrayInitializer numArray(Iterable<int> list) =>
ArrayInitializer(list.map(number).toList());
ArrayInitializer stringArray(Iterable<String> list) =>
ArrayInitializer(list.map(string).toList());
Comment comment(String text) => Comment(text);
CommentExpression commentExpression(String text, Expression expression) =>
CommentExpression(text, expression);
Call propertyCall(
Expression receiver, String fieldName, List<Expression> arguments) {
return Call(PropertyAccess.field(receiver, fieldName), arguments);
}
}
LiteralString string(String value) => js.string(value);
LiteralNumber number(num value) => js.number(value);
ArrayInitializer numArray(Iterable<int> list) => js.numArray(list);
ArrayInitializer stringArray(Iterable<String> list) => js.stringArray(list);
Call propertyCall(
Expression receiver, String fieldName, List<Expression> arguments) {
return js.propertyCall(receiver, fieldName, arguments);
}
class MiniJsParserError {
MiniJsParserError(this.parser, this.message);
final MiniJsParser parser;
final String message;
@override
String toString() {
int pos = parser.lastPosition;
// Discard lines following the line containing lastPosition.
String src = parser.src;
int newlinePos = src.indexOf('\n', pos);
if (newlinePos >= pos) src = src.substring(0, newlinePos);
// Extract the prefix of the error line before lastPosition.
String line = src;
int lastLineStart = line.lastIndexOf('\n');
if (lastLineStart >= 0) line = line.substring(lastLineStart + 1);
String prefix = line.substring(0, pos - (src.length - line.length));
// Replace non-tabs with spaces, giving a print indent that matches the text
// for tabbing.
String spaces = prefix.replaceAll(RegExp(r'[^\t]'), ' ');
return 'Error in MiniJsParser:\n${src}\n$spaces^\n$spaces$message\n';
}
}
/// Mini JavaScript parser for tiny snippets of code that we want to make into
/// AST nodes. Handles:
/// * identifiers.
/// * dot access.
/// * method calls.
/// * [] access.
/// * array, string, regexp, boolean, null and numeric literals.
/// * most operators.
/// * brackets.
/// * var declarations.
/// * operator precedence.
/// * anonymous functions and named function expressions and declarations.
/// Notable things it can't do yet include:
/// * some statements are still missing (do-while, while, switch).
///
/// It's a fairly standard recursive descent parser.
///
/// Literal strings are passed through to the final JS source code unchanged,
/// including the choice of surrounding quotes, so if you parse
/// r'var x = "foo\n\"bar\""' you will end up with
/// var x = "foo\n\"bar\"" in the final program. \x and \u escapes are not
/// allowed in string and regexp literals because the machinery for checking
/// their correctness is rather involved.
class MiniJsParser {
MiniJsParser(this.src)
: lastCategory = NONE,
lastToken = null,
lastPosition = 0,
position = 0 {
getToken();
}
int lastCategory = NONE;
String lastToken;
int lastPosition = 0;
int position = 0;
bool skippedNewline = false; // skipped newline in last getToken?
final String src;
final List<InterpolatedNode> interpolatedValues = <InterpolatedNode>[];
bool get hasNamedHoles =>
interpolatedValues.isNotEmpty && interpolatedValues.first.isNamed;
bool get hasPositionalHoles =>
interpolatedValues.isNotEmpty && interpolatedValues.first.isPositional;
static const NONE = -1;
static const ALPHA = 0;
static const NUMERIC = 1;
static const STRING = 2;
static const SYMBOL = 3;
static const ASSIGNMENT = 4;
static const DOT = 5;
static const LPAREN = 6;
static const RPAREN = 7;
static const LBRACE = 8;
static const RBRACE = 9;
static const LSQUARE = 10;
static const RSQUARE = 11;
static const COMMA = 12;
static const QUERY = 13;
static const COLON = 14;
static const SEMICOLON = 15;
static const ARROW = 16;
static const ELLIPSIS = 17;
static const HASH = 18;
static const WHITESPACE = 19;
static const OTHER = 20;
// Make sure that ]] is two symbols.
// TODO(jmesserly): => and ... are not single char tokens, should we change
// their numbers? It shouldn't matter because this is only called on values
// from the [CATEGORIES] table.
bool singleCharCategory(int category) => category > DOT;
static String categoryToString(int cat) {
switch (cat) {
case NONE:
return "NONE";
case ALPHA:
return "ALPHA";
case NUMERIC:
return "NUMERIC";
case SYMBOL:
return "SYMBOL";
case ASSIGNMENT:
return "ASSIGNMENT";
case DOT:
return "DOT";
case LPAREN:
return "LPAREN";
case RPAREN:
return "RPAREN";
case LBRACE:
return "LBRACE";
case RBRACE:
return "RBRACE";
case LSQUARE:
return "LSQUARE";
case RSQUARE:
return "RSQUARE";
case STRING:
return "STRING";
case COMMA:
return "COMMA";
case QUERY:
return "QUERY";
case COLON:
return "COLON";
case SEMICOLON:
return "SEMICOLON";
case ARROW:
return "ARROW";
case ELLIPSIS:
return "ELLIPSIS";
case HASH:
return "HASH";
case WHITESPACE:
return "WHITESPACE";
case OTHER:
return "OTHER";
}
return "Unknown: $cat";
}
static const CATEGORIES = <int>[
OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, // 0-7
OTHER, WHITESPACE, WHITESPACE, OTHER, OTHER, WHITESPACE, // 8-13
OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, // 14-21
OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, // 22-29
OTHER, OTHER, WHITESPACE, // 30-32
SYMBOL, OTHER, HASH, ALPHA, SYMBOL, SYMBOL, OTHER, // !"#$%&´
LPAREN, RPAREN, SYMBOL, SYMBOL, COMMA, SYMBOL, DOT, SYMBOL, // ()*+,-./
NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, // 01234
NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, // 56789
COLON, SEMICOLON, SYMBOL, SYMBOL, SYMBOL, QUERY, OTHER, // :;<=>?@
ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, // ABCDEFGH
ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, // IJKLMNOP
ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, // QRSTUVWX
ALPHA, ALPHA, LSQUARE, OTHER, RSQUARE, SYMBOL, ALPHA, OTHER, // YZ[\]^_'
ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, // abcdefgh
ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, // ijklmnop
ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, // qrstuvwx
ALPHA, ALPHA, LBRACE, SYMBOL, RBRACE, SYMBOL
]; // yz{|}~
// This must be a >= the highest precedence number handled by parseBinary.
static var HIGHEST_PARSE_BINARY_PRECEDENCE = 16;
static bool isAssignment(String symbol) => BINARY_PRECEDENCE[symbol] == 17;
// From https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Operators/Operator_Precedence
static final BINARY_PRECEDENCE = {
'+=': 17,
'-=': 17,
'*=': 17,
'/=': 17,
'%=': 17,
'^=': 17,
'|=': 17,
'&=': 17,
'<<=': 17,
'>>=': 17,
'>>>=': 17,
'=': 17,
'||': 14,
'&&': 13,
'|': 12,
'^': 11,
'&': 10,
'!=': 9,
'==': 9,
'!==': 9,
'===': 9,
'<': 8,
'<=': 8,
'>=': 8,
'>': 8,
'in': 8,
'instanceof': 8,
'<<': 7,
'>>': 7,
'>>>': 7,
'+': 6,
'-': 6,
'*': 5,
'/': 5,
'%': 5
};
static final UNARY_OPERATORS = [
'++',
'--',
'+',
'-',
'~',
'!',
'typeof',
'void',
'delete',
'await'
].toSet();
static final ARROW_TOKEN = '=>';
static final ELLIPSIS_TOKEN = '...';
static final OPERATORS_THAT_LOOK_LIKE_IDENTIFIERS = [
'typeof',
'void',
'delete',
'in',
'instanceof',
'await',
'extends'
].toSet();
static int category(int code) {
if (code >= CATEGORIES.length) return OTHER;
return CATEGORIES[code];
}
String getDelimited(int startPosition) {
position = startPosition;
int delimiter = src.codeUnitAt(startPosition);
int currentCode;
do {
position++;
if (position >= src.length) error("Unterminated literal");
currentCode = src.codeUnitAt(position);
if (currentCode == charCodes.$LF) error("Unterminated literal");
if (currentCode == charCodes.$BACKSLASH) {
if (++position >= src.length) error("Unterminated literal");
int escaped = src.codeUnitAt(position);
if (escaped == charCodes.$x ||
escaped == charCodes.$X ||
escaped == charCodes.$u ||
escaped == charCodes.$U ||
category(escaped) == NUMERIC) {
error('Numeric and hex escapes are not allowed in literals');
}
}
} while (currentCode != delimiter);
position++;
return src.substring(lastPosition, position);
}
void getToken() {
skippedNewline = false;
while (true) {
if (position >= src.length) break;
int code = src.codeUnitAt(position);
// Skip '//' and '/*' style comments.
if (code == charCodes.$SLASH && position + 1 < src.length) {
if (src.codeUnitAt(position + 1) == charCodes.$SLASH) {
int nextPosition = src.indexOf('\n', position);
if (nextPosition == -1) nextPosition = src.length;
position = nextPosition;
continue;
} else if (src.codeUnitAt(position + 1) == charCodes.$STAR) {
int nextPosition = src.indexOf('*/', position + 2);
if (nextPosition == -1) error('Unterminated comment');
position = nextPosition + 2;
continue;
}
}
if (category(code) != WHITESPACE) break;
if (code == charCodes.$LF) skippedNewline = true;
++position;
}
if (position == src.length) {
lastCategory = NONE;
lastToken = null;
lastPosition = position;
return;
}
int code = src.codeUnitAt(position);
lastPosition = position;
if (code == charCodes.$SQ || code == charCodes.$DQ) {
// String literal.
lastCategory = STRING;
lastToken = getDelimited(position);
} else if (code == charCodes.$0 &&
position + 2 < src.length &&
src.codeUnitAt(position + 1) == charCodes.$x) {
// Hex literal.
for (position += 2; position < src.length; position++) {
int cat = category(src.codeUnitAt(position));
if (cat != NUMERIC && cat != ALPHA) break;
}
lastCategory = NUMERIC;
lastToken = src.substring(lastPosition, position);
if (int.tryParse(lastToken) == null) {
error("Unparseable number");
}
} else if (code == charCodes.$SLASH) {
// Tokens that start with / are special due to regexp literals.
lastCategory = SYMBOL;
position++;
if (position < src.length && src.codeUnitAt(position) == charCodes.$EQ) {
position++;
}
lastToken = src.substring(lastPosition, position);
} else {
// All other tokens handled here.
int cat = category(src.codeUnitAt(position));
int newCat;
do {
position++;
if (position == src.length) break;
int code = src.codeUnitAt(position);
// Special code to disallow ! and / in non-first position in token, so
// that !! parses as two tokens and != parses as one, while =/ parses
// as a an equals token followed by a regexp literal start.
newCat = (code == charCodes.$BANG || code == charCodes.$SLASH)
? NONE
: category(code);
} while (!singleCharCategory(cat) &&
(cat == newCat ||
(cat == ALPHA && newCat == NUMERIC) || // eg. level42.
(cat == NUMERIC && newCat == DOT))); // eg. 3.1415
lastCategory = cat;
lastToken = src.substring(lastPosition, position);
if (cat == NUMERIC) {
if (double.tryParse(lastToken) == null) {
error("Unparseable number");
}
} else if (cat == DOT && lastToken.length > 1) {
if (lastToken == ELLIPSIS_TOKEN) {
lastCategory = ELLIPSIS;
} else {
error("Unknown operator");
}
} else if (cat == SYMBOL) {
if (lastToken == ARROW_TOKEN) {
lastCategory = ARROW;
} else {
int binaryPrecendence = BINARY_PRECEDENCE[lastToken];
if (binaryPrecendence == null &&
!UNARY_OPERATORS.contains(lastToken)) {
error("Unknown operator");
}
if (isAssignment(lastToken)) lastCategory = ASSIGNMENT;
}
} else if (cat == ALPHA) {
if (OPERATORS_THAT_LOOK_LIKE_IDENTIFIERS.contains(lastToken)) {
lastCategory = SYMBOL;
}
}
}
}
void expectCategory(int cat) {
if (cat != lastCategory) error("Expected ${categoryToString(cat)}");
getToken();
}
bool acceptCategory(int cat) {
if (cat == lastCategory) {
getToken();
return true;
}
return false;
}
void expectSemicolon() {
if (acceptSemicolon()) return;
error('Expected SEMICOLON');
}
bool acceptSemicolon() {
// Accept semicolon or automatically inserted semicolon before close brace.
// Miniparser forbids other kinds of semicolon insertion.
if (RBRACE == lastCategory) return true;
if (NONE == lastCategory) return true; // end of input
if (skippedNewline) {
error('No automatic semicolon insertion at preceding newline');
}
return acceptCategory(SEMICOLON);
}
bool acceptString(String string) {
if (lastToken == string) {
getToken();
return true;
}
return false;
}
void error(String message) {
throw MiniJsParserError(this, message);
}
/// Returns either the name for the hole, or its integer position.
parseHash() {
String holeName = lastToken;
if (acceptCategory(ALPHA)) {
// Named hole. Example: 'function #funName() { ... }'
if (hasPositionalHoles) {
error('Holes must all be positional or named. $holeName');
}
return holeName;
} else {
if (hasNamedHoles) {
error('Holes must all be positional or named. $holeName');
}
int position = interpolatedValues.length;
return position;
}
}
Expression parsePrimary() {
String last = lastToken;
if (acceptCategory(ALPHA)) {
if (last == "true") {
return LiteralBool(true);
} else if (last == "false") {
return LiteralBool(false);
} else if (last == "null") {
return LiteralNull();
} else if (last == "function") {
return parseFunctionExpression();
} else if (last == "this") {
return This();
} else if (last == "super") {
return Super();
} else if (last == "class") {
return parseClass();
} else {
return Identifier(last);
}
} else if (acceptCategory(LPAREN)) {
return parseExpressionOrArrowFunction();
} else if (acceptCategory(STRING)) {
return LiteralString(last);
} else if (acceptCategory(NUMERIC)) {
return LiteralNumber(last);
} else if (acceptCategory(LBRACE)) {
return parseObjectInitializer();
} else if (acceptCategory(LSQUARE)) {
var values = <Expression>[];
while (true) {
if (acceptCategory(COMMA)) {
values.add(ArrayHole());
continue;
}
if (acceptCategory(RSQUARE)) break;
values.add(parseAssignment());
if (acceptCategory(RSQUARE)) break;
expectCategory(COMMA);
}
return ArrayInitializer(values);
} else if (last != null && last.startsWith("/")) {
String regexp = getDelimited(lastPosition);
getToken();
String flags = lastToken;
if (!acceptCategory(ALPHA)) flags = "";
Expression expression = RegExpLiteral(regexp + flags);
return expression;
} else if (acceptCategory(HASH)) {
return parseInterpolatedExpression();
} else {
error("Expected primary expression");
return null;
}
}
InterpolatedExpression parseInterpolatedExpression() {
var expression = InterpolatedExpression(parseHash());
interpolatedValues.add(expression);
return expression;
}
InterpolatedIdentifier parseInterpolatedIdentifier() {
var id = InterpolatedIdentifier(parseHash());
interpolatedValues.add(id);
return id;
}
Identifier parseIdentifier() {
if (acceptCategory(HASH)) {
return parseInterpolatedIdentifier();
} else {
var id = Identifier(lastToken);
expectCategory(ALPHA);
return id;
}
}
/**
* CoverParenthesizedExpressionAndArrowParameterList[Yield] :
* ( Expression )
* ( )
* ( ... BindingIdentifier )
* ( Expression , ... BindingIdentifier )
*/
Expression parseExpressionOrArrowFunction() {
if (acceptCategory(RPAREN)) {
expectCategory(ARROW);
return parseArrowFunctionBody(<Parameter>[]);
}
if (acceptCategory(ELLIPSIS)) {
var params = <Parameter>[RestParameter(parseParameter())];
expectCategory(RPAREN);
expectCategory(ARROW);
return parseArrowFunctionBody(params);
}
Expression expression = parseAssignment();
while (acceptCategory(COMMA)) {
if (acceptCategory(ELLIPSIS)) {
var params = <Parameter>[];
_expressionToParameterList(expression, params);
params.add(RestParameter(parseParameter()));
expectCategory(RPAREN);
expectCategory(ARROW);
return parseArrowFunctionBody(params);
}
Expression right = parseAssignment();
expression = Binary(',', expression, right);
}
expectCategory(RPAREN);
if (acceptCategory(ARROW)) {
var params = <Parameter>[];
_expressionToParameterList(expression, params);
return parseArrowFunctionBody(params);
}
return expression;
}
/**
* Converts a parenthesized expression into a list of parameters, issuing an
* error if the conversion fails.
*/
void _expressionToParameterList(Expression node, List<Parameter> params) {
if (node is Identifier) {
params.add(node);
} else if (node is Binary && node.op == ',') {
// TODO(jmesserly): this will allow illegal parens, such as
// `((a, b), (c, d))`. Fixing it on the left side needs an explicit
// ParenthesizedExpression node, so we can distinguish
// `((a, b), c)` from `(a, b, c)`.
_expressionToParameterList(node.left, params);
_expressionToParameterList(node.right, params);
} else if (node is InterpolatedExpression) {
params.add(InterpolatedParameter(node.nameOrPosition));
} else {
error("Expected arrow function parameter list");
}
}
Expression parseArrowFunctionBody(List<Parameter> params) {
Node body;
if (acceptCategory(LBRACE)) {
body = parseBlock();
} else {
body = parseAssignment();
}
return ArrowFun(params, body);
}
Expression parseFunctionExpression() {
String last = lastToken;
if (acceptCategory(ALPHA)) {
String functionName = last;
return NamedFunction(Identifier(functionName), parseFun());
}
return parseFun();
}
Fun parseFun() {
List<Parameter> params = <Parameter>[];
expectCategory(LPAREN);
if (!acceptCategory(RPAREN)) {
while (true) {
if (acceptCategory(ELLIPSIS)) {
params.add(RestParameter(parseParameter()));
expectCategory(RPAREN);
break;
}
params.add(parseParameter());
if (!acceptCategory(COMMA)) {
expectCategory(RPAREN);
break;
}
}
}
AsyncModifier asyncModifier;
if (acceptString('async')) {
if (acceptString('*')) {
asyncModifier = const AsyncModifier.asyncStar();
} else {
asyncModifier = const AsyncModifier.async();
}
} else if (acceptString('sync')) {
if (!acceptString('*')) error("Only sync* is valid - sync is implied");
asyncModifier = const AsyncModifier.syncStar();
} else {
asyncModifier = const AsyncModifier.sync();
}
expectCategory(LBRACE);
Block block = parseBlock();
return Fun(params, block, asyncModifier: asyncModifier);
}
/** Parse parameter name or interpolated parameter. */
Identifier parseParameter() {
if (acceptCategory(HASH)) {
var nameOrPosition = parseHash();
var parameter = InterpolatedParameter(nameOrPosition);
interpolatedValues.add(parameter);
return parameter;
} else {
// TODO(jmesserly): validate this is not a keyword
String argumentName = lastToken;
expectCategory(ALPHA);
return Identifier(argumentName);
}
}
Expression parseObjectInitializer() {
List<Property> properties = <Property>[];
while (true) {
if (acceptCategory(RBRACE)) break;
// Limited subset of ES6 object initializers.
//
// PropertyDefinition :
// PropertyName : AssignmentExpression
// MethodDefinition
properties.add(parseMethodOrProperty());
if (acceptCategory(RBRACE)) break;
expectCategory(COMMA);
}
return ObjectInitializer(properties);
}
Expression parseMember() {
Expression receiver = parsePrimary();
while (true) {
if (acceptCategory(DOT)) {
receiver = getDotRhs(receiver);
} else if (acceptCategory(LSQUARE)) {
Expression inBraces = parseExpression();
expectCategory(RSQUARE);
receiver = PropertyAccess(receiver, inBraces);
} else {
break;
}
}
return receiver;
}
Expression parseCall() {
bool constructor = acceptString("new");
Expression receiver = parseMember();
while (true) {
if (acceptCategory(LPAREN)) {
final arguments = <Expression>[];
if (!acceptCategory(RPAREN)) {
while (true) {
if (acceptCategory(ELLIPSIS)) {
arguments.add(Spread(parseAssignment()));
expectCategory(RPAREN);
break;
}
arguments.add(parseAssignment());
if (acceptCategory(RPAREN)) break;
expectCategory(COMMA);
}
}
receiver =
constructor ? New(receiver, arguments) : Call(receiver, arguments);
constructor = false;
} else if (!constructor && acceptCategory(LSQUARE)) {
Expression inBraces = parseExpression();
expectCategory(RSQUARE);
receiver = PropertyAccess(receiver, inBraces);
} else if (!constructor && acceptCategory(DOT)) {
receiver = getDotRhs(receiver);
} else {
// JS allows new without (), but we don't.
if (constructor) error("Parentheses are required for new");
break;
}
}
return receiver;
}
Expression getDotRhs(Expression receiver) {
if (acceptCategory(HASH)) {
var nameOrPosition = parseHash();
InterpolatedSelector property = InterpolatedSelector(nameOrPosition);
interpolatedValues.add(property);
return PropertyAccess(receiver, property);
}
String identifier = lastToken;
// In ES5 keywords like delete and continue are allowed as property
// names, and the IndexedDB API uses that, so we need to allow it here.
if (acceptCategory(SYMBOL)) {
if (!OPERATORS_THAT_LOOK_LIKE_IDENTIFIERS.contains(identifier)) {
error("Expected alphanumeric identifier");
}
} else {
expectCategory(ALPHA);
}
return PropertyAccess.field(receiver, identifier);
}
Expression parsePostfix() {
Expression expression = parseCall();
String operator = lastToken;
// JavaScript grammar is:
// LeftHandSideExpression [no LineTerminator here] ++
if (lastCategory == SYMBOL &&
!skippedNewline &&
(acceptString("++") || acceptString("--"))) {
return Postfix(operator, expression);
}
// If we don't accept '++' or '--' due to skippedNewline a newline, no other
// part of the parser will accept the token and we will get an error at the
// whole expression level.
return expression;
}
Expression parseUnaryHigh() {
String operator = lastToken;
if (lastCategory == SYMBOL &&
UNARY_OPERATORS.contains(operator) &&
(acceptString("++") || acceptString("--") || acceptString('await'))) {
if (operator == "await") return Await(parsePostfix());
return Prefix(operator, parsePostfix());
}
return parsePostfix();
}
Expression parseUnaryLow() {
String operator = lastToken;
if (lastCategory == SYMBOL &&
UNARY_OPERATORS.contains(operator) &&
operator != "++" &&
operator != "--") {
expectCategory(SYMBOL);
if (operator == "await") return Await(parsePostfix());
return Prefix(operator, parseUnaryLow());
}
return parseUnaryHigh();
}
Expression parseBinary(int maxPrecedence) {
Expression lhs = parseUnaryLow();
int minPrecedence;
String lastSymbol;
Expression rhs; // This is null first time around.
while (true) {
String symbol = lastToken;
if (lastCategory != SYMBOL ||
!BINARY_PRECEDENCE.containsKey(symbol) ||
BINARY_PRECEDENCE[symbol] > maxPrecedence) {
break;
}
expectCategory(SYMBOL);
if (rhs == null || BINARY_PRECEDENCE[symbol] >= minPrecedence) {
if (rhs != null) lhs = Binary(lastSymbol, lhs, rhs);
minPrecedence = BINARY_PRECEDENCE[symbol];
rhs = parseUnaryLow();
lastSymbol = symbol;
} else {
Expression higher = parseBinary(BINARY_PRECEDENCE[symbol]);
rhs = Binary(symbol, rhs, higher);
}
}
if (rhs == null) return lhs;
return Binary(lastSymbol, lhs, rhs);
}
Expression parseConditional() {
Expression lhs = parseBinary(HIGHEST_PARSE_BINARY_PRECEDENCE);
if (!acceptCategory(QUERY)) return lhs;
Expression ifTrue = parseAssignment();
expectCategory(COLON);
Expression ifFalse = parseAssignment();
return Conditional(lhs, ifTrue, ifFalse);
}
Expression parseLeftHandSide() => parseConditional();
Expression parseAssignment() {
Expression lhs = parseLeftHandSide();
String assignmentOperator = lastToken;
if (acceptCategory(ASSIGNMENT)) {
Expression rhs = parseAssignment();
if (assignmentOperator == "=") {
return Assignment(lhs, rhs);
} else {
// Handle +=, -=, etc.
String operator =
assignmentOperator.substring(0, assignmentOperator.length - 1);
return Assignment.compound(lhs, operator, rhs);
}
}
return lhs;
}
Expression parseExpression() {
Expression expression = parseAssignment();
while (acceptCategory(COMMA)) {
Expression right = parseAssignment();
expression = Binary(',', expression, right);
}
return expression;
}
/** Parse a variable declaration list, with `var` or `let` [keyword] */
VariableDeclarationList parseVariableDeclarationList(String keyword,
[String firstIdentifier]) {
var initialization = <VariableInitialization>[];
do {
VariableBinding declarator;
if (firstIdentifier != null) {
declarator = Identifier(firstIdentifier);
firstIdentifier = null;
} else {
declarator = parseVariableBinding();
}
var initializer = acceptString("=") ? parseAssignment() : null;
initialization.add(VariableInitialization(declarator, initializer));
} while (acceptCategory(COMMA));
return VariableDeclarationList(keyword, initialization);
}
VariableBinding parseVariableBinding() {
switch (lastCategory) {
case ALPHA:
case HASH:
return parseIdentifier();
case LBRACE:
case LSQUARE:
return parseBindingPattern();
default:
error('Unexpected token $lastToken: ${categoryToString(lastCategory)}');
return null;
}
}
/// Note: this doesn't deal with general-case destructuring yet, it just
/// supports it in variable initialization.
/// See ES6 spec:
/// http://www.ecma-international.org/ecma-262/6.0/#sec-destructuring-binding-patterns
/// http://www.ecma-international.org/ecma-262/6.0/#sec-destructuring-assignment
/// TODO(ochafik): Support destructuring in LeftHandSideExpression.
BindingPattern parseBindingPattern() {
if (acceptCategory(LBRACE)) {
return parseObjectBindingPattern();
} else {
expectCategory(LSQUARE);
return parseArrayBindingPattern();
}
}
ArrayBindingPattern parseArrayBindingPattern() {
var variables = <DestructuredVariable>[];
do {
Identifier name;
BindingPattern structure;
Expression defaultValue;
var declarator = parseVariableBinding();
if (declarator is Identifier) {
name = declarator;
} else if (declarator is BindingPattern) {
structure = declarator;
} else {
error("Unexpected LHS: $declarator");
}
if (acceptString("=")) {
defaultValue = parseExpression();
}
variables.add(DestructuredVariable(
name: name, structure: structure, defaultValue: defaultValue));
} while (acceptCategory(COMMA));
expectCategory(RSQUARE);
return ArrayBindingPattern(variables);
}
ObjectBindingPattern parseObjectBindingPattern() {
var variables = <DestructuredVariable>[];
do {
var name = parseIdentifier();
BindingPattern structure;
Expression defaultValue;
if (acceptCategory(COLON)) {
structure = parseBindingPattern();
} else if (acceptString("=")) {
defaultValue = parseExpression();
}
variables.add(DestructuredVariable(
name: name, structure: structure, defaultValue: defaultValue));
} while (acceptCategory(COMMA));
expectCategory(RBRACE);
return ObjectBindingPattern(variables);
}
Expression parseVarDeclarationOrExpression() {
var keyword = acceptVarLetOrConst();
if (keyword != null) {
return parseVariableDeclarationList(keyword);
} else {
return parseExpression();
}
}
/** Accepts a `var` or `let` keyword. If neither is found, returns null. */
String acceptVarLetOrConst() {
if (acceptString('var')) return 'var';
if (acceptString('let')) return 'let';
if (acceptString('const')) return 'const';
return null;
}
Expression expression() {
Expression expression = parseVarDeclarationOrExpression();
if (lastCategory != NONE || position != src.length) {
error("Unparsed junk: ${categoryToString(lastCategory)}");
}
return expression;
}
Statement statement() {
Statement statement = parseStatement();
if (lastCategory != NONE || position != src.length) {
error("Unparsed junk: ${categoryToString(lastCategory)}");
}
// TODO(sra): interpolated capture here?
return statement;
}
Block parseBlock() {
List<Statement> statements = <Statement>[];
while (!acceptCategory(RBRACE)) {
Statement statement = parseStatement();
statements.add(statement);
}
return Block(statements);
}
Statement parseStatement() {
if (acceptCategory(LBRACE)) return parseBlock();
if (acceptCategory(SEMICOLON)) return EmptyStatement();
if (lastCategory == ALPHA) {
if (acceptString('return')) return parseReturn();
if (acceptString('throw')) return parseThrow();
if (acceptString('break')) {
return parseBreakOrContinue((label) => Break(label));
}
if (acceptString('continue')) {
return parseBreakOrContinue((label) => Continue(label));
}
if (acceptString('debugger')) {
expectSemicolon();
return DebuggerStatement();
}
if (acceptString('if')) return parseIfThenElse();
if (acceptString('for')) return parseFor();
if (acceptString('function')) return parseFunctionDeclaration();
if (acceptString('class')) return ClassDeclaration(parseClass());
if (acceptString('try')) return parseTry();
var keyword = acceptVarLetOrConst();
if (keyword != null) {
Expression declarations = parseVariableDeclarationList(keyword);
expectSemicolon();
return ExpressionStatement(declarations);
}
if (acceptString('while')) return parseWhile();
if (acceptString('do')) return parseDo();
if (acceptString('switch')) return parseSwitch();
if (lastToken == 'case') error("Case outside switch.");
if (lastToken == 'default') error("Default outside switch.");
if (lastToken == 'yield') return parseYield();
if (lastToken == 'with') {
error('Not implemented in mini parser');
}
}
bool checkForInterpolatedStatement = lastCategory == HASH;
Expression expression = parseExpression();
if (expression is Identifier && acceptCategory(COLON)) {
return LabeledStatement(expression.name, parseStatement());
}
expectSemicolon();
if (checkForInterpolatedStatement) {
// 'Promote' the interpolated expression `#;` to an interpolated
// statement.
if (expression is InterpolatedExpression) {
assert(identical(interpolatedValues.last, expression));
InterpolatedStatement statement =
InterpolatedStatement(expression.nameOrPosition);
interpolatedValues[interpolatedValues.length - 1] = statement;
return statement;
}
}
return ExpressionStatement(expression);
}
Statement parseReturn() {
if (acceptSemicolon()) return Return();
Expression expression = parseExpression();
expectSemicolon();
return Return(expression);
}
Statement parseYield() {
bool hasStar = acceptString('*');
Expression expression = parseExpression();
expectSemicolon();
return DartYield(expression, hasStar);
}
Statement parseThrow() {
if (skippedNewline) error('throw expression must be on same line');
Expression expression = parseExpression();
expectSemicolon();
return Throw(expression);
}
Statement parseBreakOrContinue(Statement Function(String) constructor) {
var identifier = lastToken;
if (!skippedNewline && acceptCategory(ALPHA)) {
expectSemicolon();
return constructor(identifier);
}
expectSemicolon();
return constructor(null);
}
Statement parseIfThenElse() {
expectCategory(LPAREN);
Expression condition = parseExpression();
expectCategory(RPAREN);
Statement thenStatement = parseStatement();
if (acceptString('else')) {
// Resolves dangling else by binding 'else' to closest 'if'.
Statement elseStatement = parseStatement();
return If(condition, thenStatement, elseStatement);
} else {
return If.noElse(condition, thenStatement);
}
}
Statement parseFor() {
// For-init-condition-increment style loops are fully supported.
//
// Only one for-in variant is currently implemented:
//
// for (var variable in Expression) Statement
//
// One variant of ES6 for-of is also implemented:
//
// for (let variable of Expression) Statement
//
Statement finishFor(Expression init) {
Expression condition;
if (!acceptCategory(SEMICOLON)) {
condition = parseExpression();
expectCategory(SEMICOLON);
}
Expression update;
if (!acceptCategory(RPAREN)) {
update = parseExpression();
expectCategory(RPAREN);
}
Statement body = parseStatement();
return For(init, condition, update, body);
}
expectCategory(LPAREN);
if (acceptCategory(SEMICOLON)) {
return finishFor(null);
}
var keyword = acceptVarLetOrConst();
if (keyword != null) {
String identifier = lastToken;
expectCategory(ALPHA);
if (acceptString('in')) {
Expression objectExpression = parseExpression();
expectCategory(RPAREN);
Statement body = parseStatement();
return ForIn(_createVariableDeclarationList(keyword, identifier),
objectExpression, body);
} else if (acceptString('of')) {
Expression iterableExpression = parseAssignment();
expectCategory(RPAREN);
Statement body = parseStatement();
return ForOf(_createVariableDeclarationList(keyword, identifier),
iterableExpression, body);
}
var declarations = parseVariableDeclarationList(keyword, identifier);
expectCategory(SEMICOLON);
return finishFor(declarations);
}
Expression init = parseExpression();
expectCategory(SEMICOLON);
return finishFor(init);
}
static VariableDeclarationList _createVariableDeclarationList(
String keyword, String identifier) {
return VariableDeclarationList(
keyword, [VariableInitialization(Identifier(identifier), null)]);
}
Statement parseFunctionDeclaration() {
String name = lastToken;
expectCategory(ALPHA);
var fun = parseFun();
return FunctionDeclaration(Identifier(name), fun);
}
Statement parseTry() {
expectCategory(LBRACE);
Block body = parseBlock();
Catch catchPart;
if (acceptString('catch')) catchPart = parseCatch();
Block finallyPart;
if (acceptString('finally')) {
expectCategory(LBRACE);
finallyPart = parseBlock();
} else {
if (catchPart == null) error("expected 'finally'");
}
return Try(body, catchPart, finallyPart);
}
SwitchCase parseSwitchClause() {
Expression expression;
if (acceptString('case')) {
expression = parseExpression();
expectCategory(COLON);
} else {
if (!acceptString('default')) {
error('expected case or default');
}
expectCategory(COLON);
}
var statements = <Statement>[];
while (lastCategory != RBRACE &&
lastToken != 'case' &&
lastToken != 'default') {
statements.add(parseStatement());
}
return SwitchCase(expression, Block(statements));
}
Statement parseWhile() {
expectCategory(LPAREN);
Expression condition = parseExpression();
expectCategory(RPAREN);
Statement body = parseStatement();
return While(condition, body);
}
Statement parseDo() {
Statement body = parseStatement();
if (lastToken != "while") error("Missing while after do body.");
getToken();
expectCategory(LPAREN);
Expression condition = parseExpression();
expectCategory(RPAREN);
expectSemicolon();
return Do(body, condition);
}
Statement parseSwitch() {
expectCategory(LPAREN);
Expression key = parseExpression();
expectCategory(RPAREN);
expectCategory(LBRACE);
var clauses = List<SwitchCase>();
while (lastCategory != RBRACE) {
clauses.add(parseSwitchClause());
}
expectCategory(RBRACE);
return Switch(key, clauses);
}
Catch parseCatch() {
expectCategory(LPAREN);
String identifier = lastToken;
expectCategory(ALPHA);
expectCategory(RPAREN);
expectCategory(LBRACE);
Block body = parseBlock();
return Catch(Identifier(identifier), body);
}
ClassExpression parseClass() {
Identifier name = parseIdentifier();
Expression heritage;
if (acceptString('extends')) {
heritage = parseConditional();
}
expectCategory(LBRACE);
var methods = List<Method>();
while (lastCategory != RBRACE) {
methods.add(parseMethodOrProperty(onlyMethods: true) as Method);
}
expectCategory(RBRACE);
return ClassExpression(name, heritage, methods);
}
/**
* Parses a [Method] or a [Property].
*
* Most of the complexity is from supporting interpolation. Several forms
* are supported:
*
* - getter/setter names: `get #() { ... }`
* - method names: `#() { ... }`
* - property names: `#: ...`
* - entire methods: `#`
*/
Property parseMethodOrProperty({bool onlyMethods = false}) {
bool isStatic = acceptString('static');
bool isGetter = lastToken == 'get';
bool isSetter = lastToken == 'set';
Expression name;
if (isGetter || isSetter) {
var token = lastToken;
getToken();
if (lastCategory == COLON) {
// That wasn't a accessor but the 'get' or 'set' property: retropedal.
isGetter = isSetter = false;
name = LiteralString('"$token"');
}
}
if (acceptCategory(HASH)) {
if (lastCategory != LPAREN && (onlyMethods || lastCategory != COLON)) {
// Interpolated method
var member = InterpolatedMethod(parseHash());
interpolatedValues.add(member);
return member;
}
name = parseInterpolatedExpression();
} else {
name ??= parsePropertyName();
}
if (!onlyMethods && acceptCategory(COLON)) {
Expression value = parseAssignment();
return Property(name, value);
} else {
var fun = parseFun();
return Method(name, fun,
isGetter: isGetter, isSetter: isSetter, isStatic: isStatic);
}
}
Expression parsePropertyName() {
String identifier = lastToken;
if (acceptCategory(STRING)) {
return LiteralString(identifier);
} else if (acceptCategory(ALPHA) || acceptCategory(SYMBOL)) {
// ALPHA or a SYMBOL, e.g. void
return LiteralString('"$identifier"');
} else if (acceptCategory(LSQUARE)) {
var expr = parseAssignment();
expectCategory(RSQUARE);
return expr;
} else if (acceptCategory(HASH)) {
return parseInterpolatedExpression();
} else {
error('Expected property name');
return null;
}
}
}