Version 2.12.0-236.0.dev

Merge commit '5e66c2b1a1efa890aef04f7c583ee74cc68f9c51' into 'dev'
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 ade689e..6809bbc 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
@@ -94,12 +94,18 @@
   AssignedVariablesNodeInfo<Variable> deferNode(
       {bool isClosureOrLateVariableInitializer: false}) {
     AssignedVariablesNodeInfo<Variable> info = _stack.removeLast();
+    info._read.removeAll(info._declared);
     info._written.removeAll(info._declared);
+    info._readCaptured.removeAll(info._declared);
     info._captured.removeAll(info._declared);
     AssignedVariablesNodeInfo<Variable> last = _stack.last;
+    last._read.addAll(info._read);
     last._written.addAll(info._written);
+    last._readCaptured.addAll(info._readCaptured);
     last._captured.addAll(info._captured);
     if (isClosureOrLateVariableInitializer) {
+      last._readCaptured.addAll(info._read);
+      _anywhere._readCaptured.addAll(info._read);
       last._captured.addAll(info._written);
       _anywhere._captured.addAll(info._written);
     }
@@ -123,7 +129,9 @@
     AssignedVariablesNodeInfo<Variable> discarded = _stack.removeLast();
     AssignedVariablesNodeInfo<Variable> last = _stack.last;
     last._declared.addAll(discarded._declared);
+    last._read.addAll(discarded._read);
     last._written.addAll(discarded._written);
+    last._readCaptured.addAll(discarded._readCaptured);
     last._captured.addAll(discarded._captured);
   }
 
@@ -154,6 +162,9 @@
           _deferredInfos.isEmpty, "Deferred infos not stored: $_deferredInfos");
       assert(_stack.length == 1, "Unexpected stack: $_stack");
       AssignedVariablesNodeInfo<Variable> last = _stack.last;
+      Set<Variable> undeclaredReads = last._read.difference(last._declared);
+      assert(undeclaredReads.isEmpty,
+          'Variables read from but not declared: $undeclaredReads');
       Set<Variable> undeclaredWrites = last._written.difference(last._declared);
       assert(undeclaredWrites.isEmpty,
           'Variables written to but not declared: $undeclaredWrites');
@@ -181,6 +192,11 @@
     _stack.add(node);
   }
 
+  void read(Variable variable) {
+    _stack.last._read.add(variable);
+    _anywhere._read.add(variable);
+  }
+
   /// Call this method to register that the node [from] for which information
   /// has been stored is replaced by the node [to].
   // TODO(johnniwinther): Remove this when unified collections are encoded as
@@ -242,6 +258,10 @@
 
   Set<Variable> get declaredAtTopLevel => _stack.first._declared;
 
+  Set<Variable> get readAnywhere => _anywhere._read;
+
+  Set<Variable> get readCapturedAnywhere => _anywhere._readCaptured;
+
   Set<Variable> get writtenAnywhere => _anywhere._written;
 
   Set<Variable> capturedInNode(Node node) => _getInfoForNode(node)._captured;
@@ -250,6 +270,11 @@
 
   bool isTracked(Node node) => _info.containsKey(node);
 
+  Set<Variable> readCapturedInNode(Node node) =>
+      _getInfoForNode(node)._readCaptured;
+
+  Set<Variable> readInNode(Node node) => _getInfoForNode(node)._read;
+
   String toString() {
     StringBuffer sb = new StringBuffer();
     sb.write('AssignedVariablesForTesting(');
@@ -263,9 +288,13 @@
 
 /// Information tracked by [AssignedVariables] for a single node.
 class AssignedVariablesNodeInfo<Variable extends Object> {
+  final Set<Variable> _read = new Set<Variable>.identity();
+
   /// The set of local variables that are potentially written in the node.
   final Set<Variable> _written = new Set<Variable>.identity();
 
+  final Set<Variable> _readCaptured = new Set<Variable>.identity();
+
   /// The set of local variables for which a potential write is captured by a
   /// local function or closure inside the node.
   final Set<Variable> _captured = new Set<Variable>.identity();
@@ -324,6 +353,10 @@
         allowLocalBooleanVarsToPromote: allowLocalBooleanVarsToPromote);
   }
 
+  factory FlowAnalysis.legacy(TypeOperations<Variable, Type> typeOperations,
+          AssignedVariables<Node, Variable> assignedVariables) =
+      _LegacyTypePromotion;
+
   /// Return `true` if the current state is reachable.
   bool get isReachable;
 
@@ -383,7 +416,8 @@
 
   /// Call this method upon reaching the "?" part of a conditional expression
   /// ("?:").  [condition] should be the expression preceding the "?".
-  void conditional_thenBegin(Expression condition);
+  /// [conditionalExpression] should be the entire conditional expression.
+  void conditional_thenBegin(Expression condition, Node conditionalExpression);
 
   /// Register a declaration of the [variable] in the current state.
   /// Should also be called for function parameters.
@@ -575,8 +609,9 @@
   void ifStatement_end(bool hasElse);
 
   /// Call this method after visiting the condition part of an if statement.
-  /// [condition] should be the if statement's condition.
-  void ifStatement_thenBegin(Expression condition);
+  /// [condition] should be the if statement's condition.  [ifNode] should be
+  /// the entire `if` statement (or the collection literal entry).
+  void ifStatement_thenBegin(Expression condition, Node ifNode);
 
   /// Call this method after visiting the initializer of a variable declaration.
   void initialize(
@@ -627,8 +662,9 @@
   /// Call this method after visiting the LHS of a logical binary operation
   /// ("||" or "&&").
   /// [rightOperand] should be the LHS.  [isAnd] should indicate whether the
-  /// logical operator is "&&" or "||".
-  void logicalBinaryOp_rightBegin(Expression leftOperand,
+  /// logical operator is "&&" or "||".  [wholeExpression] should be the whole
+  /// logical binary expression.
+  void logicalBinaryOp_rightBegin(Expression leftOperand, Node wholeExpression,
       {required bool isAnd});
 
   /// Call this method after visiting a logical not ("!") expression.
@@ -855,6 +891,14 @@
         allowLocalBooleanVarsToPromote: allowLocalBooleanVarsToPromote));
   }
 
+  factory FlowAnalysisDebug.legacy(
+      TypeOperations<Variable, Type> typeOperations,
+      AssignedVariables<Node, Variable> assignedVariables) {
+    print('FlowAnalysisDebug.legacy()');
+    return new FlowAnalysisDebug._(
+        new _LegacyTypePromotion(typeOperations, assignedVariables));
+  }
+
   FlowAnalysisDebug._(this._wrapped);
 
   @override
@@ -909,9 +953,9 @@
   }
 
   @override
-  void conditional_thenBegin(Expression condition) {
-    _wrap('conditional_thenBegin($condition)',
-        () => _wrapped.conditional_thenBegin(condition));
+  void conditional_thenBegin(Expression condition, Node conditionalExpression) {
+    _wrap('conditional_thenBegin($condition, $conditionalExpression)',
+        () => _wrapped.conditional_thenBegin(condition, conditionalExpression));
   }
 
   @override
@@ -1069,9 +1113,9 @@
   }
 
   @override
-  void ifStatement_thenBegin(Expression condition) {
-    _wrap('ifStatement_thenBegin($condition)',
-        () => _wrapped.ifStatement_thenBegin(condition));
+  void ifStatement_thenBegin(Expression condition, Node ifNode) {
+    _wrap('ifStatement_thenBegin($condition, $ifNode)',
+        () => _wrapped.ifStatement_thenBegin(condition, ifNode));
   }
 
   @override
@@ -1146,10 +1190,13 @@
   }
 
   @override
-  void logicalBinaryOp_rightBegin(Expression leftOperand,
+  void logicalBinaryOp_rightBegin(Expression leftOperand, Node wholeExpression,
       {required bool isAnd}) {
-    _wrap('logicalBinaryOp_rightBegin($leftOperand, isAnd: $isAnd)',
-        () => _wrapped.logicalBinaryOp_rightBegin(leftOperand, isAnd: isAnd));
+    _wrap(
+        'logicalBinaryOp_rightBegin($leftOperand, $wholeExpression, '
+        'isAnd: $isAnd)',
+        () => _wrapped.logicalBinaryOp_rightBegin(leftOperand, wholeExpression,
+            isAnd: isAnd));
   }
 
   @override
@@ -3244,7 +3291,7 @@
   }
 
   @override
