Version 2.14.0-131.0.dev

Merge commit 'c3ba9ac20427c4f861fd5d6a27bfa6afb3de65df' into 'dev'
diff --git a/.dart_tool/package_config.json b/.dart_tool/package_config.json
index f524ec3..b5ac7eb 100644
--- a/.dart_tool/package_config.json
+++ b/.dart_tool/package_config.json
@@ -11,7 +11,7 @@
     "constraint, update this by running tools/generate_package_config.dart."
   ],
   "configVersion": 2,
-  "generated": "2021-05-17T10:34:01.378194",
+  "generated": "2021-05-19T20:22:15.931476",
   "generator": "tools/generate_package_config.dart",
   "packages": [
     {
@@ -381,7 +381,7 @@
       "name": "js_ast",
       "rootUri": "../pkg/js_ast",
       "packageUri": "lib/",
-      "languageVersion": "2.0"
+      "languageVersion": "2.10"
     },
     {
       "name": "js_runtime",
diff --git a/pkg/compiler/lib/src/js/js.dart b/pkg/compiler/lib/src/js/js.dart
index b49abc0..9c8328e 100644
--- a/pkg/compiler/lib/src/js/js.dart
+++ b/pkg/compiler/lib/src/js/js.dart
@@ -157,7 +157,7 @@
           }
         }
       }
-      _cachedLiteral = js.escapedString(text);
+      _cachedLiteral = js.string(text);
     }
     return _cachedLiteral;
   }
diff --git a/pkg/compiler/lib/src/js/size_estimator.dart b/pkg/compiler/lib/src/js/size_estimator.dart
index 8ecfd9e..24527ec6 100644
--- a/pkg/compiler/lib/src/js/size_estimator.dart
+++ b/pkg/compiler/lib/src/js/size_estimator.dart
@@ -749,24 +749,24 @@
   }
 
   bool isValidJavaScriptId(String field) {
-    if (field.length < 3) return false;
+    if (field.length == 0) return false;
     // Ignore the leading and trailing string-delimiter.
-    for (int i = 1; i < field.length - 1; i++) {
+    for (int i = 0; i < field.length; i++) {
       // TODO(floitsch): allow more characters.
       int charCode = field.codeUnitAt(i);
       if (!(charCodes.$a <= charCode && charCode <= charCodes.$z ||
           charCodes.$A <= charCode && charCode <= charCodes.$Z ||
           charCode == charCodes.$$ ||
           charCode == charCodes.$_ ||
-          i != 1 && isDigit(charCode))) {
+          i > 0 && isDigit(charCode))) {
         return false;
       }
     }
     // TODO(floitsch): normally we should also check that the field is not a
     // reserved word.  We don't generate fields with reserved word names except
     // for 'super'.
-    if (field == '"super"') return false;
-    if (field == '"catch"') return false;
+    if (field == 'super') return false;
+    if (field == 'catch') return false;
     return true;
   }
 
