Version 2.13.0-87.0.dev

Merge commit '46434442051eb8c112da536cb4e485ba8b745f09' into 'dev'
diff --git a/DEPS b/DEPS
index 31d2194..35ba597 100644
--- a/DEPS
+++ b/DEPS
@@ -143,13 +143,13 @@
   "shelf_static_rev": "bafde9eaddb5d02040a614e41deddd971b4d67e6",
   "shelf_packages_handler_rev": "78302e67c035047e6348e692b0c1182131f0fe35",
   "shelf_proxy_tag": "0.1.0+7",
-  "shelf_rev": "fa5afaa38bd51dedeeaa25b7bfd8822cabbcc57f",
-  "shelf_web_socket_rev": "091d2ed2105827b7f376668fafc88b2b8c845d71",
+  "shelf_rev": "e9294125f0c1fc2049c958577cd586ca2395168f",
+  "shelf_web_socket_rev": "aa312d3cdeef96fb64bc3e602bc354a05a995624",
   "source_map_stack_trace_rev": "1c3026f69d9771acf2f8c176a1ab750463309cce",
   "source_maps-0.9.4_rev": "38524",
   "source_maps_rev": "53eb92ccfe6e64924054f83038a534b959b12b3e",
   "source_span_rev": "1be3c44045a06dff840d2ed3a13e6082d7a03a23",
-  "sse_tag": "8add37c0ec0419ce28153a03f299d44ce006a975",
+  "sse_tag": "5da8fedcdc56f306933d202e2d204753eecefd36",
   "stack_trace_tag": "6788afc61875079b71b3d1c3e65aeaa6a25cbc2f",
   "stagehand_rev": "e64ac90cac508981011299c4ceb819149e71f1bd",
   "stream_channel_tag": "d7251e61253ec389ee6e045ee1042311bced8f1d",
@@ -229,6 +229,13 @@
       }],
       "dep_type": "cipd",
   },
+  Var("dart_root") + "/third_party/devtools": {
+      "packages": [{
+          "package": "dart/third_party/flutter/devtools",
+          "version": "revision:6729ec62c3548839018c32fa711756202431ccf7",
+      }],
+      "dep_type": "cipd",
+  },
   Var("dart_root") + "/tests/co19/src": {
       "packages": [{
           "package": "dart/third_party/co19",
diff --git a/pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart b/pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart
index f46c760..2f8d1e5 100644
--- a/pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart
+++ b/pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart
@@ -4,16 +4,6 @@
 
 import 'package:meta/meta.dart';
 
-/// Set this boolean to `true` to permanently enable the feature of allowing
-/// local boolean variables to influence promotion (see
-/// https://github.com/dart-lang/language/issues/1274).  While this boolean is
-/// `false`, the feature remains experimental and can be activated via an
-/// optional boolean parameter to the [FlowAnalysis] constructor.
-///
-/// Changing this value to `true` will cause some dead code warnings to appear
-/// for code that only exists to support the old behavior.
-const bool allowLocalBooleanVarsToPromoteByDefault = true;
-
 /// [AssignedVariables] is a helper class capable of computing the set of
 /// variables that are potentially written to, and potentially captured by
 /// closures, at various locations inside the code being analyzed.  This class
@@ -242,6 +232,11 @@
             '{${_info.keys.map((k) => '$k (${k.hashCode})').join(',')}}'));
   }
 
