Version 2.14.0-173.0.dev

Merge commit '7baeabe0e43b09a703b919906559d99752efb7a1' into 'dev'
diff --git a/pkg/analyzer/lib/src/dart/resolver/annotation_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/annotation_resolver.dart
index efa3e86c..b05b24d 100644
--- a/pkg/analyzer/lib/src/dart/resolver/annotation_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/annotation_resolver.dart
@@ -43,7 +43,7 @@
     if (elementAnnotationImpl == null) {
       // Analyzer ignores annotations on "part of" directives.
       assert(parent is PartDirective || parent is PartOfDirective);
-    } else {
+    } else if (_resolver.shouldCloneAnnotations) {
       elementAnnotationImpl.annotationAst = _createCloner().cloneNode(node);
     }
   }
diff --git a/pkg/analyzer/lib/src/generated/resolver.dart b/pkg/analyzer/lib/src/generated/resolver.dart
index b8101a1..5812ae5 100644
--- a/pkg/analyzer/lib/src/generated/resolver.dart
+++ b/pkg/analyzer/lib/src/generated/resolver.dart
@@ -253,6 +253,13 @@
   /// always safe to use `.last` to examine the top of the stack.
   final List<Expression?> _unfinishedNullShorts = [null];
 
+  /// During resolution we clone annotation ASTs into the element model.
+  /// But we should not do this during linking element models, moreover
+  /// currently by doing this we will lose elements for parameters of
+  /// generic function types.
+  /// TODO(scheglov) Stop cloning altogether.
+  bool shouldCloneAnnotations = true;
+
   /// Initialize a newly created visitor to resolve the nodes in an AST node.
   ///
   /// The [definingLibrary] is the element for the library containing the node
diff --git a/pkg/analyzer/lib/src/summary2/ast_binary_reader.dart b/pkg/analyzer/lib/src/summary2/ast_binary_reader.dart
index 09c4891..ef9cf58 100644
--- a/pkg/analyzer/lib/src/summary2/ast_binary_reader.dart
+++ b/pkg/analyzer/lib/src/summary2/ast_binary_reader.dart
@@ -5,9 +5,12 @@
 import 'package:analyzer/dart/ast/ast.dart';
 import 'package:analyzer/dart/ast/token.dart';
 import 'package:analyzer/dart/element/element.dart';
+import 'package:analyzer/dart/element/type.dart';
 import 'package:analyzer/src/dart/ast/ast.dart';
 import 'package:analyzer/src/dart/ast/ast_factory.dart';
 import 'package:analyzer/src/dart/ast/token.dart';
+import 'package:analyzer/src/dart/element/element.dart';
+import 'package:analyzer/src/dart/element/type.dart';
 import 'package:analyzer/src/generated/testing/ast_test_factory.dart';
 import 'package:analyzer/src/generated/testing/token_factory.dart';
 import 'package:analyzer/src/generated/utilities_dart.dart';
@@ -401,6 +404,17 @@
       defaultValue,
     );
 
+    var nonDefaultElement = parameter.declaredElement!;
+    var element = DefaultParameterElementImpl(
+      nonDefaultElement.name,
+      nonDefaultElement.nameOffset,
+    );
+    element.parameterKind = kind;
+    if (parameter is SimpleFormalParameterImpl) {
+      parameter.declaredElement = element;
+    }
+    element.type = nonDefaultElement.type;
+
     return node;
   }
 
@@ -600,7 +614,17 @@
       formalParameters,
       question: AstBinaryFlags.hasQuestion(flags) ? Tokens.QUESTION : null,
     );
-    node.type = _reader.readType();
+    var type = _reader.readRequiredType() as FunctionType;
+    node.type = type;
+
+    var element = GenericFunctionTypeElementImpl.forOffset(-1);
+    element.parameters = formalParameters.parameters
+        .map((parameter) => parameter.declaredElement!)
+        .toList();
+    element.returnType = returnType?.type ?? DynamicTypeImpl.instance;
+    element.type = type;
+    node.declaredElement = element;
+
     return node;
   }
 
@@ -999,8 +1023,14 @@
       requiredKeyword:
           AstBinaryFlags.isRequired(flags) ? Tokens.REQUIRED : null,
     );
-    _reader.readType(); // TODO(scheglov) actual type
+    var actualType = _reader.readRequiredType();
     _reader.readByte(); // TODO(scheglov) inherits covariant
+
+    var element = ParameterElementImpl(identifier?.name ?? '', -1);
+    element.parameterKind = node.kind;
+    element.type = actualType;
+    node.declaredElement = element;
+
     return node;
   }
 
diff --git a/pkg/analyzer/lib/src/summary2/ast_resolver.dart b/pkg/analyzer/lib/src/summary2/ast_resolver.dart
index 83100d7..da1447b 100644
--- a/pkg/analyzer/lib/src/summary2/ast_resolver.dart
+++ b/pkg/analyzer/lib/src/summary2/ast_resolver.dart
@@ -56,6 +56,7 @@
       : _featureSet = node.thisOrAncestorOfType<CompilationUnit>()!.featureSet;
 
   void resolveAnnotation(AnnotationImpl node) {
+    _resolverVisitor.shouldCloneAnnotations = false;
     node.accept(_resolutionVisitor);
     node.accept(_variableResolverVisitor);
     _prepareEnclosingDeclarations();
diff --git a/pkg/analyzer/test/src/summary/resolved_ast_printer.dart b/pkg/analyzer/test/src/summary/resolved_ast_printer.dart
index 701c255..1ed2d25 100644
--- a/pkg/analyzer/test/src/summary/resolved_ast_printer.dart
+++ b/pkg/analyzer/test/src/summary/resolved_ast_printer.dart
@@ -694,11 +694,15 @@
   }
 
   @override
-  void visitGenericFunctionType(GenericFunctionType node) {
+  void visitGenericFunctionType(covariant GenericFunctionTypeImpl node) {
     _writeNextCodeLine(node);
     _writeln('GenericFunctionType');
     _withIndent(() {
       var properties = _Properties();
+      properties.addGenericFunctionTypeElement(
+        'declaredElement',
+        node.declaredElement,
+      );
       properties.addToken('functionKeyword', node.functionKeyword);
       properties.addNode('parameters', node.parameters);
       properties.addNode('returnType', node.returnType);
@@ -1728,6 +1732,24 @@
     }
   }
 
+  void _writeGenericFunctionTypeElement(
+    String name,
+    GenericFunctionTypeElement? element,
+  ) {
+    _sink.write(_indent);
+    _sink.write('$name: ');
+    if (element == null) {
+      _sink.writeln('<null>');
+    } else {
+      _withIndent(() {
+        _sink.writeln('GenericFunctionTypeElement');
+        _writeParameterElements(element.parameters);
+        _writeType('returnType', element.returnType);
+        _writeType('type', element.type);
+      });
+    }
+  }
+
   void _writeln(String line) {
     _sink.writeln(line);
   }
@@ -1766,6 +1788,33 @@
     }
   }
 
