| // 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, [arguments]) { | |
| Template template = _findExpressionTemplate(source); | |
| if (arguments == null) return template.instantiate([]) as Expression; | |
| // We allow a single argument to be given directly. | |
| if (arguments is! List && arguments is! Map) arguments = [arguments]; | |
| return template.instantiate(arguments) as Expression; | |
| } | |
| /** | |
| * Parses a JavaScript Statement, otherwise just like [call]. | |
| */ | |
| Statement statement(String source, [arguments]) { | |
| Template template = _findStatementTemplate(source); | |
| if (arguments == null) return template.instantiate([]) as Statement; | |
| // We allow a single argument to be given directly. | |
| if (arguments is! List && arguments is! Map) arguments = [arguments]; | |
| return template.instantiate(arguments) as Statement; | |
| } | |
| Block block(String source, [arguments]) => | |
| statement(source, arguments) as Block; | |
| Fun fun(String source, [arguments]) => call(source, arguments) as Fun; | |
| /** | |
| * Parses JavaScript written in the `JS` foreign instruction. | |
| * | |
| * The [source] must be a JavaScript expression or a JavaScript throw | |
| * statement. | |
| */ | |
| Template parseForeignJS(String source) { | |
| // TODO(sra): Parse with extra validation to forbid `#` interpolation in | |
| // functions, as this leads to unanticipated capture of temporaries that are | |
| // reused after capture. | |
| if (source.startsWith("throw ")) { | |
| return _findStatementTemplate(source); | |
| } else { | |
| return _findExpressionTemplate(source); | |
| } | |
| } | |
| Template _findExpressionTemplate(String source) { | |
| Template template = templateManager.lookupExpressionTemplate(source); | |
| if (template == null) { | |
| MiniJsParser parser = 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 functions and named function expressions and declarations. | |
| /// Notable things it can't do yet include: | |
| /// * some statements are still missing (do-while, while, switch). | |
| /// | |
| /// It's a fairly standard recursive descent parser. | |
| /// | |
| /// Literal strings are passed through to the final JS source code unchanged, | |
| /// including the choice of surrounding quotes, so if you parse | |
| /// r'var x = "foo\n\"bar\""' you will end up with | |
| /// var x = "foo\n\"bar\"" in the final program. \x and \u escapes are not | |
| /// allowed in string and regexp literals because the machinery for checking | |
| /// their correctness is rather involved. | |
| class MiniJsParser { | |
| MiniJsParser(this.src) | |
| : lastCategory = NONE, | |
| lastToken = null, | |
| lastPosition = 0, | |
| position = 0 { | |
| getToken(); | |
| } | |
| int lastCategory = NONE; | |
| String lastToken = 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(String 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(); | |
| } | |
| Fun 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 = <VariableInitialization>[]; | |
| do { | |
| VariableBinding 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 { | |
| Identifier name; | |
| BindingPattern structure; | |
| Expression defaultValue; | |
| var declarator = parseVariableBinding(); | |
| if (declarator is Identifier) { | |
| name = declarator; | |
| } else if (declarator is BindingPattern) { | |
| structure = declarator; | |
| } else { | |
| error("Unexpected LHS: $declarator"); | |
| } | |
| if (acceptString("=")) { | |
| defaultValue = parseExpression(); | |
| } | |
| variables.add(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(); | |
| BindingPattern structure; | |
| Expression 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('debugger')) { | |
| expectSemicolon(); | |
| return new DebuggerStatement(); | |
| } | |
| 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(Statement Function(String) constructor) { | |
| var identifier = lastToken; | |
| if (!skippedNewline && acceptCategory(ALPHA)) { | |
| expectSemicolon(); | |
| return constructor(identifier); | |
| } | |
| expectSemicolon(); | |
| return constructor(null); | |
| } | |
| Statement parseIfThenElse() { | |
| expectCategory(LPAREN); | |
| Expression condition = parseExpression(); | |
| expectCategory(RPAREN); | |
| Statement thenStatement = parseStatement(); | |
| if (acceptString('else')) { | |
| // Resolves dangling else by binding 'else' to closest 'if'. | |
| Statement elseStatement = parseStatement(); | |
| return 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); | |
| var 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); | |
| } | |
| SwitchCase parseSwitchClause() { | |
| Expression expression = null; | |
| if (acceptString('case')) { | |
| expression = parseExpression(); | |
| expectCategory(COLON); | |
| } else { | |
| if (!acceptString('default')) { | |
| error('expected case or default'); | |
| } | |
| expectCategory(COLON); | |
| } | |
| var statements = <Statement>[]; | |
| while (lastCategory != RBRACE && | |
| lastToken != 'case' && | |
| lastToken != 'default') { | |
| statements.add(parseStatement()); | |
| } | |
| return new SwitchCase(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); | |
| var clauses = new List<SwitchCase>(); | |
| 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) as Method); | |
| } | |
| 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; | |
| } | |
| } | |
| } |