Revert "Redo "[dart2js/js_ast] Escape strings in the printer""

This reverts commit 7817468526de896ac6d87210ff914f6af6040d89.

TBR=fishythefish@google.com

Change-Id: I6d1b4864ea483073f636718ac650d13e1f782d0d
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/201080
Reviewed-by: Stephen Adams <sra@google.com>
Commit-Queue: Stephen Adams <sra@google.com>
diff --git a/.dart_tool/package_config.json b/.dart_tool/package_config.json
index dd474e4..873b751 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-20T11:33:30.068787",
+  "generated": "2021-05-17T10:34:01.378194",
   "generator": "tools/generate_package_config.dart",
   "packages": [
     {
@@ -387,7 +387,7 @@
       "name": "js_ast",
       "rootUri": "../pkg/js_ast",
       "packageUri": "lib/",
-      "languageVersion": "2.10"
+      "languageVersion": "2.0"
     },
     {
       "name": "js_runtime",
diff --git a/pkg/compiler/lib/src/js/js.dart b/pkg/compiler/lib/src/js/js.dart
index 9c8328e..b49abc0 100644
--- a/pkg/compiler/lib/src/js/js.dart
+++ b/pkg/compiler/lib/src/js/js.dart
@@ -157,7 +157,7 @@
           }
         }
       }
-      _cachedLiteral = js.string(text);
+      _cachedLiteral = js.escapedString(text);
     }
     return _cachedLiteral;
   }
diff --git a/pkg/compiler/lib/src/js/size_estimator.dart b/pkg/compiler/lib/src/js/size_estimator.dart
index 24527ec6..8ecfd9e 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 == 0) return false;
+    if (field.length < 3) return false;
     // Ignore the leading and trailing string-delimiter.
-    for (int i = 0; i < field.length; i++) {
+    for (int i = 1; i < field.length - 1; 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 > 0 && isDigit(charCode))) {
+          i != 1 && 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,17 +776,16 @@
         newInForInit: inForInit, newAtStatementBegin: atStatementBegin);
     Node selector = access.selector;
     if (selector is LiteralString) {
-      String field = literalStringToString(selector);
-      if (isValidJavaScriptId(field)) {
+      String fieldWithQuotes = literalStringToString(selector);
+      if (isValidJavaScriptId(fieldWithQuotes)) {
         if (access.receiver is LiteralNumber) {
           // We can eliminate the space in some cases, but for simplicity we
           // always assume it is necessary.
           out(' '); // ' '
         }
 
-        // '.${field}'
-        out('.');
-        out(field);
+        // '.${fieldWithQuotes.substring(1, fieldWithQuotes.length - 1)}'
+        out('.${fieldWithQuotes.substring(1, fieldWithQuotes.length - 1)}');
         return;
       }
     } else if (selector is Name) {
@@ -876,9 +875,7 @@
 
   @override
   void visitLiteralString(LiteralString node) {
-    out('"');
     out(literalStringToString(node));
-    out('"');
   }
 
   @override
@@ -971,12 +968,10 @@
     if (name is LiteralString) {
       String text = literalStringToString(name);
       if (isValidJavaScriptId(text)) {
-        out(text);
+        // '${text.substring(1, text.length - 1)}
+        out('${text.substring(1, text.length - 1)}');
       } else {
-        // Approximation to `_handleString(text)`.
-        out('"');
-        out(text);
-        out('"');
+        out(text); // '$text'
       }
     } 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 dd67c4a..ec3928d 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.string(value);
+      return js.escapedString(value, ascii: true);
     }
     return StringReference(constant);
   }
@@ -288,7 +288,8 @@
         }
 
         // Keys in literal maps must be emitted in place.
-        jsAst.Literal keyExpression = js.string(key.stringValue);
+        jsAst.Literal keyExpression =
+            js.escapedString(key.stringValue, ascii: true);
         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 4c59ced..3c9f11f 100644
--- a/pkg/compiler/lib/src/js_backend/runtime_types_new.dart
+++ b/pkg/compiler/lib/src/js_backend/runtime_types_new.dart
@@ -109,7 +109,9 @@
       return _finishEncoding(js.string(String.fromCharCodes(_codes)));
     }
     _flushCodes();
