Version 2.13.0-182.0.dev

Merge commit '2a966bcf6bc0be5df5340c9c42d96dc8df3f6918' into 'dev'
diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/argument_type_not_assignable_nullability_error.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/argument_type_not_assignable_nullability_error.dart
index e83ebea..1484b55 100644
--- a/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/argument_type_not_assignable_nullability_error.dart
+++ b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/argument_type_not_assignable_nullability_error.dart
@@ -401,3 +401,79 @@
       /*analyzer.notPromoted(propertyNotPromoted(target: member:C26.bad, type: int?))*/ c
           . /*cfe.notPromoted(propertyNotPromoted(target: member:C26.bad, type: int?))*/ bad;
 }
+
+class C27 {
+  int? bad;
+}
+
+indexGet(C27 c, List<int> values) {
+  if (c.bad == null) return;
+  values[
+      /*analyzer.notPromoted(propertyNotPromoted(target: member:C27.bad, type: int?))*/ c
+          . /*cfe.notPromoted(propertyNotPromoted(target: member:C27.bad, type: int?))*/ bad];
+}
+
+class C28 {
+  int? bad;
+}
+
+indexSet(C28 c, List<int> values) {
+  if (c.bad == null) return;
+  values[
+      /*analyzer.notPromoted(propertyNotPromoted(target: member:C28.bad, type: int?))*/ c
+          . /*cfe.notPromoted(propertyNotPromoted(target: member:C28.bad, type: int?))*/ bad] = 0;
+}
+
+class C29 {
+  int? bad;
+}
+
+indexSetCompound(C29 c, List<int> values) {
+  // TODO(paulberry): get this to work with the CFE
+  if (c.bad == null) return;
+  values[
+      /*analyzer.notPromoted(propertyNotPromoted(target: member:C29.bad, type: int?))*/ c
+          .bad] += 1;
+}
+
+class C30 {
+  int? bad;
+}
+
+indexSetIfNull(C30 c, List<int?> values) {
+  // TODO(paulberry): get this to work with the CFE
+  if (c.bad == null) return;
+  values[
+      /*analyzer.notPromoted(propertyNotPromoted(target: member:C30.bad, type: int?))*/ c
+          .bad] ??= 1;
+}
+
+class C31 {
+  int? bad;
+}
+
+indexSetPreIncDec(C31 c, List<int> values) {
+  // TODO(paulberry): get this to work with the CFE
+  if (c.bad == null) return;
+  ++values[
+      /*analyzer.notPromoted(propertyNotPromoted(target: member:C31.bad, type: int?))*/ c
+          .bad];
+  --values[
+      /*analyzer.notPromoted(propertyNotPromoted(target: member:C31.bad, type: int?))*/ c
+          .bad];
+}
+
+class C32 {
+  int? bad;
+}
+
+indexSetPostIncDec(C32 c, List<int> values) {
+  // TODO(paulberry): get this to work with the CFE
+  if (c.bad == null) return;
+  values[
+      /*analyzer.notPromoted(propertyNotPromoted(target: member:C32.bad, type: int?))*/ c
+          .bad]++;
+  values[
+      /*analyzer.notPromoted(propertyNotPromoted(target: member:C32.bad, type: int?))*/ c
+          .bad]--;
+}
diff --git a/pkg/analyzer/lib/src/generated/error_detection_helpers.dart b/pkg/analyzer/lib/src/generated/error_detection_helpers.dart
index 350899c..af745c8 100644
--- a/pkg/analyzer/lib/src/generated/error_detection_helpers.dart
+++ b/pkg/analyzer/lib/src/generated/error_detection_helpers.dart
@@ -55,7 +55,7 @@
   void checkForArgumentTypeNotAssignableForArgument(Expression argument,
       {bool promoteParameterToNullable = false,
       Map<DartType, NonPromotionReason> Function()? whyNotPromoted}) {
-    checkForArgumentTypeNotAssignableForArgument2(
+    _checkForArgumentTypeNotAssignableForArgument2(
       argument: argument,
       parameter: argument.staticParameterElement,
       promoteParameterToNullable: promoteParameterToNullable,
@@ -63,24 +63,6 @@
     );
   }
 
-  void checkForArgumentTypeNotAssignableForArgument2({
-    required Expression argument,
-    required ParameterElement? parameter,
-    required bool promoteParameterToNullable,
-    Map<DartType, NonPromotionReason> Function()? whyNotPromoted,
-  }) {
-    var staticParameterType = parameter?.type;
-    if (promoteParameterToNullable && staticParameterType != null) {
-      staticParameterType =
-          typeSystem.makeNullable(staticParameterType as TypeImpl);
-    }
-    _checkForArgumentTypeNotAssignableWithExpectedTypes(
-        argument,
-        staticParameterType,
-        CompileTimeErrorCode.ARGUMENT_TYPE_NOT_ASSIGNABLE,
-        whyNotPromoted);
-  }
-
   /// Verify that the given constructor field [initializer] has compatible field
   /// and initializer expression types. The [fieldElement] is the static element
   /// from the name in the [ConstructorFieldInitializer].
@@ -205,6 +187,36 @@
     return true;
   }
 
+  void checkIndexExpressionIndex(
+    Expression index, {
+    required ExecutableElement? readElement,
+    required ExecutableElement? writeElement,
+    required Map<DartType, NonPromotionReason> Function()? whyNotPromoted,
+  }) {
+    if (readElement is MethodElement) {
+      var parameters = readElement.parameters;
+      if (parameters.isNotEmpty) {
+        _checkForArgumentTypeNotAssignableForArgument2(
+          argument: index,
+          parameter: parameters[0],
+          promoteParameterToNullable: false,
+          whyNotPromoted: whyNotPromoted,
+        );
+      }
+    }
+
+    if (writeElement is MethodElement) {
+      var parameters = writeElement.parameters;
+      if (parameters.isNotEmpty) {
+        _checkForArgumentTypeNotAssignableForArgument2(
+          argument: index,
+          parameter: parameters[0],
+          promoteParameterToNullable: false,
+        );
+      }
+    }
+  }
+
   /// Computes the appropriate set of context messages to report along with an
   /// error that may have occurred because [expression] was not type promoted.
   ///
@@ -233,6 +245,24 @@
     return null;
   }
 
+  void _checkForArgumentTypeNotAssignableForArgument2({
+    required Expression argument,
+    required ParameterElement? parameter,
+    required bool promoteParameterToNullable,
+    Map<DartType, NonPromotionReason> Function()? whyNotPromoted,
+  }) {
+    var staticParameterType = parameter?.type;
+    if (promoteParameterToNullable && staticParameterType != null) {
+      staticParameterType =
+          typeSystem.makeNullable(staticParameterType as TypeImpl);
+    }
+    _checkForArgumentTypeNotAssignableWithExpectedTypes(
+        argument,
+        staticParameterType,
+        CompileTimeErrorCode.ARGUMENT_TYPE_NOT_ASSIGNABLE,
+        whyNotPromoted);
+  }
+
   /// Verify that the given [expression] can be assigned to its corresponding
   /// parameters.
   ///
diff --git a/pkg/analyzer/lib/src/generated/error_verifier.dart b/pkg/analyzer/lib/src/generated/error_verifier.dart
index 307171d..c2e7559 100644
--- a/pkg/analyzer/lib/src/generated/error_verifier.dart
+++ b/pkg/analyzer/lib/src/generated/error_verifier.dart
@@ -349,13 +349,6 @@
       _checkForDeadNullCoalesce(node.readType as TypeImpl, node.rightHandSide);
     }
     _checkForAssignmentToFinal(lhs);