+  void _writeParameterElements(List<ParameterElement> parameters) {
+    _writelnWithIndent('parameters');
+    _withIndent(() {
+      for (var parameter in parameters) {
+        _writelnWithIndent(parameter.name);
+        _withIndent(() {
+          _writeParameterKind(parameter);
+          _writeType('type', parameter.type);
+        });
+      }
+    });
+  }
+
+  void _writeParameterKind(ParameterElement parameter) {
+    if (parameter.isOptionalNamed) {
+      _writelnWithIndent('kind: optional named');
+    } else if (parameter.isOptionalPositional) {
+      _writelnWithIndent('kind: optional positional');
+    } else if (parameter.isRequiredNamed) {
+      _writelnWithIndent('kind: required named');
+    } else if (parameter.isRequiredPositional) {
+      _writelnWithIndent('kind: required positional');
+    } else {
+      throw StateError('Unknown kind: $parameter');
+    }
+  }
+
   void _writeProperties(_Properties container) {
     var properties = container.properties;
     properties.sort((a, b) => a.name.compareTo(b.name));
@@ -1822,6 +1871,17 @@
   }
 }
 
+class _GenericFunctionTypeElementProperty extends _Property {
+  final GenericFunctionTypeElement? element;
+
+  _GenericFunctionTypeElementProperty(String name, this.element) : super(name);
+
+  @override
+  void write(ResolvedAstPrinter printer) {
+    printer._writeGenericFunctionTypeElement(name, element);
+  }
+}
+
 class _NodeListProperty extends _Property {
   final NodeList nodeList;
 
@@ -1853,6 +1913,15 @@
     );
   }
 