+  /// Indicates whether information is stored for the given [node].
+  bool _hasInfoForNode(Node node) {
+    return _info[node] != null;
+  }
+
   void _printOn(StringBuffer sb) {
     sb.write('_info=$_info,');
     sb.write('_stack=$_stack,');
@@ -402,10 +397,8 @@
 abstract class FlowAnalysis<Node extends Object, Statement extends Node,
     Expression extends Object, Variable extends Object, Type extends Object> {
   factory FlowAnalysis(TypeOperations<Variable, Type> typeOperations,
-      AssignedVariables<Node, Variable> assignedVariables,
-      {bool allowLocalBooleanVarsToPromote = false}) {
-    return new _FlowAnalysisImpl(typeOperations, assignedVariables,
-        allowLocalBooleanVarsToPromote: allowLocalBooleanVarsToPromote);
+      AssignedVariables<Node, Variable> assignedVariables) {
+    return new _FlowAnalysisImpl(typeOperations, assignedVariables);
   }
 
   factory FlowAnalysis.legacy(TypeOperations<Variable, Type> typeOperations,
@@ -935,6 +928,8 @@
   /// promotion, to retrieve information about why [target] was not promoted.
   /// This call must be made right after visiting [target].
   ///
+  /// If [target] is `null` it is assumed to be an implicit reference to `this`.
+  ///
   /// The returned value is a map whose keys are types that the user might have
   /// been expecting the target to be promoted to, and whose values are reasons
   /// why the corresponding promotion did not occur.  The caller is expected to
@@ -943,7 +938,7 @@
   /// occurs due to the target having a nullable type, the caller should report
   /// a non-promotion reason associated with non-promotion to a non-nullable
   /// type).
-  Map<Type, NonPromotionReason> whyNotPromoted(Expression target);
+  Map<Type, NonPromotionReason> whyNotPromoted(Expression? target);
 
   /// Register write of the given [variable] in the current state.
   /// [writtenType] should be the type of the value that was written.
@@ -974,12 +969,10 @@
   bool _exceptionOccurred = false;
 
   factory FlowAnalysisDebug(TypeOperations<Variable, Type> typeOperations,
-      AssignedVariables<Node, Variable> assignedVariables,
-      {bool allowLocalBooleanVarsToPromote = false}) {
+      AssignedVariables<Node, Variable> assignedVariables) {
     print('FlowAnalysisDebug()');
-    return new FlowAnalysisDebug._(new _FlowAnalysisImpl(
-        typeOperations, assignedVariables,
-        allowLocalBooleanVarsToPromote: allowLocalBooleanVarsToPromote));
+    return new FlowAnalysisDebug._(
+        new _FlowAnalysisImpl(typeOperations, assignedVariables));
   }
 
   factory FlowAnalysisDebug.legacy(
@@ -1460,7 +1453,7 @@
   }
 
   @override
-  Map<Type, NonPromotionReason> whyNotPromoted(Expression target) {
+  Map<Type, NonPromotionReason> whyNotPromoted(Expression? target) {
     return _wrap(
         'whyNotPromoted($target)', () => _wrapped.whyNotPromoted(target),
         isQuery: true);
@@ -1879,79 +1872,6 @@
     return _identicalOrNew(this, base, newReachable, newVariableInfo);
   }
 
-  /// Updates the state to reflect a control path that is known to have
-  /// previously passed through some [other] state.
-  ///
-  /// Approximately, this method forms the union of the definite assignments and
-  /// promotions in `this` state and the [other] state.  More precisely:
-  ///
-  /// The control flow path is considered reachable if both this state and the
-  /// other state are reachable.  Variables are considered definitely assigned
-  /// if they were definitely assigned in either this state or the other state.
-  /// Variable type promotions are taken from this state, unless the promotion
-  /// in the other state is more specific, and the variable is "safe".  A
-  /// variable is considered safe if there is no chance that it was assigned
-  /// more recently than the "other" state.
-  ///
-  /// This is used after a `try/finally` statement to combine the promotions and
-  /// definite assignments that occurred in the `try` and `finally` blocks
-  /// (where `this` is the state from the `finally` block and `other` is the
-  /// state from the `try` block).  Variables that are assigned in the `finally`
-  /// block are considered "unsafe" because the assignment might have cancelled
-  /// the effect of any promotion that occurred inside the `try` block.
-  FlowModel<Variable, Type> restrict(
-      TypeOperations<Variable, Type> typeOperations,
-      FlowModel<Variable, Type> other,
-      Set<Variable> unsafe) {
-    if (allowLocalBooleanVarsToPromoteByDefault) {
-      // TODO(paulberry): when we hardcode
-      // allowLocalBooleanVarsToPromoteByDefault to `true`, we should remove
-      // this method entirely.
-      throw new StateError('This method should not be called anymore');
-    }
-    Reachability newReachable =
-        Reachability.restrict(reachable, other.reachable);
-
-    Map<Variable?, VariableModel<Variable, Type>> newVariableInfo =
-        <Variable?, VariableModel<Variable, Type>>{};
-    bool variableInfoMatchesThis = true;
-    bool variableInfoMatchesOther = true;
-    for (MapEntry<Variable?, VariableModel<Variable, Type>> entry
-        in variableInfo.entries) {
-      Variable? variable = entry.key;
-      VariableModel<Variable, Type> thisModel = entry.value;
-      VariableModel<Variable, Type>? otherModel = other.variableInfo[variable];
-      if (otherModel == null) {
-        variableInfoMatchesThis = false;
-        continue;
-      }
-      VariableModel<Variable, Type> restricted = thisModel.restrict(
-          typeOperations, otherModel, unsafe.contains(variable));
-      newVariableInfo[variable] = restricted;
-      if (!identical(restricted, thisModel)) variableInfoMatchesThis = false;
-      if (!identical(restricted, otherModel)) variableInfoMatchesOther = false;
-    }
-    if (variableInfoMatchesOther) {
-      for (Variable? variable in other.variableInfo.keys) {
-        if (!variableInfo.containsKey(variable)) {
-          variableInfoMatchesOther = false;
-          break;
-        }
-      }
-    }
-    assert(variableInfoMatchesThis ==
-        _variableInfosEqual(newVariableInfo, variableInfo));
-    assert(variableInfoMatchesOther ==
-        _variableInfosEqual(newVariableInfo, other.variableInfo));
-    if (variableInfoMatchesThis) {
-      newVariableInfo = variableInfo;
-    } else if (variableInfoMatchesOther) {
-      newVariableInfo = other.variableInfo;
-    }
-
-    return _identicalOrNew(this, other, newReachable, newVariableInfo);
-  }
-
   /// Updates the state to indicate that the control flow path is unreachable.
   FlowModel<Variable, Type> setUnreachable() {
     if (!reachable.locallyReachable) return this;
@@ -2797,59 +2717,6 @@
         ssaNode: writeCaptured ? null : new SsaNode<Variable, Type>(null));
   }
 
-  /// Returns an updated model reflect a control path that is known to have
-  /// previously passed through some [other] state.  See [FlowModel.restrict]
-  /// for details.
-  VariableModel<Variable, Type> restrict(
-      TypeOperations<Variable, Type> typeOperations,
-      VariableModel<Variable, Type> otherModel,
-      bool unsafe) {
-    if (allowLocalBooleanVarsToPromoteByDefault) {
-      // TODO(paulberry): when we hardcode
-      // allowLocalBooleanVarsToPromoteByDefault to `true`, we should remove
-      // this method entirely.
-      throw new StateError('This method should not be called anymore');
-    }
-    List<Type>? thisPromotedTypes = promotedTypes;
-    List<Type>? otherPromotedTypes = otherModel.promotedTypes;
-    bool newAssigned = assigned || otherModel.assigned;
-    // The variable can only be unassigned in this state if it was also
-    // unassigned in the other state or if the other state didn't complete
-    // normally. For the latter case the resulting state is unreachable but to
-    // avoid creating a variable model that is both assigned and unassigned we
-    // take the intersection below.
-    //
-    // This situation can occur in try-finally like:
-    //
-    //   method() {
-    //     var local;
-    //     try {
-    //       local = 0;
-    //       return; // assigned
-    //     } finally {
-    //       local; // unassigned
-    //     }
-    //     local; // unreachable state
-    //   }
-    //
-    bool newUnassigned = unassigned && otherModel.unassigned;
-    bool newWriteCaptured = writeCaptured || otherModel.writeCaptured;
-    List<Type>? newPromotedTypes;
-    if (newWriteCaptured) {
-      // Write-captured variables can't be promoted
-      newPromotedTypes = null;
-    } else if (unsafe) {
-      // There was an assignment to the variable in the "this" path, so none of
-      // the promotions from the "other" path can be used.
-      newPromotedTypes = thisPromotedTypes;
-    } else {
-      newPromotedTypes = rebasePromotedTypes(
-          typeOperations, thisPromotedTypes, otherPromotedTypes);
-    }
-    return _identicalOrNew(this, otherModel, newPromotedTypes, tested,
-        newAssigned, newUnassigned, newWriteCaptured ? null : ssaNode);
-  }
-
   /// Updates `this` with a new set of properties.
   VariableModel<Variable, Type> setProperties(
           Map<String, VariableModel<Variable, Type>> newProperties) =>
@@ -3524,18 +3391,7 @@
 
   final AssignedVariables<Node, Variable> _assignedVariables;
 
-  /// Set this boolean to `true` to temporarily enable the feature of allowing
-  /// local boolean variables to influence promotion, for this flow analysis
-  /// session (see https://github.com/dart-lang/language/issues/1274).  Once the
-  /// top level const [allowLocalBooleanVarsToPromoteByDefault] is changed to
-  /// `true`, this field will always be `true`, so it can be safely removed.
-  final bool allowLocalBooleanVarsToPromote;
-
-  _FlowAnalysisImpl(this.typeOperations, this._assignedVariables,
-      {bool allowLocalBooleanVarsToPromote = false})
-      : allowLocalBooleanVarsToPromote =
-            allowLocalBooleanVarsToPromoteByDefault ||
-                allowLocalBooleanVarsToPromote;
+  _FlowAnalysisImpl(this.typeOperations, this._assignedVariables);
 
   @override
   bool get isReachable => _current.reachable.overallReachable;
@@ -4186,17 +4042,13 @@
 
   @override
   void tryFinallyStatement_end(Node finallyBlock) {
-    AssignedVariablesNodeInfo<Variable> info =
-        _assignedVariables._getInfoForNode(finallyBlock);
+    // We used to need info for `finally` blocks but we don't anymore.
+    assert(!_assignedVariables._hasInfoForNode(finallyBlock),
+        'No assigned variables info should have been stored for $finallyBlock');
     _TryFinallyContext<Variable, Type> context =
         _stack.removeLast() as _TryFinallyContext<Variable, Type>;
-    if (allowLocalBooleanVarsToPromote) {
-      _current = context._afterBodyAndCatches
-          .attachFinally(typeOperations, context._beforeFinally, _current);
-    } else {
-      _current = _current.restrict(
-          typeOperations, context._afterBodyAndCatches, info._written);
-    }
+    _current = context._afterBodyAndCatches
+        .attachFinally(typeOperations, context._beforeFinally, _current);
   }
 
   @override
@@ -4218,13 +4070,11 @@
     _storeExpressionReference(expression, variableReference);
     VariableModel<Variable, Type> variableModel =
         variableReference.getInfo(_current.variableInfo);
-    if (allowLocalBooleanVarsToPromote) {
-      ExpressionInfo<Variable, Type>? expressionInfo = variableModel
-          .ssaNode?.expressionInfo
-          ?.rebaseForward(typeOperations, _current);
-      if (expressionInfo != null) {
-        _storeExpressionInfo(expression, expressionInfo);
-      }
+    ExpressionInfo<Variable, Type>? expressionInfo = variableModel
+        .ssaNode?.expressionInfo
+        ?.rebaseForward(typeOperations, _current);
+    if (expressionInfo != null) {
+      _storeExpressionInfo(expression, expressionInfo);
     }
     return variableModel.promotedTypes?.last;
   }
@@ -4257,13 +4107,16 @@
   }
 
   @override
-  Map<Type, NonPromotionReason> whyNotPromoted(Expression target) {
-    if (identical(target, _expressionWithReference)) {
-      Reference<Variable, Type>? reference = _expressionReference;
-      if (reference != null) {
-        return reference.getNonPromotionReasons(
-            _current.variableInfo, typeOperations);
-      }
+  Map<Type, NonPromotionReason> whyNotPromoted(Expression? target) {
+    Reference<Variable, Type>? reference;
+    if (target == null) {
+      reference = new _ThisReference<Variable, Type>();
+    } else if (identical(target, _expressionWithReference)) {
+      reference = _expressionReference;
+    }
+    if (reference != null) {
+      return reference.getNonPromotionReasons(
+          _current.variableInfo, typeOperations);
     }
     return {};
   }
@@ -4850,7 +4703,7 @@
   void whileStatement_end() {}
 
   @override
-  Map<Type, NonPromotionReason> whyNotPromoted(Expression target) {
+  Map<Type, NonPromotionReason> whyNotPromoted(Expression? target) {
     return {};
   }
 
diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/assigned_variables/data/tryFinally.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/assigned_variables/data/tryFinally.dart
index 331b152..f9922ae 100644
--- a/pkg/_fe_analyzer_shared/test/flow_analysis/assigned_variables/data/tryFinally.dart
+++ b/pkg/_fe_analyzer_shared/test/flow_analysis/assigned_variables/data/tryFinally.dart
@@ -2,18 +2,18 @@
 // 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.
 
-/*member: tryFinally:declared={a, b}, assigned={a, b}*/
+/*member: tryFinally:declared={a, b, d}, assigned={a, b}*/
 tryFinally(int a, int b) {
   /*assigned={a}*/ try /*declared={c}, assigned={a}*/ {
     a = 0;
     var c;
-  } finally /*declared={d}, assigned={b}*/ {
+  } finally {
     b = 0;
     var d;
   }
 }
 
-/*member: tryCatchFinally:declared={a, b, c}, assigned={a, b, c}*/
+/*member: tryCatchFinally:declared={a, b, c, f}, assigned={a, b, c}*/
 tryCatchFinally(int a, int b, int c) {
   // Note: try/catch/finally is desugared into try/catch nested inside
   // try/finally.  The comment preceding the "try" refers to the outer
@@ -25,7 +25,7 @@
   } on String {
     b = 0;
     var e;
-  } finally /*declared={f}, assigned={c}*/ {
+  } finally {
     c = 0;
     var f;
   }
diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_mini_ast.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_mini_ast.dart
index 0c02855..d1a4d13 100644
--- a/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_mini_ast.dart
+++ b/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_mini_ast.dart
@@ -9,7 +9,7 @@
 import 'package:_fe_analyzer_shared/src/flow_analysis/flow_analysis.dart';
 import 'package:test/test.dart';
 
-const Expression nullLiteral = const _NullLiteral();
+Expression get nullLiteral => new _NullLiteral();
 
 Statement assert_(Expression condition, [Expression? message]) =>
     new _Assert(condition, message);
@@ -144,6 +144,10 @@
         [List<Statement>? ifFalse]) =>
     new _If(condition, ifTrue, ifFalse);
 
+Statement implicitThis_whyNotPromoted(
+        void Function(Map<Type, NonPromotionReason>) callback) =>
+    new _WhyNotPromoted_ImplicitThis(callback);
+
 Statement labeled(Statement body) => new _LabeledStatement(body);
 
 Statement localFunction(List<Statement> body) => _LocalFunction(body);
@@ -215,7 +219,7 @@
 /// analysis testing.  Methods in this class may be used to create more complex
 /// expressions based on this one.
 abstract class Expression extends Node implements _Visitable<Type> {
-  const Expression() : super._();
+  Expression() : super._();
 
   /// If `this` is an expression `x`, creates the expression `x!`.
   Expression get nonNullAssert => new _NonNullAssert(this);
@@ -419,8 +423,6 @@
     'num* - Object': Type('Never'),
   };
 
-  final bool allowLocalBooleanVarsToPromote;
-
   final bool legacy;
 
   final Map<String, bool> _subtypes = Map.of(_coreSubtypes);
@@ -431,7 +433,7 @@
 
   Map<String, Map<String, String>> _promotionExceptions = {};
 
-  Harness({this.allowLocalBooleanVarsToPromote = false, this.legacy = false});
+  Harness({this.legacy = false});
 
   @override
   Type get topType => Type('Object?');
@@ -508,8 +510,7 @@
         ? FlowAnalysis<Node, Statement, Expression, Var, Type>.legacy(
             this, assignedVariables)
         : FlowAnalysis<Node, Statement, Expression, Var, Type>(
-            this, assignedVariables,
-            allowLocalBooleanVarsToPromote: allowLocalBooleanVarsToPromote);
+            this, assignedVariables);
     statements._visit(this, flow);
     flow.finish();
   }
@@ -570,7 +571,13 @@
 /// Representation of an expression or statement in the pseudo-Dart language
 /// used for flow analysis testing.
 class Node {
-  const Node._();
+  static int _nextId = 0;
+
+  final int id;
+
+  Node._() : id = _nextId++;
+
+  String toString() => 'Node#$id';
 }
 
 /// Helper class allowing tests to examine the values of variables' SSA nodes.
@@ -1410,7 +1417,7 @@
 }
 
 class _NullLiteral extends Expression {
-  const _NullLiteral();
+  _NullLiteral();
 
   @override
   String toString() => 'null';
@@ -1648,9 +1655,7 @@
     assignedVariables.beginNode();
     body._preVisit(assignedVariables);
     assignedVariables.endNode(_bodyNode);
-    assignedVariables.beginNode();
     finally_._preVisit(assignedVariables);
-    assignedVariables.endNode(_finallyNode);
   }
 
   @override
@@ -1754,6 +1759,30 @@
   }
 }
 