-  void conditional_thenBegin(Expression condition) {
+  void conditional_thenBegin(Expression condition, Node conditionalExpression) {
     ExpressionInfo<Variable, Type> conditionInfo = _expressionEnd(condition);
     _stack.add(new _ConditionalContext(conditionInfo));
     _current = conditionInfo.ifTrue;
@@ -3512,7 +3559,7 @@
   }
 
   @override
-  void ifStatement_thenBegin(Expression condition) {
+  void ifStatement_thenBegin(Expression condition, Node ifNode) {
     ExpressionInfo<Variable, Type> conditionInfo = _expressionEnd(condition);
     _stack.add(new _IfContext(conditionInfo));
     _current = conditionInfo.ifTrue;
@@ -3625,7 +3672,7 @@
   }
 
   @override
-  void logicalBinaryOp_rightBegin(Expression leftOperand,
+  void logicalBinaryOp_rightBegin(Expression leftOperand, Node wholeExpression,
       {required bool isAnd}) {
     ExpressionInfo<Variable, Type> conditionInfo = _expressionEnd(leftOperand);
     _stack.add(new _BranchContext<Variable, Type>(conditionInfo));
@@ -3992,6 +4039,544 @@
   String toString() => '_IfNullExpressionContext(previous: $_previous)';
 }
 
+/// Contextual information tracked by legacy type promotion about a binary "and"
+/// expression (`&&`).
+class _LegacyBinaryAndContext<Variable extends Object, Type extends Object>
+    extends _LegacyContext<Variable, Type> {
+  /// Types that were shown by the LHS of the "and" expression.
+  final Map<Variable, Type> _lhsShownTypes;
+
+  /// Information about variables that might be assigned by the RHS of the "and"
+  /// expression.
+  final AssignedVariablesNodeInfo<Variable> _assignedVariablesInfoForRhs;
+
+  _LegacyBinaryAndContext(Map<Variable, Type> previousKnownTypes,
+      this._lhsShownTypes, this._assignedVariablesInfoForRhs)
+      : super(previousKnownTypes);
+}
+
+/// Contextual information tracked by legacy type promotion about a statement or
+/// expression.
+class _LegacyContext<Variable, Type> {
+  /// The set of known types in effect before the statement or expression in
+  /// question was encountered.
+  final Map<Variable, Type> _previousKnownTypes;
+
+  _LegacyContext(this._previousKnownTypes);
+}
+
+/// Data tracked by legacy type promotion about an expression.
+class _LegacyExpressionInfo<Variable, Type> {
+  /// Variables whose types are "shown" by the expression in question.
+  ///
+  /// For example, the spec says that the expression `x is T` "shows" `x` to
+  /// have type `T`, so accordingly, the [_LegacyExpressionInfo] for `x is T`
+  /// will have an entry in this map that maps `x` to `T`.
+  final Map<Variable, Type> _shownTypes;
+
+  _LegacyExpressionInfo(this._shownTypes);
+
+  @override
+  String toString() => 'LegacyExpressionInfo($_shownTypes)';
+}
+
+/// Implementation of [FlowAnalysis] that performs legacy (pre-null-safety) type
+/// promotion.
+class _LegacyTypePromotion<Node extends Object, Statement extends Node,
+        Expression extends Object, Variable extends Object, Type extends Object>
+    implements FlowAnalysis<Node, Statement, Expression, Variable, Type> {
+  /// The [TypeOperations], used to access types, and check subtyping.
+  final TypeOperations<Variable, Type> _typeOperations;
+
+  /// Information about variable assignments computed during the previous
+  /// compilation pass.
+  final AssignedVariables<Node, Variable> _assignedVariables;
+
+  /// The most recently visited expression for which a [_LegacyExpressionInfo]
+  /// object exists, or `null` if no expression has been visited that has a
+  /// corresponding [_LegacyExpressionInfo] object.
+  Expression? _expressionWithInfo;
+
+  /// If [_expressionWithInfo] is not `null`, the [_LegacyExpressionInfo] object
+  /// corresponding to it.  Otherwise `null`.
+  _LegacyExpressionInfo<Variable, Type>? _expressionInfo;
+
+  /// The set of type promotions currently in effect.
+  Map<Variable, Type> _knownTypes = {};
+
+  /// Stack of [_LegacyContext] objects representing the statements and
+  /// expressions that are currently being visited.
+  final List<_LegacyContext<Variable, Type>> _contextStack = [];
+
+  /// Stack for tracking writes occurring on the LHS of a binary "and" (`&&`)
+  /// operation.  Whenever we visit a write, we update the top entry in this
+  /// stack; whenever we begin to visit the LHS of a binary "and", we push
+  /// a fresh empty entry onto this stack; accordingly, upon reaching the RHS of
+  /// the binary "and", the top entry of the stack contains the set of variables
+  /// written to during the LHS of the "and".
+  final List<Set<Variable>> _writeStackForAnd = [{}];
+
+  _LegacyTypePromotion(this._typeOperations, this._assignedVariables);
+
+  @override
+  bool get isReachable => true;
+
+  @override
+  void asExpression_end(Expression subExpression, Type type) {}
+
+  @override
+  void assert_afterCondition(Expression condition) {}
+
+  @override
+  void assert_begin() {}
+
+  @override
+  void assert_end() {}
+
+  @override
+  void booleanLiteral(Expression expression, bool value) {}
+
+  @override
+  void conditional_conditionBegin() {}
+
+  @override
+  void conditional_elseBegin(Expression thenExpression) {
+    _knownTypes = _contextStack.removeLast()._previousKnownTypes;
+  }
+
+  @override
+  void conditional_end(
+      Expression conditionalExpression, Expression elseExpression) {}
+
+  @override
+  void conditional_thenBegin(Expression condition, Node conditionalExpression) {
+    _conditionalOrIf_thenBegin(condition, conditionalExpression);
+  }
+
+  @override
+  void declare(Variable variable, bool initialized) {}
+
+  @override
+  void doStatement_bodyBegin(Statement doStatement) {}
+
+  @override
+  void doStatement_conditionBegin() {}
+
+  @override
+  void doStatement_end(Expression condition) {}
+
+  @override
+  void equalityOp_end(Expression wholeExpression, Expression rightOperand,
+      Type rightOperandType,
+      {bool notEqual = false}) {}
+
+  @override
+  void equalityOp_rightBegin(Expression leftOperand, Type leftOperandType) {}
+
+  @override
+  ExpressionInfo<Variable, Type>? expressionInfoForTesting(Expression target) {
+    throw new StateError(
+        'expressionInfoForTesting requires null-aware flow analysis');
+  }
+
+  @override
+  void finish() {
+    assert(_contextStack.isEmpty, 'Unexpected stack: $_contextStack');
+  }
+
+  @override
+  void for_bodyBegin(Statement? node, Expression? condition) {}
+
+  @override
+  void for_conditionBegin(Node node) {}
+
+  @override
+  void for_end() {}
+
+  @override
+  void for_updaterBegin() {}
+
+  @override
+  void forEach_bodyBegin(
+      Node node, Variable? loopVariable, Type? writtenType) {}
+
+  @override
+  void forEach_end() {}
+
+  @override
+  void forwardExpression(Expression newExpression, Expression oldExpression) {
+    if (identical(_expressionWithInfo, oldExpression)) {
+      _expressionWithInfo = newExpression;
+    }
+  }
+
+  @override
+  void functionExpression_begin(Node node) {}
+
+  @override
+  void functionExpression_end() {}
+
+  @override
+  void handleBreak(Statement target) {}
+
+  @override
+  void handleContinue(Statement target) {}
+
+  @override
+  void handleExit() {}
+
+  @override
+  void ifNullExpression_end() {}
+
+  @override
+  void ifNullExpression_rightBegin(
+      Expression leftHandSide, Type leftHandSideType) {}
+
+  @override
+  void ifStatement_conditionBegin() {}
+
+  @override
+  void ifStatement_elseBegin() {
+    _knownTypes = _contextStack.removeLast()._previousKnownTypes;
+  }
+
+  @override
+  void ifStatement_end(bool hasElse) {
+    if (!hasElse) {
+      _knownTypes = _contextStack.removeLast()._previousKnownTypes;
+    }
+  }
+
+  @override
+  void ifStatement_thenBegin(Expression condition, Node ifNode) {
+    _conditionalOrIf_thenBegin(condition, ifNode);
+  }
+
+  @override
+  void initialize(
+      Variable variable, Type initializerType, Expression initializerExpression,
+      {required bool isFinal, required bool isLate}) {}
+
+  @override
+  bool isAssigned(Variable variable) {
+    return true;
+  }
+
+  @override
+  void isExpression_end(Expression isExpression, Expression subExpression,
+      bool isNot, Type type) {
+    _LegacyExpressionInfo<Variable, Type>? expressionInfo =
+        _getExpressionInfo(subExpression);
+    if (!isNot && expressionInfo is _LegacyVariableReadInfo<Variable, Type>) {
+      Variable variable = expressionInfo._variable;
+      Type currentType =
+          _knownTypes[variable] ?? _typeOperations.variableType(variable);
+      Type? promotedType = _typeOperations.tryPromoteToType(type, currentType);
+      if (promotedType != null &&
+          !_typeOperations.isSameType(currentType, promotedType)) {
+        _storeExpressionInfo(
+            isExpression,
+            new _LegacyExpressionInfo<Variable, Type>(
+                {variable: promotedType}));
+      }
+    }
+  }
+
+  @override
+  bool isUnassigned(Variable variable) {
+    return false;
+  }
+
+  @override
+  void labeledStatement_begin(Node node) {}
+
+  @override
+  void labeledStatement_end() {}
+
+  @override
+  void lateInitializer_begin(Node node) {}
+
+  @override
+  void lateInitializer_end() {}
+
+  @override
+  void logicalBinaryOp_begin() {
+    _writeStackForAnd.add({});
+  }
+
+  @override
+  void logicalBinaryOp_end(Expression wholeExpression, Expression rightOperand,
+      {required bool isAnd}) {
+    if (!isAnd) return;
+    _LegacyBinaryAndContext<Variable, Type> context =
+        _contextStack.removeLast() as _LegacyBinaryAndContext<Variable, Type>;
+    _knownTypes = context._previousKnownTypes;
+    AssignedVariablesNodeInfo<Variable> assignedVariablesInfoForRhs =
+        context._assignedVariablesInfoForRhs;
+    Map<Variable, Type> lhsShownTypes = context._lhsShownTypes;
+    Map<Variable, Type> rhsShownTypes =
+        _getExpressionInfo(rightOperand)?._shownTypes ?? {};
+    // A logical boolean expression b of the form `e1 && e2` shows that a local
+    // variable v has type T if both of the following conditions hold:
+    // - Either e1 shows that v has type T or e2 shows that v has type T.
+    // - v is not mutated in e2 or within a function other than the one where v
+    //   is declared.
+    // We don't have to worry about whether v is mutated within a function other
+    // than the one where v is declared, because that is checked every time we
+    // evaluate whether v is known to have type T.  So we just have to combine
+    // together the things shown by e1 and e2, and discard anything mutated in
+    // e2.
+    //
+    // Note, however, that there is an ambiguity that isn't addressed by the
+    // spec: what happens if e1 shows that v has type T1 and e2 shows that v has
+    // type T2?  The de facto behavior we have had for a long time is to combine
+    // the two types in the same way we would combine it if c were first
+    // promoted to T1 and then had a successful `is T2` check.
+    Map<Variable, Type> newShownTypes = {};
+    for (MapEntry<Variable, Type> entry in lhsShownTypes.entries) {
+      if (assignedVariablesInfoForRhs._written.contains(entry.key)) continue;
+      newShownTypes[entry.key] = entry.value;
+    }
+    for (MapEntry<Variable, Type> entry in rhsShownTypes.entries) {
+      if (assignedVariablesInfoForRhs._written.contains(entry.key)) continue;
+      Type? previouslyShownType = newShownTypes[entry.key];
+      if (previouslyShownType == null) {
+        newShownTypes[entry.key] = entry.value;
+      } else {
+        Type? newShownType =
+            _typeOperations.tryPromoteToType(entry.value, previouslyShownType);
+        if (newShownType != null &&
+            !_typeOperations.isSameType(previouslyShownType, newShownType)) {
+          newShownTypes[entry.key] = newShownType;
+        }
+      }
+    }
+    _storeExpressionInfo(wholeExpression,
+        new _LegacyExpressionInfo<Variable, Type>(newShownTypes));
+  }
+
+  @override
+  void logicalBinaryOp_rightBegin(Expression leftOperand, Node wholeExpression,
+      {required bool isAnd}) {
+    Set<Variable> variablesWrittenOnLhs = _writeStackForAnd.removeLast();
+    _writeStackForAnd.last.addAll(variablesWrittenOnLhs);
+    if (!isAnd) return;
+    AssignedVariablesNodeInfo<Variable> info =
+        _assignedVariables._getInfoForNode(wholeExpression);
+    Map<Variable, Type> lhsShownTypes =
+        _getExpressionInfo(leftOperand)?._shownTypes ?? {};
+    _contextStack.add(new _LegacyBinaryAndContext<Variable, Type>(
+        _knownTypes, lhsShownTypes, info));
+    Map<Variable, Type>? newKnownTypes;
+    for (MapEntry<Variable, Type> entry in lhsShownTypes.entries) {
+      // Given a statement of the form `e1 && e2`, if e1 shows that a
+      // local variable v has type T, then the type of v is known to be T in
+      // e2, unless any of the following are true:
+      // - v is potentially mutated in e1,
+      if (variablesWrittenOnLhs.contains(entry.key)) continue;
+      // - v is potentially mutated in e2,
+      if (info._written.contains(entry.key)) continue;
+      // - v is potentially mutated within a function other than the one where
+      //   v is declared, or
+      if (_assignedVariables._anywhere._captured.contains(entry.key)) {
+        continue;
+      }
+      // - v is accessed by a function defined in e2 and v is potentially
+      //   mutated anywhere in the scope of v.
+      if (info._readCaptured.contains(entry.key) &&
+          _assignedVariables._anywhere._written.contains(entry.key)) {
+        continue;
+      }
+      (newKnownTypes ??= new Map<Variable, Type>.from(_knownTypes))[entry.key] =
+          entry.value;
+    }
+    if (newKnownTypes != null) _knownTypes = newKnownTypes;
+  }
+
+  @override
+  void logicalNot_end(Expression notExpression, Expression operand) {}
+
+  @override
+  void nonNullAssert_end(Expression operand) {}
+
+  @override
+  void nullAwareAccess_end() {}
+
+  @override
+  void nullAwareAccess_rightBegin(Expression? target, Type targetType) {}
+
+  @override
+  void nullLiteral(Expression expression) {}
+
+  @override
+  void parenthesizedExpression(
+      Expression outerExpression, Expression innerExpression) {
+    forwardExpression(outerExpression, innerExpression);
+  }
+
+  @override
+  void promote(Variable variable, Type type) {
+    throw new UnimplementedError('TODO(paulberry)');
+  }
+
+  @override
+  Type? promotedType(Variable variable) {
+    return _knownTypes[variable];
+  }
+
+  @override
+  SsaNode<Variable, Type>? ssaNodeForTesting(Variable variable) {
+    throw new StateError('ssaNodeForTesting requires null-aware flow analysis');
+  }
+
+  @override
+  void switchStatement_beginCase(bool hasLabel, Node node) {}
+
+  @override
+  void switchStatement_end(bool isExhaustive) {}
+
+  @override
+  void switchStatement_expressionEnd(Statement switchStatement) {}
+
+  @override
+  void tryCatchStatement_bodyBegin() {}
+
+  @override
+  void tryCatchStatement_bodyEnd(Node body) {}
+
+  @override
+  void tryCatchStatement_catchBegin(
+      Variable? exceptionVariable, Variable? stackTraceVariable) {}
+
+  @override
+  void tryCatchStatement_catchEnd() {}
+
+  @override
+  void tryCatchStatement_end() {}
+
+  @override
+  void tryFinallyStatement_bodyBegin() {}
+
+  @override
+  void tryFinallyStatement_end(Node finallyBlock) {}
+
+  @override
+  void tryFinallyStatement_finallyBegin(Node body) {}
+
+  @override
+  Type? variableRead(Expression expression, Variable variable) {
+    _storeExpressionInfo(
+        expression, new _LegacyVariableReadInfo<Variable, Type>(variable));
+    return _knownTypes[variable];
+  }
+
+  @override
+  void whileStatement_bodyBegin(
+      Statement whileStatement, Expression condition) {}
+
+  @override
+  void whileStatement_conditionBegin(Node node) {}
+
+  @override
+  void whileStatement_end() {}
+
+  @override
+  void write(
+      Variable variable, Type writtenType, Expression? writtenExpression) {
+    assert(
+        _assignedVariables._anywhere._written.contains(variable),
+        "Variable is written to, but was not included in "
+        "_variablesWrittenAnywhere: $variable");
+    _writeStackForAnd.last.add(variable);
+  }
+
+  void _conditionalOrIf_thenBegin(Expression condition, Node node) {
+    _contextStack.add(new _LegacyContext<Variable, Type>(_knownTypes));
+    AssignedVariablesNodeInfo<Variable> info =
+        _assignedVariables._getInfoForNode(node);
+    Map<Variable, Type>? newKnownTypes;
+    _LegacyExpressionInfo<Variable, Type>? expressionInfo =
+        _getExpressionInfo(condition);
+    if (expressionInfo != null) {
+      for (MapEntry<Variable, Type> entry
+          in expressionInfo._shownTypes.entries) {
+        // Given an expression of the form n1?n2:n3 or a statement of the form
+        // `if (n1) n2 else n3`, if n1 shows that a local variable v has type T,
+        // then the type of v is known to be T in n2, unless any of the
+        // following are true:
+        // - v is potentially mutated in n2,
+        if (info._written.contains(entry.key)) continue;
+        // - v is potentially mutated within a function other than the one where
+        //   v is declared, or
+        if (_assignedVariables._anywhere._captured.contains(entry.key)) {
+          continue;
+        }
+        // - v is accessed by a function defined in n2 and v is potentially
+        //   mutated anywhere in the scope of v.
+        if (info._readCaptured.contains(entry.key) &&
+            _assignedVariables._anywhere._written.contains(entry.key)) {
+          continue;
+        }
+        (newKnownTypes ??=
+            new Map<Variable, Type>.from(_knownTypes))[entry.key] = entry.value;
+      }
+      if (newKnownTypes != null) _knownTypes = newKnownTypes;
+    }
+  }
+
+  @override
+  void _dumpState() {
+    print('  knownTypes: $_knownTypes');
+    print('  expressionWithInfo: $_expressionWithInfo');
+    print('  expressionInfo: $_expressionInfo');
+    print('  contextStack:');
+    for (_LegacyContext<Variable, Type> stackEntry in _contextStack.reversed) {
+      print('    $stackEntry');
+    }
+    print('  writeStackForAnd:');
+    for (Set<Variable> stackEntry in _writeStackForAnd.reversed) {
+      print('    $stackEntry');
+    }
+  }
+
+  /// Gets the [_LegacyExpressionInfo] associated with [expression], if any;
+  /// otherwise returns `null`.
+  _LegacyExpressionInfo<Variable, Type>? _getExpressionInfo(
+      Expression expression) {
+    if (identical(expression, _expressionWithInfo)) {
+      _LegacyExpressionInfo<Variable, Type>? expressionInfo = _expressionInfo;
+      _expressionInfo = null;
+      return expressionInfo;
+    } else {
+      return null;
+    }
+  }
+
+  /// Associates [expressionInfo] with [expression] for use by a future call to
+  /// [_getExpressionInfo].
+  void _storeExpressionInfo(Expression expression,
+      _LegacyExpressionInfo<Variable, Type> expressionInfo) {
+    _expressionWithInfo = expression;
+    _expressionInfo = expressionInfo;
+  }
+}
+
+/// Data tracked by legacy type promotion about an expression that reads the
+/// value of a local variable.
+class _LegacyVariableReadInfo<Variable, Type>
+    implements _LegacyExpressionInfo<Variable, Type> {
+  /// The variable being referred to.
+  final Variable _variable;
+
+  _LegacyVariableReadInfo(this._variable);
+
+  @override
+  Map<Variable, Type> get _shownTypes => {};
+
+  @override
+  String toString() => 'LegacyVariableReadInfo($_variable, $_shownTypes)';
+}
+
 /// [_FlowContext] representing a null aware access (`?.`).
 class _NullAwareAccessContext<Variable extends Object, Type extends Object>
     extends _SimpleContext<Variable, Type> {
diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/assigned_variables_test.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/assigned_variables_test.dart
index cae1c1c..8676f23 100644
--- a/pkg/_fe_analyzer_shared/test/flow_analysis/assigned_variables_test.dart
+++ b/pkg/_fe_analyzer_shared/test/flow_analysis/assigned_variables_test.dart
@@ -6,6 +6,39 @@
 import 'package:test/test.dart';
 
 main() {
+  test('readCapturedAnywhere records reads in closures', () {
+    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
+    var v1 = _Variable('v1');
+    var v2 = _Variable('v2');
+    var v3 = _Variable('v3');
+    assignedVariables.declare(v1);
+    assignedVariables.declare(v2);
+    assignedVariables.declare(v3);
+    assignedVariables.read(v1);
+    assignedVariables.beginNode();
+    assignedVariables.read(v2);
+    assignedVariables.endNode(_Node(),
+        isClosureOrLateVariableInitializer: true);
+    assignedVariables.read(v3);
+    assignedVariables.finish();
+    expect(assignedVariables.readCapturedAnywhere, {v2});
+  });
+
+  test('readCapturedAnywhere does not record variables local to a closure', () {
+    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
+    var v1 = _Variable('v1');
+    var v2 = _Variable('v2');
+    assignedVariables.declare(v1);
+    assignedVariables.beginNode();
+    assignedVariables.declare(v2);
+    assignedVariables.read(v1);
+    assignedVariables.read(v2);
+    assignedVariables.endNode(_Node(),
+        isClosureOrLateVariableInitializer: true);
+    assignedVariables.finish();
+    expect(assignedVariables.readCapturedAnywhere, {v1});
+  });
+
   test('capturedAnywhere records assignments in closures', () {
     var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
     var v1 = _Variable('v1');
@@ -39,6 +72,24 @@
     expect(assignedVariables.capturedAnywhere, {v1});
   });
 
+  test('readAnywhere records all reads', () {
+    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
+    var v1 = _Variable('v1');
+    var v2 = _Variable('v2');
+    var v3 = _Variable('v3');
+    assignedVariables.declare(v1);
+    assignedVariables.declare(v2);
+    assignedVariables.declare(v3);
+    assignedVariables.read(v1);
+    assignedVariables.beginNode();
+    assignedVariables.read(v2);
+    assignedVariables.endNode(_Node(),
+        isClosureOrLateVariableInitializer: true);
+    assignedVariables.read(v3);
+    assignedVariables.finish();
+    expect(assignedVariables.readAnywhere, {v1, v2, v3});
+  });
+
   test('writtenAnywhere records all assignments', () {
     var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
     var v1 = _Variable('v1');
@@ -57,6 +108,59 @@
     expect(assignedVariables.writtenAnywhere, {v1, v2, v3});
   });
 
+  test('readInNode ignores reads outside the node', () {
+    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
+    var v1 = _Variable('v1');
+    var v2 = _Variable('v2');
+    assignedVariables.declare(v1);
+    assignedVariables.declare(v2);
+    assignedVariables.read(v1);
+    assignedVariables.beginNode();
+    var node = _Node();
+    assignedVariables.endNode(node);
+    assignedVariables.read(v2);
+    assignedVariables.finish();
+    expect(assignedVariables.readInNode(node), isEmpty);
+  });
+
+  test('readInNode records reads inside the node', () {
+    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
+    var v1 = _Variable('v1');
+    assignedVariables.declare(v1);
+    assignedVariables.beginNode();
+    assignedVariables.read(v1);
+    var node = _Node();
+    assignedVariables.endNode(node);
+    assignedVariables.finish();
+    expect(assignedVariables.readInNode(node), {v1});
+  });
+
+  test('readInNode records reads in a nested node', () {
+    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
+    var v1 = _Variable('v1');
+    assignedVariables.declare(v1);
+    assignedVariables.beginNode();
+    assignedVariables.beginNode();
+    assignedVariables.read(v1);
+    assignedVariables.endNode(_Node());
+    var node = _Node();
+    assignedVariables.endNode(node);
+    assignedVariables.finish();
+    expect(assignedVariables.readInNode(node), {v1});
+  });
+
+  test('readInNode records reads in a closure', () {
+    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
+    var v1 = _Variable('v1');
+    assignedVariables.declare(v1);
+    assignedVariables.beginNode();
+    assignedVariables.read(v1);
+    var node = _Node();
+    assignedVariables.endNode(node, isClosureOrLateVariableInitializer: true);
+    assignedVariables.finish();
+    expect(assignedVariables.readInNode(node), {v1});
+  });
+
   test('writtenInNode ignores assignments outside the node', () {
     var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
     var v1 = _Variable('v1');
@@ -110,6 +214,46 @@
     expect(assignedVariables.writtenInNode(node), {v1});
   });
 
+  test('readCapturedInNode ignores reads in non-nested closures', () {
+    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
+    var v1 = _Variable('v1');
+    var v2 = _Variable('v2');
+    assignedVariables.declare(v1);
+    assignedVariables.declare(v2);
+    assignedVariables.beginNode();
+    assignedVariables.read(v1);
+    assignedVariables.endNode(_Node(),
+        isClosureOrLateVariableInitializer: true);
+    assignedVariables.beginNode();
+    var node = _Node();
+    assignedVariables.endNode(node);
+    assignedVariables.beginNode();
+    assignedVariables.read(v2);
+    assignedVariables.endNode(_Node(),
+        isClosureOrLateVariableInitializer: true);
+    assignedVariables.finish();
+    expect(assignedVariables.readCapturedInNode(node), isEmpty);
+  });
+
+  test('readCapturedInNode records assignments in nested closures', () {
+    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
+    var v1 = _Variable('v1');
+    assignedVariables.declare(v1);
+    assignedVariables.beginNode();
+    assignedVariables.beginNode();
+    assignedVariables.beginNode();
+    assignedVariables.read(v1);
+    assignedVariables.endNode(_Node(),
+        isClosureOrLateVariableInitializer: true);
+    var innerNode = _Node();
+    assignedVariables.endNode(innerNode);
+    var outerNode = _Node();
+    assignedVariables.endNode(outerNode);
+    assignedVariables.finish();
+    expect(assignedVariables.readCapturedInNode(innerNode), {v1});
+    expect(assignedVariables.readCapturedInNode(outerNode), {v1});
+  });
+
   test('capturedInNode ignores assignments in non-nested closures', () {
     var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
     var v1 = _Variable('v1');
@@ -160,8 +304,10 @@
       assignedVariables.beginNode();
       assignedVariables.declare(v1);
       assignedVariables.declare(v2);
+      assignedVariables.read(v1);
       assignedVariables.write(v1);
       assignedVariables.beginNode();
+      assignedVariables.read(v2);
       assignedVariables.write(v2);
       assignedVariables.endNode(_Node(),
           isClosureOrLateVariableInitializer: true);
@@ -171,9 +317,13 @@
       var outerNode = _Node();
       assignedVariables.endNode(outerNode);
       assignedVariables.finish();
+      expect(assignedVariables.readInNode(innerNode), isEmpty);
       expect(assignedVariables.writtenInNode(innerNode), isEmpty);
+      expect(assignedVariables.readCapturedInNode(innerNode), isEmpty);
       expect(assignedVariables.capturedInNode(innerNode), isEmpty);
+      expect(assignedVariables.readInNode(outerNode), isEmpty);
       expect(assignedVariables.writtenInNode(outerNode), isEmpty);
+      expect(assignedVariables.readCapturedInNode(outerNode), isEmpty);
       expect(assignedVariables.capturedInNode(outerNode), isEmpty);
     });
 
@@ -185,8 +335,10 @@
       assignedVariables.beginNode();
       assignedVariables.declare(v1);
       assignedVariables.declare(v2);
+      assignedVariables.read(v1);
       assignedVariables.write(v1);
       assignedVariables.beginNode();
+      assignedVariables.read(v2);
       assignedVariables.write(v2);
       assignedVariables.endNode(_Node(),
           isClosureOrLateVariableInitializer: true);
@@ -196,9 +348,13 @@
       var outerNode = _Node();
       assignedVariables.endNode(outerNode);
       assignedVariables.finish();
+      expect(assignedVariables.readInNode(innerNode), isEmpty);
       expect(assignedVariables.writtenInNode(innerNode), isEmpty);
+      expect(assignedVariables.readCapturedInNode(innerNode), isEmpty);
       expect(assignedVariables.capturedInNode(innerNode), isEmpty);
+      expect(assignedVariables.readInNode(outerNode), isEmpty);
       expect(assignedVariables.writtenInNode(outerNode), isEmpty);
+      expect(assignedVariables.readCapturedInNode(outerNode), isEmpty);
       expect(assignedVariables.capturedInNode(outerNode), isEmpty);
     });
   });
@@ -218,6 +374,23 @@
     expect(assignedVariables.declaredInNode(node), unorderedEquals([v1, v2]));
   });
 