+  void addGenericFunctionTypeElement(
+    String name,
+    GenericFunctionTypeElement? element,
+  ) {
+    properties.add(
+      _GenericFunctionTypeElementProperty(name, element),
+    );
+  }
+
   void addNode(String name, AstNode? node) {
     properties.add(
       _NodeProperty(name, node),
diff --git a/pkg/analyzer/test/src/summary/resynthesize_common.dart b/pkg/analyzer/test/src/summary/resynthesize_common.dart
index 7074657..236dd23 100644
--- a/pkg/analyzer/test/src/summary/resynthesize_common.dart
+++ b/pkg/analyzer/test/src/summary/resynthesize_common.dart
@@ -7933,6 +7933,443 @@
 ''');
   }
 
+  test_genericFunction_asTypeArgument_ofAnnotation_class() async {
+    var library = await checkLibrary(r'''
+class A<T> {
+  const A();
+}
+
+@A<int Function(String a)>()
+class B {}
+''');
+    checkElementText(
+        library,
+        r'''
+class A {
+  const A();
+}
+  typeParameters
+    T
+      bound: null
+      defaultType: dynamic
+class B {
+}
+  metadata
+    Annotation
+      arguments: ArgumentList
+      element: ConstructorMember
+        base: self::@class::A::@constructor::•
+        substitution: {T: dynamic}
+      name: SimpleIdentifier
+        staticElement: self::@class::A
+        staticType: null
+        token: A
+      typeArguments: TypeArgumentList
+        arguments
+          GenericFunctionType
+            declaredElement: GenericFunctionTypeElement
+              parameters
+                a
+                  kind: required positional
+                  type: String
+              returnType: int
+              type: int Function(String)
+            functionKeyword: Function
+            parameters: FormalParameterList
+              parameters
+                SimpleFormalParameter
+                  declaredElement: a@-1
+                  declaredElementType: String
+                  identifier: SimpleIdentifier
+                    staticElement: <null>
+                    staticType: null
+                    token: a
+                  type: TypeName
+                    name: SimpleIdentifier
+                      staticElement: dart:core::@class::String
+                      staticType: null
+                      token: String
+                    type: String
+            returnType: TypeName
+              name: SimpleIdentifier
+                staticElement: dart:core::@class::int
+                staticType: null
+                token: int
+              type: int
+            type: int Function(String)
+''',
+        withFullyResolvedAst: true);
+  }
+
+  test_genericFunction_asTypeArgument_ofAnnotation_topLevelVariable() async {
+    var library = await checkLibrary(r'''
+class A<T> {
+  const A();
+}
+
+@A<int Function(String a)>()
+var v = 0;
+''');
+    checkElementText(
+        library,
+        r'''
+class A {
+  const A();
+}
+  typeParameters
+    T
+      bound: null
+      defaultType: dynamic
+int v;
+  metadata
+    Annotation
+      arguments: ArgumentList
+      element: ConstructorMember
+        base: self::@class::A::@constructor::•
+        substitution: {T: dynamic}
+      name: SimpleIdentifier
+        staticElement: self::@class::A
+        staticType: null
+        token: A
+      typeArguments: TypeArgumentList
+        arguments
+          GenericFunctionType
+            declaredElement: GenericFunctionTypeElement
+              parameters
+                a
+                  kind: required positional
+                  type: String
+              returnType: int
+              type: int Function(String)
+            functionKeyword: Function
+            parameters: FormalParameterList
+              parameters
+                SimpleFormalParameter
+                  declaredElement: a@-1
+                  declaredElementType: String
+                  identifier: SimpleIdentifier
+                    staticElement: <null>
+                    staticType: null
+                    token: a
+                  type: TypeName
+                    name: SimpleIdentifier
+                      staticElement: dart:core::@class::String
+                      staticType: null
+                      token: String
+                    type: String
+            returnType: TypeName
+              name: SimpleIdentifier
+                staticElement: dart:core::@class::int
+                staticType: null
+                token: int
+              type: int
+            type: int Function(String)
+''',
+        withFullyResolvedAst: true);
+  }
+
+  test_genericFunction_asTypeArgument_parameters_optionalNamed() async {
+    var library = await checkLibrary(r'''
+class A<T> {
+  const A();
+}
+
+const v = A<String Function({int? a})>();
+''');
+    checkElementText(
+        library,
+        r'''
+class A {
+  const A();
+}
+  typeParameters
+    T
+      bound: null
+      defaultType: dynamic
+const A<String Function({int? a})> v;
+  constantInitializer
+    InstanceCreationExpression
+      argumentList: ArgumentList
+      constructorName: ConstructorName
+        staticElement: ConstructorMember
+          base: self::@class::A::@constructor::•
+          substitution: {T: String Function({int? a})}
+        type: TypeName
+          name: SimpleIdentifier
+            staticElement: self::@class::A
+            staticType: null
+            token: A
+          type: A<String Function({int? a})>
+          typeArguments: TypeArgumentList
+            arguments
+              GenericFunctionType
+                declaredElement: GenericFunctionTypeElement
+                  parameters
+                    a
+                      kind: optional named
+                      type: int?
+                  returnType: String
+                  type: String Function({int? a})
+                functionKeyword: Function
+                parameters: FormalParameterList
+                  parameters
+                    DefaultFormalParameter
+                      declaredElement: a@-1
+                      declaredElementType: int?
+                      identifier: SimpleIdentifier
+                        staticElement: <null>
+                        staticType: null
+                        token: a
+                      parameter: SimpleFormalParameter
+                        declaredElement: a@-1
+                        declaredElementType: int?
+                        identifier: SimpleIdentifier
+                          staticElement: <null>
+                          staticType: null
+                          token: a
+                        type: TypeName
+                          name: SimpleIdentifier
+                            staticElement: dart:core::@class::int
+                            staticType: null
+                            token: int
+                          type: int?
+                returnType: TypeName
+                  name: SimpleIdentifier
+                    staticElement: dart:core::@class::String
+                    staticType: null
+                    token: String
+                  type: String
+                type: String Function({int? a})
+      staticType: A<String Function({int? a})>
+''',
+        withFullyResolvedAst: true);
+  }
+
+  test_genericFunction_asTypeArgument_parameters_optionalPositional() async {
+    var library = await checkLibrary(r'''
+class A<T> {
+  const A();
+}
+
+const v = A<String Function([int? a])>();
+''');
+    checkElementText(
+        library,
+        r'''
+class A {
+  const A();
+}
+  typeParameters
+    T
+      bound: null
+      defaultType: dynamic
+const A<String Function([int?])> v;
+  constantInitializer
+    InstanceCreationExpression
+      argumentList: ArgumentList
+      constructorName: ConstructorName
+        staticElement: ConstructorMember
+          base: self::@class::A::@constructor::•
+          substitution: {T: String Function([int?])}
+        type: TypeName
+          name: SimpleIdentifier
+            staticElement: self::@class::A
+            staticType: null
+            token: A
+          type: A<String Function([int?])>
+          typeArguments: TypeArgumentList
+            arguments
+              GenericFunctionType
+                declaredElement: GenericFunctionTypeElement
+                  parameters
+                    a
+                      kind: optional positional
+                      type: int?
+                  returnType: String
+                  type: String Function([int?])
+                functionKeyword: Function
+                parameters: FormalParameterList
+                  parameters
+                    DefaultFormalParameter
+                      declaredElement: a@-1
+                      declaredElementType: int?
+                      identifier: SimpleIdentifier
+                        staticElement: <null>
+                        staticType: null
+                        token: a
+                      parameter: SimpleFormalParameter
+                        declaredElement: a@-1
+                        declaredElementType: int?
+                        identifier: SimpleIdentifier
+                          staticElement: <null>
+                          staticType: null
+                          token: a
+                        type: TypeName
+                          name: SimpleIdentifier
+                            staticElement: dart:core::@class::int
+                            staticType: null
+                            token: int
+                          type: int?
+                returnType: TypeName
+                  name: SimpleIdentifier
+                    staticElement: dart:core::@class::String
+                    staticType: null
+                    token: String
+                  type: String
+                type: String Function([int?])
+      staticType: A<String Function([int?])>
+''',
+        withFullyResolvedAst: true);
+  }
+
+  test_genericFunction_asTypeArgument_parameters_requiredNamed() async {
+    var library = await checkLibrary(r'''
+class A<T> {
+  const A();
+}
+
+const v = A<String Function({required int a})>();
+''');
+    checkElementText(
+        library,
+        r'''
+class A {
+  const A();
+}
+  typeParameters
+    T
+      bound: null
+      defaultType: dynamic
+const A<String Function({required int a})> v;
+  constantInitializer
+    InstanceCreationExpression
+      argumentList: ArgumentList
+      constructorName: ConstructorName
+        staticElement: ConstructorMember
+          base: self::@class::A::@constructor::•
+          substitution: {T: String Function({required int a})}
+        type: TypeName
+          name: SimpleIdentifier
+            staticElement: self::@class::A
+            staticType: null
+            token: A
+          type: A<String Function({required int a})>
+          typeArguments: TypeArgumentList
+            arguments
+              GenericFunctionType
+                declaredElement: GenericFunctionTypeElement
+                  parameters
+                    a
+                      kind: required named
+                      type: int
+                  returnType: String
+                  type: String Function({required int a})
+                functionKeyword: Function
+                parameters: FormalParameterList
+                  parameters
+                    DefaultFormalParameter
+                      declaredElement: a@-1
+                      declaredElementType: int
+                      identifier: SimpleIdentifier
+                        staticElement: <null>
+                        staticType: null
+                        token: a
+                      parameter: SimpleFormalParameter
+                        declaredElement: a@-1
+                        declaredElementType: int
+                        identifier: SimpleIdentifier
+                          staticElement: <null>
+                          staticType: null
+                          token: a
+                        requiredKeyword: required
+                        type: TypeName
+                          name: SimpleIdentifier
+                            staticElement: dart:core::@class::int
+                            staticType: null
+                            token: int
+                          type: int
+                returnType: TypeName
+                  name: SimpleIdentifier
+                    staticElement: dart:core::@class::String
+                    staticType: null
+                    token: String
+                  type: String
+                type: String Function({required int a})
+      staticType: A<String Function({required int a})>
+''',
+        withFullyResolvedAst: true);
+  }
+
+  test_genericFunction_asTypeArgument_parameters_requiredPositional() async {
+    var library = await checkLibrary(r'''
+class A<T> {
+  const A();
+}
+
+const v = A<String Function(int a)>();
+''');
+    checkElementText(
+        library,
+        r'''
+class A {
+  const A();
+}
+  typeParameters
+    T
+      bound: null
+      defaultType: dynamic
+const A<String Function(int)> v;
+  constantInitializer
+    InstanceCreationExpression
+      argumentList: ArgumentList
+      constructorName: ConstructorName
+        staticElement: ConstructorMember
+          base: self::@class::A::@constructor::•
+          substitution: {T: String Function(int)}
+        type: TypeName
+          name: SimpleIdentifier
+            staticElement: self::@class::A
+            staticType: null
+            token: A
+          type: A<String Function(int)>
+          typeArguments: TypeArgumentList
+            arguments
+              GenericFunctionType
+                declaredElement: GenericFunctionTypeElement
+                  parameters
+                    a
+                      kind: required positional
+                      type: int
+                  returnType: String
+                  type: String Function(int)
+                functionKeyword: Function
+                parameters: FormalParameterList
+                  parameters
+                    SimpleFormalParameter
+                      declaredElement: a@-1
+                      declaredElementType: int
+                      identifier: SimpleIdentifier
+                        staticElement: <null>
+                        staticType: null
+                        token: a
+                      type: TypeName
+                        name: SimpleIdentifier
+                          staticElement: dart:core::@class::int
+                          staticType: null
+                          token: int
+                        type: int
+                returnType: TypeName
+                  name: SimpleIdentifier
+                    staticElement: dart:core::@class::String
+                    staticType: null
+                    token: String
+                  type: String
+                type: String Function(int)
+      staticType: A<String Function(int)>
+''',
+        withFullyResolvedAst: true);
+  }
+
   test_genericFunction_boundOf_typeParameter_ofMixin() async {
     var library = await checkLibrary(r'''
 mixin B<X extends void Function()> {}
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..9fc5044 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;
@@ -439,4 +437,10 @@
       visitNode(node);
     }
   }