-    return _finishEncoding(jsAst.StringConcatenation(_fragments));
+    jsAst.LiteralString quote = jsAst.LiteralString('"');
+    return _finishEncoding(
+        jsAst.StringConcatenation([quote, ..._fragments, quote]));
   }
 
   void _start(TypeRecipe recipe) {
@@ -485,13 +487,13 @@
   CommonElements get _commonElements => _dartTypes.commonElements;
   ClassEntity get _objectClass => _commonElements.objectClass;
 
-  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('"');
+  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("'");
 
   bool _isObject(InterfaceType type) => identical(type.element, _objectClass);
 
@@ -520,32 +522,28 @@
 
   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([
-        _doubleQuote,
-        _emitter.typeAccessNewRti(redirection.key),
-        _doubleQuote,
+        js.quoteName(_emitter.typeAccessNewRti(redirection.key)),
         _colon,
-        _doubleQuote,
-        _emitter.typeAccessNewRti(redirection.value),
-        _doubleQuote,
+        js.quoteName(_emitter.typeAccessNewRti(redirection.value)),
       ]);
 
   jsAst.StringConcatenation _encodeEntry(
           MapEntry<InterfaceType, _RulesetEntry> entry) =>
       js.concatenateStrings([
-        _doubleQuote,
-        _emitter.typeAccessNewRti(entry.key.element),
-        _doubleQuote,
+        js.quoteName(_emitter.typeAccessNewRti(entry.key.element)),
         _colon,
         _leftBrace,
         ...js.joinLiterals([
@@ -560,9 +558,7 @@
   jsAst.StringConcatenation _encodeSupertype(
           InterfaceType targetType, InterfaceType supertype) =>
       js.concatenateStrings([
-        _doubleQuote,
-        _emitter.typeAccessNewRti(supertype.element),
-        _doubleQuote,
+        js.quoteName(_emitter.typeAccessNewRti(supertype.element)),
         _colon,
         _leftBracket,
         ...js.joinLiterals(
@@ -575,36 +571,30 @@
   jsAst.StringConcatenation _encodeTypeVariable(InterfaceType targetType,
           TypeVariableType typeVariable, DartType supertypeArgument) =>
       js.concatenateStrings([
-        _doubleQuote,
-        _emitter.typeVariableAccessNewRti(typeVariable.element),
-        _doubleQuote,
+        js.quoteName(_emitter.typeVariableAccessNewRti(typeVariable.element)),
         _colon,
         _encodeSupertypeArgument(targetType, supertypeArgument),
       ]);
 
   jsAst.Literal _encodeSupertypeArgument(
           InterfaceType targetType, DartType supertypeArgument) =>
-      js.concatenateStrings([
-        _doubleQuote,
-        _recipeEncoder.encodeMetadataRecipe(
-            _emitter, targetType, supertypeArgument),
-        _doubleQuote
-      ]);
+      _recipeEncoder.encodeMetadataRecipe(
+          _emitter, targetType, supertypeArgument);
 
   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([
-        _doubleQuote,
-        _emitter.typeAccessNewRti(entry.key),
-        _doubleQuote,
+        js.quoteName(_emitter.typeAccessNewRti(entry.key)),
         _colon,
         js.number(entry.value),
       ]);
@@ -612,20 +602,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([
-        _doubleQuote,
-        _emitter.typeAccessNewRti(classEntry.key),
-        _doubleQuote,
+        js.quoteName(_emitter.typeAccessNewRti(classEntry.key)),
         _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 590ff93..05e1b8f 100644
--- a/pkg/compiler/lib/src/js_backend/string_reference.dart
+++ b/pkg/compiler/lib/src/js_backend/string_reference.dart
@@ -256,7 +256,8 @@
     for (_ReferenceSet referenceSet in _referencesByString.values) {
       if (referenceSet.generateAtUse) {
         StringConstantValue constant = referenceSet.constant;
-        js.Expression reference = js.string(constant.stringValue);
+        js.Expression reference =
+            js.js.escapedString(constant.stringValue, ascii: true);
         for (StringReference ref in referenceSet._references) {
           ref.value = reference;
         }
@@ -274,7 +275,8 @@
     for (_ReferenceSet referenceSet in referenceSetsUsingProperties) {
       String string = referenceSet.constant.stringValue;
       var propertyName = js.string(referenceSet.propertyName);
-      properties.add(js.Property(propertyName, js.string(string)));
+      properties.add(
+          js.Property(propertyName, js.js.escapedString(string, ascii: true)));
       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 b567137..4b75ecd 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 ba7376d..f7a1537 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;
+          name = selector.value.substring(1, selector.value.length - 1);
           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;
+        name = selector.value.substring(1, selector.value.length - 1);
       }
 
       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;
+              name = selector.value.substring(1, selector.value.length - 1);
             }
             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 e9b4822..7e7ac3f 100644
--- a/pkg/compiler/test/js/js_parser_test.dart
+++ b/pkg/compiler/test/js/js_parser_test.dart
@@ -20,9 +20,7 @@
 
 testError(String expression, [String expect = ""]) {
   bool doCheck(exception) {
-    final exceptionText = '$exception';
-    Expect.isTrue(exceptionText.contains(expect),
-        'Missing "$expect" in "$exceptionText"');
+    Expect.isTrue(exception.toString().contains(expect));
     return true;
   }
 
@@ -67,9 +65,9 @@
   // String literal with \n.
   testExpression(r'var x = "\n"');
   // String literal with escaped quote.
-  testExpression(r'''var x = "\""''', r"""var x = '"'""");
+  testExpression(r'var x = "\""');
   // *No clever escapes.
-  testError(r'var x = "\x42"', 'Hex escapes not supported');
+  testError(r'var x = "\x42"', 'escapes are not allowed in literals');
   // Operator new.
   testExpression('new Foo()');
   // New with dotted access.
@@ -170,7 +168,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 2dacebb..57b911f 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 1958f19..01b0dd9 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 1f980ea..2fee946 100644
--- a/pkg/js_ast/lib/src/builder.dart
+++ b/pkg/js_ast/lib/src/builder.dart
@@ -296,29 +296,186 @@
   }
 
   /// Creates a literal js string from [value].
-  LiteralString string(String value) => LiteralString(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"');
 
   /// 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) {
-    return StringConcatenation(List.of(parts, growable: false));
-  }
-
-  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;
+  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);
   }
 
-  LiteralString quoteName(Name name) {
-    return LiteralStringFromName(name);
+  Iterable<Literal> joinLiterals(Iterable<Literal> list, Literal separator) {
+    return new _InterleaveIterable<Literal>(list, separator);
+  }
+
+  LiteralString quoteName(Name name, {allowNull: false}) {
+    if (name == null) {
+      assert(allowNull);
+      return new LiteralString('""');
+    }
+    return new LiteralStringFromName(name);
   }
 
   LiteralNumber number(num value) => new LiteralNumber('$value');
@@ -348,19 +505,18 @@
 }
 
 LiteralString string(String value) => js.string(value);
+LiteralString quoteName(Name name, {allowNull: false}) {
+  return js.quoteName(name, allowNull: allowNull);
+}
 
-/// 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);
-
+LiteralString stringPart(String value) => js.stringPart(value);
 Iterable<Literal> joinLiterals(Iterable<Literal> list, Literal separator) {
   return js.joinLiterals(list, separator);
 }
 
-StringConcatenation concatenateStrings(Iterable<Literal> parts) {
-  return js.concatenateStrings(parts);
+StringConcatenation concatenateStrings(Iterable<Literal> parts,
+    {addQuotes: false}) {
+  return js.concatenateStrings(parts, addQuotes: addQuotes);
 }
 
 LiteralNumber number(num value) => js.number(value);
@@ -580,7 +736,7 @@
     '/': 5,
     '%': 5
   };
-  static final UNARY_OPERATORS = {
+  static final UNARY_OPERATORS = [
     '++',
     '--',
     '+',
@@ -591,7 +747,7 @@
     'void',
     'delete',
     'await'
-  };
+  ].toSet();
 
   static final OPERATORS_THAT_LOOK_LIKE_IDENTIFIERS =
       ['typeof', 'void', 'delete', 'in', 'instanceof', 'await'].toSet();
@@ -601,7 +757,7 @@
     return CATEGORIES[code];
   }
 
-  String getRegExp(int startPosition) {
+  String getDelimited(int startPosition) {
     position = startPosition;
     int delimiter = src.codeUnitAt(startPosition);
     int currentCode;
@@ -618,7 +774,7 @@
             escaped == charCodes.$u ||
             escaped == charCodes.$U ||
             category(escaped) == NUMERIC) {
-          error('Numeric and hex escapes are not supported in RegExp literals');
+          error('Numeric and hex escapes are not allowed in literals');
         }
       }
     } while (currentCode != delimiter);
@@ -626,46 +782,6 @@
     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 (;;) {
@@ -701,7 +817,7 @@
     if (code == charCodes.$SQ || code == charCodes.$DQ) {
       // String literal.
       lastCategory = STRING;
-      lastToken = getString(position, code);
+      lastToken = getDelimited(position);
     } else if (code == charCodes.$0 &&
         position + 2 < src.length &&
         src.codeUnitAt(position + 1) == charCodes.$x) {
@@ -863,7 +979,7 @@
       }
       return new ArrayInitializer(values);
     } else if (last != null && last.startsWith("/")) {
-      String regexp = getRegExp(lastPosition);
+      String regexp = getDelimited(lastPosition);
       getToken();
       String flags = lastToken;
       if (!acceptCategory(ALPHA)) flags = "";
@@ -937,12 +1053,12 @@
       Literal propertyName;
       String identifier = lastToken;
       if (acceptCategory(ALPHA)) {
-        propertyName = LiteralString(identifier);
+        propertyName = new LiteralString('"$identifier"');
       } else if (acceptCategory(STRING)) {
-        propertyName = LiteralString(identifier);
+        propertyName = new LiteralString(identifier);
       } else if (acceptCategory(SYMBOL)) {
         // e.g. void
-        propertyName = LiteralString(identifier);
+        propertyName = new LiteralString('"$identifier"');
       } else if (acceptCategory(HASH)) {
         var nameOrPosition = parseHash();
         InterpolatedLiteral interpolatedLiteral =
@@ -1458,3 +1574,40 @@
     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 285e918..903026e 100644
--- a/pkg/js_ast/lib/src/nodes.dart
+++ b/pkg/js_ast/lib/src/nodes.dart
@@ -1038,8 +1038,7 @@
   @override
   bool get isFinalized => name.isFinalized;
 
-  @override
-  String get value => name.name;
+  String get value => '"${name.name}"';
 
   void visitChildren<T>(NodeVisitor<T> visitor) {
     name.accept(visitor);
@@ -1372,8 +1371,6 @@
   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;
 
@@ -1381,6 +1378,8 @@
     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;
@@ -1521,10 +1520,10 @@
   PropertyAccess(this.receiver, this.selector);
 
   PropertyAccess.field(this.receiver, String fieldName)
-      : selector = LiteralString(fieldName);
+      : selector = new LiteralString('"$fieldName"');
 
   PropertyAccess.indexed(this.receiver, int index)
-      : selector = LiteralNumber('$index');
+      : selector = new LiteralNumber('$index');
 
   T accept<T>(NodeVisitor<T> visitor) => visitor.visitAccess(this);
 
@@ -1634,11 +1633,16 @@
 class LiteralString extends Literal {
   final String value;
 
-  /// 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.
+  /**
+   * 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.
+   */
   LiteralString(this.value);
 
   T accept<T>(NodeVisitor<T> visitor) => visitor.visitLiteralString(this);
@@ -1646,39 +1650,17 @@
   R accept1<R, A>(NodeVisitor1<R, A> visitor, A arg) =>
       visitor.visitLiteralString(this, arg);
 
-  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();
-  }
+  LiteralString _clone() => new LiteralString(value);
 }
 
 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 ed165c2..c6e9a9e 100644
--- a/pkg/js_ast/lib/src/printer.dart
+++ b/pkg/js_ast/lib/src/printer.dart
@@ -5,16 +5,14 @@
 part of js_ast;
 
 class JavaScriptPrintingOptions {
-  final bool utf8;
   final bool shouldCompressOutput;
   final bool minifyLocalVariables;
   final bool preferSemicolonToNewlineInMinifiedOutput;
 
   const JavaScriptPrintingOptions({
-    this.utf8 = false,
-    this.shouldCompressOutput = false,
-    this.minifyLocalVariables = false,
-    this.preferSemicolonToNewlineInMinifiedOutput = false,
+    this.shouldCompressOutput: false,
+    this.minifyLocalVariables: false,
+    this.preferSemicolonToNewlineInMinifiedOutput: false,
   });
 }
 
@@ -58,10 +56,11 @@
   String getText() => buffer.toString();
 }
 
-String DebugPrint(Node node, {bool utf8 = false}) {
-  JavaScriptPrintingOptions options = JavaScriptPrintingOptions(utf8: utf8);
-  SimpleJavaScriptPrintingContext context = SimpleJavaScriptPrintingContext();
-  Printer printer = Printer(options, context);
+String DebugPrint(Node node) {
+  JavaScriptPrintingOptions options = new JavaScriptPrintingOptions();
+  SimpleJavaScriptPrintingContext context =
+      new SimpleJavaScriptPrintingContext();
+  Printer printer = new Printer(options, context);
   printer.visit(node);
   return context.getText();
 }
@@ -84,8 +83,8 @@
   // A cache of all indentation strings used so far.
   List<String> _indentList = <String>[""];
 
-  static final identifierCharacterRegExp = RegExp(r'^[a-zA-Z_0-9$]');
-  static final expressionContinuationRegExp = RegExp(r'^[-+([]');
+  static final identifierCharacterRegExp = new RegExp(r'^[a-zA-Z_0-9$]');
+  static final expressionContinuationRegExp = new RegExp(r'^[-+([]');
 
   Printer(JavaScriptPrintingOptions options, JavaScriptPrintingContext context)
       : options = options,
@@ -727,10 +726,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 <= 6) return true;
+      if (value is LiteralString && value.value.length <= 8) 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 <= 6) return true;
+      if (value is Name && value.name.length <= 8) return true;
     }
     return false;
   }
@@ -1019,24 +1018,24 @@
   }
 
   bool isValidJavaScriptId(String field) {
-    if (field.length == 0) return false;
+    if (field.length < 3) return false;
     // Ignore the leading and trailing string-delimiter.
-    for (int i = 0; i < field.length; i++) {
+    for (int i = 1; i < field.length - 1; 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 > 0 && isDigit(charCode))) {
+          i != 1 && 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;
   }
 
@@ -1044,41 +1043,35 @@
   void visitAccess(PropertyAccess access) {
     visitNestedExpression(access.receiver, CALL,
         newInForInit: inForInit, newAtStatementBegin: atStatementBegin);
-
     Node selector = undefer(access.selector);
     if (selector is LiteralString) {
-      _dotString(access.selector, access.receiver, selector.value);
-      return;
-    } else if (selector is StringConcatenation) {
-      _dotString(access.selector, access.receiver,
-          _StringContentsCollector().collect(selector));
-      return;
+      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;
+      }
     } else if (selector is Name) {
-      _dotString(access.selector, access.receiver, selector.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);
       return;
     }
-
-    out('[');
+    out("[");
     visitNestedExpression(access.selector, EXPRESSION,
         newInForInit: false, newAtStatementBegin: false);
-    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(']');
-    }
+    out("]");
   }
 
   @override
@@ -1140,25 +1133,12 @@
 
   @override
   void visitLiteralString(LiteralString node) {
-    _handleString(node.value);
+    out(node.value);
   }
 
   @override
   visitStringConcatenation(StringConcatenation node) {
-    _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);
+    node.visitChildren(this);
   }
 
   @override
@@ -1255,15 +1235,18 @@
     startNode(node.name);
     Node name = undefer(node.name);
     if (name is LiteralString) {
-      _outPropertyName(name.value);
+      String text = name.value;
+      if (isValidJavaScriptId(text)) {
+        out(text.substring(1, text.length - 1));
+      } else {
+        out(text);
+      }
     } else if (name is Name) {
-      _outPropertyName(name.name);
-    } else if (name is LiteralNumber) {
-      out(name.value);
+      node.name.accept(this);
     } else {
-      // TODO(sra): Handle StringConcatenation.
-      // TODO(sra): Handle general expressions, .e.g. `{[x]: 1}`.
-      throw StateError('Unexpected Property name: $name');
+      assert(name is LiteralNumber);
+      LiteralNumber nameNumber = node.name;
+      out(nameNumber.value);
     }
     endNode(node.name);
     out(":");
@@ -1272,14 +1255,6 @@
         newInForInit: false, newAtStatementBegin: false);
   }
 
