Linter Demo Errors: 0Warnings: 6File: /home/fstrocco/Dart/dart/benchmark/devcompiler/lib/src/js/builder.dart // 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('\\', '\\\\'); // Do not escape unicode characters and ' because they are allowed in the // string literal anyway. escaped = escaped.replaceAllMapped(new RegExp('\n|$quote|\b|\t|\v'), (m) { switch (m.group(0)) { case "\n" : return r"\n"; // Quotes are only replaced if they conflict with the containing quote case '"': return r'\"'; case "'": return r"\'"; case "`": return r"\`"; 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 list) => new ArrayInitializer(list.map(number).toList()); ArrayInitializer stringArray(Iterable 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 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 list) => js.numArray(list); ArrayInitializer stringArray(Iterable list) => js.stringArray(list); Call propertyCall(Expression receiver, String fieldName, List 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 interpolatedValues = []; 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 HASH = 17; static const WHITESPACE = 18; static const OTHER = 19; // Make sure that ]] is two symbols. 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 HASH: return "HASH"; case WHITESPACE: return "WHITESPACE"; case OTHER: return "OTHER"; } return "Unknown: $cat"; } static const CATEGORIES = const [ 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 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 == 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 = []; 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; } /** * CoverParenthesizedExpressionAndArrowParameterList[Yield] : * ( Expression ) * ( ) * ( ... BindingIdentifier ) * ( Expression , ... BindingIdentifier ) */ Expression parseExpressionOrArrowFunction() { if (acceptCategory(RPAREN)) { expectCategory(ARROW); return parseArrowFunctionBody([]); } Expression expression = parseExpression(); expectCategory(RPAREN); if (acceptCategory(ARROW)) { var params = []; _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 params) { if (node is Identifier) { // TODO(jmesserly): support default/rest parameters 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 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 params = []; expectCategory(LPAREN); if (!acceptCategory(RPAREN)) { for (;;) { if (acceptCategory(HASH)) { var nameOrPosition = parseHash(); InterpolatedParameter parameter = new InterpolatedParameter(nameOrPosition); interpolatedValues.add(parameter); params.add(parameter); } else { String argumentName = lastToken; expectCategory(ALPHA); params.add(new Identifier(argumentName)); } if (acceptCategory(COMMA)) continue; 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); } Expression parseObjectInitializer() { List properties = []; 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 = []; if (!acceptCategory(RPAREN)) { while (true) { Expression argument = parseAssignment(); arguments.add(argument); 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) { // Supports one form for interpolated variable declaration: // let # = ... if (acceptCategory(HASH)) { var name = new InterpolatedIdentifier(parseHash()); interpolatedValues.add(name); Expression initializer = acceptString("=") ? parseAssignment() : null; return new VariableDeclarationList(keyword, [new VariableInitialization(name, initializer)]); } String firstVariable = lastToken; expectCategory(ALPHA); return finishVariableDeclarationList(keyword, firstVariable); } VariableDeclarationList finishVariableDeclarationList( String keyword, String firstVariable) { var initialization = []; void declare(String variable) { Expression initializer = null; if (acceptString("=")) { initializer = parseAssignment(); } var declaration = new Identifier(variable); initialization.add(new VariableInitialization(declaration, initializer)); } declare(firstVariable); while (acceptCategory(COMMA)) { String variable = lastToken; expectCategory(ALPHA); declare(variable); } return new VariableDeclarationList(keyword, initialization); } Expression parseVarDeclarationOrExpression() { var keyword = acceptVarOrLet(); if (keyword != null) { return parseVariableDeclarationList(keyword); } else { return parseExpression(); } } /** Accepts a `var` or `let` keyword. If neither is found, returns null. */ String acceptVarOrLet() { if (acceptString('var')) return 'var'; if (acceptString('let')) return 'let'; 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 statements = []; 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 = acceptVarOrLet(); 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 = acceptVarOrLet(); 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 = finishVariableDeclarationList(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(); 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 clauses = new List(); 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; if (acceptCategory(HASH)) { var interpolatedName = new InterpolatedIdentifier(parseHash()); interpolatedValues.add(interpolatedName); name = interpolatedName; } else { name = new Identifier(lastToken); expectCategory(ALPHA); } Expression heritage = null; if (acceptString('extends')) { heritage = parseLeftHandSide(); } expectCategory(LBRACE); var methods = new List(); 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 = false; bool isSetter = false; Expression name = null; bool propertyNameIsIdentifier = lastCategory == ALPHA; 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(); } // Allow get or set to be followed by another property name. if (propertyNameIsIdentifier && (lastCategory == ALPHA || lastCategory == HASH)) { LiteralString p = name; isGetter = p.value == '"get"'; isSetter = p.value == '"set"'; if (isGetter || isSetter) { 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; } } }