-    if (lhs is IndexExpression) {
-      _checkIndexExpressionIndex(
-        lhs.index,
-        readElement: node.readElement as ExecutableElement?,
-        writeElement: node.writeElement as ExecutableElement?,
-      );
-    }
     super.visitAssignmentExpression(node);
   }
 
@@ -830,12 +823,6 @@
 
   @override
   void visitIndexExpression(IndexExpression node) {
-    _checkIndexExpressionIndex(
-      node.index,
-      readElement: node.staticElement,
-      writeElement: null,
-    );
-
     if (node.isNullAware) {
       _checkForUnnecessaryNullAware(
         node.realTarget,
@@ -1006,13 +993,6 @@
       _checkForAssignmentToFinal(operand);
       _checkForIntNotAssignable(operand);
     }
-    if (operand is IndexExpression) {
-      _checkIndexExpressionIndex(
-        operand.index,
-        readElement: node.readElement as ExecutableElement?,
-        writeElement: node.writeElement as ExecutableElement?,
-      );
-    }
     super.visitPostfixExpression(node);
   }
 
@@ -1038,13 +1018,6 @@
       checkForUseOfVoidResult(operand);
       _checkForIntNotAssignable(operand);
     }
-    if (operand is IndexExpression) {
-      _checkIndexExpressionIndex(
-        operand.index,
-        readElement: node.readElement as ExecutableElement?,
-        writeElement: node.writeElement as ExecutableElement?,
-      );
-    }
     super.visitPrefixExpression(node);
   }
 
@@ -4594,34 +4567,6 @@
     }
   }
 
-  void _checkIndexExpressionIndex(
-    Expression index, {
-    required ExecutableElement? readElement,
-    required ExecutableElement? writeElement,
-  }) {
-    if (readElement is MethodElement) {
-      var parameters = readElement.parameters;
-      if (parameters.isNotEmpty) {
-        checkForArgumentTypeNotAssignableForArgument2(
-          argument: index,
-          parameter: parameters[0],
-          promoteParameterToNullable: false,
-        );
-      }
-    }
-
-    if (writeElement is MethodElement) {
-      var parameters = writeElement.parameters;
-      if (parameters.isNotEmpty) {
-        checkForArgumentTypeNotAssignableForArgument2(
-          argument: index,
-          parameter: parameters[0],
-          promoteParameterToNullable: false,
-        );
-      }
-    }
-  }
-
   void _checkMixinInference(
       NamedCompilationUnitMember node, WithClause? withClause) {
     if (withClause == null) {
diff --git a/pkg/analyzer/lib/src/generated/resolver.dart b/pkg/analyzer/lib/src/generated/resolver.dart
index 5684e0e..211323d 100644
--- a/pkg/analyzer/lib/src/generated/resolver.dart
+++ b/pkg/analyzer/lib/src/generated/resolver.dart
@@ -709,6 +709,13 @@
 
       InferenceContext.setType(node.index, result.indexContextType);
       node.index.accept(this);
+      var whyNotPromoted = flowAnalysis?.flow?.whyNotPromoted(node.index);
+      checkIndexExpressionIndex(
+        node.index,
+        readElement: result.readElement as ExecutableElement?,
+        writeElement: result.writeElement as ExecutableElement?,
+        whyNotPromoted: whyNotPromoted,
+      );
 
       return result;
     } else if (node is PrefixedIdentifier) {
@@ -1702,6 +1709,13 @@
 
     InferenceContext.setType(node.index, result.indexContextType);
     node.index.accept(this);
+    var whyNotPromoted = flowAnalysis?.flow?.whyNotPromoted(node.index);
+    checkIndexExpressionIndex(
+      node.index,
+      readElement: result.readElement as ExecutableElement?,
+      writeElement: null,
+      whyNotPromoted: whyNotPromoted,
+    );
 
     DartType type;
     if (identical(node.realTarget.staticType, NeverTypeImpl.instance)) {
diff --git a/pkg/analyzer/lib/src/test_utilities/mock_sdk.dart b/pkg/analyzer/lib/src/test_utilities/mock_sdk.dart
index a9f8b0f..8c3a9a5 100644
--- a/pkg/analyzer/lib/src/test_utilities/mock_sdk.dart
+++ b/pkg/analyzer/lib/src/test_utilities/mock_sdk.dart
@@ -459,7 +459,7 @@
   int get length => 0;
   Iterable<V> get values;
 
-  V? operator [](K key);
+  V? operator [](Object? key);
   void operator []=(K key, V value);
 
   Map<RK, RV> cast<RK, RV>();
diff --git a/pkg/analyzer/test/src/diagnostics/argument_type_not_assignable_test.dart b/pkg/analyzer/test/src/diagnostics/argument_type_not_assignable_test.dart
index 1225bc4..096fcdc 100644
--- a/pkg/analyzer/test/src/diagnostics/argument_type_not_assignable_test.dart
+++ b/pkg/analyzer/test/src/diagnostics/argument_type_not_assignable_test.dart
@@ -454,6 +454,40 @@
     ]);
   }
 