-  void _outPropertyName(String name) {
-    if (isValidJavaScriptId(name)) {
-      out(name);
-    } else {
-      _handleString(name);
-    }
-  }
-
   @override
   void visitRegExpLiteral(RegExpLiteral node) {
     out(node.pattern);
@@ -1346,44 +1321,6 @@
   }
 }
 
-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
deleted file mode 100644
index 91c2bae..0000000
--- a/pkg/js_ast/lib/src/strings.dart
+++ /dev/null
@@ -1,135 +0,0 @@
-// 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 788e081..5aa45ba 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 LiteralString(value);
+      if (value is String) return new 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 c510415..7909c53 100644
--- a/pkg/js_ast/pubspec.yaml
+++ b/pkg/js_ast/pubspec.yaml
@@ -3,7 +3,7 @@
 publish_to: none
 
 environment:
-  sdk: '>=2.10.0 <3.0.0'
+  sdk: '>=2.0.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 afd9805..c91a151 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 = VariableUse('variable');
+  VariableUse variableUse = new VariableUse('variable');
   DeferredExpression deferred =
-      map[variableUse] = _DeferredExpression(variableUse);
-  VariableUse variableUseAlias = VariableUse('variable');
-  map[variableUseAlias] = _DeferredExpression(variableUseAlias);
+      map[variableUse] = new _DeferredExpression(variableUse);
+  VariableUse variableUseAlias = new VariableUse('variable');
+  map[variableUseAlias] = new _DeferredExpression(variableUseAlias);
 