@@ -776,16 +776,17 @@
         newInForInit: inForInit, newAtStatementBegin: atStatementBegin);
     Node selector = access.selector;
     if (selector is LiteralString) {
-      String fieldWithQuotes = literalStringToString(selector);
-      if (isValidJavaScriptId(fieldWithQuotes)) {
+      String field = literalStringToString(selector);
+      if (isValidJavaScriptId(field)) {
         if (access.receiver is LiteralNumber) {
           // We can eliminate the space in some cases, but for simplicity we
           // always assume it is necessary.
           out(' '); // ' '
         }
 
-        // '.${fieldWithQuotes.substring(1, fieldWithQuotes.length - 1)}'
-        out('.${fieldWithQuotes.substring(1, fieldWithQuotes.length - 1)}');
+        // '.${field}'
+        out('.');
+        out(field);
         return;
       }
     } else if (selector is Name) {
@@ -875,7 +876,9 @@
 
   @override
   void visitLiteralString(LiteralString node) {
+    out('"');
     out(literalStringToString(node));
+    out('"');
   }
 
   @override
@@ -968,10 +971,12 @@
     if (name is LiteralString) {
       String text = literalStringToString(name);
       if (isValidJavaScriptId(text)) {
-        // '${text.substring(1, text.length - 1)}
-        out('${text.substring(1, text.length - 1)}');
+        out(text);
       } else {
-        out(text); // '$text'
+        // Approximation to `_handleString(text)`.
+        out('"');
+        out(text);
+        out('"');
       }
     } else if (name is Name) {
       node.name.accept(this);
diff --git a/pkg/compiler/lib/src/js_backend/constant_emitter.dart b/pkg/compiler/lib/src/js_backend/constant_emitter.dart
index ec3928d..dd67c4a 100644
--- a/pkg/compiler/lib/src/js_backend/constant_emitter.dart
+++ b/pkg/compiler/lib/src/js_backend/constant_emitter.dart
@@ -148,7 +148,7 @@
   jsAst.Expression visitString(StringConstantValue constant, [_]) {
     String value = constant.stringValue;
     if (value.length < StringReferencePolicy.minimumLength) {
-      return js.escapedString(value, ascii: true);
+      return js.string(value);
     }
     return StringReference(constant);
   }
@@ -288,8 +288,7 @@
         }
 
         // Keys in literal maps must be emitted in place.
-        jsAst.Literal keyExpression =
-            js.escapedString(key.stringValue, ascii: true);
+        jsAst.Literal keyExpression = js.string(key.stringValue);
         jsAst.Expression valueExpression =
             _constantReferenceGenerator(constant.values[i]);
         properties.add(new jsAst.Property(keyExpression, valueExpression));
diff --git a/pkg/compiler/lib/src/js_backend/runtime_types_new.dart b/pkg/compiler/lib/src/js_backend/runtime_types_new.dart
index 3c9f11f..4c59ced 100644
--- a/pkg/compiler/lib/src/js_backend/runtime_types_new.dart
+++ b/pkg/compiler/lib/src/js_backend/runtime_types_new.dart
@@ -109,9 +109,7 @@
       return _finishEncoding(js.string(String.fromCharCodes(_codes)));
     }
     _flushCodes();
-    jsAst.LiteralString quote = jsAst.LiteralString('"');
-    return _finishEncoding(
-        jsAst.StringConcatenation([quote, ..._fragments, quote]));
+    return _finishEncoding(jsAst.StringConcatenation(_fragments));
   }
 
   void _start(TypeRecipe recipe) {
@@ -487,13 +485,13 @@
   CommonElements get _commonElements => _dartTypes.commonElements;
   ClassEntity get _objectClass => _commonElements.objectClass;
 
-  final _leftBrace = js.stringPart('{');
-  final _rightBrace = js.stringPart('}');
-  final _leftBracket = js.stringPart('[');
-  final _rightBracket = js.stringPart(']');
-  final _colon = js.stringPart(':');
-  final _comma = js.stringPart(',');
-  final _quote = js.stringPart("'");
+  final _leftBrace = js.string('{');
+  final _rightBrace = js.string('}');
+  final _leftBracket = js.string('[');
+  final _rightBracket = js.string(']');
+  final _colon = js.string(':');
+  final _comma = js.string(',');
+  final _doubleQuote = js.string('"');
 
   bool _isObject(InterfaceType type) => identical(type.element, _objectClass);
 
@@ -522,28 +520,32 @@
 
   jsAst.StringConcatenation _encodeRuleset(Ruleset ruleset) =>
       js.concatenateStrings([
-        _quote,
         _leftBrace,
         ...js.joinLiterals([
           ...ruleset._redirections.entries.map(_encodeRedirection),
           ...ruleset._entries.entries.map(_encodeEntry),
         ], _comma),
         _rightBrace,
-        _quote,
       ]);
 
   jsAst.StringConcatenation _encodeRedirection(
           MapEntry<ClassEntity, ClassEntity> redirection) =>
       js.concatenateStrings([
-        js.quoteName(_emitter.typeAccessNewRti(redirection.key)),
+        _doubleQuote,
+        _emitter.typeAccessNewRti(redirection.key),
+        _doubleQuote,
         _colon,
-        js.quoteName(_emitter.typeAccessNewRti(redirection.value)),
+        _doubleQuote,
+        _emitter.typeAccessNewRti(redirection.value),
+        _doubleQuote,
       ]);
 
   jsAst.StringConcatenation _encodeEntry(
           MapEntry<InterfaceType, _RulesetEntry> entry) =>
       js.concatenateStrings([
-        js.quoteName(_emitter.typeAccessNewRti(entry.key.element)),
+        _doubleQuote,
+        _emitter.typeAccessNewRti(entry.key.element),
+        _doubleQuote,
         _colon,
         _leftBrace,
         ...js.joinLiterals([
@@ -558,7 +560,9 @@
   jsAst.StringConcatenation _encodeSupertype(
           InterfaceType targetType, InterfaceType supertype) =>
       js.concatenateStrings([
-        js.quoteName(_emitter.typeAccessNewRti(supertype.element)),
+        _doubleQuote,
+        _emitter.typeAccessNewRti(supertype.element),
+        _doubleQuote,
         _colon,
         _leftBracket,
         ...js.joinLiterals(
@@ -571,30 +575,36 @@
   jsAst.StringConcatenation _encodeTypeVariable(InterfaceType targetType,
           TypeVariableType typeVariable, DartType supertypeArgument) =>
       js.concatenateStrings([
-        js.quoteName(_emitter.typeVariableAccessNewRti(typeVariable.element)),
+        _doubleQuote,
+        _emitter.typeVariableAccessNewRti(typeVariable.element),
+        _doubleQuote,
         _colon,
         _encodeSupertypeArgument(targetType, supertypeArgument),
       ]);
 
   jsAst.Literal _encodeSupertypeArgument(
           InterfaceType targetType, DartType supertypeArgument) =>
-      _recipeEncoder.encodeMetadataRecipe(
-          _emitter, targetType, supertypeArgument);
+      js.concatenateStrings([
+        _doubleQuote,
+        _recipeEncoder.encodeMetadataRecipe(
+            _emitter, targetType, supertypeArgument),
+        _doubleQuote
+      ]);
 
   jsAst.StringConcatenation encodeErasedTypes(
           Map<ClassEntity, int> erasedTypes) =>
       js.concatenateStrings([
-        _quote,
         _leftBrace,
         ...js.joinLiterals(erasedTypes.entries.map(encodeErasedType), _comma),
         _rightBrace,
-        _quote,
       ]);
 
   jsAst.StringConcatenation encodeErasedType(
           MapEntry<ClassEntity, int> entry) =>
       js.concatenateStrings([
-        js.quoteName(_emitter.typeAccessNewRti(entry.key)),
+        _doubleQuote,
+        _emitter.typeAccessNewRti(entry.key),
+        _doubleQuote,
         _colon,
         js.number(entry.value),
       ]);
@@ -602,20 +612,20 @@
   jsAst.StringConcatenation encodeTypeParameterVariances(
           Map<ClassEntity, List<Variance>> typeParameterVariances) =>
       js.concatenateStrings([
-        _quote,
         _leftBrace,
         ...js.joinLiterals(
             typeParameterVariances.entries
                 .map(_encodeTypeParameterVariancesForClass),
             _comma),
         _rightBrace,
-        _quote,
       ]);
 
   jsAst.StringConcatenation _encodeTypeParameterVariancesForClass(
           MapEntry<ClassEntity, List<Variance>> classEntry) =>
       js.concatenateStrings([
-        js.quoteName(_emitter.typeAccessNewRti(classEntry.key)),
+        _doubleQuote,
+        _emitter.typeAccessNewRti(classEntry.key),
+        _doubleQuote,
         _colon,
         _leftBracket,
         ...js.joinLiterals(
diff --git a/pkg/compiler/lib/src/js_backend/string_reference.dart b/pkg/compiler/lib/src/js_backend/string_reference.dart
index 05e1b8f..590ff93 100644
--- a/pkg/compiler/lib/src/js_backend/string_reference.dart
+++ b/pkg/compiler/lib/src/js_backend/string_reference.dart
@@ -256,8 +256,7 @@
     for (_ReferenceSet referenceSet in _referencesByString.values) {
       if (referenceSet.generateAtUse) {
         StringConstantValue constant = referenceSet.constant;
-        js.Expression reference =
-            js.js.escapedString(constant.stringValue, ascii: true);
+        js.Expression reference = js.string(constant.stringValue);
         for (StringReference ref in referenceSet._references) {
           ref.value = reference;
         }
@@ -275,8 +274,7 @@
     for (_ReferenceSet referenceSet in referenceSetsUsingProperties) {
       String string = referenceSet.constant.stringValue;
       var propertyName = js.string(referenceSet.propertyName);
-      properties.add(
-          js.Property(propertyName, js.js.escapedString(string, ascii: true)));
+      properties.add(js.Property(propertyName, js.string(string)));
       var access = js.js('#.#', [holderLocalName, propertyName]);
       for (StringReference ref in referenceSet._references) {
         ref.value = access;
diff --git a/pkg/compiler/test/codegen/jsarray_indexof_test.dart b/pkg/compiler/test/codegen/jsarray_indexof_test.dart
index 4b75ecd..b567137 100644
--- a/pkg/compiler/test/codegen/jsarray_indexof_test.dart
+++ b/pkg/compiler/test/codegen/jsarray_indexof_test.dart
@@ -67,7 +67,7 @@
         "${js.nodeToString(method.code, pretty: true)}");
   }, onPropertyAccess: (js.PropertyAccess node) {
     js.Node selector = node.selector;
-    if (selector is js.LiteralString && selector.value == '"length"') {
+    if (selector is js.LiteralString && selector.value == 'length') {
       lengthCount++;
     }
   });
diff --git a/pkg/compiler/test/codegen/model_test.dart b/pkg/compiler/test/codegen/model_test.dart
index f7a1537..ba7376d 100644
--- a/pkg/compiler/test/codegen/model_test.dart
+++ b/pkg/compiler/test/codegen/model_test.dart
@@ -101,7 +101,7 @@
           /// Call to fixed backend name, so we include the argument
           /// values to test encoding of optional parameters in native
           /// methods.
-          name = selector.value.substring(1, selector.value.length - 1);
+          name = selector.value;
           fixedNameCall = true;
         }
         if (name != null) {
@@ -146,7 +146,7 @@
         /// Call to fixed backend name, so we include the argument
         /// values to test encoding of optional parameters in native
         /// methods.
-        name = selector.value.substring(1, selector.value.length - 1);
+        name = selector.value;
       }
 
       if (receiverName != null && name != null) {
@@ -236,7 +236,7 @@
             if (selector is js.Name) {
               name = selector.key;
             } else if (selector is js.LiteralString) {
-              name = selector.value.substring(1, selector.value.length - 1);
+              name = selector.value;
             }
             if (name != null) {
               features.addElement(Tags.assignment, '${name}');
diff --git a/pkg/compiler/test/js/js_parser_test.dart b/pkg/compiler/test/js/js_parser_test.dart
index 7e7ac3f..e9b4822 100644
--- a/pkg/compiler/test/js/js_parser_test.dart
+++ b/pkg/compiler/test/js/js_parser_test.dart
@@ -20,7 +20,9 @@
 
 testError(String expression, [String expect = ""]) {
   bool doCheck(exception) {
-    Expect.isTrue(exception.toString().contains(expect));
+    final exceptionText = '$exception';
+    Expect.isTrue(exceptionText.contains(expect),
+        'Missing "$expect" in "$exceptionText"');
     return true;
   }
 
@@ -65,9 +67,9 @@
   // String literal with \n.
   testExpression(r'var x = "\n"');
   // String literal with escaped quote.
-  testExpression(r'var x = "\""');
+  testExpression(r'''var x = "\""''', r"""var x = '"'""");
   // *No clever escapes.
-  testError(r'var x = "\x42"', 'escapes are not allowed in literals');
+  testError(r'var x = "\x42"', 'Hex escapes not supported');
   // Operator new.
   testExpression('new Foo()');
   // New with dotted access.
@@ -168,7 +170,7 @@
   testExpression("x << y + 1");
   testExpression("x <<= y + 1");
   // Array initializers.
-  testExpression("x = ['foo', 'bar', x[4]]");
+  testExpression('x = ["foo", "bar", x[4]]');
   testExpression("[]");
   testError("[42 42]");
   testExpression('beebop([1, 2, 3])');
diff --git a/pkg/compiler/test/js/size_estimator_expectations.json b/pkg/compiler/test/js/size_estimator_expectations.json
index 57b911f..2dacebb 100644
--- a/pkg/compiler/test/js/size_estimator_expectations.json
+++ b/pkg/compiler/test/js/size_estimator_expectations.json
@@ -102,8 +102,8 @@
     },
     {
       "original": "x = ['a', 'b', 'c']",
-      "expected": "#=['a','b','c']",
-      "minified": "x=['a','b','c']"
+      "expected": "#=[\"a\",\"b\",\"c\"]",
+      "minified": "x=[\"a\",\"b\",\"c\"]"
     },
     {
       "original": "a = {'b': 1, 'c': 2}",
@@ -154,8 +154,8 @@
     },
     {
       "original": "if (x == true) { return true; } else if (y < 3 || z > 5) { return l != null ? 'a' : 4; } else { foo(); return; }",
-      "expected": "if(#==!0)return !0;else if(#<3||#>5)return #!=null?'a':4;else{#();return;}",
-      "minified": "if(x==true)return true;else if(y<3||z>5)return l!=null?'a':4;else{foo();return}"
+      "expected": "if(#==!0)return !0;else if(#<3||#>5)return #!=null?\"a\":4;else{#();return;}",
+      "minified": "if(x==true)return true;else if(y<3||z>5)return l!=null?\"a\":4;else{foo();return}"
     },
     {
       "original": "for (var a = 0; a < 10; a++) { foo(a); }",
@@ -179,8 +179,8 @@
     },
     {
       "original": "switch (foo) { case 'a': case 'b': bar(); break; case 'c': 1; break; default: boo(); }",
-      "expected": "switch(#){case 'a':case 'b':#();break;case 'c':1;break;default:#();}",
-      "minified": "switch(foo){case'a':case'b':bar();break;case'c':1;break;default:boo()}"
+      "expected": "switch(#){case \"a\":case \"b\":#();break;case \"c\":1;break;default:#();}",
+      "minified": "switch(foo){case\"a\":case\"b\":bar();break;case\"c\":1;break;default:boo()}"
     },
     {
       "original": "foo.prototype.Goo = function(a) { return a.bar(); }",
@@ -193,4 +193,4 @@
       "minified": "try{null=4}catch(e){print(e)}"
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/pkg/js_ast/lib/js_ast.dart b/pkg/js_ast/lib/js_ast.dart
index 01b0dd9..1958f19 100644
--- a/pkg/js_ast/lib/js_ast.dart
+++ b/pkg/js_ast/lib/js_ast.dart
@@ -4,9 +4,9 @@
 
 library js_ast;
 
-import 'dart:collection' show IterableBase;
 import 'src/precedence.dart';
 import 'src/characters.dart' as charCodes;
+import 'src/strings.dart';
 
 part 'src/nodes.dart';
 part 'src/builder.dart';
diff --git a/pkg/js_ast/lib/src/builder.dart b/pkg/js_ast/lib/src/builder.dart
index 2fee946..1f980ea 100644
--- a/pkg/js_ast/lib/src/builder.dart
+++ b/pkg/js_ast/lib/src/builder.dart
@@ -296,186 +296,29 @@
   }
 
   /// Creates a literal js string from [value].
-  LiteralString _legacyEscapedString(String value) {
-    // 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|"|\b|\t|\v|\r'), (match) {
-      switch (match.group(0)) {
-        case "\n":
-          return r"\n";
-        case "\"":
-          return r'\"';
-        case "\b":
-          return r"\b";
-        case "\t":
-          return r"\t";
-        case "\f":
-          return r"\f";
-        case "\r":
-          return r"\r";
-        case "\v":
-          return r"\v";
-      }
-      throw new UnsupportedError("Unexpected match: ${match.group(0)}");
-    });
-    LiteralString result = string(escaped);
-    // We don't escape ' under the assumption that the string is wrapped
-    // into ". Verify that assumption.
-    assert(result.value.codeUnitAt(0) == '"'.codeUnitAt(0));
-    return result;
-  }
-
-  /// Creates a literal js string from [value].
-  LiteralString escapedString(String value,
-      {bool utf8: false, bool ascii: false}) {
-    if (utf8 == false && ascii == false) return _legacyEscapedString(value);
-    if (utf8 && ascii) throw new ArgumentError('Cannot be both UTF8 and ASCII');
-
-    int singleQuotes = 0;
-    int doubleQuotes = 0;
-    int otherEscapes = 0;
-    int unpairedSurrogates = 0;
-
-    for (int rune in value.runes) {
-      if (rune == charCodes.$BACKSLASH) {
-        ++otherEscapes;
-      } else if (rune == charCodes.$SQ) {
-        ++singleQuotes;
-      } else if (rune == charCodes.$DQ) {
-        ++doubleQuotes;
-      } else if (rune == charCodes.$LF ||
-          rune == charCodes.$CR ||
-          rune == charCodes.$LS ||
-          rune == charCodes.$PS) {
-        // Line terminators.
-        ++otherEscapes;
-      } else if (rune == charCodes.$BS ||
-          rune == charCodes.$TAB ||
-          rune == charCodes.$VTAB ||
-          rune == charCodes.$FF) {
-        ++otherEscapes;
-      } else if (_isUnpairedSurrogate(rune)) {
-        ++unpairedSurrogates;
-      } else {
-        if (ascii && (rune < charCodes.$SPACE || rune >= charCodes.$DEL)) {
-          ++otherEscapes;
-        }
-      }
-    }
-
-    LiteralString finish(String quote, String contents) {
-      return new LiteralString('$quote$contents$quote');
-    }
-
-    if (otherEscapes == 0 && unpairedSurrogates == 0) {
-      if (doubleQuotes == 0) return finish('"', value);
-      if (singleQuotes == 0) return finish("'", value);
-    }
-
-    bool useSingleQuotes = singleQuotes < doubleQuotes;
-
-    StringBuffer sb = new StringBuffer();
-
-    for (int rune in value.runes) {
-      String escape = _irregularEscape(rune, useSingleQuotes);
-      if (escape != null) {
-        sb.write(escape);
-        continue;
-      }
-      if (rune == charCodes.$LS ||
-          rune == charCodes.$PS ||
-          _isUnpairedSurrogate(rune) ||
-          ascii && (rune < charCodes.$SPACE || rune >= charCodes.$DEL)) {
-        if (rune < 0x100) {
-          sb.write(r'\x');
-          sb.write(rune.toRadixString(16).padLeft(2, '0'));
-        } else if (rune < 0x10000) {
-          sb.write(r'\u');
-          sb.write(rune.toRadixString(16).padLeft(4, '0'));
-        } else {
-          // Not all browsers accept the ES6 \u{zzzzzz} encoding, so emit two
-          // surrogate pairs.
-          var bits = rune - 0x10000;
-          var leading = 0xD800 | (bits >> 10);
-          var trailing = 0xDC00 | (bits & 0x3ff);
-          sb.write(r'\u');
-          sb.write(leading.toRadixString(16));
-          sb.write(r'\u');
-          sb.write(trailing.toRadixString(16));
-        }
-      } else {
-        sb.writeCharCode(rune);
-      }
-    }
-
-    return finish(useSingleQuotes ? "'" : '"', sb.toString());
-  }
-
-  static bool _isUnpairedSurrogate(int code) => (code & 0xFFFFF800) == 0xD800;
-
-  static String _irregularEscape(int code, bool useSingleQuotes) {
-    switch (code) {
-      case charCodes.$SQ:
-        return useSingleQuotes ? r"\'" : r"'";
-      case charCodes.$DQ:
-        return useSingleQuotes ? r'"' : r'\"';
-      case charCodes.$BACKSLASH:
-        return r'\\';
-      case charCodes.$BS:
-        return r'\b';
-      case charCodes.$TAB:
-        return r'\t';
-      case charCodes.$LF:
-        return r'\n';
-      case charCodes.$VTAB:
-        return r'\v';
-      case charCodes.$FF:
-        return r'\f';
-      case charCodes.$CR:
-        return r'\r';
-    }
-    return null;
-  }
-
-  /// Creates a literal js string from [value].
-  ///
-  /// Note that this function only puts quotes around [value]. It does not do
-  /// any escaping, so use only when you can guarantee that [value] does not
-  /// contain newlines or backslashes. For escaping the string use
-  /// [escapedString].
-  LiteralString string(String value) => new LiteralString('"$value"');
+  LiteralString string(String value) => LiteralString(value);
 
   /// Creates an instance of [LiteralString] from [value].
   ///
   /// Does not add quotes or do any escaping.
   LiteralString stringPart(String value) => new LiteralString(value);
 
-  StringConcatenation concatenateStrings(Iterable<Literal> parts,
-      {addQuotes: false}) {
-    List<Literal> _parts;
-    if (addQuotes) {
-      Literal quote = stringPart('"');
-      _parts = <Literal>[quote]
-        ..addAll(parts)
-        ..add(quote);
-    } else {
-      _parts = new List.from(parts, growable: false);
-    }
-    return new StringConcatenation(_parts);
+  StringConcatenation concatenateStrings(Iterable<Literal> parts) {
+    return StringConcatenation(List.of(parts, growable: false));
   }
 
-  Iterable<Literal> joinLiterals(Iterable<Literal> list, Literal separator) {
-    return new _InterleaveIterable<Literal>(list, separator);
+  Iterable<Literal> joinLiterals(
+      Iterable<Literal> items, Literal separator) sync* {
+    bool first = true;
+    for (final item in items) {
+      if (!first) yield separator;
+      yield item;
+      first = false;
+    }
   }
 
-  LiteralString quoteName(Name name, {allowNull: false}) {
-    if (name == null) {
-      assert(allowNull);
-      return new LiteralString('""');
-    }
-    return new LiteralStringFromName(name);
+  LiteralString quoteName(Name name) {
+    return LiteralStringFromName(name);
   }
 
   LiteralNumber number(num value) => new LiteralNumber('$value');
@@ -505,18 +348,19 @@
 }
 
 LiteralString string(String value) => js.string(value);
-LiteralString quoteName(Name name, {allowNull: false}) {
-  return js.quoteName(name, allowNull: allowNull);
-}
 
-LiteralString stringPart(String value) => js.stringPart(value);
+/// Returns a LiteralString which has contents determined by [Name].
+///
+/// This is used to force a Name to be a string literal regardless of
+/// context. It is not necessary for properties.
+LiteralString quoteName(Name name) => js.quoteName(name);
+
 Iterable<Literal> joinLiterals(Iterable<Literal> list, Literal separator) {
   return js.joinLiterals(list, separator);
 }
 
-StringConcatenation concatenateStrings(Iterable<Literal> parts,
-    {addQuotes: false}) {
-  return js.concatenateStrings(parts, addQuotes: addQuotes);
+StringConcatenation concatenateStrings(Iterable<Literal> parts) {
+  return js.concatenateStrings(parts);
 }
 
 LiteralNumber number(num value) => js.number(value);
@@ -736,7 +580,7 @@
     '/': 5,
     '%': 5
   };
-  static final UNARY_OPERATORS = [
+  static final UNARY_OPERATORS = {
     '++',
     '--',
     '+',
@@ -747,7 +591,7 @@
     'void',
     'delete',
     'await'
-  ].toSet();
+  };
 
   static final OPERATORS_THAT_LOOK_LIKE_IDENTIFIERS =
       ['typeof', 'void', 'delete', 'in', 'instanceof', 'await'].toSet();
@@ -757,7 +601,7 @@
     return CATEGORIES[code];
   }
 
-  String getDelimited(int startPosition) {
+  String getRegExp(int startPosition) {
     position = startPosition;
     int delimiter = src.codeUnitAt(startPosition);
     int currentCode;
@@ -774,7 +618,7 @@
             escaped == charCodes.$u ||
             escaped == charCodes.$U ||
             category(escaped) == NUMERIC) {
-          error('Numeric and hex escapes are not allowed in literals');
+          error('Numeric and hex escapes are not supported in RegExp literals');
         }
       }
     } while (currentCode != delimiter);
@@ -782,6 +626,46 @@
     return src.substring(lastPosition, position);
   }
 
+  String getString(int startPosition, int quote) {
+    assert(src.codeUnitAt(startPosition) == quote);
+    position = startPosition + 1;
+    final value = StringBuffer();
+    while (true) {
+      if (position >= src.length) error("Unterminated literal");
+      int code = src.codeUnitAt(position++);
+      if (code == quote) break;
+      if (code == charCodes.$LF) error("Unterminated literal");
+      if (code == charCodes.$BACKSLASH) {
+        if (position >= src.length) error("Unterminated literal");
+        code = src.codeUnitAt(position++);
+        if (code == charCodes.$f) {
+          value.writeCharCode(12);
+        } else if (code == charCodes.$n) {
+          value.writeCharCode(10);
+        } else if (code == charCodes.$r) {
+          value.writeCharCode(13);
+        } else if (code == charCodes.$t) {
+          value.writeCharCode(8);
+        } else if (code == charCodes.$BACKSLASH ||
+            code == charCodes.$SQ ||
+            code == charCodes.$DQ) {
+          value.writeCharCode(code);
+        } else if (code == charCodes.$x || code == charCodes.$X) {
+          error('Hex escapes not supported in string literals');
+        } else if (code == charCodes.$u || code == charCodes.$U) {
+          error('Unicode escapes not supported in string literals');
+        } else if (charCodes.$0 <= code && code <= charCodes.$9) {
+          error('Numeric escapes not supported in string literals');
+        } else {
+          error('Unknown escape U+${code.toRadixString(16).padLeft(4, '0')}');
+        }
+        continue;
+      }
+      value.writeCharCode(code);
+    }
+    return value.toString();
+  }
+
   void getToken() {
     skippedNewline = false;
     for (;;) {
@@ -817,7 +701,7 @@
     if (code == charCodes.$SQ || code == charCodes.$DQ) {
       // String literal.
       lastCategory = STRING;
-      lastToken = getDelimited(position);
+      lastToken = getString(position, code);
     } else if (code == charCodes.$0 &&
         position + 2 < src.length &&
         src.codeUnitAt(position + 1) == charCodes.$x) {
@@ -979,7 +863,7 @@
       }
       return new ArrayInitializer(values);
     } else if (last != null && last.startsWith("/")) {
-      String regexp = getDelimited(lastPosition);
+      String regexp = getRegExp(lastPosition);
       getToken();
       String flags = lastToken;
       if (!acceptCategory(ALPHA)) flags = "";
@@ -1053,12 +937,12 @@
       Literal propertyName;
       String identifier = lastToken;
       if (acceptCategory(ALPHA)) {
-        propertyName = new LiteralString('"$identifier"');
+        propertyName = LiteralString(identifier);
       } else if (acceptCategory(STRING)) {
-        propertyName = new LiteralString(identifier);
+        propertyName = LiteralString(identifier);
       } else if (acceptCategory(SYMBOL)) {
         // e.g. void
-        propertyName = new LiteralString('"$identifier"');
+        propertyName = LiteralString(identifier);
       } else if (acceptCategory(HASH)) {
         var nameOrPosition = parseHash();
         InterpolatedLiteral interpolatedLiteral =
@@ -1574,40 +1458,3 @@
     return new Catch(errorName, body);
   }
 }
-
-class _InterleaveIterator<T extends Node> implements Iterator<T> {
-  Iterator<T> source;
-  T separator;
-  bool isNextSeparator = false;
-  bool isInitialized = false;
-
-  _InterleaveIterator(this.source, this.separator);
-
-  bool moveNext() {
-    if (!isInitialized) {
-      isInitialized = true;
-      return source.moveNext();
-    } else if (isNextSeparator) {
-      isNextSeparator = false;
-      return true;
-    } else {
-      return isNextSeparator = source.moveNext();
-    }
-  }
-
-  T get current {
-    if (isNextSeparator) return separator;
-    return source.current;
-  }
-}
-
-class _InterleaveIterable<T extends Node> extends IterableBase<T> {
-  Iterable<T> source;
-  T separator;
-
-  _InterleaveIterable(this.source, this.separator);
-
-  Iterator<T> get iterator {
-    return new _InterleaveIterator<T>(source.iterator, separator);
-  }
-}
diff --git a/pkg/js_ast/lib/src/nodes.dart b/pkg/js_ast/lib/src/nodes.dart
index 903026e..285e918 100644
--- a/pkg/js_ast/lib/src/nodes.dart
+++ b/pkg/js_ast/lib/src/nodes.dart
@@ -1038,7 +1038,8 @@
   @override
   bool get isFinalized => name.isFinalized;
 
-  String get value => '"${name.name}"';
+  @override
+  String get value => name.name;
 
   void visitChildren<T>(NodeVisitor<T> visitor) {
     name.accept(visitor);
@@ -1371,6 +1372,8 @@
   int get precedenceLevel => UNARY;
 }
 
+RegExp _identifierRE = new RegExp(r'^[A-Za-z_$][A-Za-z_$0-9]*$');
+
 abstract class VariableReference extends Expression {
   final String name;
 
@@ -1378,8 +1381,6 @@
     assert(_identifierRE.hasMatch(name), "Non-identifier name '$name'");
   }
 
-  static RegExp _identifierRE = new RegExp(r'^[A-Za-z_$][A-Za-z_$0-9]*$');
-
   T accept<T>(NodeVisitor<T> visitor);
 
   int get precedenceLevel => PRIMARY;
@@ -1520,10 +1521,10 @@
   PropertyAccess(this.receiver, this.selector);
 
   PropertyAccess.field(this.receiver, String fieldName)
-      : selector = new LiteralString('"$fieldName"');
+      : selector = LiteralString(fieldName);
 
   PropertyAccess.indexed(this.receiver, int index)
-      : selector = new LiteralNumber('$index');
+      : selector = LiteralNumber('$index');
 
   T accept<T>(NodeVisitor<T> visitor) => visitor.visitAccess(this);
 
@@ -1633,16 +1634,11 @@
 class LiteralString extends Literal {
   final String value;
 
-  /**
-   * Constructs a LiteralString from a string value.
-   *
-   * The constructor does not add the required quotes.  If [value] is not
-   * surrounded by quotes and properly escaped, the resulting object is invalid
-   * as a JS value.
-   *
-   * TODO(sra): Introduce variants for known valid strings that don't allocate a
-   * new string just to add quotes.
-   */
+  /// Constructs a LiteralString for a string containing the characters of
+  /// `value`.
+  ///
+  /// When printed, the string will be escaped and quoted according to the
+  /// printer's settings.
   LiteralString(this.value);
 
   T accept<T>(NodeVisitor<T> visitor) => visitor.visitLiteralString(this);
@@ -1650,17 +1646,39 @@
   R accept1<R, A>(NodeVisitor1<R, A> visitor, A arg) =>
       visitor.visitLiteralString(this, arg);
 
-  LiteralString _clone() => new LiteralString(value);
+  LiteralString _clone() => LiteralString(value);
+
+  @override
+  String toString() {
+    final sb = StringBuffer('$runtimeType("');
+    String end = '"';
+    int count = 0;
+    for (int rune in value.runes) {
+      if (++count > 20) {
+        end = '"...';
+        break;
+      }
+      if (32 <= rune && rune < 127) {
+        sb.writeCharCode(rune);
+      } else {
+        sb.write(r'\u{');
+        sb.write(rune.toRadixString(16));
+        sb.write(r'}');
+      }
+    }
+    sb.write(end);
+    sb.write(')');
+    return sb.toString();
+  }
 }
 
 class StringConcatenation extends Literal {
   final List<Literal> parts;
 
-  /**
-   * Constructs a StringConcatenation from a list of Literal elements.
-   * The constructor does not add surrounding quotes to the resulting
-   * concatenated string.
-   */
+  /// Constructs a StringConcatenation from a list of Literal elements.
+  ///
+  /// The constructor does not add surrounding quotes to the resulting
+  /// concatenated string.
   StringConcatenation(this.parts);
 
   T accept<T>(NodeVisitor<T> visitor) => visitor.visitStringConcatenation(this);
diff --git a/pkg/js_ast/lib/src/printer.dart b/pkg/js_ast/lib/src/printer.dart
index c6e9a9e..ed165c2 100644
--- a/pkg/js_ast/lib/src/printer.dart
+++ b/pkg/js_ast/lib/src/printer.dart
@@ -5,14 +5,16 @@
 part of js_ast;
 
 class JavaScriptPrintingOptions {
+  final bool utf8;
   final bool shouldCompressOutput;
   final bool minifyLocalVariables;
   final bool preferSemicolonToNewlineInMinifiedOutput;
 
   const JavaScriptPrintingOptions({
-    this.shouldCompressOutput: false,
-    this.minifyLocalVariables: false,
-    this.preferSemicolonToNewlineInMinifiedOutput: false,
+    this.utf8 = false,
+    this.shouldCompressOutput = false,
+    this.minifyLocalVariables = false,
+    this.preferSemicolonToNewlineInMinifiedOutput = false,
   });
 }
 
@@ -56,11 +58,10 @@
   String getText() => buffer.toString();
 }
 
-String DebugPrint(Node node) {
-  JavaScriptPrintingOptions options = new JavaScriptPrintingOptions();
-  SimpleJavaScriptPrintingContext context =
-      new SimpleJavaScriptPrintingContext();
-  Printer printer = new Printer(options, context);
+String DebugPrint(Node node, {bool utf8 = false}) {
+  JavaScriptPrintingOptions options = JavaScriptPrintingOptions(utf8: utf8);
+  SimpleJavaScriptPrintingContext context = SimpleJavaScriptPrintingContext();
+  Printer printer = Printer(options, context);
   printer.visit(node);
   return context.getText();
 }
@@ -83,8 +84,8 @@
   // A cache of all indentation strings used so far.
   List<String> _indentList = <String>[""];
 
-  static final identifierCharacterRegExp = new RegExp(r'^[a-zA-Z_0-9$]');
-  static final expressionContinuationRegExp = new RegExp(r'^[-+([]');
+  static final identifierCharacterRegExp = RegExp(r'^[a-zA-Z_0-9$]');
+  static final expressionContinuationRegExp = RegExp(r'^[-+([]');
 
   Printer(JavaScriptPrintingOptions options, JavaScriptPrintingContext context)
       : options = options,
@@ -726,10 +727,10 @@
       if (value is This) return true;
       if (value is LiteralNull) return true;
       if (value is LiteralNumber) return true;
-      if (value is LiteralString && value.value.length <= 8) return true;
+      if (value is LiteralString && value.value.length <= 6) return true;
       if (value is ObjectInitializer && value.properties.isEmpty) return true;
       if (value is ArrayInitializer && value.elements.isEmpty) return true;
-      if (value is Name && value.name.length <= 8) return true;
+      if (value is Name && value.name.length <= 6) return true;
     }
     return false;
   }
@@ -1018,24 +1019,24 @@
   }
 
   bool isValidJavaScriptId(String field) {
-    if (field.length < 3) return false;
+    if (field.length == 0) return false;
     // Ignore the leading and trailing string-delimiter.
-    for (int i = 1; i < field.length - 1; i++) {
+    for (int i = 0; i < field.length; i++) {
       // TODO(floitsch): allow more characters.
       int charCode = field.codeUnitAt(i);
       if (!(charCodes.$a <= charCode && charCode <= charCodes.$z ||
           charCodes.$A <= charCode && charCode <= charCodes.$Z ||
           charCode == charCodes.$$ ||
           charCode == charCodes.$_ ||
-          i != 1 && isDigit(charCode))) {
+          i > 0 && isDigit(charCode))) {
         return false;
       }
     }
     // TODO(floitsch): normally we should also check that the field is not a
     // reserved word.  We don't generate fields with reserved word names except
     // for 'super'.
-    if (field == '"super"') return false;
-    if (field == '"catch"') return false;
+    if (field == 'super') return false;
+    if (field == 'catch') return false;
     return true;
   }
 
@@ -1043,35 +1044,41 @@
   void visitAccess(PropertyAccess access) {
     visitNestedExpression(access.receiver, CALL,
         newInForInit: inForInit, newAtStatementBegin: atStatementBegin);
+
     Node selector = undefer(access.selector);
     if (selector is LiteralString) {
-      String fieldWithQuotes = selector.value;
-      if (isValidJavaScriptId(fieldWithQuotes)) {
-        if (access.receiver is LiteralNumber &&
-            lastCharCode != charCodes.$CLOSE_PAREN) {
-          out(" ", isWhitespace: true);
-        }
-        out(".");
-        startNode(access.selector);
-        out(fieldWithQuotes.substring(1, fieldWithQuotes.length - 1));
-        endNode(access.selector);
-        return;
-      }
+      _dotString(access.selector, access.receiver, selector.value);
+      return;
+    } else if (selector is StringConcatenation) {
+      _dotString(access.selector, access.receiver,
+          _StringContentsCollector().collect(selector));
+      return;
     } else if (selector is Name) {
-      Node receiver = undefer(access.receiver);
-      if (receiver is LiteralNumber && lastCharCode != charCodes.$CLOSE_PAREN) {
-        out(" ", isWhitespace: true);
-      }
-      out(".");
-      startNode(access.selector);
-      selector.accept(this);
-      endNode(access.selector);
+      _dotString(access.selector, access.receiver, selector.name);
       return;
     }
-    out("[");
+
+    out('[');
     visitNestedExpression(access.selector, EXPRESSION,
         newInForInit: false, newAtStatementBegin: false);
-    out("]");
+    out(']');
+  }
+
+  void _dotString(Node selector, Node receiver, String selectorValue) {
+    if (isValidJavaScriptId(selectorValue)) {
+      if (undefer(receiver) is LiteralNumber &&
+          lastCharCode != charCodes.$CLOSE_PAREN) {
+        out(' ', isWhitespace: true);
+      }
+      out('.');
+      startNode(selector);
+      out(selectorValue);
+      endNode(selector);
+    } else {
+      out('[');
+      _handleString(selectorValue);
+      out(']');
+    }
   }
 
   @override
@@ -1133,12 +1140,25 @@
 
   @override
   void visitLiteralString(LiteralString node) {
-    out(node.value);
+    _handleString(node.value);
   }
 
   @override
   visitStringConcatenation(StringConcatenation node) {
-    node.visitChildren(this);
+    _handleString(_StringContentsCollector().collect(node));
+  }
+
+  void _handleString(String value) {
+    final kind = StringToSource.analyze(value, utf8: options.utf8);
+    out(kind.quote);
+    if (kind.simple) {
+      out(value);
+    } else {
+      final sb = StringBuffer();
+      StringToSource.writeString(sb, value, kind, utf8: options.utf8);
+      out(sb.toString());
+    }
+    out(kind.quote);
   }
 
   @override
@@ -1235,18 +1255,15 @@
     startNode(node.name);
     Node name = undefer(node.name);
     if (name is LiteralString) {
-      String text = name.value;
-      if (isValidJavaScriptId(text)) {
-        out(text.substring(1, text.length - 1));
-      } else {
-        out(text);
-      }
+      _outPropertyName(name.value);
     } else if (name is Name) {
-      node.name.accept(this);
+      _outPropertyName(name.name);
+    } else if (name is LiteralNumber) {
+      out(name.value);
     } else {
-      assert(name is LiteralNumber);
-      LiteralNumber nameNumber = node.name;
-      out(nameNumber.value);
+      // TODO(sra): Handle StringConcatenation.
+      // TODO(sra): Handle general expressions, .e.g. `{[x]: 1}`.
+      throw StateError('Unexpected Property name: $name');
     }
     endNode(node.name);
     out(":");
@@ -1255,6 +1272,14 @@
         newInForInit: false, newAtStatementBegin: false);
   }
 
+  void _outPropertyName(String name) {
+    if (isValidJavaScriptId(name)) {
+      out(name);
+    } else {
+      _handleString(name);
+    }
+  }
+
   @override
   void visitRegExpLiteral(RegExpLiteral node) {
     out(node.pattern);
@@ -1321,6 +1346,44 @@
   }
 }
 
+class _StringContentsCollector extends BaseVisitor<void> {
+  final StringBuffer _buffer = StringBuffer();
+
+  String collect(Node node) {
+    node.accept(this);
+    return _buffer.toString();
+  }
+
+  void _add(String value) {
+    _buffer.write(value);
+  }
+
+  @override
+  void visitNode(Node node) {
+    throw StateError('Node should not be part of StringConcatenation: $node');
+  }
+
+  @override
+  void visitLiteralString(LiteralString node) {
+    _add(node.value);
+  }
+
+  @override
+  void visitLiteralNumber(LiteralNumber node) {
+    _add(node.value);
+  }
+
+  @override
+  void visitName(Name node) {
+    _add(node.name);
+  }
+
+  @override
+  void visitStringConcatenation(StringConcatenation node) {
+    node.visitChildren(this);
+  }
+}
+
 class OrderedSet<T> {
   final Set<T> set;
   final List<T> list;
diff --git a/pkg/js_ast/lib/src/strings.dart b/pkg/js_ast/lib/src/strings.dart
new file mode 100644
index 0000000..91c2bae
--- /dev/null
+++ b/pkg/js_ast/lib/src/strings.dart
@@ -0,0 +1,135 @@
+// Copyright (c) 2021, 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 converting between JavaScript source-code Strings and the
+// String value they represent.
+
+import 'characters.dart' as charCodes;
+
+class StringToSourceKind {
+  /// [true] if preferable to use double quotes, [false] if preferable to use
+  /// single quotes.
+  final bool doubleQuotes;
+
+  /// [true] if contents require no escaping with the preferred quoting.
+  final bool simple;
+
+  const StringToSourceKind({this.doubleQuotes, this.simple});
+
+  String get quote => doubleQuotes ? '"' : "'";
+}
+
+class StringToSource {
+  const StringToSource();
+
+  static StringToSourceKind analyze(String value, {/*required*/ bool utf8}) {
+    final ascii = !utf8;
+    int singleQuotes = 0;
+    int doubleQuotes = 0;
+    int otherEscapes = 0;
+    int unpairedSurrogates = 0;
+
+    for (int rune in value.runes) {
+      if (rune == charCodes.$BACKSLASH) {
+        ++otherEscapes;
+      } else if (rune == charCodes.$SQ) {
+        ++singleQuotes;
+      } else if (rune == charCodes.$DQ) {
+        ++doubleQuotes;
+      } else if (rune == charCodes.$LF ||
+          rune == charCodes.$CR ||
+          rune == charCodes.$LS ||
+          rune == charCodes.$PS) {
+        // Line terminators.
+        ++otherEscapes;
+      } else if (rune == charCodes.$BS ||
+          rune == charCodes.$TAB ||
+          rune == charCodes.$VTAB ||
+          rune == charCodes.$FF) {
+        ++otherEscapes;
+      } else if (ascii && (rune < charCodes.$SPACE || rune >= charCodes.$DEL)) {
+        ++otherEscapes;
+      } else if (_isUnpairedSurrogate(rune)) {
+        // Need to escape unpaired surrogates in a UTF8-encoded output otherwise
+        // the output would be malformed.
+        ++unpairedSurrogates;
+      }
+    }
+
+    if (otherEscapes == 0 && unpairedSurrogates == 0) {
+      if (doubleQuotes == 0) {
+        return const StringToSourceKind(doubleQuotes: true, simple: true);
+      }
+      if (singleQuotes == 0) {
+        return const StringToSourceKind(doubleQuotes: false, simple: true);
+      }
+    }
+
+    return doubleQuotes <= singleQuotes
+        ? const StringToSourceKind(doubleQuotes: true, simple: false)
+        : const StringToSourceKind(doubleQuotes: false, simple: false);
+  }
+
+  static void writeString(
+      StringBuffer sb, String string, StringToSourceKind kind,
+      {/*required*/ bool utf8}) {
+    for (int rune in string.runes) {
+      String escape = _irregularEscape(rune, kind.doubleQuotes);
+      if (escape != null) {
+        sb.write(escape);
+        continue;
+      }
+      if (rune == charCodes.$LS ||
+          rune == charCodes.$PS ||
+          _isUnpairedSurrogate(rune) ||
+          !utf8 && (rune < charCodes.$SPACE || rune >= charCodes.$DEL)) {
+        if (rune < 0x100) {
+          sb.write(r'\x');
+          sb.write(rune.toRadixString(16).padLeft(2, '0'));
+        } else if (rune < 0x10000) {
+          sb.write(r'\u');
+          sb.write(rune.toRadixString(16).padLeft(4, '0'));
+        } else {
+          // Not all browsers accept the ES6 \u{zzzzzz} encoding, so emit two
+          // surrogate pairs.
+          var bits = rune - 0x10000;
+          var leading = 0xD800 | (bits >> 10);
+          var trailing = 0xDC00 | (bits & 0x3ff);
+          sb.write(r'\u');
+          sb.write(leading.toRadixString(16));
+          sb.write(r'\u');
+          sb.write(trailing.toRadixString(16));
+        }
+      } else {
+        sb.writeCharCode(rune);
+      }
+    }
+  }
+
+  static bool _isUnpairedSurrogate(int code) => (code & 0xFFFFF800) == 0xD800;
+
+  static String _irregularEscape(int code, bool useDoubleQuotes) {
+    switch (code) {
+      case charCodes.$SQ:
+        return useDoubleQuotes ? r"'" : r"\'";
+      case charCodes.$DQ:
+        return useDoubleQuotes ? r'\"' : r'"';
+      case charCodes.$BACKSLASH:
+        return r'\\';
+      case charCodes.$BS:
+        return r'\b';
+      case charCodes.$TAB:
+        return r'\t';
+      case charCodes.$LF:
+        return r'\n';
+      case charCodes.$VTAB:
+        return r'\v';
+      case charCodes.$FF:
+        return r'\f';
+      case charCodes.$CR:
+        return r'\r';
+    }
+    return null;
+  }
+}
diff --git a/pkg/js_ast/lib/src/template.dart b/pkg/js_ast/lib/src/template.dart
index 5aa45ba..788e081 100644
--- a/pkg/js_ast/lib/src/template.dart
+++ b/pkg/js_ast/lib/src/template.dart
@@ -268,7 +268,7 @@
     return (arguments) {
       var value = arguments[nameOrPosition];
       if (value is Expression) return value;
-      if (value is String) return new LiteralString('"$value"');
+      if (value is String) return LiteralString(value);
       throw error(
           'Interpolated value #$nameOrPosition is not a selector: $value');
     };
diff --git a/pkg/js_ast/pubspec.yaml b/pkg/js_ast/pubspec.yaml
index 7909c53..c510415 100644
--- a/pkg/js_ast/pubspec.yaml
+++ b/pkg/js_ast/pubspec.yaml
@@ -3,7 +3,7 @@
 publish_to: none
 
 environment:
-  sdk: '>=2.0.0 <3.0.0'
+  sdk: '>=2.10.0 <3.0.0'
 
 dev_dependencies:
   expect:
diff --git a/pkg/js_ast/test/deferred_expression_test.dart b/pkg/js_ast/test/deferred_expression_test.dart
index c91a151..afd9805 100644
--- a/pkg/js_ast/test/deferred_expression_test.dart
+++ b/pkg/js_ast/test/deferred_expression_test.dart
@@ -7,15 +7,15 @@
 
 main() {
   Map<Expression, DeferredExpression> map = {};
-  VariableUse variableUse = new VariableUse('variable');
+  VariableUse variableUse = VariableUse('variable');
   DeferredExpression deferred =
-      map[variableUse] = new _DeferredExpression(variableUse);
-  VariableUse variableUseAlias = new VariableUse('variable');
-  map[variableUseAlias] = new _DeferredExpression(variableUseAlias);
+      map[variableUse] = _DeferredExpression(variableUse);
+  VariableUse variableUseAlias = VariableUse('variable');
+  map[variableUseAlias] = _DeferredExpression(variableUseAlias);
 
-  map[deferred] = new _DeferredExpression(deferred);
-  Literal literal = new LiteralString('"literal"');
-  map[literal] = new _DeferredExpression(literal);
+  map[deferred] = _DeferredExpression(deferred);
+  Literal literal = LiteralString('literal');
+  map[literal] = _DeferredExpression(literal);
 
   test(map, '#', [variableUse], 'variable');
   test(map, '#', [deferred], 'variable');
@@ -54,18 +54,18 @@
     List<Expression> arguments, String expectedOutput) {
   Expression directExpression =
       js.expressionTemplateFor(template).instantiate(arguments);
-  _Context directContext = new _Context();
+  _Context directContext = _Context();
   Printer directPrinter =
-      new Printer(const JavaScriptPrintingOptions(), directContext);
+      Printer(const JavaScriptPrintingOptions(), directContext);
   directPrinter.visit(directExpression);
   Expect.equals(expectedOutput, directContext.text);
 
   Expression deferredExpression = js
       .expressionTemplateFor(template)
       .instantiate(arguments.map((e) => map[e]).toList());
-  _Context deferredContext = new _Context();
+  _Context deferredContext = _Context();
   Printer deferredPrinter =
-      new Printer(const JavaScriptPrintingOptions(), deferredContext);
+      Printer(const JavaScriptPrintingOptions(), deferredContext);
   deferredPrinter.visit(deferredExpression);
   Expect.equals(expectedOutput, deferredContext.text);
 
@@ -121,7 +121,7 @@
 }
 
 class _Context implements JavaScriptPrintingContext {
-  StringBuffer sb = new StringBuffer();
+  StringBuffer sb = StringBuffer();
   List<String> errors = [];
   Map<Node, int> enterPositions = {};
   Map<Node, _Position> exitPositions = {};
@@ -140,7 +140,7 @@
   void exitNode(
       Node node, int startPosition, int endPosition, int closingPosition) {
     exitPositions[node] =
-        new _Position(startPosition, endPosition, closingPosition);
+        _Position(startPosition, endPosition, closingPosition);
     Expect.equals(enterPositions[node], startPosition);
   }
 
diff --git a/pkg/js_ast/test/printer_callback_test.dart b/pkg/js_ast/test/printer_callback_test.dart
index 7d8fb30..153c41d 100644
--- a/pkg/js_ast/test/printer_callback_test.dart
+++ b/pkg/js_ast/test/printer_callback_test.dart
@@ -170,9 +170,6 @@
   String get key => name;
 
   FixedName(this.name);
-
-  @override
-  int compareTo(other) => 0;
 }
 
 void check(TestCase testCase) {
diff --git a/pkg/js_ast/test/string_escape_test.dart b/pkg/js_ast/test/string_escape_test.dart
index 9277ed6..78bc758 100644
--- a/pkg/js_ast/test/string_escape_test.dart
+++ b/pkg/js_ast/test/string_escape_test.dart
@@ -12,9 +12,9 @@
 const int $RCURLY = $CLOSE_CURLY_BRACKET;
 
 void main() {
-  check(input, expected, {ascii: false, utf8: false}) {
-    if (input is List) input = new String.fromCharCodes(input);
-    String actual = js.escapedString(input, ascii: ascii, utf8: utf8).value;
+  check(input, expected, {bool utf8 = false}) {
+    if (input is List) input = String.fromCharCodes(input);
+    String actual = DebugPrint(js.string(input), utf8: utf8);
     if (expected is List) {
       expect(actual.codeUnits, expected);
     } else {
@@ -29,79 +29,57 @@
 
   test('simple-escapes', () {
     check([$BS], [$DQ, $BACKSLASH, $b, $DQ]);
-    check([$BS], [$DQ, $BACKSLASH, $b, $DQ], ascii: true);
     check([$BS], [$DQ, $BACKSLASH, $b, $DQ], utf8: true);
 
     check([$LF], [$DQ, $BACKSLASH, $n, $DQ]);
-    check([$LF], [$DQ, $BACKSLASH, $n, $DQ], ascii: true);
     check([$LF], [$DQ, $BACKSLASH, $n, $DQ], utf8: true);
 
-    check([$FF], [$DQ, $FF, $DQ]);
-    check([$FF], [$DQ, $BACKSLASH, $f, $DQ], ascii: true);
+    check([$FF], [$DQ, $BACKSLASH, $f, $DQ]);
     check([$FF], [$DQ, $BACKSLASH, $f, $DQ], utf8: true);
 
     check([$CR], [$DQ, $BACKSLASH, $r, $DQ]);
-    check([$CR], [$DQ, $BACKSLASH, $r, $DQ], ascii: true);
     check([$CR], [$DQ, $BACKSLASH, $r, $DQ], utf8: true);
 
     check([$TAB], [$DQ, $BACKSLASH, $t, $DQ]);
-    check([$TAB], [$DQ, $BACKSLASH, $t, $DQ], ascii: true);
     check([$TAB], [$DQ, $BACKSLASH, $t, $DQ], utf8: true);
 
     check([$VTAB], [$DQ, $BACKSLASH, $v, $DQ]);
-    check([$VTAB], [$DQ, $BACKSLASH, $v, $DQ], ascii: true);
     check([$VTAB], [$DQ, $BACKSLASH, $v, $DQ], utf8: true);
   });
 
   test('unnamed-control-codes-escapes', () {
-    check([0, 1, 2, 3], [$DQ, 0, 1, 2, 3, $DQ]);
-    check([0, 1, 2, 3], r'''"\x00\x01\x02\x03"''', ascii: true);
+    check([0, 1, 2, 3], r'''"\x00\x01\x02\x03"''');
     check([0, 1, 2, 3], [$DQ, 0, 1, 2, 3, $DQ], utf8: true);
   });
 
   test('line-separator', () {
-    // Legacy escaper is broken.
-    // check([$LS], [$DQ, $BACKSLASH, $u, $2, $0, $2, $8, $DQ]);
-    check([$LS], [$DQ, $BACKSLASH, $u, $2, $0, $2, $8, $DQ], ascii: true);
+    check([$LS], [$DQ, $BACKSLASH, $u, $2, $0, $2, $8, $DQ]);
     check([$LS], [$DQ, $BACKSLASH, $u, $2, $0, $2, $8, $DQ], utf8: true);
   });
 
   test('page-separator', () {
-    // Legacy escaper is broken.
-    // check([$PS], [$DQ, $BACKSLASH, $u, $2, $0, $2, $9, $DQ]);
-    check([$PS], [$DQ, $BACKSLASH, $u, $2, $0, $2, $9, $DQ], ascii: true);
+    check([$PS], [$DQ, $BACKSLASH, $u, $2, $0, $2, $9, $DQ]);
     check([$PS], [$DQ, $BACKSLASH, $u, $2, $0, $2, $9, $DQ], utf8: true);
   });
 
-  test('legacy-escaper-is-broken', () {
-    check([$LS], [$DQ, 0x2028, $DQ]);
-    check([$PS], [$DQ, 0x2029, $DQ]);
-  });
-
   test('choose-quotes', () {
-    check('\'', [$DQ, $SQ, $DQ]);
-    check('"', [$SQ, $DQ, $SQ], ascii: true);
-    check("'", [$DQ, $SQ, $DQ], ascii: true);
-    // Legacy always double-quotes
-    check([$DQ, $DQ, $SQ], [$DQ, $BACKSLASH, $DQ, $BACKSLASH, $DQ, $SQ, $DQ]);
+    check('"', [$SQ, $DQ, $SQ]);
+    check("'", [$DQ, $SQ, $DQ]);
     // Using single quotes saves us one backslash:
-    check([$DQ, $DQ, $SQ], [$SQ, $DQ, $DQ, $BACKSLASH, $SQ, $SQ], ascii: true);
-    check([$DQ, $SQ, $SQ], [$DQ, $BACKSLASH, $DQ, $SQ, $SQ, $DQ], ascii: true);
+    check([$DQ, $DQ, $SQ], [$SQ, $DQ, $DQ, $BACKSLASH, $SQ, $SQ]);
+    check([$DQ, $SQ, $SQ], [$DQ, $BACKSLASH, $DQ, $SQ, $SQ, $DQ]);
   });
 
   test('u1234', () {
-    check('\u1234', [$DQ, 0x1234, $DQ]);
-    check('\u1234', [$DQ, $BACKSLASH, $u, $1, $2, $3, $4, $DQ], ascii: true);
+    check('\u1234', [$DQ, $BACKSLASH, $u, $1, $2, $3, $4, $DQ]);
     check('\u1234', [$DQ, 0x1234, $DQ], utf8: true);
   });
 
   test('u12345', () {
-    check([0x12345], [$DQ, 55304, 57157, $DQ]);
     // TODO: ES6 option:
     //check([0x12345],
-    //      [$DQ, $BACKSLASH, $u, $LCURLY, $1, $2, $3, $4, $5, $RCURLY, $DQ],
-    //      ascii: true);
-    check([0x12345], r'''"\ud808\udf45"''', ascii: true);
+    //      [$DQ, $BACKSLASH, $u, $LCURLY, $1, $2, $3, $4, $5, $RCURLY, $DQ]);
+    check([0x12345], r'''"\ud808\udf45"''');
     check([
       0x12345
     ], [
@@ -119,7 +97,7 @@
       $4,
       $5,
       $DQ
-    ], ascii: true);
+    ]);
     check([0x12345], [$DQ, 55304, 57157, $DQ], utf8: true);
   });
 
@@ -127,21 +105,16 @@
     // (0xD834, 0xDD1E) = 0x1D11E
     // Strings containing unpaired surrogates must be encoded to prevent
     // problems with the utf8 file-level encoding.
-    check([0xD834], [$DQ, 0xD834, $DQ]); // Legacy escapedString broken.
-    check([0xD834], [$DQ, $BACKSLASH, $u, $d, $8, $3, $4, $DQ], ascii: true);
+    check([0xD834], [$DQ, $BACKSLASH, $u, $d, $8, $3, $4, $DQ]);
     check([0xD834], [$DQ, $BACKSLASH, $u, $d, $8, $3, $4, $DQ], utf8: true);
 
-    check([0xDD1E], [$DQ, 0xDD1E, $DQ]); // Legacy escapedString broken.
-    check([0xDD1E], [$DQ, $BACKSLASH, $u, $d, $d, $1, $e, $DQ], ascii: true);
+    check([0xDD1E], [$DQ, $BACKSLASH, $u, $d, $d, $1, $e, $DQ]);
     check([0xDD1E], [$DQ, $BACKSLASH, $u, $d, $d, $1, $e, $DQ], utf8: true);
 
-    check([0xD834, $A], [$DQ, 0xD834, $A, $DQ]); // Legacy escapedString broken.
-    check([0xD834, $A], [$DQ, $BACKSLASH, $u, $d, $8, $3, $4, $A, $DQ],
-        ascii: true);
+    check([0xD834, $A], [$DQ, $BACKSLASH, $u, $d, $8, $3, $4, $A, $DQ]);
     check([0xD834, $A], [$DQ, $BACKSLASH, $u, $d, $8, $3, $4, $A, $DQ],
         utf8: true);
 
-    check([0xD834, 0xDD1E], [$DQ, 0xD834, 0xDD1E, $DQ]); // Legacy ok.
     check([
       0xD834,
       0xDD1E
@@ -160,8 +133,8 @@
       $1,
       $e,
       $DQ
-    ], ascii: true);
-    check([0xD834, 0xDD1E], r'''"\ud834\udd1e"''', ascii: true);
+    ]);
+    check([0xD834, 0xDD1E], r'''"\ud834\udd1e"''');
     check([0xD834, 0xDD1E], [$DQ, 0xD834, 0xDD1E, $DQ], utf8: true);
   });
 }
diff --git a/runtime/vm/compiler/backend/il_arm.cc b/runtime/vm/compiler/backend/il_arm.cc
index 0795592..61558c5 100644
--- a/runtime/vm/compiler/backend/il_arm.cc
+++ b/runtime/vm/compiler/backend/il_arm.cc
@@ -6438,12 +6438,13 @@
     __ b(deopt, CS);
   }
 }
-  
+
 static bool CanBePairOfImmediateOperands(Value* value,
                                          compiler::Operand* low,
                                          compiler::Operand* high) {
   int64_t imm;
-  if (value->BindsToConstant() && compiler::HasIntegerValue(value->BoundConstant(), &imm)) {
+  if (value->BindsToConstant() &&
+      compiler::HasIntegerValue(value->BoundConstant(), &imm)) {
     return compiler::Operand::CanHold(Utils::Low32Bits(imm), low) &&
       compiler::Operand::CanHold(Utils::High32Bits(imm), high);
   }
diff --git a/tools/VERSION b/tools/VERSION
index 220da86..ecfba87 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 14
 PATCH 0
-PRERELEASE 130
+PRERELEASE 131
 PRERELEASE_PATCH 0
\ No newline at end of file