+  test_map_indexGet() async {
+    // Any type may be passed to Map.operator[].
+    await assertNoErrorsInCode('''
+main() {
+  Map<int, int> m = <int, int>{};
+  m['x'];
+}
+''');
+  }
+
+  test_map_indexSet() async {
+    // The type passed to Map.operator[]= must match the key type.
+    await assertErrorsInCode('''
+main() {
+  Map<int, int> m = <int, int>{};
+  m['x'] = 0;
+}
+''', [
+      error(CompileTimeErrorCode.ARGUMENT_TYPE_NOT_ASSIGNABLE, 47, 3),
+    ]);
+  }
+
+  test_map_indexSet_ifNull() async {
+    // The type passed to Map.operator[]= must match the key type.
+    await assertErrorsInCode('''
+main() {
+  Map<int, int> m = <int, int>{};
+  m['x'] ??= 0;
+}
+''', [
+      error(CompileTimeErrorCode.ARGUMENT_TYPE_NOT_ASSIGNABLE, 47, 3),
+    ]);
+  }
+
   test_new_generic() async {
     await assertErrorsInCode('''
 class A<T> {
diff --git a/pkg/compiler/lib/src/js_emitter/code_emitter_task.dart b/pkg/compiler/lib/src/js_emitter/code_emitter_task.dart
index 3b14775..d52a591 100644
--- a/pkg/compiler/lib/src/js_emitter/code_emitter_task.dart
+++ b/pkg/compiler/lib/src/js_emitter/code_emitter_task.dart
@@ -213,6 +213,9 @@
 
   List<PreFragment> get preDeferredFragmentsForTesting;
 
+  /// The set of omitted [OutputUnits].
+  Set<OutputUnit> get omittedOutputUnits;
+
   /// A map of loadId to list of [FinalizedFragments].
   Map<String, List<FinalizedFragment>> get finalizedFragmentsToLoad;
 
diff --git a/pkg/compiler/lib/src/js_emitter/startup_emitter/emitter.dart b/pkg/compiler/lib/src/js_emitter/startup_emitter/emitter.dart
index 17184dd..01c863f 100644
--- a/pkg/compiler/lib/src/js_emitter/startup_emitter/emitter.dart
+++ b/pkg/compiler/lib/src/js_emitter/startup_emitter/emitter.dart
@@ -160,6 +160,9 @@
   List<PreFragment> preDeferredFragmentsForTesting;
 
   @override
+  Set<OutputUnit> omittedOutputUnits;
+
+  @override
   Map<String, List<FinalizedFragment>> finalizedFragmentsToLoad;
 
   @override
@@ -206,6 +209,7 @@
     }
     return _task.measureSubtask('emit program', () {
       var size = _emitter.emitProgram(program, codegenWorld);
+      omittedOutputUnits = _emitter.omittedOutputUnits;
       finalizedFragmentsToLoad = _emitter.finalizedFragmentsToLoad;
       fragmentMerger = _emitter.fragmentMerger;
       finalizedFragmentsToLoad.values.forEach((fragments) {
diff --git a/pkg/compiler/test/deferred_loading/deferred_loading_test.dart b/pkg/compiler/test/deferred_loading/deferred_loading_test.dart
index 374d091..dcd98df 100644
--- a/pkg/compiler/test/deferred_loading/deferred_loading_test.dart
+++ b/pkg/compiler/test/deferred_loading/deferred_loading_test.dart
@@ -156,9 +156,12 @@
         .backendStrategy.emitterTask.emitter.preDeferredFragmentsForTesting;
     Map<String, List<FinalizedFragment>> fragmentsToLoad =
         compiler.backendStrategy.emitterTask.emitter.finalizedFragmentsToLoad;
+    Set<OutputUnit> omittedOutputUnits =
+        compiler.backendStrategy.emitterTask.emitter.omittedOutputUnits;
     Map<String, List<PreFragment>> preFragmentMap =
         buildPreFragmentMap(fragmentsToLoad, preDeferredFragments);
-    PreFragmentsIrComputer(compiler.reporter, actualMap, preFragmentMap)
+    PreFragmentsIrComputer(
+            compiler.reporter, actualMap, preFragmentMap, omittedOutputUnits)
         .computeForLibrary(node);
   }
 
@@ -169,9 +172,13 @@
 
 class PreFragmentsIrComputer extends IrDataExtractor<Features> {
   final Map<String, List<PreFragment>> _preFragmentMap;
+  final Set<OutputUnit> _omittedOutputUnits;
 
-  PreFragmentsIrComputer(DiagnosticReporter reporter,
-      Map<Id, ActualData<Features>> actualMap, this._preFragmentMap)
+  PreFragmentsIrComputer(
+      DiagnosticReporter reporter,
+      Map<Id, ActualData<Features>> actualMap,
+      this._preFragmentMap,
+      this._omittedOutputUnits)
       : super(reporter, actualMap);
 
   @override
@@ -214,11 +221,16 @@
       }
 
       for (var emittedOutputUnit in preFragment.emittedOutputUnits) {
-        supplied.add(emittedOutputUnit.outputUnit);
+        var outputUnit = emittedOutputUnit.outputUnit;
+        if (!_omittedOutputUnits.contains(outputUnit)) {
+          supplied.add(outputUnit);
+        }
       }
-      var suppliedString = '[${supplied.map(outputUnitString).join(', ')}]';
-      features.addElement(Tags.outputUnits,
-          'f$i: {units: $suppliedString, usedBy: $usedBy, needs: $needs}');
+      if (supplied.isNotEmpty) {
+        var suppliedString = '[${supplied.map(outputUnitString).join(', ')}]';
+        features.addElement(Tags.outputUnits,
+            'f$i: {units: $suppliedString, usedBy: $usedBy, needs: $needs}');
+      }
     }
 
     return features;
diff --git a/pkg/test_runner/lib/src/command_output.dart b/pkg/test_runner/lib/src/command_output.dart
index 0e85567..0f9738e 100644
--- a/pkg/test_runner/lib/src/command_output.dart
+++ b/pkg/test_runner/lib/src/command_output.dart
@@ -984,8 +984,9 @@
   ///
   /// The test runner only validates the main error message, and not the
   /// suggested fixes, so we only parse the first line.
+  // TODO(rnystrom): Support validating context messages.
   static final _errorRegexp =
-      RegExp(r"^([^:]+):(\d+):(\d+):\nError: (.*)$", multiLine: true);
+      RegExp(r"^([^:]+):(\d+):(\d+):\n(Error): (.*)$", multiLine: true);
 
   Dart2jsCompilerCommandOutput(
       Command command,
@@ -1017,8 +1018,9 @@
   ///
   /// The test runner only validates the main error message, and not the
   /// suggested fixes, so we only parse the first line.
+  // TODO(rnystrom): Support validating context messages.
   static final _errorRegexp = RegExp(
-      r"^org-dartlang-app:/([^:]+):(\d+):(\d+): Error: (.*)$",
+      r"^org-dartlang-app:/([^:]+):(\d+):(\d+): (Error): (.*)$",
       multiLine: true);
 
   DevCompilerCommandOutput(
@@ -1252,36 +1254,22 @@
   static void parseErrors(
       String stdout, List<StaticError> errors, List<StaticError> warnings) {
     _StaticErrorOutput._parseCfeErrors(
-        ErrorSource.cfe, _errorRegexp, stdout, errors);
-    _StaticErrorOutput._parseCfeErrors(
-        ErrorSource.cfe, _warningRegexp, stdout, warnings);
+        ErrorSource.cfe, _errorRegexp, stdout, errors, warnings);
   }
 
-  /// Matches the first line of a Fasta error message. Fasta prints errors to
-  /// stdout that look like:
+  /// Matches the first line of a Fasta error, warning, or context message.
+  /// Fasta prints to stdout like:
   ///
   ///     tests/language_2/some_test.dart:7:21: Error: Some message.
   ///     Try fixing the code to be less bad.
   ///       var _ = <int>[if (1) 2];
   ///                    ^
   ///
-  /// The test runner only validates the main error message, and not the
-  /// suggested fixes, so we only parse the first line.
-  static final _errorRegexp =
-      RegExp(r"^([^:]+):(\d+):(\d+): Error: (.*)$", multiLine: true);
-
-  /// Matches the first line of a Fasta warning message. Fasta prints errors to
-  /// stdout that look like:
-  ///
-  ///     tests/language_2/some_test.dart:7:21: Warning: Some message.
-  ///     Try fixing the code to be less bad.
-  ///       var _ = <int>[if (1) 2];
-  ///                    ^
-  ///
-  /// The test runner only validates the main error message, and not the
-  /// suggested fixes, so we only parse the first line.
-  static final _warningRegexp =
-      RegExp(r"^([^:]+):(\d+):(\d+): Warning: (.*)$", multiLine: true);
+  /// The test runner only validates the first line of the message, and not the
+  /// suggested fixes.
+  static final _errorRegexp = RegExp(
+      r"^([^:]+):(\d+):(\d+): (Context|Error|Warning): (.*)$",
+      multiLine: true);
 
   FastaCommandOutput(
       Command command,
@@ -1310,12 +1298,35 @@
   /// Parses compile errors reported by CFE using the given [regExp] and adds
   /// them to [errors] as coming from [errorSource].
   static void _parseCfeErrors(ErrorSource errorSource, RegExp regExp,
-      String stdout, List<StaticError> errors) {
+      String stdout, List<StaticError> errors,
+      [List<StaticError> warnings]) {
+    StaticError previousError;
     for (var match in regExp.allMatches(stdout)) {
       var line = int.parse(match.group(2));
       var column = int.parse(match.group(3));
-      var message = match.group(4);
-      errors.add(StaticError(errorSource, message, line: line, column: column));
+      var severity = match.group(4);
+      var message = match.group(5);
+
+      var error = StaticError(
+          severity == "Context" ? ErrorSource.context : errorSource, message,
+          line: line, column: column);
+
+      if (severity == "Context") {
+        // Attach context messages to the preceding error/warning.
+        if (previousError == null) {
+          DebugLogger.error("Got context message in CFE output before "
+              "error to attach it to.");
+        } else {
+          previousError.contextMessages.add(error);
+        }
+      } else {
+        if (severity == "Error") {
+          errors.add(error);
+        } else {
+          warnings.add(error);
+        }
+        previousError = error;
+      }
     }
   }
 
diff --git a/pkg/test_runner/lib/src/static_error.dart b/pkg/test_runner/lib/src/static_error.dart
index d8ede3a..a24aad6 100644
--- a/pkg/test_runner/lib/src/static_error.dart
+++ b/pkg/test_runner/lib/src/static_error.dart
@@ -11,6 +11,9 @@
   static const cfe = ErrorSource._("CFE");
   static const web = ErrorSource._("web");
 
+  /// Pseudo-front end for context messages.
+  static const context = ErrorSource._("context");
+
   /// All of the supported front ends.
   ///
   /// The order is significant here. In static error tests, error expectations
@@ -24,6 +27,8 @@
       if (source.marker == name) return source;
     }
 
+    if (name == "context") return context;
+
     return null;
   }
 
@@ -113,6 +118,9 @@
   ///
   /// *   Otherwise, any remaining expected errors are considered missing
   ///     errors and remaining actual errors are considered unexpected.
+  ///
+  /// Also describes any mismatches between the context messages in the expected
+  /// and actual errors.
   static String validateExpectations(Iterable<StaticError> expectedErrors,
       Iterable<StaticError> actualErrors) {
     var expected = expectedErrors.toList();
@@ -122,16 +130,20 @@
     expected.sort();
     actual.sort();
 
-    // Discard matched errors.
+    var buffer = StringBuffer();
+
+    // Pair up errors by location and message.
     for (var i = 0; i < expected.length; i++) {
       var matchedExpected = false;
 
       for (var j = 0; j < actual.length; j++) {
         if (actual[j] == null) continue;
 
-        if (expected[i]._matchLocation(actual[j]) == null &&
-            (expected[i].message == actual[j].message ||
-                !expected[i].isSpecified)) {
+        if (expected[i]._matchMessage(actual[j]) &&
+            expected[i]._matchLocation(actual[j])) {
+          // Report any mismatches in the context messages.
+          expected[i]._validateContext(actual[j], buffer);
+
           actual[j] = null;
           matchedExpected = true;
 
@@ -147,12 +159,13 @@
     expected.removeWhere((error) => error == null);
     actual.removeWhere((error) => error == null);
 
-    // If everything matched, we're done.
-    if (expected.isEmpty && actual.isEmpty) return null;
+    // If every error was paired up, and the contexts matched, we're done.
+    if (expected.isEmpty && actual.isEmpty && buffer.isEmpty) return null;
 
-    var buffer = StringBuffer();
+    void fail(StaticError error, String label, String contextLabel,
+        [String secondary]) {
+      if (error.isContext) label = contextLabel;
 
-    void fail(StaticError error, String label, [String secondary]) {
       if (error.isSpecified) {
         buffer.writeln("- $label ${error.location}: ${error.message}");
       } else {
@@ -172,8 +185,10 @@
         if (actual[j] == null) continue;
 
         if (expected[i].message == actual[j].message) {
-          fail(expected[i], "Wrong error location",
-              expected[i]._matchLocation(actual[j]));
+          fail(expected[i], "Wrong error location", "Wrong context location",
+              expected[i]._locationError(actual[j]));
+          // Report any mismatches in the context messages.
+          expected[i]._validateContext(actual[j], buffer);
 
           // Only report this mismatch once.
           expected[i] = null;
@@ -189,9 +204,11 @@
       for (var j = 0; j < actual.length; j++) {
         if (actual[j] == null) continue;
 
-        if (expected[i]._matchLocation(actual[j]) == null) {
-          fail(actual[j], "Wrong message at",
+        if (expected[i]._matchLocation(actual[j])) {
+          fail(actual[j], "Wrong message at", "Wrong context message at",
               "Expected: ${expected[i].message}");
+          // Report any mismatches in the context messages.
+          expected[i]._validateContext(actual[j], buffer);
 
           // Only report this mismatch once.
           expected[i] = null;
@@ -204,13 +221,14 @@
     // Any remaining expected errors are missing.
     for (var i = 0; i < expected.length; i++) {
       if (expected[i] == null) continue;
-      fail(expected[i], "Missing expected error at");
+      fail(expected[i], "Missing expected error at",
+          "Missing expected context message at");
     }
 
     // Any remaining actual errors are unexpected.
     for (var j = 0; j < actual.length; j++) {
       if (actual[j] == null) continue;
-      fail(actual[j], "Unexpected error at");
+      fail(actual[j], "Unexpected error at", "Unexpected context message at");
     }
 
     return buffer.toString().trimRight();
@@ -232,6 +250,9 @@
 
   final String message;
 
+  /// Additional context messages associated with this error.
+  final List<StaticError> contextMessages = [];
+
   /// The zero-based numbers of the lines in the [TestFile] containing comments
   /// that were parsed to produce this error.
   ///
@@ -271,6 +292,9 @@
   /// met.
   bool get isSpecified => message != _unspecified;
 
+  /// Whether this is a context message instead of an error.
+  bool get isContext => source == ErrorSource.context;
+
   /// Whether this error is only considered a warning on all front ends that
   /// report it.
   bool get isWarning {
@@ -290,7 +314,21 @@
     throw FallThroughError();
   }
 
-  String toString() => "[${source.marker} $location] $message";
+  String toString() {
+    var buffer = StringBuffer("StaticError(");
+    buffer.write("line: $line, column: $column");
+    if (length != null) buffer.write(", length: $length");
+    buffer.write(", message: '$message'");
+
+    if (contextMessages.isNotEmpty) {
+      buffer.write(", context: [ ");
+      buffer.writeAll(contextMessages, ", ");
+      buffer.write(" ]");
+    }
+
+    buffer.write(")");
+    return buffer.toString();
+  }
 
   /// Orders errors primarily by location, then by other fields if needed.
   @override
@@ -311,7 +349,20 @@
   }
 
   @override
-  bool operator ==(other) => other is StaticError && compareTo(other) == 0;
+  bool operator ==(other) {
+    if (other is StaticError) {
+      if (compareTo(other) != 0) return false;
+
+      if (contextMessages.length != other.contextMessages.length) return false;
+      for (var i = 0; i < contextMessages.length; i++) {
+        if (contextMessages[i] != other.contextMessages[i]) return false;
+      }
+
+      return true;
+    }
+
+    return false;
+  }
 
   @override
   int get hashCode =>
@@ -321,11 +372,31 @@
       11 * source.hashCode +
       13 * message.hashCode;
 
-  /// Returns a string describing how this error's expected location differs
-  /// from [actual], or `null` if [actual]'s location matches this one.
+  /// Returns true if [actual]'s message matches this one.
+  ///
+  /// Takes unspecified errors into account.
+  bool _matchMessage(StaticError actual) {
+    return !isSpecified || message == actual.message;
+  }
+
+  /// Returns true if [actual]'s location matches this one.
   ///
   /// Takes into account unspecified errors and errors without lengths.
-  String _matchLocation(StaticError actual) {
+  bool _matchLocation(StaticError actual) {
+    if (line != actual.line) return false;
+
+    // Ignore column and length for unspecified errors.
+    if (isSpecified) {
+      if (column != actual.column) return false;
+      if (actual.length != null && length != actual.length) return false;
+    }
+
+    return true;
+  }
+
+  /// Returns a string describing how this error's expected location differs
+  /// from [actual].
+  String _locationError(StaticError actual) {
     var expectedMismatches = <String>[];
     var actualMismatches = <String>[];
 
@@ -347,15 +418,28 @@
       }
     }
 
-    if (expectedMismatches.isEmpty) {
-      // Everything matches.
-      return null;
-    }
+    // Should only call this when the locations don't match.
+    assert(expectedMismatches.isNotEmpty);
 
     var expectedList = expectedMismatches.join(", ");
     var actualList = actualMismatches.join(", ");
     return "Expected $expectedList but was $actualList.";
   }
+
+  /// Validates that this expected error's context messages match [actual]'s.
+  ///
+  /// Writes any mismatch errors to [buffer].
+  void _validateContext(StaticError actual, StringBuffer buffer) {
+    // If the expected error has no context, then ignore actual context
+    // messages.
+    if (contextMessages.isEmpty) return;
+
+    var result = validateExpectations(contextMessages, actual.contextMessages);
+    if (result != null) {
+      buffer.writeln(result);
+      buffer.writeln();
+    }
+  }
 }
 
 class _ErrorExpectationParser {
@@ -382,7 +466,10 @@
       RegExp(r"^\s*//\s*\[\s*error line\s+(\d+)\s*,\s*column\s+(\d+)\s*\]\s*$");
 
   /// Matches the beginning of an error message, like `// [analyzer]`.
-  static final _errorMessageRegExp = RegExp(r"^\s*// \[(\w+)\]\s*(.*)");
+  ///
+  /// May have an optional number like `// [cfe 32]`.
+  static final _errorMessageRegExp =
+      RegExp(r"^\s*// \[(\w+)(\s+\d+)?\]\s*(.*)");
 
   /// An analyzer error code is a dotted identifier or the magic string
   /// "unspecified".
@@ -394,6 +481,17 @@
 
   final List<String> _lines;
   final List<StaticError> _errors = [];
+
+  /// The parsed context messages.
+  ///
+  /// Once parsing is done, these are added to the errors that own them.
+  final List<StaticError> _contextMessages = [];
+
+  /// For errors that have a number associated with them, tracks that number.
+  ///
+  /// These are used after parsing to attach context messages to their errors.
+  final Map<StaticError, int> _errorNumbers = {};
+
   int _currentLine = 0;
 
   // One-based index of the last line that wasn't part of an error expectation.
@@ -402,6 +500,7 @@
   _ErrorExpectationParser(String source) : _lines = source.split("\n");
 
   List<StaticError> _parse() {
+    // Read all the lines.
     while (_canPeek(0)) {
       var sourceLine = _peek(0);
 
@@ -441,6 +540,7 @@
       _advance();
     }
 
+    _attachContext();
     return _errors;
   }
 
@@ -454,11 +554,16 @@
       var match = _errorMessageRegExp.firstMatch(_peek(1));
       if (match == null) break;
 
+      var number = match.group(2) != null ? int.parse(match.group(2)) : null;
+
       var sourceName = match.group(1);
       var source = ErrorSource.find(sourceName);
       if (source == null) _fail("Unknown front end '[$sourceName]'.");
+      if (source == ErrorSource.context && number == null) {
+        _fail("Context messages must have an error number.");
+      }
 
-      var message = match.group(2);
+      var message = match.group(3);
       _advance();
       var sourceLines = {locationLine, _currentLine};
 
@@ -498,11 +603,32 @@
         errorLength = null;
       }
 
-      _errors.add(StaticError(source, message,
+      var error = StaticError(source, message,
           line: line,
           column: column,
           length: errorLength,
-          sourceLines: sourceLines));
+          sourceLines: sourceLines);
+
+      if (number != null) {
+        // Make sure two errors don't claim the same number.
+        if (source != ErrorSource.context) {
+          var existingError = _errors.firstWhere(
+              (error) => _errorNumbers[error] == number,
+              orElse: () => null);
+          if (existingError != null) {
+            _fail("Already have an error with number $number.");
+          }
+        }
+
+        _errorNumbers[error] = number;
+      }
+
+      if (source == ErrorSource.context) {
+        _contextMessages.add(error);
+      } else {
+        _errors.add(error);
+      }
+
       parsedError = true;
     }
 
@@ -511,6 +637,38 @@
     }
   }
 
+  /// Attach context messages to their errors and validate that everything lines
+  /// up.
+  void _attachContext() {
+    for (var contextMessage in _contextMessages) {
+      var number = _errorNumbers[contextMessage];
+
+      var error = _errors.firstWhere((error) => _errorNumbers[error] == number,
+          orElse: () => null);
+      if (error == null) {
+        throw FormatException("No error with number $number for context "
+            "message '${contextMessage.message}'.");
+      }
+
+      error.contextMessages.add(contextMessage);
+    }
+
+    // Make sure every numbered error does have some context, otherwise the
+    // number is pointless.
+    for (var error in _errors) {
+      var number = _errorNumbers[error];
+      if (number == null) continue;
+
+      var context = _contextMessages.firstWhere(
+          (context) => _errorNumbers[context] == number,
+          orElse: () => null);
+      if (context == null) {
+        throw FormatException("Missing context for numbered error $number "
+            "'${error.message}'.");
+      }
+    }
+  }
+
   bool _canPeek(int offset) => _currentLine + offset < _lines.length;
 
   void _advance() {
diff --git a/pkg/test_runner/lib/src/testing_servers.dart b/pkg/test_runner/lib/src/testing_servers.dart
index 5e9df68..bbd5cfd 100644
--- a/pkg/test_runner/lib/src/testing_servers.dart
+++ b/pkg/test_runner/lib/src/testing_servers.dart
@@ -343,7 +343,7 @@
       ]) {
         response.headers.set(header, content_header_value);
       }
-      if (const ["safari"].contains(runtime)) {
+      if (runtime == Runtime.safari) {
         response.headers.set("X-WebKit-CSP", content_header_value);
       }
     }
diff --git a/pkg/test_runner/lib/src/update_errors.dart b/pkg/test_runner/lib/src/update_errors.dart
index 5003a52..0023b02 100644
--- a/pkg/test_runner/lib/src/update_errors.dart
+++ b/pkg/test_runner/lib/src/update_errors.dart
@@ -13,9 +13,10 @@
 /// for the given [errors].
 ///
 /// If [remove] is not `null`, then only removes existing errors for the given
-/// sources.
+/// sources. If [includeContext] is `true`, then includes context messages in
+/// the output. Otherwise discards them.
 String updateErrorExpectations(String source, List<StaticError> errors,
-    {Set<ErrorSource> remove}) {
+    {Set<ErrorSource> remove, bool includeContext = false}) {
   remove ??= {};
 
   // Split the existing errors into kept and deleted lists.
@@ -39,15 +40,20 @@
   // Remove all existing marker comments in the file, even for errors we are
   // preserving. We will regenerate marker comments for those errors too so
   // they can properly share location comments with new errors if needed.
+  void removeLine(int line) {
+    if (lines[line] == null) return;
+
+    indentation[line] = _countIndentation(lines[line]);
+
+    // Null the line instead of removing it so that line numbers in the
+    // reported errors are still correct.
+    lines[line] = null;
+  }
+
   for (var error in existingErrors) {
-    for (var line in error.sourceLines) {
-      if (lines[line] == null) continue;
-
-      indentation[line] = _countIndentation(lines[line]);
-
-      // Null the line instead of removing it so that line numbers in the
-      // reported errors are still correct.
-      lines[line] = null;
+    error.sourceLines.forEach(removeLine);
+    for (var contextMessage in error.contextMessages) {
+      contextMessage.sourceLines.forEach(removeLine);
     }
   }
 
@@ -59,6 +65,14 @@
   for (var error in errors) {
     // -1 to translate from one-based to zero-based index.
     errorMap.putIfAbsent(error.line - 1, () => []).add(error);
+
+    // Flatten out and include context messages.
+    if (includeContext) {
+      for (var context in error.contextMessages) {
+        // -1 to translate from one-based to zero-based index.
+        errorMap.putIfAbsent(context.line - 1, () => []).add(context);
+      }
+    }
   }
 
   // If there are multiple errors on the same line, order them
@@ -67,6 +81,8 @@
     errorList.sort();
   }
 
+  var errorNumbers = _numberErrors(errors);
+
   // Rebuild the source file a line at a time.
   var previousIndent = 0;
   var codeLine = 1;
@@ -131,7 +147,12 @@
       previousLength = error.length;
 
       var errorLines = error.message.split("\n");
-      result.add("$comment [${error.source.marker}] ${errorLines[0]}");
+      var line = "$comment [${error.source.marker}";
+      if (includeContext && errorNumbers.containsKey(error)) {
+        line += " ${errorNumbers[error]}";
+      }
+      line += "] ${errorLines[0]}";
+      result.add(line);
       for (var errorLine in errorLines.skip(1)) {
         result.add("$comment $errorLine");
       }
@@ -150,6 +171,25 @@
   return result.join("\n");
 }
 
+/// Assigns unique numbers to all [errors] that have context messages, as well
+/// as their context messages.
+Map<StaticError, int> _numberErrors(List<StaticError> errors) {
+  var result = <StaticError, int>{};
+  var number = 1;
+  for (var error in errors) {
+    if (error.contextMessages.isEmpty) continue;
+
+    result[error] = number;
+    for (var context in error.contextMessages) {
+      result[context] = number;
+    }
+
+    number++;
+  }
+
+  return result;
+}
+
 /// Returns the number of characters of leading spaces in [line].
 int _countIndentation(String line) {
   var match = _indentationRegExp.firstMatch(line);
diff --git a/pkg/test_runner/test/static_error_test.dart b/pkg/test_runner/test/static_error_test.dart
index 891db234..285e41d 100644
--- a/pkg/test_runner/test/static_error_test.dart
+++ b/pkg/test_runner/test/static_error_test.dart
@@ -12,7 +12,6 @@
   testProperties();
   testIsWarning();
   testCompareTo();
-  // testDescribeDifferences();
   testValidate();
 }
 
@@ -223,6 +222,105 @@
     makeError(line: 1, column: 2, length: 3, webError: "Web 2."),
     makeError(line: 1, column: 3, length: 3, webError: "Web 3."),
   ], null);
+
+  // If expectation has context, actual must match it.
+  expectValidate([
+    makeError(line: 1, column: 2, length: 3, cfeError: "Error.", context: [
+      makeError(line: 4, column: 5, length: 6, contextError: "Context A."),
+      makeError(line: 7, column: 8, length: 9, contextError: "Context B."),
+    ]),
+  ], [
+    makeError(line: 1, column: 2, length: 3, cfeError: "Error.", context: [
+      makeError(line: 4, column: 5, length: 6, contextError: "Context A."),
+      makeError(line: 7, column: 8, length: 9, contextError: "Context B."),
+    ]),
+  ], null);
+
+  // Actual context is different.
+  expectValidate([
+    makeError(line: 1, column: 2, length: 3, cfeError: "Error.", context: [
+      makeError(line: 4, column: 5, length: 6, contextError: "Context A."),
+    ]),
+  ], [
+    makeError(line: 1, column: 2, length: 3, cfeError: "Error.", context: [
+      makeError(line: 4, column: 5, length: 6, contextError: "Context Z."),
+    ]),
+  ], """
+- Wrong context message at line 4, column 5, length 6: Context Z.
+  Expected: Context A.""");
+
+  // Missing some actual context.
+  expectValidate([
+    makeError(line: 1, column: 2, length: 3, cfeError: "Error.", context: [
+      makeError(line: 4, column: 5, length: 6, contextError: "Context A."),
+      makeError(line: 7, column: 8, length: 9, contextError: "Context B."),
+    ]),
+  ], [
+    makeError(line: 1, column: 2, length: 3, cfeError: "Error.", context: [
+      makeError(line: 7, column: 8, length: 9, contextError: "Context B."),
+    ]),
+  ], """
+- Missing expected context message at line 4, column 5, length 6: Context A.""");
+
+  // Missing all actual context.
+  expectValidate([
+    makeError(line: 1, column: 2, length: 3, cfeError: "Error.", context: [
+      makeError(line: 4, column: 5, length: 6, contextError: "Context A."),
+      makeError(line: 7, column: 8, length: 9, contextError: "Context B."),
+    ]),
+  ], [
+    makeError(line: 1, column: 2, length: 3, cfeError: "Error."),
+  ], """
+- Missing expected context message at line 4, column 5, length 6: Context A.
+
+- Missing expected context message at line 7, column 8, length 9: Context B.""");
+
+  // Unexpected extra actual context.
+  expectValidate([
+    makeError(line: 1, column: 2, length: 3, cfeError: "Error.", context: [
+      makeError(line: 4, column: 5, length: 6, contextError: "Context A."),
+    ]),
+  ], [
+    makeError(line: 1, column: 2, length: 3, cfeError: "Error.", context: [
+      makeError(line: 4, column: 5, length: 6, contextError: "Context A."),
+      makeError(line: 7, column: 8, length: 9, contextError: "Context B."),
+    ]),
+  ], """
+- Unexpected context message at line 7, column 8, length 9: Context B.""");
+
+  // Actual context owned by wrong error.
+  // TODO(rnystrom): This error is pretty confusing. Ideally we would detect
+  // this case specifically and give better guidance.
+  expectValidate([
+    makeError(line: 1, column: 2, length: 3, cfeError: "Error A.", context: [
+      makeError(line: 4, column: 5, length: 6, contextError: "Context A."),
+      makeError(line: 33, column: 5, length: 6, contextError: "Context."),
+    ]),
+    makeError(line: 10, column: 2, length: 3, cfeError: "Error B.", context: [
+      makeError(line: 11, column: 5, length: 6, contextError: "Context B."),
+    ]),
+  ], [
+    makeError(line: 1, column: 2, length: 3, cfeError: "Error A.", context: [
+      makeError(line: 4, column: 5, length: 6, contextError: "Context A."),
+    ]),
+    makeError(line: 10, column: 2, length: 3, cfeError: "Error B.", context: [
+      makeError(line: 11, column: 5, length: 6, contextError: "Context B."),
+      makeError(line: 33, column: 5, length: 6, contextError: "Context."),
+    ]),
+  ], """
+- Missing expected context message at line 33, column 5, length 6: Context.
+
+- Unexpected context message at line 33, column 5, length 6: Context.""");
+
+  // If expectation has no context at all, then ignore actual context.
+  expectValidate([
+    makeError(line: 1, column: 2, length: 3, cfeError: "Error."),
+  ], [
+    makeError(line: 1, column: 2, length: 3, cfeError: "Error.", context: [
+      makeError(line: 4, column: 5, length: 6, contextError: "Context A."),
+      makeError(line: 7, column: 8, length: 9, contextError: "Context B."),
+    ]),
+  ], null);
 }
 
 void expectValidate(List<StaticError> expected, List<StaticError> actual,
diff --git a/pkg/test_runner/test/test_file_test.dart b/pkg/test_runner/test/test_file_test.dart
index 1e793ef..8e5ddc1 100644
--- a/pkg/test_runner/test/test_file_test.dart
+++ b/pkg/test_runner/test/test_file_test.dart
@@ -28,6 +28,7 @@
   testParseMultitest();
   testParseErrorFlags();
   testParseErrorExpectations();
+  testParseContextMessages();
   testIsRuntimeTest();
   testName();
   testMultitest();
@@ -556,6 +557,145 @@
   ]);
 }
 
+void testParseContextMessages() {
+  // Multiple messages.
+  expectParseErrorExpectations("""
+var string = "str";
+/\/  ^^^^^^
+/\/ [context 1] Analyzer context before.
+/\/ [context 2] CFE context before.
+
+int j = string;
+/\/      ^^^^^^
+/\/ [analyzer 1] Error.BAD
+/\/ [cfe 2] Error message.
+
+var string = "str";
+/\/            ^^^
+/\/ [context 2] CFE context after.
+
+var string = "str";
+/\/            ^^^
+/\/ [context 1] Analyzer context after.
+""", [
+    makeError(
+        line: 6,
+        column: 9,
+        length: 6,
+        analyzerError: "Error.BAD",
+        context: [
+          makeError(
+              line: 1,
+              column: 5,
+              length: 6,
+              analyzerError: "Analyzer context before."),
+          makeError(
+              line: 15,
+              column: 15,
+              length: 3,
+              analyzerError: "Analyzer context after.")
+        ]),
+    makeError(
+        line: 6,
+        column: 9,
+        length: 6,
+        cfeError: "Error message.",
+        context: [
+          makeError(
+              line: 1,
+              column: 5,
+              length: 6,
+              analyzerError: "CFE context before."),
+          makeError(
+              line: 11,
+              column: 15,
+              length: 3,
+              analyzerError: "CFE context after.")
+        ]),
+  ]);
+
+  // Context before error.
+  expectParseErrorExpectations("""
+var string = "str";
+/\/  ^^^^^^
+/\/ [context 1] Context.
+
+int j = string;
+/\/      ^^^^^^
+/\/ [analyzer 1] Error.BAD
+""", [
+    makeError(
+        line: 5,
+        column: 9,
+        length: 6,
+        analyzerError: "Error.BAD",
+        context: [
+          makeError(line: 1, column: 5, length: 6, analyzerError: "Context.")
+        ]),
+  ]);
+
+  // Context after error.
+  expectParseErrorExpectations("""
+int j = string;
+/\/      ^^^^^^
+/\/ [analyzer 1] Error.BAD
+
+var string = "str";
+/\/  ^^^^^^
+/\/ [context 1] Context.
+""", [
+    makeError(
+        line: 1,
+        column: 9,
+        length: 6,
+        analyzerError: "Error.BAD",
+        context: [
+          makeError(line: 5, column: 5, length: 6, analyzerError: "Context.")
+        ]),
+  ]);
+
+  // Context must have a number.
+  expectFormatError("""
+int i = "s";
+/\/      ^^^
+/\/ [context] No number.
+
+int i = "s";
+/\/      ^^^
+/\/ [cfe 1] Error.
+""");
+
+  // Context number must match an error.
+  expectFormatError("""
+int i = "s";
+/\/      ^^^
+/\/ [context 2] Wrong number.
+
+int i = "s";
+/\/      ^^^
+/\/ [cfe 1] Error.
+""");
+
+  // Two errors with same number.
+  expectFormatError("""
+int i = "s";
+/\/      ^^^
+/\/ [context 1] Context.
+
+int i = "s";
+/\/      ^^^
+/\/ [cfe 1] Error.
+/\/ [analyzer 1] Error.CODE
+""");
+
+  // Numbered error with no context.
+  expectFormatError("""
+int i = "s";
+/\/      ^^^
+/\/ [cfe 1] Error.
+""");
+}
+
 void testIsRuntimeTest() {
   // No static errors at all.
   var file = parseTestFile("");
diff --git a/pkg/test_runner/test/update_errors_test.dart b/pkg/test_runner/test/update_errors_test.dart
index 1d459d4..afe5db9 100644
--- a/pkg/test_runner/test/update_errors_test.dart
+++ b/pkg/test_runner/test/update_errors_test.dart
@@ -403,9 +403,192 @@
 // [error line 1, column 1, length 0]
 // [cfe] Foo""");
 
+  contextMessages();
   regression();
 }
 
+void contextMessages() {
+  // Inserts context messages.
+  expectUpdate(
+      """
+int i = "bad";
+
+int another = "wrong";
+
+int third = "boo";
+""",
+      errors: [
+        makeError(
+            line: 3,
+            column: 15,
+            length: 7,
+            analyzerError: "some.error",
+            context: [
+              makeError(
+                  line: 1,
+                  column: 9,
+                  length: 5,
+                  contextError: "Analyzer context."),
+              makeError(
+                  line: 5,
+                  column: 13,
+                  length: 5,
+                  contextError: "More context."),
+            ]),
+        makeError(
+            line: 3,
+            column: 15,
+            length: 7,
+            cfeError: "CFE error.",
+            context: [
+              makeError(
+                  line: 1, column: 9, length: 5, contextError: "CFE context."),
+            ]),
+      ],
+      remove: {ErrorSource.analyzer},
+      includeContext: true,
+      expected: """
+int i = "bad";
+/\/      ^^^^^
+/\/ [context 1] Analyzer context.
+/\/ [context 2] CFE context.
+
+int another = "wrong";
+/\/            ^^^^^^^
+/\/ [analyzer 1] some.error
+/\/ [cfe 2] CFE error.
+
+int third = "boo";
+/\/          ^^^^^
+/\/ [context 1] More context.
+""");
+
+  // Removes context messages for removed errors.
+  expectUpdate(
+      """
+int i = "bad";
+/\/      ^^^^^
+/\/ [context 1] Analyzer context.
+/\/ [context 2] CFE context.
+
+int another = "wrong";
+/\/            ^^^^^^^
+/\/ [analyzer 1] some.error
+/\/ [cfe 2] CFE error.
+
+int third = "boo";
+/\/          ^^^^^
+/\/ [context 1] More context.
+""",
+      remove: {ErrorSource.analyzer},
+      includeContext: true,
+      expected: """
+int i = "bad";
+/\/      ^^^^^
+/\/ [context 1] CFE context.
+
+int another = "wrong";
+/\/            ^^^^^^^
+/\/ [cfe 1] CFE error.
+
+int third = "boo";
+""");
+
+  // Discards context messages when not told to include them.
+  expectUpdate(
+      """
+int i = "bad";
+
+int another = "wrong";
+
+int third = "boo";
+""",
+      errors: [
+        makeError(
+            line: 3,
+            column: 15,
+            length: 7,
+            analyzerError: "some.error",
+            context: [
+              makeError(
+                  line: 1,
+                  column: 9,
+                  length: 5,
+                  contextError: "Analyzer context."),
+              makeError(
+                  line: 5,
+                  column: 13,
+                  length: 5,
+                  contextError: "More context."),
+            ]),
+        makeError(
+            line: 3,
+            column: 15,
+            length: 7,
+            cfeError: "CFE error.",
+            context: [
+              makeError(
+                  line: 1, column: 9, length: 5, contextError: "CFE context."),
+            ]),
+      ],
+      includeContext: false,
+      expected: """
+int i = "bad";
+
+int another = "wrong";
+/\/            ^^^^^^^
+/\/ [analyzer] some.error
+/\/ [cfe] CFE error.
+
+int third = "boo";
+""");
+
+  // Discards existing context messages when not told to include them.
+  expectUpdate(
+      """
+int i = "bad";
+/\/      ^^^^^
+/\/ [context 1] CFE context.
+
+int another = "wrong";
+/\/            ^^^^^^^
+/\/ [cfe 1] CFE error.
+
+int third = "boo";
+""",
+      errors: [
+        makeError(
+            line: 5,
+            column: 15,
+            length: 7,
+            analyzerError: "some.error",
+            context: [
+              makeError(
+                  line: 1,
+                  column: 9,
+                  length: 5,
+                  contextError: "Analyzer context."),
+              makeError(
+                  line: 7,
+                  column: 13,
+                  length: 5,
+                  contextError: "More context."),
+            ]),
+      ],
+      remove: {ErrorSource.analyzer},
+      includeContext: false,
+      expected: """
+int i = "bad";
+
+int another = "wrong";
+/\/            ^^^^^^^
+/\/ [analyzer] some.error
+/\/ [cfe] CFE error.
+
+int third = "boo";
+""");
+}
+
 void regression() {
   // https://github.com/dart-lang/sdk/issues/37990.
   expectUpdate("""
@@ -462,11 +645,16 @@
 }
 
 void expectUpdate(String original,
-    {List<StaticError> errors, Set<ErrorSource> remove, String expected}) {
+    {List<StaticError> errors,
+    Set<ErrorSource> remove,
+    bool includeContext,
+    String expected}) {
   errors ??= const [];
   remove ??= ErrorSource.all.toSet();
+  includeContext ??= false;
 
-  var actual = updateErrorExpectations(original, errors, remove: remove);
+  var actual = updateErrorExpectations(original, errors,
+      remove: remove, includeContext: includeContext);
   if (actual != expected) {
     // Not using Expect.equals() because the diffs it shows aren't helpful for
     // strings this large.
diff --git a/pkg/test_runner/test/utils.dart b/pkg/test_runner/test/utils.dart
index 2a9152d..e0d8f07 100644
--- a/pkg/test_runner/test/utils.dart
+++ b/pkg/test_runner/test/utils.dart
@@ -28,27 +28,44 @@
 
 /// Creates a [StaticError].
 ///
-/// Only one of [analyzerError], [cfeError], or [webError] may be passed.
+/// Only one of [analyzerError], [cfeError], [webError], or [contextError] may
+/// be passed.
 StaticError makeError(
     {int line = 1,
     int column = 2,
     int length,
     String analyzerError,
     String cfeError,
-    String webError}) {
+    String webError,
+    String contextError,
+    List<StaticError> context}) {
+  ErrorSource source;
+  String message;
   if (analyzerError != null) {
-    assert(cfeError == null && webError == null);
-    return StaticError(ErrorSource.analyzer, analyzerError,
-        line: line, column: column, length: length);
+    assert(cfeError == null);
+    assert(webError == null);
+    assert(contextError == null);
+    source = ErrorSource.analyzer;
+    message = analyzerError;
   } else if (cfeError != null) {
     assert(webError == null);
-    return StaticError(ErrorSource.cfe, cfeError,
-        line: line, column: column, length: length);
+    assert(contextError == null);
+    source = ErrorSource.cfe;
+    message = cfeError;
+  } else if (webError != null) {
+    assert(contextError == null);
+    source = ErrorSource.web;
+    message = webError;
   } else {
-    assert(webError != null);
-    return StaticError(ErrorSource.web, webError,
-        line: line, column: column, length: length);
+    assert(contextError != null);
+    source = ErrorSource.context;
+    message = contextError;
   }
+
+  var error =
+      StaticError(source, message, line: line, column: column, length: length);
+  if (context != null) error.contextMessages.addAll(context);
+  return error;
 }
 
 class _MockTestSuite extends StandardTestSuite {
diff --git a/pkg/test_runner/tool/update_static_error_tests.dart b/pkg/test_runner/tool/update_static_error_tests.dart
index 96e521e..ed9bad5 100644
--- a/pkg/test_runner/tool/update_static_error_tests.dart
+++ b/pkg/test_runner/tool/update_static_error_tests.dart
@@ -37,6 +37,8 @@
       abbr: "n",
       help: "Print result but do not overwrite any files.",
       negatable: false);
+  parser.addFlag("context",
+      abbr: "c", help: "Include context messages in output.");
 
   parser.addSeparator("What operations to perform:");
   parser.addFlag("remove-all",
@@ -61,8 +63,6 @@
       help: "Update error expectations for given front ends.",
       allowed: sources);
 
-  parser.addSeparator("Other flags:");
-
   var results = parser.parse(args);
 
   if (results["help"] as bool) {
@@ -76,6 +76,8 @@
 
   var dryRun = results["dry-run"] as bool;
 
+  var includeContext = results["context"] as bool;
+
   var removeSources = <ErrorSource>{};
   var insertSources = <ErrorSource>{};
 
@@ -132,7 +134,10 @@
 
     if (entry is pkg_file.File) {
       await _processFile(entry,
-          dryRun: dryRun, remove: removeSources, insert: insertSources);
+          dryRun: dryRun,
+          includeContext: includeContext,
+          remove: removeSources,
+          insert: insertSources);
     }
   }
 }
@@ -146,7 +151,10 @@
 }
 
 Future<void> _processFile(File file,
-    {bool dryRun, Set<ErrorSource> remove, Set<ErrorSource> insert}) async {
+    {bool dryRun,
+    bool includeContext,
+    Set<ErrorSource> remove,
+    Set<ErrorSource> insert}) async {
   stdout.write("${file.path}...");
   var source = file.readAsStringSync();
   var testFile = TestFile.parse(Path("."), file.absolute.path, source);
@@ -203,7 +211,8 @@
     }
   }
 
-  var result = updateErrorExpectations(source, errors, remove: remove);
+  var result = updateErrorExpectations(source, errors,
+      remove: remove, includeContext: includeContext);
 
   stdout.writeln("\r${file.path} (Updated with ${errors.length} errors)");
 
diff --git a/tools/VERSION b/tools/VERSION
index 2715580..705ebe0 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 13
 PATCH 0
-PRERELEASE 181
+PRERELEASE 182
 PRERELEASE_PATCH 0
\ No newline at end of file