-  map[deferred] = _DeferredExpression(deferred);
-  Literal literal = LiteralString('literal');
-  map[literal] = _DeferredExpression(literal);
+  map[deferred] = new _DeferredExpression(deferred);
+  Literal literal = new LiteralString('"literal"');
+  map[literal] = new _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 = _Context();
+  _Context directContext = new _Context();
   Printer directPrinter =
-      Printer(const JavaScriptPrintingOptions(), directContext);
+      new 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 = _Context();
+  _Context deferredContext = new _Context();
   Printer deferredPrinter =
-      Printer(const JavaScriptPrintingOptions(), deferredContext);
+      new Printer(const JavaScriptPrintingOptions(), deferredContext);
   deferredPrinter.visit(deferredExpression);
   Expect.equals(expectedOutput, deferredContext.text);
 
@@ -121,7 +121,7 @@
 }
 
 class _Context implements JavaScriptPrintingContext {
-  StringBuffer sb = StringBuffer();
+  StringBuffer sb = new 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] =
-        _Position(startPosition, endPosition, closingPosition);
+        new _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 153c41d..7d8fb30 100644
--- a/pkg/js_ast/test/printer_callback_test.dart
+++ b/pkg/js_ast/test/printer_callback_test.dart
@@ -170,6 +170,9 @@
   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 78bc758..9277ed6 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, {bool utf8 = false}) {
-    if (input is List) input = String.fromCharCodes(input);
-    String actual = DebugPrint(js.string(input), utf8: utf8);
+  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;
     if (expected is List) {
       expect(actual.codeUnits, expected);
     } else {
@@ -29,57 +29,79 @@
 
   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, $BACKSLASH, $f, $DQ]);