+  test('discardNode percolates reads to enclosing node', () {
+    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
+    var v1 = _Variable('v1');
+    var v2 = _Variable('v2');
+    var node = _Node();
+    assignedVariables.declare(v1);
+    assignedVariables.declare(v2);
+    assignedVariables.beginNode();
+    assignedVariables.beginNode();
+    assignedVariables.read(v1);
+    assignedVariables.read(v2);
+    assignedVariables.discardNode();
+    assignedVariables.endNode(node);
+    assignedVariables.finish();
+    expect(assignedVariables.readInNode(node), unorderedEquals([v1, v2]));
+  });
+
   test('discardNode percolates writes to enclosing node', () {
     var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
     var v1 = _Variable('v1');
@@ -235,7 +408,28 @@
     expect(assignedVariables.writtenInNode(node), unorderedEquals([v1, v2]));
   });
 
-  test('discardNode percolates captures to enclosing node', () {
+  test('discardNode percolates read captures to enclosing node', () {
+    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
+    var v1 = _Variable('v1');
+    var v2 = _Variable('v2');
+    var node = _Node();
+    assignedVariables.declare(v1);
+    assignedVariables.declare(v2);
+    assignedVariables.beginNode();
+    assignedVariables.beginNode();
+    assignedVariables.beginNode();
+    assignedVariables.read(v1);
+    assignedVariables.read(v2);
+    assignedVariables.endNode(_Node(),
+        isClosureOrLateVariableInitializer: true);
+    assignedVariables.discardNode();
+    assignedVariables.endNode(node);
+    assignedVariables.finish();
+    expect(
+        assignedVariables.readCapturedInNode(node), unorderedEquals([v1, v2]));
+  });
+
+  test('discardNode percolates write captures to enclosing node', () {
     var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
     var v1 = _Variable('v1');
     var v2 = _Variable('v2');
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 84f68e4..9838684 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
@@ -209,8 +209,8 @@
 /// Representation of an expression in the pseudo-Dart language used for flow
 /// analysis testing.  Methods in this class may be used to create more complex
 /// expressions based on this one.
-abstract class Expression implements _Visitable<Type> {
-  const Expression();
+abstract class Expression extends Node implements _Visitable<Type> {
+  const Expression() : super._();
 
   /// If `this` is an expression `x`, creates the expression `x!`.
   Expression get nonNullAssert => new _NonNullAssert(this);
@@ -398,13 +398,17 @@
 
   final bool allowLocalBooleanVarsToPromote;
 
+  final bool legacy;
+
   final Map<String, bool> _subtypes = Map.of(_coreSubtypes);
 
   final Map<String, Type> _factorResults = Map.of(_coreFactors);
 
   Node? _currentSwitch;
 
-  Harness({this.allowLocalBooleanVarsToPromote = false});
+  Map<String, Map<String, String>> _promotionExceptions = {};
+
+  Harness({this.allowLocalBooleanVarsToPromote = false, this.legacy = false});
 
   /// Updates the harness so that when a [factor] query is invoked on types
   /// [from] and [what], [result] will be returned.
@@ -413,6 +417,10 @@
     _factorResults[query] = Type(result);
   }
 
+  void addPromotionException(String from, String to, String result) {
+    (_promotionExceptions[from] ??= {})[to] = result;
+  }
+
   /// Updates the harness so that when an [isSubtypeOf] query is invoked on
   /// types [leftType] and [rightType], [isSubtype] will be returned.
   void addSubtype(String leftType, String rightType, bool isSubtype) {
@@ -470,15 +478,22 @@
   void run(List<Statement> statements) {
     var assignedVariables = AssignedVariables<Node, Var>();
     statements._preVisit(assignedVariables);
-    var flow = FlowAnalysis<Node, Statement, Expression, Var, Type>(
-        this, assignedVariables,
-        allowLocalBooleanVarsToPromote: allowLocalBooleanVarsToPromote);
+    var flow = legacy
+        ? FlowAnalysis<Node, Statement, Expression, Var, Type>.legacy(
+            this, assignedVariables)
+        : FlowAnalysis<Node, Statement, Expression, Var, Type>(
+            this, assignedVariables,
+            allowLocalBooleanVarsToPromote: allowLocalBooleanVarsToPromote);
     statements._visit(this, flow);
     flow.finish();
   }
 
   @override
   Type? tryPromoteToType(Type to, Type from) {
+    var exception = (_promotionExceptions[from.type] ?? {})[to.type];
+    if (exception != null) {
+      return Type(exception);
+    }
     if (isSubtypeOf(to, from)) {
       return to;
     } else {
@@ -529,7 +544,7 @@
 /// Representation of an expression or statement in the pseudo-Dart language
 /// used for flow analysis testing.
 class Node {
-  Node._();
+  const Node._();
 }
 
 /// Helper class allowing tests to examine the values of variables' SSA nodes.
@@ -606,7 +621,12 @@
   Var(this.name, String typeStr) : type = Type(typeStr);
 
   /// Creates an expression representing a read of this variable.
-  Expression get read => new _VariableRead(this);
+  Expression get read => new _VariableRead(this, null);
+
+  /// Creates an expression representing a read of this variable, which as a
+  /// side effect will call the given callback with the returned promoted type.
+  Expression readAndCheckPromotedType(void Function(Type?) callback) =>
+      new _VariableRead(this, callback);
 
   @override
   String toString() => '$type $name';
@@ -828,7 +848,9 @@
   @override
   void _preVisit(AssignedVariables<Node, Var> assignedVariables) {
     condition._preVisit(assignedVariables);
+    assignedVariables.beginNode();
     ifTrue._preVisit(assignedVariables);
+    assignedVariables.endNode(this);
     ifFalse._preVisit(assignedVariables);
   }
 
@@ -836,7 +858,7 @@
   Type _visit(
       Harness h, FlowAnalysis<Node, Statement, Expression, Var, Type> flow) {
     flow.conditional_conditionBegin();
-    flow.conditional_thenBegin(condition.._visit(h, flow));
+    flow.conditional_thenBegin(condition.._visit(h, flow), this);
     var ifTrueType = ifTrue._visit(h, flow);
     flow.conditional_elseBegin(ifTrue);
     var ifFalseType = ifFalse._visit(h, flow);
@@ -1131,7 +1153,9 @@
   @override
   void _preVisit(AssignedVariables<Node, Var> assignedVariables) {
     condition._preVisit(assignedVariables);
+    assignedVariables.beginNode();
     ifTrue._preVisit(assignedVariables);
+    assignedVariables.endNode(this);
     ifFalse?._preVisit(assignedVariables);
   }
 
@@ -1139,7 +1163,7 @@
   void _visit(
       Harness h, FlowAnalysis<Node, Statement, Expression, Var, Type> flow) {
     flow.ifStatement_conditionBegin();
-    flow.ifStatement_thenBegin(condition.._visit(h, flow));
+    flow.ifStatement_thenBegin(condition.._visit(h, flow), this);
     ifTrue._visit(h, flow);
     if (ifFalse == null) {
       flow.ifStatement_end(false);
@@ -1259,14 +1283,16 @@
   @override
   void _preVisit(AssignedVariables<Node, Var> assignedVariables) {
     lhs._preVisit(assignedVariables);
+    assignedVariables.beginNode();
     rhs._preVisit(assignedVariables);
+    assignedVariables.endNode(this);
   }
 
   @override
   Type _visit(
       Harness h, FlowAnalysis<Node, Statement, Expression, Var, Type> flow) {
     flow.logicalBinaryOp_begin();
-    flow.logicalBinaryOp_rightBegin(lhs.._visit(h, flow), isAnd: isAnd);
+    flow.logicalBinaryOp_rightBegin(lhs.._visit(h, flow), this, isAnd: isAnd);
     flow.logicalBinaryOp_end(this, rhs.._visit(h, flow), isAnd: isAnd);
     return Type('bool');
   }
@@ -1540,18 +1566,24 @@
 class _VariableRead extends Expression {
   final Var variable;
 
-  _VariableRead(this.variable);
+  final void Function(Type?)? callback;
+
+  _VariableRead(this.variable, this.callback);
 
   @override
   String toString() => variable.name;
 
   @override
-  void _preVisit(AssignedVariables<Node, Var> assignedVariables) {}
+  void _preVisit(AssignedVariables<Node, Var> assignedVariables) {
+    assignedVariables.read(variable);
+  }
 
   @override
   Type _visit(
       Harness h, FlowAnalysis<Node, Statement, Expression, Var, Type> flow) {
-    return flow.variableRead(this, variable) ?? variable.type;
+    var readResult = flow.variableRead(this, variable);
+    callback?.call(readResult);
+    return readResult ?? variable.type;
   }
 }
 
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 5d522be..9621ff7 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
@@ -502,10 +502,11 @@
     test('finish checks proper nesting', () {
       var h = Harness();
       var e = expr('Null');
+      var s = if_(e, []);
       var flow = FlowAnalysis<Node, Statement, Expression, Var, Type>(
           h, AssignedVariables<Node, Var>());
       flow.ifStatement_conditionBegin();
-      flow.ifStatement_thenBegin(e);
+      flow.ifStatement_thenBegin(e, s);
       expect(() => flow.finish(), _asserts);
     });
 
@@ -4602,6 +4603,766 @@
       expect(m1.inheritTested(h, m2), same(m1));
     });
   });
+
+  group('Legacy promotion', () {
+    group('if statement', () {
+      group('promotes a variable whose type is shown by its condition', () {
+        test('within then-block', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          h.run([
+            if_(x.read.is_('int'), [
+              checkPromoted(x, 'int'),
+            ]),
+          ]);
+        });
+
+        test('but not within else-block', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          h.run([
+            if_(x.read.is_('int'), [], [
+              checkNotPromoted(x),
+            ]),
+          ]);
+        });
+
+        test('unless the then-block mutates it', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          h.run([
+            if_(x.read.is_('int'), [
+              checkNotPromoted(x),
+              x.write(expr('int')).stmt,
+            ]),
+          ]);
+        });
+
+        test('even if the condition mutates it', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          h.run([
+            if_(
+                x
+                    .write(expr('int'))
+                    .parenthesized
+                    .eq(expr('int'))
+                    .and(x.read.is_('int')),
+                [
+                  checkPromoted(x, 'int'),
+                ]),
+          ]);
+        });
+
+        test('even if the else-block mutates it', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          h.run([
+            if_(x.read.is_('int'), [
+              checkPromoted(x, 'int'),
+            ], [
+              x.write(expr('int')).stmt,
+            ]),
+          ]);
+        });
+
+        test('unless a closure mutates it', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          h.run([
+            if_(x.read.is_('int'), [
+              checkNotPromoted(x),
+            ]),
+            localFunction([
+              x.write(expr('int')).stmt,
+            ]),
+          ]);
+        });
+
+        test(
+            'unless a closure in the then-block accesses it and it is mutated '
+            'anywhere', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          h.run([
+            if_(x.read.is_('int'), [
+              checkNotPromoted(x),
+              localFunction([
+                x.read.stmt,
+              ]),
+            ]),
+            x.write(expr('int')).stmt,
+          ]);
+        });
+
+        test(
+            'unless a closure in the then-block accesses it and it is mutated '
+            'anywhere, even if the access is deeply nested', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          h.run([
+            if_(x.read.is_('int'), [
+              checkNotPromoted(x),
+              localFunction([
+                localFunction([
+                  x.read.stmt,
+                ]),
+              ]),
+            ]),
+            x.write(expr('int')).stmt,
+          ]);
+        });
+
+        test(
+            'even if a closure in the condition accesses it and it is mutated '
+            'somewhere', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          h.run([
+            if_(
+                localFunction([
+                  x.read.stmt,
+                ]).thenExpr(expr('bool')).and(x.read.is_('int')),
+                [
+                  checkPromoted(x, 'int'),
+                ]),
+            x.write(expr('int')).stmt,
+          ]);
+        });
+
+        test(
+            'even if a closure in the else-block accesses it and it is mutated '
+            'somewhere', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          h.run([
+            if_(x.read.is_('int'), [
+              checkPromoted(x, 'int'),
+            ], [
+              localFunction([
+                x.read.stmt,
+              ]),
+            ]),
+            x.write(expr('int')).stmt,
+          ]);
+        });
+
+        test(
+            'even if a closure in the then-block accesses it, provided it is '
+            'not mutated anywhere', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          h.run([
+            if_(x.read.is_('int'), [
+              checkPromoted(x, 'int'),
+              localFunction([
+                x.read.stmt,
+              ]),
+            ]),
+          ]);
+        });
+      });
+
+      test('handles arbitrary conditions', () {
+        var h = Harness(legacy: true);
+        h.run([
+          if_(expr('bool'), []),
+        ]);
+      });
+
+      test('handles a condition that is a variable', () {
+        var h = Harness(legacy: true);
+        var x = Var('x', 'bool');
+        h.run([
+          if_(x.read, []),
+        ]);
+      });
+
+      test('handles multiple promotions', () {
+        var h = Harness(legacy: true);
+        var x = Var('x', 'Object');
+        var y = Var('y', 'Object');
+        h.run([
+          if_(x.read.is_('int').and(y.read.is_('String')), [
+            checkPromoted(x, 'int'),
+            checkPromoted(y, 'String'),
+          ]),
+        ]);
+      });
+    });
+
+    group('conditional expression', () {
+      group('promotes a variable whose type is shown by its condition', () {
+        test('within then-expression', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          h.run([
+            x.read
+                .is_('int')
+                .conditional(checkPromoted(x, 'int').thenExpr(expr('Object')),
+                    expr('Object'))
+                .stmt,
+          ]);
+        });
+
+        test('but not within else-expression', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          h.run([
+            x.read
+                .is_('int')
+                .conditional(expr('Object'),
+                    checkNotPromoted(x).thenExpr(expr('Object')))
+                .stmt,
+          ]);
+        });
+
+        test('unless the then-expression mutates it', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          h.run([
+            x.read
+                .is_('int')
+                .conditional(
+                    block([
+                      checkNotPromoted(x),
+                      x.write(expr('int')).stmt,
+                    ]).thenExpr(expr('Object')),
+                    expr('Object'))
+                .stmt,
+          ]);
+        });
+
+        test('even if the condition mutates it', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          h.run([
+            x
+                .write(expr('int'))
+                .parenthesized
+                .eq(expr('int'))
+                .and(x.read.is_('int'))
+                .conditional(checkPromoted(x, 'int').thenExpr(expr('Object')),
+                    expr('Object'))
+                .stmt,
+          ]);
+        });
+
+        test('even if the else-expression mutates it', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          h.run([
+            x.read
+                .is_('int')
+                .conditional(checkPromoted(x, 'int').thenExpr(expr('int')),
+                    x.write(expr('int')))
+                .stmt,
+          ]);
+        });
+
+        test('unless a closure mutates it', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          h.run([
+            x.read
+                .is_('int')
+                .conditional(checkNotPromoted(x).thenExpr(expr('Object')),
+                    expr('Object'))
+                .stmt,
+            localFunction([
+              x.write(expr('int')).stmt,
+            ]),
+          ]);
+        });
+
+        test(
+            'unless a closure in the then-expression accesses it and it is '
+            'mutated anywhere', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          h.run([
+            x.read
+                .is_('int')
+                .conditional(
+                    block([
+                      checkNotPromoted(x),
+                      localFunction([
+                        x.read.stmt,
+                      ]),
+                    ]).thenExpr(expr('Object')),
+                    expr('Object'))
+                .stmt,
+            x.write(expr('int')).stmt,
+          ]);
+        });
+
+        test(
+            'even if a closure in the condition accesses it and it is mutated '
+            'somewhere', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          h.run([
+            localFunction([
+              x.read.stmt,
+            ])
+                .thenExpr(expr('Object'))
+                .and(x.read.is_('int'))
+                .conditional(checkPromoted(x, 'int').thenExpr(expr('Object')),
+                    expr('Object'))
+                .stmt,
+            x.write(expr('int')).stmt,
+          ]);
+        });
+
+        test(
+            'even if a closure in the else-expression accesses it and it is '
+            'mutated somewhere', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          h.run([
+            x.read
+                .is_('int')
+                .conditional(
+                    checkPromoted(x, 'int').thenExpr(expr('Object')),
+                    localFunction([
+                      x.read.stmt,
+                    ]).thenExpr(expr('Object')))
+                .stmt,
+            x.write(expr('int')).stmt,
+          ]);
+        });
+
+        test(
+            'even if a closure in the then-expression accesses it, provided it '
+            'is not mutated anywhere', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          h.run([
+            x.read
+                .is_('int')
+                .conditional(
+                    block([
+                      checkPromoted(x, 'int'),
+                      localFunction([
+                        x.read.stmt,
+                      ]),
+                    ]).thenExpr(expr('Object')),
+                    expr('Object'))
+                .stmt,
+          ]);
+        });
+      });
+
+      test('handles arbitrary conditions', () {
+        var h = Harness(legacy: true);
+        h.run([
+          expr('bool').conditional(expr('Object'), expr('Object')).stmt,
+        ]);
+      });
+
+      test('handles a condition that is a variable', () {
+        var h = Harness(legacy: true);
+        var x = Var('x', 'bool');
+        h.run([
+          x.read.conditional(expr('Object'), expr('Object')).stmt,
+        ]);
+      });
+
+      test('handles multiple promotions', () {
+        var h = Harness(legacy: true);
+        var x = Var('x', 'Object');
+        var y = Var('y', 'Object');
+        h.run([
+          x.read
+              .is_('int')
+              .and(y.read.is_('String'))
+              .conditional(
+                  block([
+                    checkPromoted(x, 'int'),
+                    checkPromoted(y, 'String'),
+                  ]).thenExpr(expr('Object')),
+                  expr('Object'))
+              .stmt
+        ]);
+      });
+    });
+
+    group('logical', () {
+      group('and', () {
+        group("shows a variable's type", () {
+          test('if the lhs shows the type', () {
+            var h = Harness(legacy: true);
+            var x = Var('x', 'Object');
+            h.run([
+              if_(x.read.is_('int').and(expr('bool')), [
+                checkPromoted(x, 'int'),
+              ]),
+            ]);
+          });
+
+          test('if the rhs shows the type', () {
+            var h = Harness(legacy: true);
+            var x = Var('x', 'Object');
+            h.run([
+              if_(expr('bool').and(x.read.is_('int')), [
+                checkPromoted(x, 'int'),
+              ]),
+            ]);
+          });
+
+          test('unless the rhs mutates it', () {
+            var h = Harness(legacy: true);
+            var x = Var('x', 'Object');
+            h.run([
+              if_(x.read.is_('int').and(x.write(expr('bool'))), [
+                checkNotPromoted(x),
+              ]),
+            ]);
+          });
+
+          test('unless the rhs mutates it, even if the rhs also shows the type',
+              () {
+            var h = Harness(legacy: true);
+            var x = Var('x', 'Object');
+            h.run([
+              if_(
+                  expr('bool').and(x
+                      .write(expr('Object'))
+                      .and(x.read.is_('int'))
+                      .parenthesized),
+                  [
+                    checkNotPromoted(x),
+                  ]),
+            ]);
+          });
+
+          test('unless a closure mutates it', () {
+            var h = Harness(legacy: true);
+            var x = Var('x', 'Object');
+            h.run([
+              if_(x.read.is_('int').and(expr('bool')), [
+                checkNotPromoted(x),
+              ]),
+              localFunction([
+                x.write(expr('int')).stmt,
+              ]),
+            ]);
+          });
+        });
+
+        group('promotes a variable whose type is shown by its lhs', () {
+          test('within its rhs', () {
+            var h = Harness(legacy: true);
+            var x = Var('x', 'Object');
+            h.run([
+              x.read
+                  .is_('int')
+                  .and(checkPromoted(x, 'int').thenExpr(expr('bool')))
+                  .stmt,
+            ]);
+          });
+
+          test('unless the lhs mutates it', () {
+            var h = Harness(legacy: true);
+            var x = Var('x', 'Object');
+            h.run([
+              x
+                  .write(expr('int'))
+                  .parenthesized
+                  .eq(expr('int'))
+                  .and(x.read.is_('int'))
+                  .parenthesized
+                  .and(checkNotPromoted(x).thenExpr(expr('bool')))
+                  .stmt,
+            ]);
+          });
+
+          test('unless the rhs mutates it', () {
+            var h = Harness(legacy: true);
+            var x = Var('x', 'Object');
+            h.run([
+              x.read
+                  .is_('int')
+                  .and(checkNotPromoted(x).thenExpr(x.write(expr('bool'))))
+                  .stmt,
+            ]);
+          });
+
+          test('unless a closure mutates it', () {
+            var h = Harness(legacy: true);
+            var x = Var('x', 'Object');
+            h.run([
+              x.read
+                  .is_('int')
+                  .and(checkNotPromoted(x).thenExpr(expr('bool')))
+                  .stmt,
+              localFunction([
+                x.write(expr('int')).stmt,
+              ]),
+            ]);
+          });
+
+          test(
+              'unless a closure in the rhs accesses it and it is mutated '
+              'anywhere', () {
+            var h = Harness(legacy: true);
+            var x = Var('x', 'Object');
+            h.run([
+              x.read
+                  .is_('int')
+                  .and(block([
+                    checkNotPromoted(x),
+                    localFunction([
+                      x.read.stmt,
+                    ]),
+                  ]).thenExpr(expr('bool')))
+                  .stmt,
+              x.write(expr('int')).stmt,
+            ]);
+          });
+
+          test(
+              'even if a closure in the lhs accesses it and it is mutated '
+              'somewhere', () {
+            var h = Harness(legacy: true);
+            var x = Var('x', 'Object');
+            h.run([
+              localFunction([
+                x.read.stmt,
+              ])
+                  .thenExpr(expr('Object'))
+                  .and(x.read.is_('int'))
+                  .parenthesized
+                  .and(checkPromoted(x, 'int').thenExpr(expr('bool')))
+                  .stmt,
+              x.write(expr('int')).stmt,
+            ]);
+          });
+
+          test(
+              'even if a closure in the rhs accesses it, provided it is not '
+              'mutated anywhere', () {
+            var h = Harness(legacy: true);
+            var x = Var('x', 'Object');
+            h.run([
+              x.read
+                  .is_('int')
+                  .and(block([
+                    checkPromoted(x, 'int'),
+                    localFunction([
+                      x.read.stmt,
+                    ]),
+                  ]).thenExpr(expr('bool')))
+                  .stmt,
+            ]);
+          });
+        });
+
+        test('uses lhs promotion if rhs is not to a subtype', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          // Note: for this to be an effective test, we need to mutate `x` on
+          // the LHS of the outer `&&` so that `x` is not promoted on the RHS
+          // (and thus the lesser promotion on the RHS can take effect).
+          h.run([
+            if_(
+                x
+                    .write(expr('Object'))
+                    .parenthesized
+                    .and(x.read.is_('int'))
+                    .parenthesized
+                    .and(x.read.is_('num')),
+                [
+                  checkPromoted(x, 'int'),
+                ]),
+          ]);
+        });
+
+        test('uses rhs promotion if rhs is to a subtype', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          h.run([
+            if_(x.read.is_('num').and(x.read.is_('int')), [
+              checkPromoted(x, 'int'),
+            ]),
+          ]);
+        });
+
+        test('can handle multiple promotions on lhs', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          var y = Var('y', 'Object');
+          h.run([
+            x.read
+                .is_('int')
+                .and(y.read.is_('String'))
+                .parenthesized
+                .and(block([
+                  checkPromoted(x, 'int'),
+                  checkPromoted(y, 'String'),
+                ]).thenExpr(expr('bool')))
+                .stmt,
+          ]);
+        });
+
+        test('handles variables', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'bool');
+          var y = Var('y', 'bool');
+          h.run([
+            if_(x.read.and(y.read), []),
+          ]);
+        });
+
+        test('handles arbitrary expressions', () {
+          var h = Harness(legacy: true);
+          h.run([
+            if_(expr('bool').and(expr('bool')), []),
+          ]);
+        });
+      });
+
+      test('or is ignored', () {
+        var h = Harness(legacy: true);
+        var x = Var('x', 'Object');
+        h.run([
+          if_(x.read.is_('int').or(x.read.is_('int')), [
+            checkNotPromoted(x),
+          ], [
+            checkNotPromoted(x),
+          ])
+        ]);
+      });
+    });
+
+    group('is test', () {
+      group("shows a variable's type", () {
+        test('normally', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          h.run([
+            if_(x.read.is_('int'), [
+              checkPromoted(x, 'int'),
+            ], [
+              checkNotPromoted(x),
+            ])
+          ]);
+        });
+
+        test('unless the test is inverted', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          h.run([
+            if_(x.read.is_('int', isInverted: true), [
+              checkNotPromoted(x),
+            ], [
+              checkNotPromoted(x),
+            ])
+          ]);
+        });
+
+        test('unless the tested type is not a subtype of the declared type',
+            () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'String');
+          h.run([
+            if_(x.read.is_('int'), [
+              checkNotPromoted(x),
+            ], [
+              checkNotPromoted(x),
+            ])
+          ]);
+        });
+
+        test("even when the variable's type has been previously promoted", () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          h.run([
+            if_(x.read.is_('num'), [
+              if_(x.read.is_('int'), [
+                checkPromoted(x, 'int'),
+              ], [
+                checkPromoted(x, 'num'),
+              ])
+            ]),
+          ]);
+        });
+
+        test(
+            'unless the tested type is not a subtype of the previously '
+            'promoted type', () {
+          var h = Harness(legacy: true);
+          var x = Var('x', 'Object');
+          h.run([
+            if_(x.read.is_('String'), [
+              if_(x.read.is_('int'), [
+                checkPromoted(x, 'String'),
+              ], [
+                checkPromoted(x, 'String'),
+              ])
+            ]),
+          ]);
+        });
+
+        test('even when the declared type is a type variable', () {
+          var h = Harness(legacy: true);
+          h.addPromotionException('T', 'int', 'T&int');
+          var x = Var('x', 'T');
+          h.run([
+            if_(x.read.is_('int'), [
+              checkPromoted(x, 'T&int'),
+            ]),
+          ]);
+        });
+      });
+
+      test('handles arbitrary expressions', () {
+        var h = Harness(legacy: true);
+        h.run([
+          if_(expr('Object').is_('int'), []),
+        ]);
+      });
+    });
+
+    test('forwardExpression does not re-activate a deeply nested expression',
+        () {
+      var h = Harness(legacy: true);
+      var x = Var('x', 'Object');
+      h.run([
+        if_(x.read.is_('int').eq(expr('Object')).thenStmt(block([])), [
+          checkNotPromoted(x),
+        ]),
+      ]);
+    });
+
+    test(
+        'parenthesizedExpression does not re-activate a deeply nested '
+        'expression', () {
+      var h = Harness(legacy: true);
+      var x = Var('x', 'Object');
+      h.run([
+        if_(x.read.is_('int').eq(expr('Object')).parenthesized, [
+          checkNotPromoted(x),
+        ]),
+      ]);
+    });
+
+    test('variableRead returns the promoted type if promoted', () {
+      var h = Harness(legacy: true);
+      var x = Var('x', 'Object');
+      h.run([
+        if_(
+            x
+                .readAndCheckPromotedType((type) => expect(type, isNull))
+                .is_('int'),
+            [
+              x
+                  .readAndCheckPromotedType((type) => expect(type!.type, 'int'))
+                  .stmt,
+            ]),
+      ]);
+    });
+  });
 }
 
 /// Returns the appropriate matcher for expecting an assertion error to be