+
+  @override
+  void visitLiteralString(js.LiteralString node) {
+    // [js.LiteralString] and [js.LiteralStringFromName] do not contain embedded
+    // [StringReference] or [StringReferenceResource] nodes.
+  }
 }
diff --git a/pkg/compiler/lib/src/js_emitter/parameter_stub_generator.dart b/pkg/compiler/lib/src/js_emitter/parameter_stub_generator.dart
index 0acdc0b..819b321 100644
--- a/pkg/compiler/lib/src/js_emitter/parameter_stub_generator.dart
+++ b/pkg/compiler/lib/src/js_emitter/parameter_stub_generator.dart
@@ -328,9 +328,8 @@
       // Function.apply calls the function with no type arguments, so generic
       // methods need the stub to default the type arguments.
       // This has to be the first stub.
-      Selector namedSelector = new Selector.fromElement(member).toNonGeneric();
-      Selector closureSelector =
-          namedSelector.isClosureCall ? null : namedSelector.toCallSelector();
+      Selector namedSelector = Selector.fromElement(member).toNonGeneric();
+      Selector closureSelector = namedSelector.toCallSelector();
 
       renamedCallSelectors.add(namedSelector);
       stubSelectors.add(namedSelector);
diff --git a/pkg/compiler/lib/src/js_emitter/startup_emitter/fragment_emitter.dart b/pkg/compiler/lib/src/js_emitter/startup_emitter/fragment_emitter.dart
index f84c045..bea472a 100644
--- a/pkg/compiler/lib/src/js_emitter/startup_emitter/fragment_emitter.dart
+++ b/pkg/compiler/lib/src/js_emitter/startup_emitter/fragment_emitter.dart
@@ -1400,14 +1400,20 @@
       // The first entry in the funsOrNames-array must be a string.
       funsOrNames.add(js.quoteName(method.name));
       for (ParameterStubMethod stubMethod in method.parameterStubs) {
-        callNames.add(stubMethod.callName);
-        addFunOrName(stubMethod);
+        js.Name callName = stubMethod.callName;
+        // `callName` might be `null` if the method is called directly with some
+        // CallStructure but it can be proven that the tearoff not called with
+        // with that CallStructure, e.g. the closure does no need the defaulting
+        // of arguments but some direct call does.
+        if (callName != null) {
+          callNames.add(callName);
+          addFunOrName(stubMethod);
+        }
       }
 
-      js.ArrayInitializer callNameArray =
-          new js.ArrayInitializer(callNames.map(js.quoteName).toList());
-      js.ArrayInitializer funsOrNamesArray =
-          new js.ArrayInitializer(funsOrNames);
+      final callNameArray =
+          js.ArrayInitializer([...callNames.map(js.quoteName)]);
+      final funsOrNamesArray = js.ArrayInitializer(funsOrNames);
 
       bool isIntercepted = false;
       if (method is InstanceMethod) {
@@ -1415,7 +1421,7 @@
       }
 
       int requiredParameterCount = method.requiredParameterCount;
-      js.Expression optionalParameterDefaultValues = new js.LiteralNull();
+      js.Expression optionalParameterDefaultValues = js.LiteralNull();
       if (method.canBeApplied) {
         optionalParameterDefaultValues =
             _encodeOptionalParameterDefaultValues(method);
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 d741180..6f2f1fa 100644
--- a/pkg/js_ast/lib/src/builder.dart
+++ b/pkg/js_ast/lib/src/builder.dart
@@ -296,186 +296,24 @@
   }
 
   /// 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;
+  LiteralString string(String value) => LiteralString(value);
+
+  StringConcatenation concatenateStrings(Iterable<Literal> parts) {
+    return StringConcatenation(List.of(parts, growable: false));
   }
 
-  /// 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;
-        }
-      }
+  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 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,
-      {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);
-  }
-
-  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);
+  LiteralString quoteName(Name name) {
+    return LiteralStringFromName(name);
   }
 
   LiteralNumber number(num value) => new LiteralNumber('$value');
@@ -505,18 +343,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);
@@ -763,7 +602,7 @@
     return CATEGORIES[code];
   }
 