+    check([$FF], [$DQ, $FF, $DQ]);
+    check([$FF], [$DQ, $BACKSLASH, $f, $DQ], ascii: true);
     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], r'''"\x00\x01\x02\x03"''');
+    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], [$DQ, 0, 1, 2, 3, $DQ], utf8: true);
   });
 
   test('line-separator', () {
-    check([$LS], [$DQ, $BACKSLASH, $u, $2, $0, $2, $8, $DQ]);
+    // 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], utf8: true);
   });
 
   test('page-separator', () {
-    check([$PS], [$DQ, $BACKSLASH, $u, $2, $0, $2, $9, $DQ]);
+    // 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], utf8: true);
   });
 
+  test('legacy-escaper-is-broken', () {
+    check([$LS], [$DQ, 0x2028, $DQ]);
+    check([$PS], [$DQ, 0x2029, $DQ]);
+  });
+
   test('choose-quotes', () {
-    check('"', [$SQ, $DQ, $SQ]);
-    check("'", [$DQ, $SQ, $DQ]);
+    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]);
     // Using single quotes saves us one backslash:
-    check([$DQ, $DQ, $SQ], [$SQ, $DQ, $DQ, $BACKSLASH, $SQ, $SQ]);
-    check([$DQ, $SQ, $SQ], [$DQ, $BACKSLASH, $DQ, $SQ, $SQ, $DQ]);
+    check([$DQ, $DQ, $SQ], [$SQ, $DQ, $DQ, $BACKSLASH, $SQ, $SQ], ascii: true);
+    check([$DQ, $SQ, $SQ], [$DQ, $BACKSLASH, $DQ, $SQ, $SQ, $DQ], ascii: true);
   });
 
   test('u1234', () {
-    check('\u1234', [$DQ, $BACKSLASH, $u, $1, $2, $3, $4, $DQ]);
+    check('\u1234', [$DQ, 0x1234, $DQ]);
+    check('\u1234', [$DQ, $BACKSLASH, $u, $1, $2, $3, $4, $DQ], ascii: true);
     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]);