+class _WhyNotPromoted_ImplicitThis extends Statement {
+  final void Function(Map<Type, NonPromotionReason>) callback;
+
+  _WhyNotPromoted_ImplicitThis(this.callback) : super._();
+
+  @override
+  String toString() => 'implicit this (whyNotPromoted)';
+
+  @override
+  void _preVisit(AssignedVariables<Node, Var> assignedVariables) {}
+
+  @override
+  void _visit(
+      Harness h, FlowAnalysis<Node, Statement, Expression, Var, Type> flow) {
+    assert(!Type._allowComparisons);
+    Type._allowComparisons = true;
+    try {
+      callback(flow.whyNotPromoted(null));
+    } finally {
+      Type._allowComparisons = false;
+    }
+  }
+}
+
 class _WrappedExpression extends Expression {
   final Statement? before;
   final Expression expr;
diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart
index a344d9e..f594559 100644
--- a/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart
+++ b/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart
@@ -2275,7 +2275,7 @@
       test(
           'tryFinallyStatement_end() restores SSA nodes from try block when it'
           'is sound to do so', () {
-        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var h = Harness();
         var x = Var('x', 'int?');
         var y = Var('y', 'int?');
         late SsaNode<Var, Type> xSsaAtEndOfTry;
@@ -2322,7 +2322,7 @@
       test(
           'tryFinallyStatement_end() sets unreachable if end of try block '
           'unreachable', () {
-        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var h = Harness();
         h.run([
           tryFinally([
             return_(),
@@ -2337,7 +2337,7 @@
       test(
           'tryFinallyStatement_end() sets unreachable if end of finally block '
           'unreachable', () {
-        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var h = Harness();
         h.run([
           tryFinally([
             checkReachable(true),
@@ -2352,7 +2352,7 @@
       test(
           'tryFinallyStatement_end() handles a variable declared only in the '
           'try block', () {
-        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var h = Harness();
         var x = Var('x', 'int?');
         h.run([
           tryFinally([
@@ -2364,7 +2364,7 @@
       test(
           'tryFinallyStatement_end() handles a variable declared only in the '
           'finally block', () {
-        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var h = Harness();
         var x = Var('x', 'int?');
         h.run([
           tryFinally([], [
@@ -2376,7 +2376,7 @@
       test(
           'tryFinallyStatement_end() handles a variable that was write '
           'captured in the try block', () {
-        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var h = Harness();
         var x = Var('x', 'int?');
         h.run([
           declare(x, initialized: true),
@@ -2394,7 +2394,7 @@
       test(
           'tryFinallyStatement_end() handles a variable that was write '
           'captured in the finally block', () {
-        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var h = Harness();
         var x = Var('x', 'int?');
         h.run([
           declare(x, initialized: true),
@@ -2412,7 +2412,7 @@
       test(
           'tryFinallyStatement_end() handles a variable that was promoted in '
           'the try block and write captured in the finally block', () {
-        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var h = Harness();
         var x = Var('x', 'int?');
         h.run([
           declare(x, initialized: true),
@@ -2438,7 +2438,7 @@
       test(
           'tryFinallyStatement_end() keeps promotions from both try and '
           'finally blocks when there is no write in the finally block', () {
-        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var h = Harness();
         var x = Var('x', 'Object');
         h.run([
           declare(x, initialized: true),
@@ -2462,7 +2462,7 @@
       test(
           'tryFinallyStatement_end() keeps promotions from the finally block '
           'when there is a write in the finally block', () {
-        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var h = Harness();
         var x = Var('x', 'Object');
         h.run([
           declare(x, initialized: true),
@@ -2484,7 +2484,7 @@
       test(
           'tryFinallyStatement_end() keeps tests from both the try and finally '
           'blocks', () {
-        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var h = Harness();
         var x = Var('x', 'Object');
         h.run([
           declare(x, initialized: true),
@@ -2509,7 +2509,7 @@
       test(
           'tryFinallyStatement_end() handles variables not definitely assigned '
           'in either the try or finally block', () {
-        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var h = Harness();
         var x = Var('x', 'Object');
         h.run([
           declare(x, initialized: false),
@@ -2532,7 +2532,7 @@
       test(
           'tryFinallyStatement_end() handles variables definitely assigned in '
           'the try block', () {
-        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var h = Harness();
         var x = Var('x', 'Object');
         h.run([
           declare(x, initialized: false),
@@ -2553,7 +2553,7 @@
       test(
           'tryFinallyStatement_end() handles variables definitely assigned in '
           'the finally block', () {
-        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var h = Harness();
         var x = Var('x', 'Object');
         h.run([
           declare(x, initialized: false),
@@ -2574,7 +2574,7 @@
       test(
           'tryFinallyStatement_end() handles variables definitely unassigned '
           'in both the try and finally blocks', () {
-        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var h = Harness();
         var x = Var('x', 'Object');
         h.run([
           declare(x, initialized: false),
@@ -2591,7 +2591,7 @@
       test(
           'tryFinallyStatement_end() handles variables definitely unassigned '
           'in the try but not the finally block', () {
-        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var h = Harness();
         var x = Var('x', 'Object');
         h.run([
           declare(x, initialized: false),
@@ -2611,7 +2611,7 @@
       test(
           'tryFinallyStatement_end() handles variables definitely unassigned '
           'in the finally but not the try block', () {
-        var h = Harness(allowLocalBooleanVarsToPromote: true);
+        var h = Harness();
         var x = Var('x', 'Object');
         h.run([
           declare(x, initialized: false),
@@ -2630,7 +2630,7 @@
     });
 
     test('variableRead() restores promotions from previous write()', () {
-      var h = Harness(allowLocalBooleanVarsToPromote: true);
+      var h = Harness();
       var x = Var('x', 'int?');
       var y = Var('y', 'int?');
       var z = Var('z', 'bool');
@@ -2664,7 +2664,7 @@
     });
 
     test('variableRead() restores promotions from previous initialization', () {
-      var h = Harness(allowLocalBooleanVarsToPromote: true);
+      var h = Harness();
       var x = Var('x', 'int?');
       var y = Var('y', 'int?');
       var z = Var('z', 'bool');
@@ -2697,7 +2697,7 @@
     });
 
     test('variableRead() rebases old promotions', () {
-      var h = Harness(allowLocalBooleanVarsToPromote: true);
+      var h = Harness();
       var w = Var('w', 'int?');
       var x = Var('x', 'int?');
       var y = Var('y', 'int?');
@@ -2739,7 +2739,7 @@
       // Note: we have the available infrastructure to do this if we want, but
       // we think it will give an inconsistent feel because comparisons like
       // `if (i == null)` *don't* promote.
-      var h = Harness(allowLocalBooleanVarsToPromote: true);
+      var h = Harness();
       var x = Var('x', 'int?');
       var y = Var('y', 'int?');
       h.run([
@@ -2967,7 +2967,7 @@
       //   if (b) { /* x promoted again */ }
       // But there are a lot of corner cases to test and it's not clear how much
       // the benefit will be, so for now we're not doing it.
-      var h = Harness(allowLocalBooleanVarsToPromote: true);
+      var h = Harness();
       var x = Var('x', 'int?');
       var y = Var('y', 'int?');
       late SsaNode<Var, Type> xSsaBeforeWrite;
@@ -5683,6 +5683,40 @@
         ]);
       });
     });
+
+    group('because this', () {
+      test('explicit', () {
+        var h = Harness()
+          ..addSubtype('D', 'Object?', true)
+          ..addFactor('Object?', 'D', 'Object?');
+        h.run([
+          if_(this_('C').isNot('D'), [
+            return_(),
+          ]),
+          this_('C').whyNotPromoted((reasons) {
+            expect(reasons.keys, unorderedEquals([Type('D')]));
+            var nonPromotionReason = reasons.values.single;
+            expect(nonPromotionReason, TypeMatcher<ThisNotPromoted>());
+          }).stmt,
+        ]);
+      });
+
+      test('implicit', () {
+        var h = Harness()
+          ..addSubtype('D', 'Object?', true)
+          ..addFactor('Object?', 'D', 'Object?');
+        h.run([
+          if_(this_('C').isNot('D'), [
+            return_(),
+          ]),
+          implicitThis_whyNotPromoted((reasons) {
+            expect(reasons.keys, unorderedEquals([Type('D')]));
+            var nonPromotionReason = reasons.values.single;
+            expect(nonPromotionReason, TypeMatcher<ThisNotPromoted>());
+          }),
+        ]);
+      });
+    });
   });
 }
 
diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/this.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/this.dart
new file mode 100644
index 0000000..47012b6
--- /dev/null
+++ b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/this.dart
@@ -0,0 +1,22 @@
+// Copyright (c) 2020, 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.
+
+// This test validates integration of "why not promoted" when the user tries to
+// promote `this`.
+
+// TODO(paulberry): once we support adding "why not promoted" information to
+// errors that aren't related to null safety, test references to `this` in
+// classes and mixins.
+
+extension on int? {
+  extension_explicit_this() {
+    if (this == null) return;
+    this. /*notPromoted(thisNotPromoted)*/ isEven;
+  }
+
+  extension_implicit_this() {
+    if (this == null) return;
+    /*notPromoted(thisNotPromoted)*/ isEven;
+  }
+}
diff --git a/pkg/analysis_server/tool/code_completion/completion_metrics.dart b/pkg/analysis_server/tool/code_completion/completion_metrics.dart
index fbb23b7..38e22d5 100644
--- a/pkg/analysis_server/tool/code_completion/completion_metrics.dart
+++ b/pkg/analysis_server/tool/code_completion/completion_metrics.dart
@@ -2,11 +2,13 @@
 // 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.
 
+import 'dart:convert';
 import 'dart:io' as io;
 import 'dart:math' as math;
 
 import 'package:_fe_analyzer_shared/src/base/syntactic_entity.dart';
 import 'package:analysis_server/src/domains/completion/available_suggestions.dart';
+import 'package:analysis_server/src/protocol/protocol_internal.dart';
 import 'package:analysis_server/src/protocol_server.dart' as protocol;
 import 'package:analysis_server/src/services/completion/completion_core.dart';
 import 'package:analysis_server/src/services/completion/completion_performance.dart';
@@ -36,6 +38,7 @@
         VariableElement;
 import 'package:analyzer/diagnostic/diagnostic.dart';
 import 'package:analyzer/error/error.dart' as err;
+import 'package:analyzer/file_system/file_system.dart';
 import 'package:analyzer/file_system/overlay_file_system.dart';
 import 'package:analyzer/file_system/physical_file_system.dart';
 import 'package:analyzer/src/dart/analysis/byte_store.dart';
@@ -50,6 +53,7 @@
 
 import 'metrics_util.dart';
 import 'output_utilities.dart';
+import 'relevance_table_generator.dart';
 import 'visitors.dart';
 
 Future<void> main(List<String> args) async {
@@ -61,17 +65,75 @@
   }
 
   var options = CompletionMetricsOptions(result);
-  var root = result.rest[0];
-  print('Analyzing root: "$root"');
+  var provider = PhysicalResourceProvider.INSTANCE;
+  if (result.wasParsed('reduceDir')) {
+    var targetMetrics = <CompletionMetrics>[];
+    var dir = provider.getFolder(result['reduceDir']);
+    var computer = CompletionMetricsComputer('', options);
+    for (var child in dir.getChildren()) {
+      if (child is File) {
+        var metricsList =
+            (json.decode(child.readAsStringSync()) as List<dynamic>)
+                .map((map) =>
+                    CompletionMetrics.fromJson(map as Map<String, dynamic>))
+                .toList();
+        if (targetMetrics.isEmpty) {
+          targetMetrics.addAll(metricsList);
+        } else if (targetMetrics.length != metricsList.length) {
+          throw StateError('metrics lengths differ');
+        } else {
+          for (var i = 0; i < targetMetrics.length; i++) {
+            targetMetrics[i].addData(metricsList[i]);
+          }
+        }
+      }
+    }
+    computer.targetMetrics.addAll(targetMetrics);
+    computer.printResults();
+    return;
+  }
+
+  var rootPath = result.rest[0];
+  print('Analyzing root: "$rootPath"');
   var stopwatch = Stopwatch()..start();
-  var computer = CompletionMetricsComputer(root, options);
+  var computer = CompletionMetricsComputer(rootPath, options);
   var code = await computer.computeMetrics();
   stopwatch.stop();
 
   var duration = Duration(milliseconds: stopwatch.elapsedMilliseconds);
   print('');
   print('Metrics computed in $duration');
-  computer.printResults();
+
+  File uniqueDataFile() {
+    var dataDir = result['mapDir'];
+    var baseFileName = provider.pathContext.basename(rootPath);
+    var index = 1;
+    while (index < 10000) {
+      var suffix = (index++).toString();
+      suffix = '0000'.substring(suffix.length) + suffix + '.json';
+      var fileName = baseFileName + suffix;
+      var filePath = provider.pathContext.join(dataDir, fileName);
+      var file = provider.getFile(filePath);
+      if (!file.exists) {
+        return file;
+      }
+    }
+
+    /// If there are more than 10000 directories with the same name, just
+    /// overwrite a previously generated file.
+    var fileName = baseFileName + '9999';
+    var filePath = provider.pathContext.join(dataDir, fileName);
+    return provider.getFile(filePath);
+  }
+
+  if (result.wasParsed('mapDir')) {
+    var dataFile = uniqueDataFile();
+    var map =
+        computer.targetMetrics.map((metrics) => metrics.toJson()).toList();
+    dataFile.writeAsStringSync(json.encode(map));
+  } else {
+    computer.printResults();
+  }
   return io.exit(code);
 }
 
@@ -148,7 +210,18 @@
         defaultsTo: false,
         help: 'Print information about the completion requests that had the '
             'worst mrr scores.',
-        negatable: false);
+        negatable: false)
+    ..addOption(
+      'mapDir',
+      help: 'The absolute path of the directory to which the completion '
+          'metrics data will be written. Using this option will prevent the '
+          'completion results from being written in a textual form.',
+    )
+    ..addOption(
+      'reduceDir',
+      help: 'The absolute path of the directory from which the completion '
+          'metrics data will be read.',
+    );
 }
 
 /// Print usage information for this tool.
@@ -170,16 +243,16 @@
   if (result.wasParsed('help')) {
     printUsage(parser);
     return false;
+  } else if (result.wasParsed('reduceDir')) {
+    return validateDir(parser, result['reduceDir']);
   } else if (result.rest.length != 1) {
     printUsage(parser, error: 'No package path specified.');
     return false;
   }
-  var rootPath = result.rest[0];
-  if (!io.Directory(rootPath).existsSync()) {
-    printUsage(parser, error: 'The directory "$rootPath" does not exist.');
-    return false;
+  if (result.wasParsed('mapDir')) {
+    return validateDir(parser, result['mapDir']);
   }
-  return true;
+  return validateDir(parser, result.rest[0]);
 }
 
 /// An indication of the group in which the completion falls for the purposes of
@@ -233,56 +306,153 @@
   /// The function to be executed when this metrics collector is disabled.
   final void Function() disableFunction;
 
-  Counter completionCounter = Counter('all completions');
+  final Counter completionCounter = Counter('all completions');
 
-  Counter completionMissedTokenCounter =
+  final Counter completionMissedTokenCounter =
       Counter('unsuccessful completion token counter');
 
-  Counter completionKindCounter =
+  final Counter completionKindCounter =
       Counter('unsuccessful completion kind counter');
 
-  Counter completionElementKindCounter =
+  final Counter completionElementKindCounter =
       Counter('unsuccessful completion element kind counter');
 
-  ArithmeticMeanComputer meanCompletionMS =
+  final ArithmeticMeanComputer meanCompletionMS =
       ArithmeticMeanComputer('ms per completion');
 
-  MeanReciprocalRankComputer mrrComputer =
+  final MeanReciprocalRankComputer mrrComputer =
       MeanReciprocalRankComputer('all completions');
 
-  MeanReciprocalRankComputer successfulMrrComputer =
+  final MeanReciprocalRankComputer successfulMrrComputer =
       MeanReciprocalRankComputer('successful completions');
 
   /// A table mapping completion groups to the mrr computer used to track the
   /// quality of suggestions for those groups.
-  Map<CompletionGroup, MeanReciprocalRankComputer> groupMrrComputers = {};
+  final Map<CompletionGroup, MeanReciprocalRankComputer> groupMrrComputers = {};
 
   /// A table mapping locations to the mrr computer used to track the quality of
   /// suggestions for those locations.
-  Map<String, MeanReciprocalRankComputer> locationMrrComputers = {};
+  final Map<String, MeanReciprocalRankComputer> locationMrrComputers = {};
 
-  ArithmeticMeanComputer charsBeforeTop =
+  final ArithmeticMeanComputer charsBeforeTop =
       ArithmeticMeanComputer('chars_before_top');
 
-  ArithmeticMeanComputer charsBeforeTopFive =
+  final ArithmeticMeanComputer charsBeforeTopFive =
       ArithmeticMeanComputer('chars_before_top_five');
 
-  ArithmeticMeanComputer insertionLengthTheoretical =
+  final ArithmeticMeanComputer insertionLengthTheoretical =
       ArithmeticMeanComputer('insertion_length_theoretical');
 
   /// The places in which a completion location was requested when none was
   /// available.
-  Set<String> missingCompletionLocations = {};
+  final Set<String> missingCompletionLocations = {};
 
   /// The completion locations for which no relevance table was available.
-  Set<String> missingCompletionLocationTables = {};
+  final Set<String> missingCompletionLocationTables = {};
 
-  Map<CompletionGroup, List<CompletionResult>> slowestResults = {};
+  final Map<CompletionGroup, List<CompletionResult>> slowestResults = {};
 
-  Map<CompletionGroup, List<CompletionResult>> worstResults = {};
+  final Map<CompletionGroup, List<CompletionResult>> worstResults = {};
 
   CompletionMetrics(this.name, {this.enableFunction, this.disableFunction});
 
+  /// Return an instance extracted from the decoded JSON [map].
+  factory CompletionMetrics.fromJson(Map<String, dynamic> map) {
+    var metrics = CompletionMetrics(map['name'] as String);
+    metrics.completionCounter
+        .fromJson(map['completionCounter'] as Map<String, dynamic>);
+    metrics.completionMissedTokenCounter
+        .fromJson(map['completionMissedTokenCounter'] as Map<String, dynamic>);
+    metrics.completionKindCounter
+        .fromJson(map['completionKindCounter'] as Map<String, dynamic>);
+    metrics.completionElementKindCounter
+        .fromJson(map['completionElementKindCounter'] as Map<String, dynamic>);
+    metrics.meanCompletionMS
+        .fromJson(map['meanCompletionMS'] as Map<String, dynamic>);
+    metrics.mrrComputer.fromJson(map['mrrComputer'] as Map<String, dynamic>);
+    metrics.successfulMrrComputer
+        .fromJson(map['successfulMrrComputer'] as Map<String, dynamic>);
+    for (var entry
+        in (map['groupMrrComputers'] as Map<String, dynamic>).entries) {
+      var group = CompletionGroup.values[int.parse(entry.key)];
+      metrics.groupMrrComputers[group] = MeanReciprocalRankComputer(group.name)
+        ..fromJson(entry.value);
+    }
+    for (var entry
+        in (map['locationMrrComputers'] as Map<String, dynamic>).entries) {
+      var location = entry.key;
+      metrics.locationMrrComputers[location] =
+          MeanReciprocalRankComputer(location)..fromJson(entry.value);
+    }
+    metrics.charsBeforeTop
+        .fromJson(map['charsBeforeTop'] as Map<String, dynamic>);
+    metrics.charsBeforeTopFive
+        .fromJson(map['charsBeforeTopFive'] as Map<String, dynamic>);
+    metrics.insertionLengthTheoretical
+        .fromJson(map['insertionLengthTheoretical'] as Map<String, dynamic>);
+    for (var element in map['missingCompletionLocations'] as List<dynamic>) {
+      metrics.missingCompletionLocations.add(element);
+    }
+    for (var element
+        in map['missingCompletionLocationTables'] as List<dynamic>) {
+      metrics.missingCompletionLocationTables.add(element);
+    }
+    for (var entry in (map['slowestResults'] as Map<String, dynamic>).entries) {
+      var group = CompletionGroup.values[int.parse(entry.key)];
+      var results = (entry.value as List<dynamic>)
+          .map((map) => CompletionResult.fromJson(map as Map<String, dynamic>))
+          .toList();
+      metrics.slowestResults[group] = results;
+    }
+    for (var entry in (map['worstResults'] as Map<String, dynamic>).entries) {
+      var group = CompletionGroup.values[int.parse(entry.key)];
+      var results = (entry.value as List<dynamic>)
+          .map((map) => CompletionResult.fromJson(map as Map<String, dynamic>))
+          .toList();
+      metrics.worstResults[group] = results;
+    }
+    return metrics;
+  }
+
+  /// Add the data from the given [metrics] to this metrics.
+  void addData(CompletionMetrics metrics) {
+    completionCounter.addData(metrics.completionCounter);
+    completionMissedTokenCounter.addData(metrics.completionMissedTokenCounter);
+    completionKindCounter.addData(metrics.completionKindCounter);
+    completionElementKindCounter.addData(metrics.completionElementKindCounter);
+    meanCompletionMS.addData(metrics.meanCompletionMS);
+    mrrComputer.addData(metrics.mrrComputer);
+    successfulMrrComputer.addData(metrics.successfulMrrComputer);
+    for (var entry in metrics.groupMrrComputers.entries) {
+      var group = entry.key;
+      groupMrrComputers
+          .putIfAbsent(group, () => MeanReciprocalRankComputer(group.name))
+          .addData(entry.value);
+    }
+    for (var entry in metrics.locationMrrComputers.entries) {
+      var location = entry.key;
+      locationMrrComputers
+          .putIfAbsent(location, () => MeanReciprocalRankComputer(location))
+          .addData(entry.value);
+    }
+    charsBeforeTop.addData(metrics.charsBeforeTop);
+    charsBeforeTopFive.addData(metrics.charsBeforeTopFive);
+    insertionLengthTheoretical.addData(metrics.insertionLengthTheoretical);
+    missingCompletionLocations.addAll(metrics.missingCompletionLocations);
+    missingCompletionLocationTables
+        .addAll(metrics.missingCompletionLocationTables);
+    for (var resultList in metrics.slowestResults.values) {
+      for (var result in resultList) {
+        _recordSlowestResult(result);
+      }
+    }
+    for (var resultList in metrics.worstResults.values) {
+      for (var result in resultList) {
+        _recordWorstResult(result);
+      }
+    }
+  }
+
   /// Perform any operations required in order to revert computing the kind of
   /// completions represented by this metrics collector.
   void disable() {
@@ -310,6 +480,35 @@
     _recordMissingInformation(listener);
   }
 
+  Map<String, dynamic> toJson() {
+    return {
+      'name': name,
+      'completionCounter': completionCounter.toJson(),
+      'completionMissedTokenCounter': completionMissedTokenCounter.toJson(),
+      'completionKindCounter': completionKindCounter.toJson(),
+      'completionElementKindCounter': completionElementKindCounter.toJson(),
+      'meanCompletionMS': meanCompletionMS.toJson(),
+      'mrrComputer': mrrComputer.toJson(),
+      'successfulMrrComputer': successfulMrrComputer.toJson(),
+      'groupMrrComputers': groupMrrComputers
+          .map((key, value) => MapEntry(key.index.toString(), value.toJson())),
+      'locationMrrComputers': locationMrrComputers
+          .map((key, value) => MapEntry(key, value.toJson())),
+      'charsBeforeTop': charsBeforeTop.toJson(),
+      'charsBeforeTopFive': charsBeforeTopFive.toJson(),
+      'insertionLengthTheoretical': insertionLengthTheoretical.toJson(),
+      'missingCompletionLocations': missingCompletionLocations.toList(),
+      'missingCompletionLocationTables':
+          missingCompletionLocationTables.toList(),
+      'slowestResults': slowestResults.map((key, value) => MapEntry(
+          key.index.toString(),
+          value.map((result) => result.toJson()).toList())),
+      'worstResults': worstResults.map((key, value) => MapEntry(
+          key.index.toString(),
+          value.map((result) => result.toJson()).toList())),
+    };
+  }
+
   /// If the completion location was requested but missing when computing the
   /// [result], then record where that happened.
   void _recordMissingInformation(MetricsSuggestionListener listener) {
@@ -1325,6 +1524,32 @@
       this.completionLocation,
       this.elapsedMS);
 
+  /// Return an instance extracted from the decoded JSON [map].
+  factory CompletionResult.fromJson(Map<String, dynamic> map) {
+    var place = Place.fromJson(map['place'] as Map<String, dynamic>);
+    var actualSuggestion = SuggestionData.fromJson(
+        map['actualSuggestion'] as Map<String, dynamic>);
+    var topSuggestions = (map['topSuggestions'] as List<dynamic>)
+        ?.map((map) => SuggestionData.fromJson(map as Map<String, dynamic>))
+        ?.toList();
+    var precedingRelevanceCounts =
+        (map['precedingRelevanceCounts'] as Map<String, dynamic>)
+            ?.map((key, value) => MapEntry(int.parse(key), value as int));
+    var expectedCompletion = ExpectedCompletion.fromJson(
+        map['expectedCompletion'] as Map<String, dynamic>);
+    var completionLocation = map['completionLocation'] as String;
+    var elapsedMS = map['elapsedMS'] as int;
+    return CompletionResult(
+        place,
+        null,
+        actualSuggestion,
+        topSuggestions,
+        precedingRelevanceCounts,
+        expectedCompletion,
+        completionLocation,
+        elapsedMS);
+  }
+
   /// Return the completion group for the location at which completion was
   /// requested.
   CompletionGroup get group {
@@ -1381,6 +1606,23 @@
     return CompletionGroup.unknown;
   }
 
+  /// Return a map used to represent this completion result in a JSON structure.
+  Map<String, dynamic> toJson() {
+    return {
+      'place': place.toJson(),
+      'actualSuggestion': actualSuggestion.toJson(),
+      if (topSuggestions != null)
+        'topSuggestions':
+            topSuggestions.map((suggestion) => suggestion.toJson()).toList(),
+      if (precedingRelevanceCounts != null)
+        'precedingRelevanceCounts': precedingRelevanceCounts
+            .map((key, value) => MapEntry(key.toString(), value)),
+      'expectedCompletion': expectedCompletion.toJson(),
+      'completionLocation': completionLocation,
+      'elapsedMS': elapsedMS,
+    };
+  }
+
   /// Return the element associated with the syntactic [entity], or `null` if
   /// there is no such element.
   Element _getElement(SyntacticEntity entity) {
@@ -1527,6 +1769,22 @@
   List<double> features;
 
   SuggestionData(this.suggestion, this.features);
+
+  /// Return an instance extracted from the decoded JSON [map].
+  factory SuggestionData.fromJson(Map<String, dynamic> map) {
+    return SuggestionData(
+        protocol.CompletionSuggestion.fromJson(ResponseDecoder(null), '',
+            map['suggestion'] as Map<String, dynamic>),
+        (map['features'] as List<dynamic>).cast<double>());
+  }
+
+  /// Return a map used to represent this suggestion data in a JSON structure.
+  Map<String, dynamic> toJson() {
+    return {
+      'suggestion': suggestion.toJson(),
+      'features': features,
+    };
+  }
 }
 
 extension on CompletionGroup {
diff --git a/pkg/analysis_server/tool/code_completion/metrics_util.dart b/pkg/analysis_server/tool/code_completion/metrics_util.dart
index 98acfce..f5c385e 100644
--- a/pkg/analysis_server/tool/code_completion/metrics_util.dart
+++ b/pkg/analysis_server/tool/code_completion/metrics_util.dart
@@ -17,6 +17,12 @@
 
   num get mean => sum / count;
 
+  /// Add the data from the given [computer] to this computer.
+  void addData(ArithmeticMeanComputer computer) {
+    sum += computer.sum;
+    count += computer.count;
+  }
+
   void addValue(num val) {
     sum += val;
     count++;
@@ -27,12 +33,27 @@
     count = 0;
   }
 
+  /// Set the state of this computer to the state recorded in the decoded JSON
+  /// [map].
+  void fromJson(Map<String, dynamic> map) {
+    sum = map['sum'] as num;
+    count = map['count'] as int;
+  }
+
   void printMean() {
     print('Mean \'$name\' ${mean.toStringAsFixed(6)} (total = $count)');
   }
+
+  /// Return a map used to represent this computer in a JSON structure.
+  Map<String, dynamic> toJson() {
+    return {
+      'sum': sum,
+      'count': count,
+    };
+  }
 }
 
-/// A simple counter class.  A [String] name is passed to name the counter. Each
+/// A simple counter class. A [String] name is passed to name the counter. Each
 /// time something is counted, a non-null, non-empty [String] key is passed to
 /// [count] to increment the amount from zero. [printCounterValues] is provided
 /// to have a [String] summary of the generated counts, example:
@@ -58,6 +79,15 @@
 
   int get totalCount => _totalCount;
 
+  /// Add the data from the given [counter] to this counter.
+  void addData(Counter counter) {
+    for (var entry in counter._buckets.entries) {
+      var bucket = entry.key;
+      _buckets[bucket] = (_buckets[bucket] ?? 0) + entry.value;
+    }
+    _totalCount += counter._totalCount;
+  }
+
   void clear() {
     _buckets.clear();
     _totalCount = 0;
@@ -73,6 +103,15 @@
     _totalCount += countNumber;
   }
 
+  /// Set the state of this counter to the state recorded in the decoded JSON
+  /// [map].
+  void fromJson(Map<String, dynamic> map) {
+    for (var entry in (map['buckets'] as Map<String, dynamic>).entries) {
+      _buckets[entry.key] = entry.value as int;
+    }
+    _totalCount = map['totalCount'] as int;
+  }
+
   int getCountOf(String id) => _buckets[id] ?? 0;
 
   void printCounterValues() {
@@ -93,6 +132,14 @@
       print('<no counts>');
     }
   }
+
+  /// Return a map used to represent this counter in a JSON structure.
+  Map<String, dynamic> toJson() {
+    return {
+      'buckets': _buckets,
+      'totalCount': _totalCount,
+    };
+  }
 }
 
 /// A computer for the mean reciprocal rank. The MRR as well as the MRR only
@@ -125,6 +172,13 @@
     return _sum_5 / count;
   }
 
+  /// Add the data from the given [computer] to this computer.
+  void addData(MeanReciprocalRankComputer computer) {
+    _sum += computer._sum;
+    _sum_5 += computer._sum_5;
+    _count += computer._count;
+  }
+
   void addRank(int rank) {
     if (rank != 0) {
       _sum += 1 / rank;
@@ -141,6 +195,14 @@
     _count = 0;
   }
 
+  /// Set the state of this computer to the state recorded in the decoded JSON
+  /// [map].
+  void fromJson(Map<String, dynamic> map) {
+    _sum = map['sum'] as double;
+    _sum_5 = map['sum_5'] as double;
+    _count = map['count'] as int;
+  }
+
   void printMean() {
     print('Mean Reciprocal Rank \'$name\' (total = $count)');
     print('mrr   = ${mrr.toStringAsFixed(6)} '
@@ -149,6 +211,15 @@
     print('mrr_5 = ${mrr_5.toStringAsFixed(6)} '
         '(inverse = ${(1 / mrr_5).toStringAsFixed(3)})');
   }
+
+  /// Return a map used to represent this computer in a JSON structure.
+  Map<String, dynamic> toJson() {
+    return {
+      'sum': _sum,
+      'sum_5': _sum_5,
+      'count': _count,
+    };
+  }
 }
 
 /// An immutable class to represent the placement in some list, for example '2nd
@@ -164,6 +235,11 @@
       : assert(_numerator > 0),
         assert(_denominator >= _numerator);
 
+  /// Return an instance extracted from the decoded JSON [map].
+  factory Place.fromJson(Map<String, dynamic> map) {
+    return Place(map['numerator'] as int, map['denominator'] as int);
+  }
+
   const Place.none()
       : _numerator = 0,
         _denominator = 0;
@@ -182,4 +258,12 @@
       other is Place &&
       _numerator == other._numerator &&
       _denominator == other._denominator;
+
+  /// Return a map used to represent this place in a JSON structure.
+  Map<String, dynamic> toJson() {
+    return {
+      'numerator': _numerator,
+      'denominator': _denominator,
+    };
+  }
 }
diff --git a/pkg/analysis_server/tool/code_completion/visitors.dart b/pkg/analysis_server/tool/code_completion/visitors.dart
index db428f9..69d68eb 100644
--- a/pkg/analysis_server/tool/code_completion/visitors.dart
+++ b/pkg/analysis_server/tool/code_completion/visitors.dart
@@ -2,6 +2,7 @@
 // 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.
 
+import 'package:analysis_server/src/protocol/protocol_internal.dart';
 import 'package:analysis_server/src/protocol_server.dart' as protocol;
 import 'package:analysis_server/src/services/completion/dart/keyword_contributor.dart';
 import 'package:analyzer/dart/ast/ast.dart';
@@ -32,6 +33,32 @@
       this._columnNumber, this._kind, this._elementKind)
       : _completionString = null;
 
+  /// Return an instance extracted from the decoded JSON [map].
+  factory ExpectedCompletion.fromJson(Map<String, dynamic> map) {
+    var jsonDecoder = ResponseDecoder(null);
+    var filePath = map['filePath'] as String;
+    var offset = map['offset'] as int;
+    var lineNumber = map['lineNumber'] as int;
+    var columnNumber = map['columnNumber'] as int;
+    var completionString = map['completionString'] as String;
+    var kind = map['kind'] != null
+        ? protocol.CompletionSuggestionKind.fromJson(
+            jsonDecoder, '', map['kind'] as String)
+        : null;
+    var elementKind = map['elementKind'] != null
+        ? protocol.ElementKind.fromJson(
+            jsonDecoder, '', map['elementKind'] as String)
+        : null;
+    return ExpectedCompletion.specialCompletionString(
+        filePath,
+        _SyntacticEntity(offset),
+        lineNumber,
+        columnNumber,
+        completionString,
+        kind,
+        elementKind);
+  }
+
   ExpectedCompletion.specialCompletionString(
       this._filePath,
       this._entity,
@@ -76,6 +103,19 @@
     return false;
   }
 
+  /// Return a map used to represent this expected completion in a JSON structure.
+  Map<String, dynamic> toJson() {
+    return {
+      'filePath': _filePath,
+      'offset': _entity.offset,
+      'lineNumber': _lineNumber,
+      'columnNumber': _columnNumber,
+      'completionString': _completionString,
+      if (_kind != null) 'kind': _kind.toJson(),
+      if (_elementKind != null) 'elementKind': _elementKind.toJson(),
+    };
+  }
+
   @override
   String toString() =>
       "'$completion', kind = $kind, elementKind = $elementKind, $location";
@@ -757,3 +797,16 @@
     return true;
   }
 }
+
+class _SyntacticEntity implements SyntacticEntity {
+  @override
+  final int offset;
+
+  _SyntacticEntity(this.offset);
+
+  @override
+  int get end => offset + length;
+
+  @override
+  int get length => 0;
+}
diff --git a/pkg/analyzer/lib/src/dart/resolver/flow_analysis_visitor.dart b/pkg/analyzer/lib/src/dart/resolver/flow_analysis_visitor.dart
index 3775ad7..40726d1 100644
--- a/pkg/analyzer/lib/src/dart/resolver/flow_analysis_visitor.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/flow_analysis_visitor.dart
@@ -592,11 +592,7 @@
     node.catchClauses.accept(this);
     assignedVariables.endNode(node);
 
-    if (finallyBlock != null) {
-      assignedVariables.beginNode();
-      finallyBlock.accept(this);
-      assignedVariables.endNode(finallyBlock);
-    }
+    finallyBlock?.accept(this);
   }
 
   @override
diff --git a/pkg/analyzer/lib/src/generated/resolver.dart b/pkg/analyzer/lib/src/generated/resolver.dart
index d65859f..fcdafed 100644
--- a/pkg/analyzer/lib/src/generated/resolver.dart
+++ b/pkg/analyzer/lib/src/generated/resolver.dart
@@ -516,29 +516,27 @@
   List<DiagnosticMessage> computeWhyNotPromotedMessages(
       Expression? receiver, SyntacticEntity errorEntity) {
     List<DiagnosticMessage> messages = [];
-    if (receiver != null) {
-      var whyNotPromoted = flowAnalysis?.flow?.whyNotPromoted(receiver);
-      if (whyNotPromoted != null) {
-        for (var entry in whyNotPromoted.entries) {
-          var whyNotPromotedVisitor = _WhyNotPromotedVisitor(
-              source, receiver, flowAnalysis!.dataForTesting);
-          if (typeSystem.isPotentiallyNullable(entry.key)) continue;
-          var message = entry.value.accept(whyNotPromotedVisitor);
-          if (message != null) {
-            if (flowAnalysis!.dataForTesting != null) {
-              var nonPromotionReasonText = entry.value.shortName;
-              if (whyNotPromotedVisitor.propertyReference != null) {
-                var id =
-                    computeMemberId(whyNotPromotedVisitor.propertyReference!);
-                nonPromotionReasonText += '($id)';
-              }
-              flowAnalysis!.dataForTesting!.nonPromotionReasons[errorEntity] =
-                  nonPromotionReasonText;
+    var whyNotPromoted = flowAnalysis?.flow?.whyNotPromoted(receiver);
+    if (whyNotPromoted != null) {
+      for (var entry in whyNotPromoted.entries) {
+        var whyNotPromotedVisitor = _WhyNotPromotedVisitor(
+            source, receiver, errorEntity, flowAnalysis!.dataForTesting);
+        if (typeSystem.isPotentiallyNullable(entry.key)) continue;
+        var message = entry.value.accept(whyNotPromotedVisitor);
+        if (message != null) {
+          if (flowAnalysis!.dataForTesting != null) {
+            var nonPromotionReasonText = entry.value.shortName;
+            if (whyNotPromotedVisitor.propertyReference != null) {
+              var id =
+                  computeMemberId(whyNotPromotedVisitor.propertyReference!);
+              nonPromotionReasonText += '($id)';
             }
-            messages = [message];
+            flowAnalysis!.dataForTesting!.nonPromotionReasons[errorEntity] =
+                nonPromotionReasonText;
           }
-          break;
+          messages = [message];
         }
+        break;
       }
     }
     return messages;
@@ -3343,13 +3341,16 @@
             PromotableElement> {
   final Source source;
 
-  final Expression _receiver;
+  final Expression? _receiver;
+
+  final SyntacticEntity _errorEntity;
 
   final FlowAnalysisDataForTesting? _dataForTesting;
 
   PropertyAccessorElement? propertyReference;
 
-  _WhyNotPromotedVisitor(this.source, this._receiver, this._dataForTesting);
+  _WhyNotPromotedVisitor(
+      this.source, this._receiver, this._errorEntity, this._dataForTesting);
 
   @override
   DiagnosticMessage? visitDemoteViaExplicitWrite(
@@ -3420,8 +3421,8 @@
     return DiagnosticMessageImpl(
         filePath: source.fullName,
         message: "'this' can't be promoted.",
-        offset: _receiver.offset,
-        length: _receiver.length);
+        offset: _errorEntity.offset,
+        length: _errorEntity.length);
   }
 
   DiagnosticMessageImpl _contextMessageForProperty(
diff --git a/pkg/dartdev/doc/dart-fix.md b/pkg/dartdev/doc/dart-fix.md
index 95dbf1a..2fd0f73 100644
--- a/pkg/dartdev/doc/dart-fix.md
+++ b/pkg/dartdev/doc/dart-fix.md
@@ -2,19 +2,18 @@
 
 ## What is it?
 
-`dart fix` is a command line tool and part of the regular `dart` tool. It is used
-to batch apply fixes for analysis issues.
+`dart fix` is a command line tool and part of the regular `dart` tool. It is
+used to batch apply fixes for analysis issues.
 
 ## How does it work?
 
-`dart fix` runs over your project looking for analysis issues. For each issue
-it checks whether there is an automated fix that can be applied. These fixes
-are generaly either in response to a lint or hint in your code, or part of
-upgrading your source to newer package APIs.
+`dart fix` runs over your project looking for analysis issues. For each issue it
+checks whether there is an automated fix that can be applied. These fixes are
+generaly either in response to a lint or hint in your code, or part of upgrading
+your source to newer package APIs.
 
-For the first type of change, the fixes are generally in response to the set
-of lints and analysis configuration specified in your [analysis_options.yaml]
-file.
+For the first type of change, the fixes are generally in response to the set of
+lints and analysis configuration specified in your [analysis_options.yaml] file.
 
 The second type of change - upgrading to newer package APIs - is performed
 based on API changes defined for specific packages. This declarative definition
@@ -24,18 +23,16 @@
 ## Command line usage
 
 ```
-Fix Dart source code.
+Apply automated fixes to Dart source code.
 
-This tool looks for and fixes analysis issues that have associated automated
-fixes or issues that have associated package API migration information.
+This tool looks for and fixes analysis issues that have associated automated fixes.
 
-To use the tool, run one of:
-- 'dart fix --dry-run' for a preview of the proposed changes for a project
-- 'dart fix --apply' to apply the changes
+To use the tool, run either 'dart fix --dry-run' for a preview of the proposed changes for a project, or 'dart fix --apply' to
+apply the changes.
 
 Usage: dart fix [arguments]
 -h, --help       Print this usage information.
--n, --dry-run    Show which files would be modified but make no changes.
+-n, --dry-run    Preview the proposed changes but make no changes.
     --apply      Apply the proposed changes.
 
 Run "dart help" to see global options.
diff --git a/pkg/front_end/lib/src/fasta/kernel/body_builder.dart b/pkg/front_end/lib/src/fasta/kernel/body_builder.dart
index e5da386..06e8346 100644
--- a/pkg/front_end/lib/src/fasta/kernel/body_builder.dart
+++ b/pkg/front_end/lib/src/fasta/kernel/body_builder.dart
@@ -2757,9 +2757,6 @@
       // This is matched by the call to [beginNode] in [beginTryStatement].
       tryStatementInfoStack = tryStatementInfoStack
           .prepend(typeInferrer?.assignedVariables?.deferNode());
-
-      // This is matched by the call to [endNode] in [endTryStatement].
-      typeInferrer?.assignedVariables?.beginNode();
     }
     super.beginBlock(token, blockKind);
   }
@@ -3918,8 +3915,6 @@
     Statement finallyBlock;
     if (finallyKeyword != null) {
       finallyBlock = pop();
-      // This is matched by the call to [beginNode] in [beginBlock].
-      typeInferrer?.assignedVariables?.endNode(finallyBlock);
     } else {
       // This is matched by the call to [beginNode] in [beginTryStatement].
       tryStatementInfoStack = tryStatementInfoStack
diff --git a/pkg/front_end/lib/src/fasta/kernel/constant_evaluator.dart b/pkg/front_end/lib/src/fasta/kernel/constant_evaluator.dart
index 4987b10..4c4e74d 100644
--- a/pkg/front_end/lib/src/fasta/kernel/constant_evaluator.dart
+++ b/pkg/front_end/lib/src/fasta/kernel/constant_evaluator.dart
@@ -2598,6 +2598,17 @@
   Constant visitStaticInvocation(StaticInvocation node) {
     final Procedure target = node.target;
     final Arguments arguments = node.arguments;
+    List<DartType> types = _evaluateTypeArguments(node, arguments);
+    if (types == null && _gotError != null) {
+      AbortConstant error = _gotError;
+      _gotError = null;
+      return error;
+    }
+    assert(_gotError == null);
+    assert(types != null);
+
+    final List<DartType> typeArguments = convertTypes(types);
+
     final List<Constant> positionals = _evaluatePositionalArguments(arguments);
     if (positionals == null && _gotError != null) {
       AbortConstant error = _gotError;
@@ -2684,6 +2695,8 @@
       }
     } else if (target.isExtensionMember) {
       return createErrorConstant(node, messageConstEvalExtension);
+    } else if (enableConstFunctions && target.kind == ProcedureKind.Method) {
+      return _handleStaticInvocation(node, typeArguments, positionals, named);
     }
 
     String name = target.name.text;
@@ -2697,6 +2710,47 @@
     return createInvalidExpressionConstant(node, "Invocation of $name");
   }
 
+  Constant _handleStaticInvocation(
+      StaticInvocation node,
+      List<DartType> typeArguments,
+      List<Constant> positionalArguments,
+      Map<String, Constant> namedArguments) {
+    return withNewEnvironment(() {
+      final FunctionNode function = node.target.function;
+
+      // Map arguments from caller to callee.
+      for (int i = 0; i < function.typeParameters.length; i++) {
+        env.addTypeParameterValue(function.typeParameters[i], typeArguments[i]);
+      }
+      for (int i = 0; i < function.positionalParameters.length; i++) {
+        final VariableDeclaration parameter = function.positionalParameters[i];
+        final Constant value = (i < positionalArguments.length)
+            ? positionalArguments[i]
+            // TODO(johnniwinther): This should call [_evaluateSubexpression].
+            : _evaluateNullableSubexpression(parameter.initializer);
+        if (value is AbortConstant) return value;
+        env.addVariableValue(parameter, value);
+      }
+      for (final VariableDeclaration parameter in function.namedParameters) {
+        final Constant value = namedArguments[parameter.name] ??
+            // TODO(johnniwinther): This should call [_evaluateSubexpression].
+            _evaluateNullableSubexpression(parameter.initializer);
+        if (value is AbortConstant) return value;
+        env.addVariableValue(parameter, value);
+      }
+      return function.body.accept(this);
+    });
+  }
+
+  @override
+  Constant visitReturnStatement(ReturnStatement node) {
+    if (!enableConstFunctions) {
+      return createInvalidExpressionConstant(
+          node, "Return statements are not supported.");
+    }
+    return node.expression.accept(this);
+  }
+
   @override
   Constant visitAsExpression(AsExpression node) {
     final Constant constant = _evaluateSubexpression(node.operand);
diff --git a/pkg/front_end/lib/src/fasta/kernel/expression_generator.dart b/pkg/front_end/lib/src/fasta/kernel/expression_generator.dart
index 3208247..4e59fb4 100644
--- a/pkg/front_end/lib/src/fasta/kernel/expression_generator.dart
+++ b/pkg/front_end/lib/src/fasta/kernel/expression_generator.dart
@@ -1487,7 +1487,8 @@
       int offset, List<UnresolvedType> typeArguments, Arguments arguments,
       {bool isTypeArgumentsInForest = false}) {
     if (_helper.constantContext != ConstantContext.none &&
-        !_helper.isIdentical(readTarget)) {
+        !_helper.isIdentical(readTarget) &&
+        !_helper.enableConstFunctionsInLibrary) {
       return _helper.buildProblem(
           templateNotConstantExpression.withArguments('Method invocation'),
           offset,
diff --git a/pkg/test_runner/lib/src/compiler_configuration.dart b/pkg/test_runner/lib/src/compiler_configuration.dart
index 502eaf3..55bf077 100644
--- a/pkg/test_runner/lib/src/compiler_configuration.dart
+++ b/pkg/test_runner/lib/src/compiler_configuration.dart
@@ -558,7 +558,7 @@
     args.addAll(options);
     args.addAll(_configuration.sharedOptions);
 
-    bool d8Runtime = _configuration.runtime == Runtime.d8;
+    var d8Runtime = _configuration.runtime == Runtime.d8;
 
     args.addAll([
       "--ignore-unrecognized-flags",
diff --git a/tests/language/const_functions/const_functions_disabled_simple_invocations.dart b/tests/language/const_functions/const_functions_disabled_simple_invocations.dart
new file mode 100644
index 0000000..522f4fa
--- /dev/null
+++ b/tests/language/const_functions/const_functions_disabled_simple_invocations.dart
@@ -0,0 +1,67 @@
+// 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.
+
+// Tests 'const-function' flag disabled for simple function invocations.
+
+const binary = binaryFn(2, 1);
+//             ^^^^^^^^^^^^^^
+// [analyzer] COMPILE_TIME_ERROR.CONST_INITIALIZED_WITH_NON_CONSTANT_VALUE
+// [cfe] Method invocation is not a constant expression.
+int binaryFn(int a, int b) => a - b;
+
+const optional = optionalFn(2);
+//               ^^^^^^^^^^^^^
+// [analyzer] COMPILE_TIME_ERROR.CONST_INITIALIZED_WITH_NON_CONSTANT_VALUE
+// [cfe] Method invocation is not a constant expression.
+const optional1 = optionalFn(2, 1);
+//                ^^^^^^^^^^^^^^^^
+// [analyzer] COMPILE_TIME_ERROR.CONST_INITIALIZED_WITH_NON_CONSTANT_VALUE
+// [cfe] Method invocation is not a constant expression.
+int optionalFn(int c, [int d = 0]) => c + d;
+
+const named = namedFn(2, f: 2);
+//            ^^^^^^^^^^^^^^^^
+// [analyzer] COMPILE_TIME_ERROR.CONST_INITIALIZED_WITH_NON_CONSTANT_VALUE
+// [cfe] Method invocation is not a constant expression.
+const named1 = namedFn(2);
+//             ^^^^^^^^^^
+// [analyzer] COMPILE_TIME_ERROR.CONST_INITIALIZED_WITH_NON_CONSTANT_VALUE
+// [cfe] Method invocation is not a constant expression.
+int namedFn(int e, {int f = 3}) => e + f;
+
+const type = typeFn(6);
+//           ^^^^^^^^^
+// [analyzer] COMPILE_TIME_ERROR.CONST_INITIALIZED_WITH_NON_CONSTANT_VALUE
+// [cfe] Method invocation is not a constant expression.
+T typeFn<T>(T x) => x;
+
+const str = stringFn("str");
+//          ^^^^^^^^^^^^^^^
+// [analyzer] COMPILE_TIME_ERROR.CONST_INITIALIZED_WITH_NON_CONSTANT_VALUE
+// [cfe] Method invocation is not a constant expression.
+String stringFn(String s) => s + "ing";
+
+const eq = equalFn(2, 2);
+//         ^^^^^^^^^^^^^
+// [analyzer] COMPILE_TIME_ERROR.CONST_INITIALIZED_WITH_NON_CONSTANT_VALUE
+// [cfe] Method invocation is not a constant expression.
+bool equalFn(int a, int b) => a == b;
+
+const neg = unary(2);
+//          ^^^^^^^^
+// [analyzer] COMPILE_TIME_ERROR.CONST_INITIALIZED_WITH_NON_CONSTANT_VALUE
+// [cfe] Method invocation is not a constant expression.
+int unary(int a) => -a;
+
+const boolean = boolFn(true, false);
+//              ^^^^^^^^^^^^^^^^^^^
+// [analyzer] COMPILE_TIME_ERROR.CONST_INITIALIZED_WITH_NON_CONSTANT_VALUE
+// [cfe] Method invocation is not a constant expression.
+bool boolFn(bool a, bool b) => a || b;
+
+const doub = doubleFn(2.2, 2);
+//           ^^^^^^^^^^^^^^^^
+// [analyzer] COMPILE_TIME_ERROR.CONST_INITIALIZED_WITH_NON_CONSTANT_VALUE
+// [cfe] Method invocation is not a constant expression.
+double doubleFn(double a, double b) => a * b;
diff --git a/tests/language/const_functions/const_functions_simple_invocations.dart b/tests/language/const_functions/const_functions_simple_invocations.dart
new file mode 100644
index 0000000..2280197
--- /dev/null
+++ b/tests/language/const_functions/const_functions_simple_invocations.dart
@@ -0,0 +1,74 @@
+// 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.
+
+// Tests function invocations that immediately return simple expressions.
+
+// SharedOptions=--enable-experiment=const-functions
+
+import "package:expect/expect.dart";
+
+const binary = binaryFn(2, 1);
+//             ^^^^^^^^^^^^^^
+// [analyzer] COMPILE_TIME_ERROR.CONST_INITIALIZED_WITH_NON_CONSTANT_VALUE
+int binaryFn(int a, int b) => a - b;
+
+const optional = optionalFn(2);
+//               ^^^^^^^^^^^^^
+// [analyzer] COMPILE_TIME_ERROR.CONST_INITIALIZED_WITH_NON_CONSTANT_VALUE
+const optional1 = optionalFn(2, 1);
+//                ^^^^^^^^^^^^^^^^
+// [analyzer] COMPILE_TIME_ERROR.CONST_INITIALIZED_WITH_NON_CONSTANT_VALUE
+int optionalFn(int c, [int d = 0]) => c + d;
+
+const named = namedFn(2, f: 2);
+//            ^^^^^^^^^^^^^^^^
+// [analyzer] COMPILE_TIME_ERROR.CONST_INITIALIZED_WITH_NON_CONSTANT_VALUE
+const named1 = namedFn(2);
+//             ^^^^^^^^^^
+// [analyzer] COMPILE_TIME_ERROR.CONST_INITIALIZED_WITH_NON_CONSTANT_VALUE
+int namedFn(int e, {int f = 3}) => e + f;
+
+const type = typeFn(6);
+//           ^^^^^^^^^
+// [analyzer] COMPILE_TIME_ERROR.CONST_INITIALIZED_WITH_NON_CONSTANT_VALUE
+T typeFn<T>(T x) => x;
+
+const str = stringFn("str");
+//          ^^^^^^^^^^^^^^^
+// [analyzer] COMPILE_TIME_ERROR.CONST_INITIALIZED_WITH_NON_CONSTANT_VALUE
+String stringFn(String s) => s + "ing";
+
+const eq = equalFn(2, 2);
+//         ^^^^^^^^^^^^^
+// [analyzer] COMPILE_TIME_ERROR.CONST_INITIALIZED_WITH_NON_CONSTANT_VALUE
+bool equalFn(int a, int b) => a == b;
+
+const neg = unary(2);
+//          ^^^^^^^^
+// [analyzer] COMPILE_TIME_ERROR.CONST_INITIALIZED_WITH_NON_CONSTANT_VALUE
+int unary(int a) => -a;
+
+const boolean = boolFn(true, false);
+//              ^^^^^^^^^^^^^^^^^^^
+// [analyzer] COMPILE_TIME_ERROR.CONST_INITIALIZED_WITH_NON_CONSTANT_VALUE
+bool boolFn(bool a, bool b) => a || b;
+
+const doub = doubleFn(2.2, 2);
+//           ^^^^^^^^^^^^^^^^
+// [analyzer] COMPILE_TIME_ERROR.CONST_INITIALIZED_WITH_NON_CONSTANT_VALUE
+double doubleFn(double a, double b) => a * b;
+
+void main() {
+  Expect.equals(binary, 1);
+  Expect.equals(optional, 2);
+  Expect.equals(optional1, 3);
+  Expect.equals(named, 4);
+  Expect.equals(named1, 5);
+  Expect.equals(type, 6);
+  Expect.equals(str, "string");
+  Expect.equals(eq, true);
+  Expect.equals(neg, -2);
+  Expect.equals(boolean, true);
+  Expect.equals(doub, 4.4);
+}
diff --git a/tests/language/metadata/metadata_location_test.dart b/tests/language/metadata/metadata_location_test.dart
index 8e7c225..2702eda 100644
--- a/tests/language/metadata/metadata_location_test.dart
+++ b/tests/language/metadata/metadata_location_test.dart
@@ -159,7 +159,16 @@
 typedef void F1<@m X>();
 
 @m
-typedef F2<@m X>= void Function();
+typedef F2<@m X> = void Function();
+
+@m
+typedef F3 = void Function<@m X, @m Y extends X>(@m X, {@m List<X> name});
+
+@m
+typedef F4 = F3 Function<@m X>([@m X, @m int]);
+
+@m
+const int Function<@m X extends Y, @m Y>(@m void, [@m List<X>])? c = null;
 
 @m
 void main() {
diff --git a/third_party/devtools/README b/third_party/devtools/README
new file mode 100644
index 0000000..d35a323
--- /dev/null
+++ b/third_party/devtools/README
@@ -0,0 +1,14 @@
+This folder contains a pre-built Dart DevTools instance which can be served
+as a web application, as well as the package:devtools_server and package:devtools_shared
+from the same revision, used to host and launch DevTools from a Dart process.
+
+First, ensure Flutter is installed and on your path (see https://flutter.dev/docs/get-started/install/linux#install-flutter-manually).
+
+With `flutter` on your path, do the following:
+
+- Run ./update.sh <revision> to build DevTools at a given revision and upload it
+  to CIPD. The uploaded CIPD entry will be tagged with `revision:<revision>`
+- Update DEPS to point to the newly updated DevTools by providing "revision:<revision>"
+  as the version entry for "third_party/devtools"
+
+DevTools CIPD packages are located at https://chrome-infra-packages.appspot.com/p/dart/third_party/flutter/devtools/+/.
diff --git a/third_party/devtools/update.sh b/third_party/devtools/update.sh
new file mode 100755
index 0000000..a69f6ee
--- /dev/null
+++ b/third_party/devtools/update.sh
@@ -0,0 +1,41 @@
+#!/bin/bash
+
+# Copyright 2021 The Dart Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+set -ex #echo on
+
+if [ -z "$1" ]; then
+  echo "Usage: update.sh revision"
+  exit 1
+fi
+
+tmpdir=$(mktemp -d)
+cleanup() {
+  rm -rf "$tmpdir"
+}
+
+trap cleanup EXIT HUP INT QUIT TERM PIPE
+cd "$tmpdir"
+
+# Clone DevTools and build.
+git clone git@github.com:flutter/devtools.git
+cd devtools
+git checkout -b cipd_release $1
+
+./tool/build_release.sh
+
+# Copy the build output as well as the devtools packages needed
+# to serve from DDS.
+mkdir cipd_package
+cp -R packages/devtools/build/ cipd_package/web
+cp -r packages/devtools_server cipd_package
+cp -r packages/devtools_shared cipd_package
+
+cipd create \
+  -name dart/third_party/flutter/devtools \
+  -in cipd_package \
+  -install-mode copy \
+  -tag revision:$1
+
diff --git a/tools/VERSION b/tools/VERSION
index 8789cc9..06f4903 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 13
 PATCH 0
-PRERELEASE 86
+PRERELEASE 87
 PRERELEASE_PATCH 0
\ No newline at end of file