-  String getDelimited(int startPosition) {
+  String getRegExp(int startPosition) {
     position = startPosition;
     int delimiter = src.codeUnitAt(startPosition);
     int currentCode;
@@ -780,7 +619,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);
@@ -788,6 +627,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 (;;) {
@@ -823,7 +702,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) {
@@ -985,7 +864,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 = "";
@@ -1059,12 +938,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 =
@@ -1580,40 +1459,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..c90dc08 100644
--- a/pkg/js_ast/lib/src/nodes.dart
+++ b/pkg/js_ast/lib/src/nodes.dart
@@ -1031,14 +1031,17 @@
 }
 
 class LiteralStringFromName extends LiteralString {
-  Name name;
+  final Name name;
 
-  LiteralStringFromName(this.name) : super(null);
+  LiteralStringFromName(this.name) : super(null) {
+    ArgumentError.checkNotNull(name, 'name');
+  }
 
   @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 +1374,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 +1383,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 +1523,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 +1636,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 +1648,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/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/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/pkg/test_runner/lib/src/command_output.dart b/pkg/test_runner/lib/src/command_output.dart
index d2807a7a4..3e587e3 100644
--- a/pkg/test_runner/lib/src/command_output.dart
+++ b/pkg/test_runner/lib/src/command_output.dart
@@ -9,7 +9,6 @@
 
 import 'package:dart2js_tools/deobfuscate_stack_trace.dart';
 import 'package:status_file/expectation.dart';
-import 'package:test_runner/src/repository.dart';
 import 'package:test_runner/src/static_error.dart';
 
 import 'browser_controller.dart';
@@ -461,67 +460,53 @@
 /// A parsed analyzer error diagnostic.
 class AnalyzerError implements Comparable<AnalyzerError> {
   /// Parses all errors from analyzer [stderr] output.
-  static Iterable<AnalyzerError> parseStderr(String stderr) sync* {
-    for (var outputLine in stderr.split("\n")) {
-      var error = _tryParse(outputLine);
-      if (error != null) yield error;
+  static List<AnalyzerError> parseStderr(String stderr) {
+    var result = <AnalyzerError>[];
+
+    var jsonData = json.decode(stderr) as Map<String, dynamic>;
+    var version = jsonData['version'];
+    if (version != 1) {
+      DebugLogger.error('Unexpected analyzer JSON data version: $version');
+      throw UnimplementedError();
     }
-  }
 
-  static AnalyzerError _tryParse(String line) {
-    if (line.isEmpty) return null;
+    for (var diagnostic in jsonData['diagnostics'] as List<dynamic>) {
+      var diagnosticMap = diagnostic as Map<String, dynamic>;
 
-    // Split and unescape the fields.
-    // The escaping is implemented in:
-    // pkg/analyzer_cli/lib/src/error_formatter.dart#L392
-    var fields = <String>[];
-    var field = StringBuffer();
-    var inEscape = false;
-    for (var i = 0; i < line.length; i++) {
-      var c = line[i];
+      var type = diagnosticMap['type'] as String;
+      var code = diagnosticMap['code'] as String;
+      var errorCode = '$type.${code.toUpperCase()}';
 
-      if (inEscape) {
-        switch (c) {
-          case '\\':
-            field.write('\\');
-            break;
-          case '|':
-            field.write('|');
-            break;
-          case 'n':
-            field.write('\n');
-            break;
-          // TODO(rnystrom): Are there other escapes?
-          default:
-            field.write(c);
-            break;
-        }
+      var error = _parse(
+          diagnosticMap, diagnosticMap['problemMessage'] as String, errorCode);
+      result.add(error);
 
-        inEscape = false;
-      } else if (c == '\\') {
-        inEscape = true;
-      } else if (c == '|') {
-        fields.add(field.toString());
-        field = StringBuffer();
-      } else {
-        field.write(c);
+      var contextMessages = diagnosticMap['contextMessages'] as List<dynamic>;
+      for (var contextMessage in contextMessages ?? const []) {
+        var contextMessageMap = contextMessage as Map<String, dynamic>;
+        error.contextMessages.add(
+            _parse(contextMessageMap, contextMessageMap['message'] as String));
       }
     }
 
-    // Add the last field.
-    fields.add(field.toString());
+    return result;
+  }
 
-    // Lines without enough fields are other output we don't care about.
-    if (fields.length < 8) return null;
+  static AnalyzerError _parse(Map<String, dynamic> diagnostic, String message,
+      [String errorCode]) {
+    var location = diagnostic['location'] as Map<String, dynamic>;
 
+    var range = location['range'] as Map<String, dynamic>;
+    var start = range['start'] as Map<String, dynamic>;
+    var end = range['end'] as Map<String, dynamic>;
     return AnalyzerError._(
-        severity: fields[0],
-        errorCode: "${fields[1]}.${fields[2]}",
-        file: fields[3],
-        message: fields[7],
-        line: int.parse(fields[4]),
-        column: int.parse(fields[5]),
-        length: int.parse(fields[6]));
+        severity: diagnostic['severity'] as String,
+        errorCode: errorCode,
+        file: location['file'] as String,
+        message: message,
+        line: start['line'] as int,
+        column: start['column'] as int,
+        length: (end['offset'] as int) - (start['offset'] as int));
   }
 
   final String severity;
@@ -532,6 +517,8 @@
   final int column;
   final int length;
 
+  final List<AnalyzerError> contextMessages = [];
+
   AnalyzerError._(
       {this.severity,
       this.errorCode,
@@ -558,57 +545,53 @@
 class AnalysisCommandOutput extends CommandOutput with _StaticErrorOutput {
   static void parseErrors(String stderr, List<StaticError> errors,
       [List<StaticError> warnings]) {
-    var jsonData = json.decode(stderr) as Map<String, dynamic>;
-    var version = jsonData['version'];
-    if (version != 1) {
-      DebugLogger.error('Unexpected analyzer JSON data version: $version');
-      throw UnimplementedError();
-    }
-    for (var diagnostic in jsonData['diagnostics'] as List<dynamic>) {
-      var diagnosticMap = diagnostic as Map<String, dynamic>;
-      var location = diagnosticMap['location'] as Map<String, dynamic>;
-      var file = location['file'] as String;
-      var type = diagnosticMap['type'] as String;
-      var code = (diagnosticMap['code'] as String).toUpperCase();
-      var staticError =
-          _decodeStaticError(ErrorSource.analyzer, '$type.$code', location);
-      var contextMessages = diagnosticMap['contextMessages'] as List<dynamic>;
-      for (var contextMessage in contextMessages ?? const []) {
-        var contextMessageMap = contextMessage as Map<String, dynamic>;
-        var contextLocation =
-            contextMessageMap['location'] as Map<String, dynamic>;
-        if (contextLocation['file'] == file) {
-          staticError.contextMessages.add(_decodeStaticError(
-              ErrorSource.context,
-              contextMessageMap['message'] as String,
-              contextLocation));
-        } else {
+    StaticError convert(AnalyzerError error) {
+      var staticError = StaticError(ErrorSource.analyzer, error.errorCode,
+          line: error.line, column: error.column, length: error.length);
+
+      for (var context in error.contextMessages) {
+        // TODO(rnystrom): Include these when static error tests get support
+        // for errors/context in other files.
+        if (context.file != error.file) {
           DebugLogger.warning(
               "Context messages in other files not currently supported.");
+          continue;
         }
+
+        staticError.contextMessages.add(StaticError(
+            ErrorSource.context, context.message,
+            line: context.line,
+            column: context.column,
+            length: context.length));
       }
 
-      var severity = diagnosticMap['severity'] as String;
-      if (severity == 'ERROR') {
-        errors.add(staticError);
-      } else if (severity == 'WARNING') {
-        warnings?.add(staticError);
+      return staticError;
+    }
+
+    // Parse as Analyzer errors and then convert them to the StaticError objects
+    // the static error tests expect.
+    for (var diagnostic in AnalyzerError.parseStderr(stderr)) {
+      if (diagnostic.severity == 'ERROR') {
+        errors.add(convert(diagnostic));
+      } else if (diagnostic.severity == 'WARNING' && warnings != null) {
+        warnings.add(convert(diagnostic));
       }
     }
   }
 
-  static StaticError _decodeStaticError(
-      ErrorSource errorSource, String message, Map<String, dynamic> location) {
-    var locationRange = location['range'] as Map<String, dynamic>;
-    var locationRangeStart = locationRange['start'] as Map<String, dynamic>;
-    var locationRangeEnd = locationRange['end'] as Map<String, dynamic>;
-    return StaticError(errorSource, message,
-        line: locationRangeStart['line'] as int,
-        column: locationRangeStart['column'] as int,
-        length: (locationRangeEnd['offset'] as int) -
-            (locationRangeStart['offset'] as int));
+  /// If the stderr of analyzer could not be parsed as valid JSON, this will
+  /// be the stderr as a string instead. Otherwise it will be null.
+  String get invalidJsonStderr {
+    if (!_parsedErrors) {
+      _parseErrors();
+      _parsedErrors = true;
+    }
+
+    return _invalidJsonStderr;
   }
 
+  String _invalidJsonStderr;
+
   AnalysisCommandOutput(
       Command command,
       int exitCode,
@@ -631,22 +614,7 @@
     if (hasNonUtf8) return Expectation.nonUtf8Error;
     if (truncatedOutput) return Expectation.truncatedOutput;
 
-    List<StaticError> errors;
-    try {
-      errors = this.errors;
-    } catch (_) {
-      // This can happen in at least two scenarios:
-      // - The analyzer output was too long so it got truncated (so the
-      //   resulting output was ill-formed).  See also
-      //   https://github.com/dart-lang/sdk/issues/44493.
-      // - The analyzer did not find a file to analyze at the specified
-      //   location, so it generated the output "No dart files found at
-      //   <path>.dart" (which is not valid JSON).  See
-      //   https://github.com/dart-lang/sdk/issues/45556.
-      // TODO(paulberry,rnystrom): remove this logic once the two above bugs are
-      // fixed.
-      return Expectation.crash;
-    }
+    if (invalidJsonStderr != null) return Expectation.fail;
 
     // If it's a static error test, validate the exact errors.
     if (testCase.testFile.isStaticErrorTest) {
@@ -693,22 +661,7 @@
     if (hasNonUtf8) return Expectation.nonUtf8Error;
     if (truncatedOutput) return Expectation.truncatedOutput;
 
-    List<StaticError> errors;
-    try {
-      errors = this.errors;
-    } catch (_) {
-      // This can happen in at least two scenarios:
-      // - The analyzer output was too long so it got truncated (so the
-      //   resulting output was ill-formed).  See also
-      //   https://github.com/dart-lang/sdk/issues/44493.
-      // - The analyzer did not find a file to analyze at the specified
-      //   location, so it generated the output "No dart files found at
-      //   <path>.dart" (which is not valid JSON).  See
-      //   https://github.com/dart-lang/sdk/issues/45556.
-      // TODO(paulberry,rnystrom): remove this logic once the two above bugs are
-      // fixed.
-      return Expectation.crash;
-    }
+    if (invalidJsonStderr != null) return Expectation.fail;
 
     // If it's a static error test, validate the exact errors.
     if (testCase.testFile.isStaticErrorTest) {
@@ -726,16 +679,19 @@
 
   @override
   void describe(TestCase testCase, Progress progress, OutputWriter output) {
+    if (invalidJsonStderr != null) {
+      output.subsection("invalid JSON on analyzer stderr");
+      output.write(invalidJsonStderr);
+      return;
+    }
+
     // Handle static error test output specially. We don't want to show the raw
     // stdout if we can give the user the parsed expectations instead.
     if (testCase.testFile.isStaticErrorTest || hasCrashed || hasTimedOut) {
       super.describe(testCase, progress, output);
     } else {
-      output.subsection("unexpected analysis errors");
-
-      var errorsByFile = <String, List<AnalyzerError>>{};
-
       // Parse and sort the errors.
+      var errorsByFile = <String, List<AnalyzerError>>{};
       for (var error in AnalyzerError.parseStderr(decodeUtf8(stderr))) {
         errorsByFile.putIfAbsent(error.file, () => []).add(error);
       }
@@ -744,8 +700,10 @@
       files.sort();
 
       for (var file in files) {
-        var path = Path(file).relativeTo(Repository.dir).toString();
-        output.write("In $path:");
+        var path = Path(file)
+            .relativeTo(testCase.testFile.path.directoryPath)
+            .toString();
+        output.subsection("unexpected analysis errors in $path");
 
         var errors = errorsByFile[file];
         errors.sort();
@@ -762,24 +720,21 @@
     }
   }
 
-  /// Parses the machine-readable output of analyzer, which looks like:
-  ///
-  ///     ERROR|STATIC_TYPE_WARNING|SOME_ERROR_CODE|/path/to/some_test.dart|9|26|1|Error message.
-  ///
-  /// Pipes can be escaped with backslashes:
-  ///
-  ///     FOO|BAR|FOO\|BAR|FOO\\BAZ
-  ///
-  /// Is parsed as:
-  ///
-  ///     FOO BAR FOO|BAR FOO\BAZ
+  /// Parses the JSON output of analyzer.
   @override
   void _parseErrors() {
-    var errors = <StaticError>[];
-    var warnings = <StaticError>[];
-    parseErrors(decodeUtf8(stderr), errors, warnings);
-    errors.forEach(addError);
-    warnings.forEach(addWarning);
+    var stderrString = decodeUtf8(stderr);
+    try {
+      var errors = <StaticError>[];
+      var warnings = <StaticError>[];
+      parseErrors(stderrString, errors, warnings);
+      errors.forEach(addError);
+      warnings.forEach(addWarning);
+    } on FormatException {
+      // It wasn't JSON. This can happen if analyzer instead prints:
+      // "No dart files found at: ..."
+      _invalidJsonStderr = stderrString;
+    }
   }
 }
 
diff --git a/pkg/test_runner/lib/src/compiler_configuration.dart b/pkg/test_runner/lib/src/compiler_configuration.dart
index 95c7dcb..ab31327 100644
--- a/pkg/test_runner/lib/src/compiler_configuration.dart
+++ b/pkg/test_runner/lib/src/compiler_configuration.dart
@@ -1066,27 +1066,10 @@
 
   CommandArtifact computeCompilationArtifact(String tempDir,
       List<String> arguments, Map<String, String> environmentOverrides) {
-    const legacyTestDirectories = {
-      "co19_2",
-      "corelib_2",
-      "ffi_2",
-      "language_2",
-      "lib_2",
-      "service_2",
-      "standalone_2"
-    };
-
-    // If we are running a legacy test with NNBD enabled, tell analyzer to use
-    // a pre-NNBD language version for the test.
-    var testPath = arguments.last;
-    var segments = Path(testPath).relativeTo(Repository.dir).segments();
-    var setLegacyVersion = segments.any(legacyTestDirectories.contains);
-
     var args = [
       ...arguments,
       if (_configuration.useAnalyzerCfe) '--use-cfe',
       if (_configuration.useAnalyzerFastaParser) '--use-fasta-parser',
-      if (setLegacyVersion) '--default-language-version=2.7',
     ];
 
     // Since this is not a real compilation, no artifacts are produced.
diff --git a/runtime/observatory_2/tests/service_2/evaluate_activation_test.dart b/runtime/observatory_2/tests/service_2/evaluate_activation_test.dart
index a1c3a5b..179dcc5 100644
--- a/runtime/observatory_2/tests/service_2/evaluate_activation_test.dart
+++ b/runtime/observatory_2/tests/service_2/evaluate_activation_test.dart
@@ -2,6 +2,8 @@
 // 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.
 
+// @dart = 2.9
+
 import 'package:observatory_2/service_io.dart';
 import 'package:test/test.dart';
 import 'test_helper.dart';
diff --git a/runtime/observatory_2/tests/service_2/service_test_common.dart b/runtime/observatory_2/tests/service_2/service_test_common.dart
index 7775b29..e1dc008 100644
--- a/runtime/observatory_2/tests/service_2/service_test_common.dart
+++ b/runtime/observatory_2/tests/service_2/service_test_common.dart
@@ -2,6 +2,8 @@
 // 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.
 
+// @dart = 2.9
+
 library service_test_common;
 
 import 'dart:async';
diff --git a/runtime/observatory_2/tests/service_2/test_helper.dart b/runtime/observatory_2/tests/service_2/test_helper.dart
index 0a352fe..570e830 100644
--- a/runtime/observatory_2/tests/service_2/test_helper.dart
+++ b/runtime/observatory_2/tests/service_2/test_helper.dart
@@ -2,6 +2,8 @@
 // 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.
 
+// @dart = 2.9
+
 library test_helper;
 
 import 'dart:async';
diff --git a/tests/dartdevc_2/hot_restart_expando_test.dart b/tests/dartdevc_2/hot_restart_expando_test.dart
index 4802a52..12ce127 100644
--- a/tests/dartdevc_2/hot_restart_expando_test.dart
+++ b/tests/dartdevc_2/hot_restart_expando_test.dart
@@ -2,6 +2,8 @@
 // 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.
 
+// @dart = 2.9
+
 import 'dart:_runtime' as dart;
 
 import 'package:expect/expect.dart';
diff --git a/tests/ffi_2/regress_45988_test.dart b/tests/ffi_2/regress_45988_test.dart
index 8677840..bcb6f4e 100644
--- a/tests/ffi_2/regress_45988_test.dart
+++ b/tests/ffi_2/regress_45988_test.dart
@@ -2,6 +2,8 @@
 // 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.
 
+// @dart = 2.9
+
 import 'dart:ffi';
 
 import "package:expect/expect.dart";
diff --git a/tests/ffi_2/regress_46004_test.dart b/tests/ffi_2/regress_46004_test.dart
index 627611f..90d8963 100644
--- a/tests/ffi_2/regress_46004_test.dart
+++ b/tests/ffi_2/regress_46004_test.dart
@@ -2,6 +2,8 @@
 // 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.
 
+// @dart = 2.9
+
 import 'dart:ffi';
 
 import "package:expect/expect.dart";
diff --git a/tests/ffi_2/regress_46085_test.dart b/tests/ffi_2/regress_46085_test.dart
index 0f90373..8ef3154 100644
--- a/tests/ffi_2/regress_46085_test.dart
+++ b/tests/ffi_2/regress_46085_test.dart
@@ -2,6 +2,8 @@
 // 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.
 
+// @dart = 2.9
+
 import "dart:ffi";
 
 class MyStruct extends Struct {
diff --git a/tests/ffi_2/regress_46127_test.dart b/tests/ffi_2/regress_46127_test.dart
index 73d9f61..b3f5b27 100644
--- a/tests/ffi_2/regress_46127_test.dart
+++ b/tests/ffi_2/regress_46127_test.dart
@@ -1,7 +1,9 @@
 // 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.
-//
+
+// @dart = 2.9
+
 // SharedObjects=ffi_test_functions
 
 import 'dart:ffi';
diff --git a/tests/ffi_2/vmspecific_leaf_call_test.dart b/tests/ffi_2/vmspecific_leaf_call_test.dart
index f4c0445..1cb94fb 100644
--- a/tests/ffi_2/vmspecific_leaf_call_test.dart
+++ b/tests/ffi_2/vmspecific_leaf_call_test.dart
@@ -1,7 +1,9 @@
 // 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.
-//
+
+// @dart = 2.9
+
 // SharedObjects=ffi_test_functions
 
 import 'dart:ffi';
diff --git a/tests/language_2/generic_methods/generic_invocation_all_subexpression_types_test.dart b/tests/language_2/generic_methods/generic_invocation_all_subexpression_types_test.dart
index c1e64c5..b395dc9b 100644
--- a/tests/language_2/generic_methods/generic_invocation_all_subexpression_types_test.dart
+++ b/tests/language_2/generic_methods/generic_invocation_all_subexpression_types_test.dart
@@ -2,6 +2,8 @@
 // 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.
 
+// @dart = 2.9
+
 // This test verifies that `EXPR<typeArguments>(arguments)` is properly parsed
 // as a generic invocation, for all types of expressions that may appear as
 // EXPR.  We try to pay extra attention to ambiguous expressions (that is, where
diff --git a/tests/language_2/generic_methods/generic_invocation_all_types_test.dart b/tests/language_2/generic_methods/generic_invocation_all_types_test.dart
index f37efda..1c9fda2 100644
--- a/tests/language_2/generic_methods/generic_invocation_all_types_test.dart
+++ b/tests/language_2/generic_methods/generic_invocation_all_types_test.dart
@@ -2,6 +2,8 @@
 // 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.
 
+// @dart = 2.9
+
 // This test verifies that `f<T, U>(0)` is properly parsed as a generic
 // invocation, for all type syntaxes that may appear as T and U.
 
diff --git a/tests/language_2/generic_methods/non_generic_invocation_all_first_expression_types_test.dart b/tests/language_2/generic_methods/non_generic_invocation_all_first_expression_types_test.dart
index 1b56800..779ebd5 100644
--- a/tests/language_2/generic_methods/non_generic_invocation_all_first_expression_types_test.dart
+++ b/tests/language_2/generic_methods/non_generic_invocation_all_first_expression_types_test.dart
@@ -2,6 +2,8 @@
 // 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.
 
+// @dart = 2.9
+
 // This test verifies that `EXPR<a,b>-x` is properly parsed as a pair of
 // expressions separated by a `,`, for all types of expressions that may appear
 // as EXPR.  We try to pay extra attention to expressions that will become
diff --git a/tests/language_2/generic_methods/non_generic_invocation_all_first_types_test.dart b/tests/language_2/generic_methods/non_generic_invocation_all_first_types_test.dart
index 59a76a8..7d817ee 100644
--- a/tests/language_2/generic_methods/non_generic_invocation_all_first_types_test.dart
+++ b/tests/language_2/generic_methods/non_generic_invocation_all_first_types_test.dart
@@ -2,6 +2,8 @@
 // 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.
 
+// @dart = 2.9
+
 // This test verifies that `f<EXPR,b>(x)` is properly parsed as a pair of
 // expressions separated by a `,`, for all types of expressions that may appear
 // as EXPR that can't be parsed as types.
diff --git a/tests/language_2/generic_methods/non_generic_invocation_all_second_expression_types_test.dart b/tests/language_2/generic_methods/non_generic_invocation_all_second_expression_types_test.dart
index daead6d..53bfeb7 100644
--- a/tests/language_2/generic_methods/non_generic_invocation_all_second_expression_types_test.dart
+++ b/tests/language_2/generic_methods/non_generic_invocation_all_second_expression_types_test.dart
@@ -2,6 +2,8 @@
 // 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.
 
+// @dart = 2.9
+
 // This test verifies that `f<a,b>EXPR` is properly parsed as a pair of
 // expressions separated by a `,`, for all types of expressions that may appear
 // as EXPR.  We try to pay extra attention to expressions that will become
diff --git a/tests/language_2/generic_methods/non_generic_invocation_all_second_types_test.dart b/tests/language_2/generic_methods/non_generic_invocation_all_second_types_test.dart
index 2036af2..c388dda 100644
--- a/tests/language_2/generic_methods/non_generic_invocation_all_second_types_test.dart
+++ b/tests/language_2/generic_methods/non_generic_invocation_all_second_types_test.dart
@@ -2,6 +2,8 @@
 // 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.
 
+// @dart = 2.9
+
 // This test verifies that `f<a,EXPR>(x)` is properly parsed as a pair of
 // expressions separated by a `,`, for all types of expressions that may appear
 // as EXPR that can't be parsed as types.
diff --git a/tests/language_2/regress/regress45890_test.dart b/tests/language_2/regress/regress45890_test.dart
index abf9a84..8dae9eb 100644
--- a/tests/language_2/regress/regress45890_test.dart
+++ b/tests/language_2/regress/regress45890_test.dart
@@ -2,6 +2,8 @@
 // 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.
 
+// @dart = 2.9
+
 import "package:expect/expect.dart";
 
 class C {
diff --git a/tests/language_2/syntax_helper.dart b/tests/language_2/syntax_helper.dart
index bf6f995..f4965b4 100644
--- a/tests/language_2/syntax_helper.dart
+++ b/tests/language_2/syntax_helper.dart
@@ -2,6 +2,8 @@
 // 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.
 
+// @dart = 2.9
+
 import 'dart:convert';
 
 import "package:expect/expect.dart";
diff --git a/tests/language_2/vm/regress_45260_test.dart b/tests/language_2/vm/regress_45260_test.dart
index 17991e3..a75143f 100644
--- a/tests/language_2/vm/regress_45260_test.dart
+++ b/tests/language_2/vm/regress_45260_test.dart
@@ -1,7 +1,9 @@
 // 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.
-//
+
+// @dart = 2.9
+
 // VMOptions=--optimization-counter-threshold=5 --deterministic
 
 import 'package:expect/expect.dart';
diff --git a/tests/language_2/vm/regress_45855_test.dart b/tests/language_2/vm/regress_45855_test.dart
index b99676e..d35675c 100644
--- a/tests/language_2/vm/regress_45855_test.dart
+++ b/tests/language_2/vm/regress_45855_test.dart
@@ -1,7 +1,9 @@
 // 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.
-//
+
+// @dart = 2.9
+
 // VMOptions=--optimization-counter-threshold=100 --deterministic
 //
 // The Dart Project Fuzz Tester (1.89).
diff --git a/tests/lib_2/typed_data/restricted_types_error_test.dart b/tests/lib_2/typed_data/restricted_types_error_test.dart
index 02b5721..51cffb0 100644
--- a/tests/lib_2/typed_data/restricted_types_error_test.dart
+++ b/tests/lib_2/typed_data/restricted_types_error_test.dart
@@ -2,6 +2,8 @@
 // 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.
 
+// @dart = 2.9
+
 import 'dart:typed_data';
 
 abstract class CEByteBuffer extends ByteBuffer {}
diff --git a/tests/standalone_2/dwarf_stack_trace_invisible_functions_test.dart b/tests/standalone_2/dwarf_stack_trace_invisible_functions_test.dart
index a54f3eb..f4907f5 100644
--- a/tests/standalone_2/dwarf_stack_trace_invisible_functions_test.dart
+++ b/tests/standalone_2/dwarf_stack_trace_invisible_functions_test.dart
@@ -2,7 +2,9 @@
 // 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.
 
-/// VMOptions=--dwarf-stack-traces --save-debugging-info=dwarf_invisible_functions.so
+// @dart = 2.9
+
+// VMOptions=--dwarf-stack-traces --save-debugging-info=dwarf_invisible_functions.so
 
 import 'dart:io';
 
diff --git a/tests/standalone_2/io/http_on_unix_socket_test.dart b/tests/standalone_2/io/http_on_unix_socket_test.dart
index e9611dc..c98afb5 100644
--- a/tests/standalone_2/io/http_on_unix_socket_test.dart
+++ b/tests/standalone_2/io/http_on_unix_socket_test.dart
@@ -2,6 +2,8 @@
 // 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.
 
+// @dart = 2.9
+
 import 'dart:async';
 import 'dart:io';
 import 'dart:convert';
diff --git a/tests/web_2/regress/43520_safari_test.dart b/tests/web_2/regress/43520_safari_test.dart
index 2dee2bb..52c059a 100644
--- a/tests/web_2/regress/43520_safari_test.dart
+++ b/tests/web_2/regress/43520_safari_test.dart
@@ -2,6 +2,8 @@
 // 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.
 
+// @dart = 2.9
+
 /// Regression test for dartbug.com/43520.
 ///
 /// Safari has a bug that makes it a syntax error for a function name to overlap
diff --git a/tests/web_2/regress/45943_test.dart b/tests/web_2/regress/45943_test.dart
index 4b40c44a..1cdee39 100644
--- a/tests/web_2/regress/45943_test.dart
+++ b/tests/web_2/regress/45943_test.dart
@@ -2,6 +2,8 @@
 // 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.
 
+// @dart = 2.9
+
 class _InspectorOverlayLayer {
   String selection;
 
diff --git a/tools/VERSION b/tools/VERSION
index 432be1f..f3750fc 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 14
 PATCH 0
-PRERELEASE 172
+PRERELEASE 173
 PRERELEASE_PATCH 0
\ No newline at end of file