-    check([0x12345], r'''"\ud808\udf45"''');
+    //      [$DQ, $BACKSLASH, $u, $LCURLY, $1, $2, $3, $4, $5, $RCURLY, $DQ],
+    //      ascii: true);
+    check([0x12345], r'''"\ud808\udf45"''', ascii: true);
     check([
       0x12345
     ], [
@@ -97,7 +119,7 @@
       $4,
       $5,
       $DQ
-    ]);
+    ], ascii: true);
     check([0x12345], [$DQ, 55304, 57157, $DQ], utf8: true);
   });
 
@@ -105,16 +127,21 @@
     // (0xD834, 0xDD1E) = 0x1D11E
     // Strings containing unpaired surrogates must be encoded to prevent
     // problems with the utf8 file-level encoding.
-    check([0xD834], [$DQ, $BACKSLASH, $u, $d, $8, $3, $4, $DQ]);
+    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], utf8: true);
 
-    check([0xDD1E], [$DQ, $BACKSLASH, $u, $d, $d, $1, $e, $DQ]);
+    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], utf8: true);
 
-    check([0xD834, $A], [$DQ, $BACKSLASH, $u, $d, $8, $3, $4, $A, $DQ]);
+    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],
         utf8: true);
 
+    check([0xD834, 0xDD1E], [$DQ, 0xD834, 0xDD1E, $DQ]); // Legacy ok.
     check([
       0xD834,
       0xDD1E
@@ -133,8 +160,8 @@
       $1,
       $e,
       $DQ
-    ]);
-    check([0xD834, 0xDD1E], r'''"\ud834\udd1e"''');
+    ], ascii: true);
+    check([0xD834, 0xDD1E], r'''"\ud834\udd1e"''', ascii: true);
     check([0xD834, 0xDD1E], [$DQ, 0xD834, 0xDD1E, $DQ], utf8: true);
   });
 }