| Index: lib/src/js/builder.dart
|
| diff --git a/lib/src/js/builder.dart b/lib/src/js/builder.dart
|
| deleted file mode 100644
|
| index 5037a3f0ef142003125f4e83838acf2d5f2f8dba..0000000000000000000000000000000000000000
|
| --- a/lib/src/js/builder.dart
|
| +++ /dev/null
|
| @@ -1,1651 +0,0 @@
|
| -// 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.
|
| -
|
| -// 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 = new TemplateManager();
|
| -
|
| -
|
| -/**
|
| -
|
| -[js] is a singleton instace 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 paceholder 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
|
| -ExpessionStatement. 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 tenplates 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 writted with `[]` if non-strings are accepted.)
|
| -
|
| -
|
| -Object initialiers 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 = const 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, [var arguments]) {
|
| - Template template = _findExpressionTemplate(source);
|
| - if (arguments == null) return template.instantiate([]);
|
| - // We allow a single argument to be given directly.
|
| - if (arguments is! List && arguments is! Map) arguments = [arguments];
|
| - return template.instantiate(arguments);
|
| - }
|
| -
|
| - /**
|
| - * Parses a JavaScript Statement, otherwise just like [call].
|
| - */
|
| - Statement statement(String source, [var arguments]) {
|
| - Template template = _findStatementTemplate(source);
|
| - if (arguments == null) return template.instantiate([]);
|
| - // We allow a single argument to be given directly.
|
| - if (arguments is! List && arguments is! Map) arguments = [arguments];
|
| - return template.instantiate(arguments);
|
| - }
|
| -
|
| - /**
|
| - * 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 = new 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 = new 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 = new MiniJsParser(source);
|
| - Expression expression = parser.expression();
|
| - return new Template(
|
| - source, expression, isExpression: true, forceCopy: false);
|
| - }
|
| -
|
| - /**
|
| - * Creates a Statement template without caching the result.
|
| - */
|
| - Template uncachedStatementTemplate(String source) {
|
| - MiniJsParser parser = new MiniJsParser(source);
|
| - Statement statement = parser.statement();
|
| - return new 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 new Template.withExpressionResult(ast);
|
| - }
|
| -
|
| - Template statementTemplateYielding(Node ast) {
|
| - return new Template.withStatementResult(ast);
|
| - }
|
| -
|
| - /// Creates a literal js string from [value].
|
| - LiteralString escapedString(String value, [String quote = '"']) {
|
| - // Start by escaping the backslashes.
|
| - String escaped = value.replaceAll('\\', '\\\\');
|
| -
|
| -
|
| - // Replace $ in template strings:
|
| - // http://www.ecma-international.org/ecma-262/6.0/#sec-template-literal-lexical-components
|
| - var quoteReplace = quote == '`' ? r'`$' : quote;
|
| -
|
| - // http://www.ecma-international.org/ecma-262/6.0/#sec-literals-string-literals
|
| - // > All code points may appear literally in a string literal except for the
|
| - // > closing quote code points, U+005C (REVERSE SOLIDUS),
|
| - // > U+000D (CARRIAGE RETURN), U+2028 (LINE SEPARATOR),
|
| - // > U+2029 (PARAGRAPH SEPARATOR), and U+000A (LINE FEED).
|
| - var re = new RegExp('[\n\r$quoteReplace\b\f\t\v\u2028\u2029]');
|
| - escaped = escaped.replaceAllMapped(re, (m) {
|
| - switch (m.group(0)) {
|
| - case "\n" : return r"\n";
|
| - case "\r" : return r"\r";
|
| - case "\u2028": return r"\u2028";
|
| - case "\u2029": return r"\u2029";
|
| - // Quotes and $ are only replaced if they conflict with the containing
|
| - // quote, see regex above.
|
| - case '"': return r'\"';
|
| - case "'": return r"\'";
|
| - case "`": return r"\`";
|
| - case r"$": return r"\$";
|
| - // TODO(jmesserly): these don't need to be escaped for correctness,
|
| - // but they are conventionally escaped.
|
| - case "\b": return r"\b";
|
| - case "\t": return r"\t";
|
| - case "\f": return r"\f";
|
| - case "\v": return r"\v";
|
| - }
|
| - });
|
| - LiteralString result = new LiteralString('$quote$escaped$quote');
|
| - // We don't escape quotes of a different style under the assumption that the
|
| - // string is wrapped into quotes. Verify that assumption.
|
| - assert(result.value.codeUnitAt(0) == quote.codeUnitAt(0));
|
| - return result;
|
| - }
|
| -
|
| - /// 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 = '"']) =>
|
| - new LiteralString('$quote$value$quote');
|
| -
|
| - LiteralNumber number(num value) => new LiteralNumber('$value');
|
| -
|
| - LiteralBool boolean(bool value) => new LiteralBool(value);
|
| -
|
| - ArrayInitializer numArray(Iterable<int> list) =>
|
| - new ArrayInitializer(list.map(number).toList());
|
| -
|
| - ArrayInitializer stringArray(Iterable<String> list) =>
|
| - new ArrayInitializer(list.map(string).toList());
|
| -
|
| - Comment comment(String text) => new Comment(text);
|
| - CommentExpression commentExpression(String text, Expression expression) =>
|
| - new CommentExpression(text, expression);
|
| -
|
| - Call propertyCall(Expression receiver,
|
| - String fieldName,
|
| - List<Expression> arguments) {
|
| - return new Call(new 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;
|
| -
|
| - 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(new 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 funtions 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 = null;
|
| - 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 = const <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'].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;
|
| - for (;;) {
|
| - 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);
|
| - int.parse(lastToken, onError: (_) {
|
| - 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) {
|
| - double.parse(lastToken, (_) {
|
| - 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(message) {
|
| - throw new 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 new LiteralBool(true);
|
| - } else if (last == "false") {
|
| - return new LiteralBool(false);
|
| - } else if (last == "null") {
|
| - return new LiteralNull();
|
| - } else if (last == "function") {
|
| - return parseFunctionExpression();
|
| - } else if (last == "this") {
|
| - return new This();
|
| - } else if (last == "super") {
|
| - return new Super();
|
| - } else if (last == "class") {
|
| - return parseClass();
|
| - } else {
|
| - return new Identifier(last);
|
| - }
|
| - } else if (acceptCategory(LPAREN)) {
|
| - return parseExpressionOrArrowFunction();
|
| - } else if (acceptCategory(STRING)) {
|
| - return new LiteralString(last);
|
| - } else if (acceptCategory(NUMERIC)) {
|
| - return new LiteralNumber(last);
|
| - } else if (acceptCategory(LBRACE)) {
|
| - return parseObjectInitializer();
|
| - } else if (acceptCategory(LSQUARE)) {
|
| - var values = <Expression>[];
|
| -
|
| - while (true) {
|
| - if (acceptCategory(COMMA)) {
|
| - values.add(new ArrayHole());
|
| - continue;
|
| - }
|
| - if (acceptCategory(RSQUARE)) break;
|
| - values.add(parseAssignment());
|
| - if (acceptCategory(RSQUARE)) break;
|
| - expectCategory(COMMA);
|
| - }
|
| - return new ArrayInitializer(values);
|
| - } else if (last != null && last.startsWith("/")) {
|
| - String regexp = getDelimited(lastPosition);
|
| - getToken();
|
| - String flags = lastToken;
|
| - if (!acceptCategory(ALPHA)) flags = "";
|
| - Expression expression = new RegExpLiteral(regexp + flags);
|
| - return expression;
|
| - } else if (acceptCategory(HASH)) {
|
| - return parseInterpolatedExpression();
|
| - } else {
|
| - error("Expected primary expression");
|
| - return null;
|
| - }
|
| - }
|
| -
|
| - InterpolatedExpression parseInterpolatedExpression() {
|
| - var expression = new InterpolatedExpression(parseHash());
|
| - interpolatedValues.add(expression);
|
| - return expression;
|
| - }
|
| -
|
| - InterpolatedIdentifier parseInterpolatedIdentifier() {
|
| - var id = new InterpolatedIdentifier(parseHash());
|
| - interpolatedValues.add(id);
|
| - return id;
|
| - }
|
| -
|
| - Identifier parseIdentifier() {
|
| - if (acceptCategory(HASH)) {
|
| - return parseInterpolatedIdentifier();
|
| - } else {
|
| - var id = new 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>[new 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(new RestParameter(parseParameter()));
|
| - expectCategory(RPAREN);
|
| - expectCategory(ARROW);
|
| - return parseArrowFunctionBody(params);
|
| - }
|
| - Expression right = parseAssignment();
|
| - expression = new 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(new 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 new ArrowFun(params, body);
|
| - }
|
| -
|
| - Expression parseFunctionExpression() {
|
| - String last = lastToken;
|
| - if (acceptCategory(ALPHA)) {
|
| - String functionName = last;
|
| - return new NamedFunction(new Identifier(functionName),
|
| - parseFun());
|
| - }
|
| - return parseFun();
|
| - }
|
| -
|
| - Expression parseFun() {
|
| - List<Parameter> params = <Parameter>[];
|
| -
|
| - expectCategory(LPAREN);
|
| - if (!acceptCategory(RPAREN)) {
|
| - for (;;) {
|
| - if (acceptCategory(ELLIPSIS)) {
|
| - params.add(new 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 new Fun(params, block, asyncModifier: asyncModifier);
|
| - }
|
| -
|
| - /** Parse parameter name or interpolated parameter. */
|
| - Identifier parseParameter() {
|
| - if (acceptCategory(HASH)) {
|
| - var nameOrPosition = parseHash();
|
| - var parameter = new InterpolatedParameter(nameOrPosition);
|
| - interpolatedValues.add(parameter);
|
| - return parameter;
|
| - } else {
|
| - // TODO(jmesserly): validate this is not a keyword
|
| - String argumentName = lastToken;
|
| - expectCategory(ALPHA);
|
| - return new Identifier(argumentName);
|
| - }
|
| - }
|
| -
|
| - Expression parseObjectInitializer() {
|
| - List<Property> properties = <Property>[];
|
| - for (;;) {
|
| - if (acceptCategory(RBRACE)) break;
|
| - // Limited subset of ES6 object initializers.
|
| - //
|
| - // PropertyDefinition :
|
| - // PropertyName : AssignmentExpression
|
| - // MethodDefinition
|
| - properties.add(parseMethodOrProperty());
|
| -
|
| - if (acceptCategory(RBRACE)) break;
|
| - expectCategory(COMMA);
|
| - }
|
| - return new 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 = new 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(new Spread(parseAssignment()));
|
| - expectCategory(RPAREN);
|
| - break;
|
| - }
|
| - arguments.add(parseAssignment());
|
| - if (acceptCategory(RPAREN)) break;
|
| - expectCategory(COMMA);
|
| - }
|
| - }
|
| - receiver = constructor ?
|
| - new New(receiver, arguments) :
|
| - new Call(receiver, arguments);
|
| - constructor = false;
|
| - } else if (!constructor && acceptCategory(LSQUARE)) {
|
| - Expression inBraces = parseExpression();
|
| - expectCategory(RSQUARE);
|
| - receiver = new 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 = new InterpolatedSelector(nameOrPosition);
|
| - interpolatedValues.add(property);
|
| - return new 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 new 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 new 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 new Await(parsePostfix());
|
| - return new 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 new Await(parsePostfix());
|
| - return new 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 = new Binary(lastSymbol, lhs, rhs);
|
| - minPrecedence = BINARY_PRECEDENCE[symbol];
|
| - rhs = parseUnaryLow();
|
| - lastSymbol = symbol;
|
| - } else {
|
| - Expression higher = parseBinary(BINARY_PRECEDENCE[symbol]);
|
| - rhs = new Binary(symbol, rhs, higher);
|
| - }
|
| - }
|
| - if (rhs == null) return lhs;
|
| - return new 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 new Conditional(lhs, ifTrue, ifFalse);
|
| - }
|
| -
|
| - Expression parseLeftHandSide() => parseConditional();
|
| -
|
| - Expression parseAssignment() {
|
| - Expression lhs = parseLeftHandSide();
|
| - String assignmentOperator = lastToken;
|
| - if (acceptCategory(ASSIGNMENT)) {
|
| - Expression rhs = parseAssignment();
|
| - if (assignmentOperator == "=") {
|
| - return new Assignment(lhs, rhs);
|
| - } else {
|
| - // Handle +=, -=, etc.
|
| - String operator =
|
| - assignmentOperator.substring(0, assignmentOperator.length - 1);
|
| - return new Assignment.compound(lhs, operator, rhs);
|
| - }
|
| - }
|
| - return lhs;
|
| - }
|
| -
|
| - Expression parseExpression() {
|
| - Expression expression = parseAssignment();
|
| - while (acceptCategory(COMMA)) {
|
| - Expression right = parseAssignment();
|
| - expression = new Binary(',', expression, right);
|
| - }
|
| - return expression;
|
| - }
|
| -
|
| - /** Parse a variable declaration list, with `var` or `let` [keyword] */
|
| - VariableDeclarationList parseVariableDeclarationList(
|
| - String keyword, [String firstIdentifier]) {
|
| - var initialization = [];
|
| -
|
| - do {
|
| - var declarator;
|
| - if (firstIdentifier != null) {
|
| - declarator = new Identifier(firstIdentifier);
|
| - firstIdentifier = null;
|
| - } else {
|
| - declarator = parseVariableBinding();
|
| - }
|
| -
|
| - var initializer = acceptString("=") ? parseAssignment() : null;
|
| - initialization.add(new VariableInitialization(declarator, initializer));
|
| - } while (acceptCategory(COMMA));
|
| -
|
| - return new 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 {
|
| - var name;
|
| - var structure;
|
| - var 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(new DestructuredVariable(
|
| - name: name, structure: structure, defaultValue: defaultValue));
|
| - } while (acceptCategory(COMMA));
|
| -
|
| - expectCategory(RSQUARE);
|
| - return new ArrayBindingPattern(variables);
|
| - }
|
| -
|
| - ObjectBindingPattern parseObjectBindingPattern() {
|
| - var variables = <DestructuredVariable>[];
|
| - do {
|
| - var name = parseIdentifier();
|
| - var structure;
|
| - var defaultValue;
|
| -
|
| - if (acceptCategory(COLON)) {
|
| - structure = parseBindingPattern();
|
| - } else if (acceptString("=")) {
|
| - defaultValue = parseExpression();
|
| - }
|
| - variables.add(new DestructuredVariable(
|
| - name: name, structure: structure, defaultValue: defaultValue));
|
| - } while (acceptCategory(COMMA));
|
| -
|
| - expectCategory(RBRACE);
|
| - return new 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 new Block(statements);
|
| - }
|
| -
|
| - Statement parseStatement() {
|
| - if (acceptCategory(LBRACE)) return parseBlock();
|
| -
|
| - if (acceptCategory(SEMICOLON)) return new EmptyStatement();
|
| -
|
| - if (lastCategory == ALPHA) {
|
| - if (acceptString('return')) return parseReturn();
|
| -
|
| - if (acceptString('throw')) return parseThrow();
|
| -
|
| - if (acceptString('break')) {
|
| - return parseBreakOrContinue((label) => new Break(label));
|
| - }
|
| -
|
| - if (acceptString('continue')) {
|
| - return parseBreakOrContinue((label) => new Continue(label));
|
| - }
|
| -
|
| - if (acceptString('if')) return parseIfThenElse();
|
| -
|
| - if (acceptString('for')) return parseFor();
|
| -
|
| - if (acceptString('function')) return parseFunctionDeclaration();
|
| -
|
| - if (acceptString('class')) return new ClassDeclaration(parseClass());
|
| -
|
| - if (acceptString('try')) return parseTry();
|
| -
|
| - var keyword = acceptVarLetOrConst();
|
| - if (keyword != null) {
|
| - Expression declarations = parseVariableDeclarationList(keyword);
|
| - expectSemicolon();
|
| - return new 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 new 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 =
|
| - new InterpolatedStatement(expression.nameOrPosition);
|
| - interpolatedValues[interpolatedValues.length - 1] = statement;
|
| - return statement;
|
| - }
|
| - }
|
| -
|
| - return new ExpressionStatement(expression);
|
| - }
|
| -
|
| - Statement parseReturn() {
|
| - if (acceptSemicolon()) return new Return();
|
| - Expression expression = parseExpression();
|
| - expectSemicolon();
|
| - return new Return(expression);
|
| - }
|
| -
|
| - Statement parseYield() {
|
| - bool hasStar = acceptString('*');
|
| - Expression expression = parseExpression();
|
| - expectSemicolon();
|
| - return new DartYield(expression, hasStar);
|
| - }
|
| -
|
| - Statement parseThrow() {
|
| - if (skippedNewline) error('throw expression must be on same line');
|
| - Expression expression = parseExpression();
|
| - expectSemicolon();
|
| - return new Throw(expression);
|
| - }
|
| -
|
| - Statement parseBreakOrContinue(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 new If(condition, thenStatement, elseStatement);
|
| - } else {
|
| - return new 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 = null;
|
| - if (!acceptCategory(SEMICOLON)) {
|
| - condition = parseExpression();
|
| - expectCategory(SEMICOLON);
|
| - }
|
| - Expression update = null;
|
| - if (!acceptCategory(RPAREN)) {
|
| - update = parseExpression();
|
| - expectCategory(RPAREN);
|
| - }
|
| - Statement body = parseStatement();
|
| - return new 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 new ForIn(
|
| - _createVariableDeclarationList(keyword, identifier),
|
| - objectExpression,
|
| - body);
|
| - } else if (acceptString('of')) {
|
| - Expression iterableExpression = parseAssignment();
|
| - expectCategory(RPAREN);
|
| - Statement body = parseStatement();
|
| - return new 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 new VariableDeclarationList(keyword, [
|
| - new VariableInitialization(
|
| - new Identifier(identifier), null)]);
|
| - }
|
| -
|
| - Statement parseFunctionDeclaration() {
|
| - String name = lastToken;
|
| - expectCategory(ALPHA);
|
| - Expression fun = parseFun();
|
| - return new FunctionDeclaration(new Identifier(name), fun);
|
| - }
|
| -
|
| - Statement parseTry() {
|
| - expectCategory(LBRACE);
|
| - Block body = parseBlock();
|
| - Catch catchPart = null;
|
| - if (acceptString('catch')) catchPart = parseCatch();
|
| - Block finallyPart = null;
|
| - if (acceptString('finally')) {
|
| - expectCategory(LBRACE);
|
| - finallyPart = parseBlock();
|
| - } else {
|
| - if (catchPart == null) error("expected 'finally'");
|
| - }
|
| - return new Try(body, catchPart, finallyPart);
|
| - }
|
| -
|
| - SwitchClause parseSwitchClause() {
|
| - Expression expression = null;
|
| - if (acceptString('case')) {
|
| - expression = parseExpression();
|
| - expectCategory(COLON);
|
| - } else {
|
| - if (!acceptString('default')) {
|
| - error('expected case or default');
|
| - }
|
| - expectCategory(COLON);
|
| - }
|
| - List statements = new List<Statement>();
|
| - while (lastCategory != RBRACE &&
|
| - lastToken != 'case' &&
|
| - lastToken != 'default') {
|
| - statements.add(parseStatement());
|
| - }
|
| - return expression == null
|
| - ? new Default(new Block(statements))
|
| - : new Case(expression, new Block(statements));
|
| - }
|
| -
|
| - Statement parseWhile() {
|
| - expectCategory(LPAREN);
|
| - Expression condition = parseExpression();
|
| - expectCategory(RPAREN);
|
| - Statement body = parseStatement();
|
| - return new 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 new Do(body, condition);
|
| - }
|
| -
|
| - Statement parseSwitch() {
|
| - expectCategory(LPAREN);
|
| - Expression key = parseExpression();
|
| - expectCategory(RPAREN);
|
| - expectCategory(LBRACE);
|
| - List<SwitchClause> clauses = new List<SwitchClause>();
|
| - while(lastCategory != RBRACE) {
|
| - clauses.add(parseSwitchClause());
|
| - }
|
| - expectCategory(RBRACE);
|
| - return new Switch(key, clauses);
|
| - }
|
| -
|
| - Catch parseCatch() {
|
| - expectCategory(LPAREN);
|
| - String identifier = lastToken;
|
| - expectCategory(ALPHA);
|
| - expectCategory(RPAREN);
|
| - expectCategory(LBRACE);
|
| - Block body = parseBlock();
|
| - return new Catch(new Identifier(identifier), body);
|
| - }
|
| -
|
| - ClassExpression parseClass() {
|
| - Identifier name = parseIdentifier();
|
| - Expression heritage = null;
|
| - if (acceptString('extends')) {
|
| - heritage = parseConditional();
|
| - }
|
| - expectCategory(LBRACE);
|
| - var methods = new List<Method>();
|
| - while (lastCategory != RBRACE) {
|
| - methods.add(parseMethodOrProperty(onlyMethods: true));
|
| - }
|
| - expectCategory(RBRACE);
|
| - return new 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 = null;
|
| - 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 = new LiteralString('"$token"');
|
| - }
|
| - }
|
| - if (acceptCategory(HASH)) {
|
| - if (lastCategory != LPAREN && (onlyMethods || lastCategory != COLON)) {
|
| - // Interpolated method
|
| - var member = new InterpolatedMethod(parseHash());
|
| - interpolatedValues.add(member);
|
| - return member;
|
| - }
|
| - name = parseInterpolatedExpression();
|
| - } else {
|
| - name ??= parsePropertyName();
|
| - }
|
| -
|
| - if (!onlyMethods && acceptCategory(COLON)) {
|
| - Expression value = parseAssignment();
|
| - return new Property(name, value);
|
| - } else {
|
| - var fun = parseFun();
|
| - return new Method(name, fun,
|
| - isGetter: isGetter, isSetter: isSetter, isStatic: isStatic);
|
| - }
|
| - }
|
| -
|
| - Expression parsePropertyName() {
|
| - String identifier = lastToken;
|
| - if (acceptCategory(STRING)) {
|
| - return new LiteralString(identifier);
|
| - } else if (acceptCategory(ALPHA) || acceptCategory(SYMBOL)) {
|
| - // ALPHA or a SYMBOL, e.g. void
|
| - return new 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;
|
| - }
|
| - }
|
| -}
|
|
|