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; |
- } |
- } |
-} |