diff --git a/pkg/analysis_server/lib/src/services/correction/bulk_fix_processor.dart b/pkg/analysis_server/lib/src/services/correction/bulk_fix_processor.dart
index 6d94b51..ae7dbd0 100644
--- a/pkg/analysis_server/lib/src/services/correction/bulk_fix_processor.dart
+++ b/pkg/analysis_server/lib/src/services/correction/bulk_fix_processor.dart
@@ -83,7 +83,15 @@
 import 'package:analyzer_plugin/utilities/change_builder/conflicting_edit_exception.dart';
 import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
 
-/// A fix producer that produces changes to fix multiple diagnostics.
+/// A fix producer that produces changes that will fix multiple diagnostics in
+/// one or more files.
+///
+/// Each diagnostic should have a single fix (correction producer) associated
+/// with it except in cases where at most one of the given producers will ever
+/// produce a fix.
+///
+/// The correction producers that are associated with the diagnostics should not
+/// produce changes that alter the semantics of the code.
 class BulkFixProcessor {
   /// A map from the name of a lint rule to a list of generators used to create
   /// the correction producer used to build a fix for that diagnostic. The
diff --git a/pkg/analyzer/lib/src/dart/analysis/driver.dart b/pkg/analyzer/lib/src/dart/analysis/driver.dart
index cb191c0..6a9b1ed 100644
--- a/pkg/analyzer/lib/src/dart/analysis/driver.dart
+++ b/pkg/analyzer/lib/src/dart/analysis/driver.dart
@@ -86,7 +86,7 @@
 /// TODO(scheglov) Clean up the list of implicitly analyzed files.
 class AnalysisDriver implements AnalysisDriverGeneric {
   /// The version of data format, should be incremented on every format change.
-  static const int DATA_VERSION = 119;
+  static const int DATA_VERSION = 120;
 
   /// The length of the list returned by [_computeDeclaredVariablesSignature].
   static const int _declaredVariablesSignatureLength = 4;
diff --git a/pkg/analyzer/lib/src/dart/resolver/binary_expression_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/binary_expression_resolver.dart
index 7572eeb..0414591 100644
--- a/pkg/analyzer/lib/src/dart/resolver/binary_expression_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/binary_expression_resolver.dart
@@ -181,7 +181,7 @@
     left = node.leftOperand;
 
     if (_flowAnalysis != null) {
-      flow?.logicalBinaryOp_rightBegin(left, isAnd: true);
+      flow?.logicalBinaryOp_rightBegin(left, node, isAnd: true);
       _resolver.checkUnreachableNode(right);
 
       right.accept(_resolver);
@@ -218,7 +218,7 @@
     left.accept(_resolver);
     left = node.leftOperand;
 
-    flow?.logicalBinaryOp_rightBegin(left, isAnd: false);
+    flow?.logicalBinaryOp_rightBegin(left, node, isAnd: false);
     _resolver.checkUnreachableNode(right);
 
     right.accept(_resolver);
diff --git a/pkg/analyzer/lib/src/generated/resolver.dart b/pkg/analyzer/lib/src/generated/resolver.dart
index 2112e2d..2579d39 100644
--- a/pkg/analyzer/lib/src/generated/resolver.dart
+++ b/pkg/analyzer/lib/src/generated/resolver.dart
@@ -1110,7 +1110,7 @@
 
     if (_flowAnalysis != null) {
       if (flow != null) {
-        flow.conditional_thenBegin(condition);
+        flow.conditional_thenBegin(condition, node);
         checkUnreachableNode(thenExpression);
       }
       thenExpression.accept(this);
@@ -1517,7 +1517,7 @@
 
     CollectionElement thenElement = node.thenElement;
     if (_flowAnalysis != null) {
-      _flowAnalysis.flow?.ifStatement_thenBegin(condition);
+      _flowAnalysis.flow?.ifStatement_thenBegin(condition, node);
       thenElement.accept(this);
     } else {
       _promoteManager.visitIfElement_thenElement(
@@ -1557,7 +1557,7 @@
 
     Statement thenStatement = node.thenStatement;
     if (_flowAnalysis != null) {
-      _flowAnalysis.flow?.ifStatement_thenBegin(condition);
+      _flowAnalysis.flow?.ifStatement_thenBegin(condition, node);
       visitStatementInScope(thenStatement);
       nullSafetyDeadCodeVerifier?.flowEnd(thenStatement);
     } else {
diff --git a/pkg/analyzer/lib/src/summary2/apply_resolution.dart b/pkg/analyzer/lib/src/summary2/apply_resolution.dart
index e254ae4..f1094c6 100644
--- a/pkg/analyzer/lib/src/summary2/apply_resolution.dart
+++ b/pkg/analyzer/lib/src/summary2/apply_resolution.dart
@@ -528,6 +528,15 @@
   }
 
   @override
+  void visitForPartsWithExpression(ForPartsWithExpression node) {
+    _expectMarker(MarkerTag.ForPartsWithExpression_initialization);
+    node.initialization?.accept(this);
+    _expectMarker(MarkerTag.ForPartsWithExpression_forParts);
+    _forParts(node);
+    _expectMarker(MarkerTag.ForPartsWithExpression_end);
+  }
+
+  @override
   void visitFunctionDeclaration(FunctionDeclaration node) {
     _assertNoLocalElements();
 
diff --git a/pkg/analyzer/lib/src/summary2/ast_binary_reader.dart b/pkg/analyzer/lib/src/summary2/ast_binary_reader.dart
index 3bc726f..6313f8e 100644
--- a/pkg/analyzer/lib/src/summary2/ast_binary_reader.dart
+++ b/pkg/analyzer/lib/src/summary2/ast_binary_reader.dart
@@ -94,6 +94,8 @@
         return _readForElement();
       case Tag.ForPartsWithDeclarations:
         return _readForPartsWithDeclarations();
+      case Tag.ForPartsWithExpression:
+        return _readForPartsWithExpression();
       case Tag.FieldFormalParameter:
         return _readFieldFormalParameter();
       case Tag.FormalParameterList:
@@ -857,6 +859,19 @@
     );
   }
 
+  ForPartsWithExpression _readForPartsWithExpression() {
+    var initialization = _readOptionalNode() as Expression;
+    var condition = _readOptionalNode() as Expression;
+    var updaters = _readNodeList<Expression>();
+    return astFactory.forPartsWithExpression(
+      condition: condition,
+      initialization: initialization,
+      leftSeparator: Tokens.SEMICOLON,
+      rightSeparator: Tokens.SEMICOLON,
+      updaters: updaters,
+    );
+  }
+
   FunctionDeclaration _readFunctionDeclaration() {
     var flags = _readByte();
     var codeOffset = _readInformativeUint30();
diff --git a/pkg/analyzer/lib/src/summary2/ast_binary_tag.dart b/pkg/analyzer/lib/src/summary2/ast_binary_tag.dart
index cf220f2..bcda462 100644
--- a/pkg/analyzer/lib/src/summary2/ast_binary_tag.dart
+++ b/pkg/analyzer/lib/src/summary2/ast_binary_tag.dart
@@ -131,6 +131,9 @@
   ForPartsWithDeclarations_variables,
   ForPartsWithDeclarations_forParts,
   ForPartsWithDeclarations_end,
+  ForPartsWithExpression_initialization,
+  ForPartsWithExpression_forParts,
+  ForPartsWithExpression_end,
   FunctionDeclaration_functionExpression,
   FunctionDeclaration_returnType,
   FunctionDeclaration_namedCompilationUnitMember,
@@ -365,6 +368,7 @@
   static const int ForEachPartsWithDeclaration = 89;
   static const int ForElement = 88;
   static const int ForPartsWithDeclarations = 91;
+  static const int ForPartsWithExpression = 99;
   static const int FormalParameterList = 17;
   static const int FunctionDeclaration = 18;
   static const int FunctionDeclaration_getter = 57;
diff --git a/pkg/analyzer/lib/src/summary2/ast_binary_writer.dart b/pkg/analyzer/lib/src/summary2/ast_binary_writer.dart
index 6f7f8a9..c805bbd 100644
--- a/pkg/analyzer/lib/src/summary2/ast_binary_writer.dart
+++ b/pkg/analyzer/lib/src/summary2/ast_binary_writer.dart
@@ -752,6 +752,16 @@
   }
 
   @override
+  void visitForPartsWithExpression(ForPartsWithExpression node) {
+    _writeByte(Tag.ForPartsWithExpression);
+    _writeMarker(MarkerTag.ForPartsWithExpression_initialization);
+    _writeOptionalNode(node.initialization);
+    _writeMarker(MarkerTag.ForPartsWithExpression_forParts);
+    _storeForParts(node);
+    _writeMarker(MarkerTag.ForPartsWithExpression_end);
+  }
+
+  @override
   void visitFunctionDeclaration(FunctionDeclaration node) {
     var indexTag = Tag.FunctionDeclaration;
     if (node.isGetter) {
diff --git a/pkg/analyzer/test/generated/invalid_code_test.dart b/pkg/analyzer/test/generated/invalid_code_test.dart
index f459e36..4b043ca 100644
--- a/pkg/analyzer/test/generated/invalid_code_test.dart
+++ b/pkg/analyzer/test/generated/invalid_code_test.dart
@@ -20,6 +20,13 @@
 /// and analysis finishes without exceptions.
 @reflectiveTest
 class InvalidCodeTest extends PubPackageResolutionTest {
+  test_const_ForPartsWithExpression() async {
+    await _assertCanBeAnalyzed(r'''
+@A([for (;;) 0])
+void f() {}
+''');
+  }
+
   /// This code results in a method with the empty name, and the default
   /// constructor, which also has the empty name. The `Map` in `f` initializer
   /// references the empty name.
diff --git a/pkg/front_end/lib/src/fasta/kernel/inference_visitor.dart b/pkg/front_end/lib/src/fasta/kernel/inference_visitor.dart
index 36a1be3..7762bfb 100644
--- a/pkg/front_end/lib/src/fasta/kernel/inference_visitor.dart
+++ b/pkg/front_end/lib/src/fasta/kernel/inference_visitor.dart
@@ -374,7 +374,7 @@
     Expression condition =
         inferrer.ensureAssignableResult(expectedType, conditionResult);
     node.condition = condition..parent = node;
-    inferrer.flowAnalysis.conditional_thenBegin(node.condition);
+    inferrer.flowAnalysis.conditional_thenBegin(node.condition, node);
     bool isThenReachable = inferrer.flowAnalysis.isReachable;
     ExpressionInferenceResult thenResult = inferrer
         .inferExpression(node.then, typeContext, true, isVoidAllowed: true);
@@ -1205,7 +1205,7 @@
     Expression condition =
         inferrer.ensureAssignableResult(expectedType, conditionResult);
     node.condition = condition..parent = node;
-    inferrer.flowAnalysis.ifStatement_thenBegin(condition);
+    inferrer.flowAnalysis.ifStatement_thenBegin(condition, node);
     StatementInferenceResult thenResult = inferrer.inferStatement(node.then);
     if (thenResult.hasChanged) {
       node.then = thenResult.statement..parent = node;
@@ -1471,7 +1471,7 @@
       Expression condition =
           inferrer.ensureAssignableResult(boolType, conditionResult);
       element.condition = condition..parent = element;
-      inferrer.flowAnalysis.ifStatement_thenBegin(condition);
+      inferrer.flowAnalysis.ifStatement_thenBegin(condition, element);
       ExpressionInferenceResult thenResult = inferElement(
           element.then,
           inferredTypeArgument,
@@ -1774,7 +1774,7 @@
         isVoidAllowed: false);
     Expression left = inferrer.ensureAssignableResult(boolType, leftResult);
     node.left = left..parent = node;
-    inferrer.flowAnalysis.logicalBinaryOp_rightBegin(node.left,
+    inferrer.flowAnalysis.logicalBinaryOp_rightBegin(node.left, node,
         isAnd: node.operatorEnum == LogicalExpressionOperator.AND);
     ExpressionInferenceResult rightResult = inferrer.inferExpression(
         node.right, boolType, !inferrer.isTopLevel,
@@ -2049,7 +2049,7 @@
       Expression condition =
           inferrer.ensureAssignableResult(boolType, conditionResult);
       entry.condition = condition..parent = entry;
-      inferrer.flowAnalysis.ifStatement_thenBegin(condition);
+      inferrer.flowAnalysis.ifStatement_thenBegin(condition, entry);
       // Note that this recursive invocation of inferMapEntry will add two types
       // to actualTypes; they are the actual types of the current invocation if
       // the 'else' branch is empty.
diff --git a/pkg/front_end/test/spell_checking_list_common.txt b/pkg/front_end/test/spell_checking_list_common.txt
index 4d427f4..9ff711a 100644
--- a/pkg/front_end/test/spell_checking_list_common.txt
+++ b/pkg/front_end/test/spell_checking_list_common.txt
@@ -119,6 +119,7 @@
 although
 altogether
 always
+ambiguity
 ambiguous
 among
 amongst
@@ -1140,6 +1141,7 @@
 facilitate
 fact
 fact's
+facto
 factories
 factory
 facts
@@ -3338,6 +3340,7 @@
 worklist
 works
 world
+worry
 worth
 would
 wouldn't
diff --git a/pkg/nnbd_migration/lib/migration_cli.dart b/pkg/nnbd_migration/lib/migration_cli.dart
index 7013c22..785b5a6 100644
--- a/pkg/nnbd_migration/lib/migration_cli.dart
+++ b/pkg/nnbd_migration/lib/migration_cli.dart
@@ -604,7 +604,7 @@
       String summaryPath,
       @required String sdkPath}) {
     return NonNullableFix(listener, resourceProvider, getLineInfo, bindAddress,
-        logger, (String path) => shouldBeMigrated(null, path),
+        logger, (String path) => shouldBeMigrated(path),
         included: included,
         preferredPort: preferredPort,
         summaryPath: summaryPath,
@@ -813,12 +813,7 @@
   /// return additional paths that aren't inside the user's project, but doesn't
   /// override this method, then those additional paths will be analyzed but not
   /// migrated.
-  ///
-  /// Note: in a future version of the code, the [context] argument will be
-  /// removed; to ease the transition, clients should stop overriding this
-  /// method and should override [shouldBeMigrated2] instead.
-  bool shouldBeMigrated(DriverBasedAnalysisContext context, String path) =>
-      shouldBeMigrated2(path);
+  bool shouldBeMigrated(String path) => shouldBeMigrated2(path);
 
   /// Determines whether a migrated version of the file at [path] should be
   /// output by the migration too.  May be overridden by a derived class.
@@ -832,6 +827,10 @@
   /// return additional paths that aren't inside the user's project, but doesn't
   /// override this method, then those additional paths will be analyzed but not
   /// migrated.
+  ///
+  /// Note: in a future version of the code, this method will be removed;
+  /// clients that are overriding this method should switch to overriding
+  /// [shouldBeMigrated] instead.
   bool shouldBeMigrated2(String path) {
     return analysisContext.contextRoot.isAnalyzed(path);
   }
@@ -1171,7 +1170,7 @@
     });
     await processResources((ResolvedUnitResult result) async {
       _progressBar.tick();
-      if (_migrationCli.shouldBeMigrated(context, result.path)) {
+      if (_migrationCli.shouldBeMigrated(result.path)) {
         await _task.finalizeUnit(result);
       }
     });
diff --git a/pkg/nnbd_migration/lib/src/edge_builder.dart b/pkg/nnbd_migration/lib/src/edge_builder.dart
index 58f484b..bca2e93 100644
--- a/pkg/nnbd_migration/lib/src/edge_builder.dart
+++ b/pkg/nnbd_migration/lib/src/edge_builder.dart
@@ -470,7 +470,8 @@
       bool isAnd = operatorType == TokenType.AMPERSAND_AMPERSAND;
       _flowAnalysis.logicalBinaryOp_begin();
       _checkExpressionNotNull(leftOperand);
-      _flowAnalysis.logicalBinaryOp_rightBegin(node.leftOperand, isAnd: isAnd);
+      _flowAnalysis.logicalBinaryOp_rightBegin(node.leftOperand, node,
+          isAnd: isAnd);
       _postDominatedLocals.doScoped(
           action: () => _checkExpressionNotNull(rightOperand));
       _flowAnalysis.logicalBinaryOp_end(node, rightOperand, isAnd: isAnd);
@@ -659,7 +660,7 @@
     // Note: we don't have to create a scope for each branch because they can't
     // define variables.
     _postDominatedLocals.doScoped(action: () {
-      _flowAnalysis.conditional_thenBegin(node.condition);
+      _flowAnalysis.conditional_thenBegin(node.condition, node);
       if (trueGuard != null) {
         _guards.add(trueGuard);
       }
@@ -992,7 +993,7 @@
       _guards.add(trueGuard);
     }
     try {
-      _flowAnalysis.ifStatement_thenBegin(node.condition);
+      _flowAnalysis.ifStatement_thenBegin(node.condition, node);
       // We branched, so create a new scope for post-dominators.
       _postDominatedLocals.doScoped(
           action: () => _dispatch(node.thenStatement));
diff --git a/tools/VERSION b/tools/VERSION
index 623857b..235cc3e 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 12
 PATCH 0
-PRERELEASE 235
+PRERELEASE 236
 PRERELEASE_PATCH 0
\ No newline at end of file