Flow analysis: begin tracking non-promotion reasons.

This CL implements the core flow analysis infrastructure for tracking
reasons why an expression was not promoted.  It supports the following
reasons:

- Expression was a property access
- Expression has been written to since it was promoted

I expect to add support for other non-promotion reasons in the future,
for example:

- `this` cannot be promoted
- Expression has been write captured
- Expression was a reference to a static field or top level variable

These non-promotion reasons are plumbed through to the CFE and
analyzer for the purpose of making errors easier for the user to
understand.  For example, given the following code:

  class C {
    int? i;
    f() {
      if (i == null) return;
      print(i.isEven);
    }
  }

The front end now prints:

  ../../tmp/test.dart:5:13: Error: Property 'isEven' cannot be accessed on 'int?' because it is potentially null.
  Try accessing using ?. instead.
      print(i.isEven);
              ^^^^^^
  Context: 'i' refers to a property so it could not be promoted.

Much work still needs to be done to round out this feature, for example:

- Currently the analyzer only shows the new "why not promoted"
  messages when the "--verbose" flag is specified; this means the
  feature is unlikely to be noticed by users.

- Currently the analyzer doesn't show a "why not promoted" message
  when the non-promotion reason is that the expression is a property
  access.

- We need one or more web pages explaining non-promotion reasons in
  more detail so that the error messages can contain pointers to them.

- The analyzer and front end currently only show non-promotion reasons
  for expressions of the form `x.y` where `x` fails to be promoted to
  non-nullable.  There are many other scenarios that should be
  handled.

Change-Id: I0a12df74d0fc6274dfb3cb555abea81a75884231
Bug: https://github.com/dart-lang/sdk/issues/38773
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/181741
Commit-Queue: Paul Berry <paulberry@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
diff --git a/.dart_tool/package_config.json b/.dart_tool/package_config.json
index 429b915..70cfbb2 100644
--- a/.dart_tool/package_config.json
+++ b/.dart_tool/package_config.json
@@ -56,6 +56,11 @@
       "packageUri": ".nonexisting/"
     },
     {
+      "name": "_fe_analyzer_shared_why_not_promoted",
+      "rootUri": "../pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted",
+      "packageUri": ".nonexisting/"
+    },
+    {
       "name": "_js_interop_checks",
       "rootUri": "../pkg/_js_interop_checks",
       "packageUri": "lib/",
diff --git a/pkg/_fe_analyzer_shared/analysis_options_no_lints.yaml b/pkg/_fe_analyzer_shared/analysis_options_no_lints.yaml
index bba3f74..467f323 100644
--- a/pkg/_fe_analyzer_shared/analysis_options_no_lints.yaml
+++ b/pkg/_fe_analyzer_shared/analysis_options_no_lints.yaml
@@ -15,4 +15,5 @@
     - test/flow_analysis/nullability/data/**
     - test/flow_analysis/reachability/data/**
     - test/flow_analysis/type_promotion/data/**
+    - test/flow_analysis/why_not_promoted/data/**
     - test/inheritance/data/**
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 6809bbc..327b003 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
@@ -307,6 +307,61 @@
       '_declared=$_declared)';
 }
 
+/// Non-promotion reason describing the situation where a variable was not
+/// promoted due to an explicit write to the variable appearing somewhere in the
+/// source code.
+class DemoteViaExplicitWrite<Variable extends Object, Expression extends Object>
+    extends NonPromotionReason {
+  /// The local variable that was not promoted.
+  final Variable variable;
+
+  /// The expression that wrote to the variable; this corresponds to an
+  /// expression that was passed to [FlowAnalysis.write].
+  final Expression writeExpression;
+
+  DemoteViaExplicitWrite(this.variable, this.writeExpression);
+
+  @override
+  String get shortName => 'explicitWrite';
+
+  @override
+  R accept<R, Node extends Object, Expression extends Object,
+              Variable extends Object>(
+          NonPromotionReasonVisitor<R, Node, Expression, Variable> visitor) =>
+      visitor.visitDemoteViaExplicitWrite(
+          this as DemoteViaExplicitWrite<Variable, Expression>);
+
+  @override
+  String toString() => 'DemoteViaExplicitWrite($writeExpression)';
+}
+
+/// Non-promotion reason describing the situation where a variable was not
+/// promoted due to the variable appearing before the word `in` in a "for each"
+/// statement or a "for each" collection element.
+class DemoteViaForEachVariableWrite<Variable extends Object,
+    Node extends Object> extends NonPromotionReason {
+  /// The local variable that was not promoted.
+  final Variable variable;
+
+  /// The "for each" statement or collection element that wrote to the variable.
+  final Node node;
+
+  DemoteViaForEachVariableWrite(this.variable, this.node);
+
+  @override
+  String get shortName => 'explicitWrite';
+
+  @override
+  R accept<R, Node extends Object, Expression extends Object,
+              Variable extends Object>(
+          NonPromotionReasonVisitor<R, Node, Expression, Variable> visitor) =>
+      visitor.visitDemoteViaForEachVariableWrite(
+          this as DemoteViaForEachVariableWrite<Variable, Node>);
+
+  @override
+  String toString() => 'DemoteViaForEachVariableWrite($node)';
+}
+
 /// A collection of flow models representing the possible outcomes of evaluating
 /// an expression that are relevant to flow analysis.
 class ExpressionInfo<Variable extends Object, Type extends Object> {
@@ -338,6 +393,24 @@
       'ExpressionInfo(after: $after, _ifTrue: $ifTrue, ifFalse: $ifFalse)';
 }
 
+/// Non-promotion reason describing the situation where an expression was not
+/// promoted due to the fact that it's a field (technically, a property get).
+class FieldNotPromoted extends NonPromotionReason {
+  /// The name of the property.
+  final String propertyName;
+
+  FieldNotPromoted(this.propertyName);
+
+  @override
+  String get shortName => 'fieldNotPromoted($propertyName)';
+
+  @override
+  R accept<R, Node extends Object, Expression extends Object,
+              Variable extends Object>(
+          NonPromotionReasonVisitor<R, Node, Expression, Variable> visitor) =>
+      visitor.visitFieldNotPromoted(this);
+}
+
 /// Implementation of flow analysis to be shared between the analyzer and the
 /// front end.
 ///
@@ -715,6 +788,13 @@
   /// is currently promoted.  Otherwise returns `null`.
   Type? promotedType(Variable variable);
 
+  /// Call this method just after visiting a property get expression.
+  /// [wholeExpression] should be the whole property get, [target] should be the
+  /// expression to the left hand side of the `.`, and [propertyName] should be
+  /// the identifier to the right hand side of the `.`.
+  void propertyGet(
+      Expression wholeExpression, Expression target, String propertyName);
+
   /// Retrieves the SSA node associated with [variable], or `null` if [variable]
   /// is not associated with an SSA node because it is write captured.  For
   /// testing only.
@@ -750,6 +830,20 @@
   /// - Call [switchStatement_end].
   void switchStatement_expressionEnd(Statement switchStatement);
 
+  /// Call this method just after visiting the expression `this` (or the
+  /// pseudo-expression `super`, in the case of the analyzer, which represents
+  /// `super.x` as a property get whose target is `super`).  [expression] should
+  /// be the `this` or `super` expression.
+  void thisOrSuper(Expression expression);
+
+  /// Call this method just after visiting an expression that represents a
+  /// property get on `this` or `super`.  This handles situations where there is
+  /// an implicit reference to `this`, or the case of the front end, where
+  /// `super.x` is represented by a single expression.  [expression] should be
+  /// the whole property get, and [propertyName] should be the name of the
+  /// property being read.
+  void thisOrSuperPropertyGet(Expression expression, String propertyName);
+
   /// Call this method just before visiting the body of a "try/catch" statement.
   ///
   /// The order of visiting a "try/catch" statement should be:
@@ -855,8 +949,23 @@
   /// Call this method after visiting a "while" statement.
   void whileStatement_end();
 
+  /// Call this method when an error occurs that may be due to a lack of type
+  /// promotion, to retrieve information about why [target] was not promoted.
+  /// This call must be made right after visiting [target].
+  ///
+  /// 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
+  /// select which non-promotion reason to report to the user by seeing which
+  /// promotion would have prevented the error.  (For example, if an error
+  /// 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);
+
   /// Register write of the given [variable] in the current state.
   /// [writtenType] should be the type of the value that was written.
+  /// [expression] should be the whole expression performing the write.
   /// [writtenExpression] should be the expression that was written, or `null`
   /// if the expression that was written is not directly represented in the
   /// source code (this happens, for example, with compound assignments and with
@@ -865,8 +974,8 @@
   /// This should also be used for the implicit write to a non-final variable in
   /// its initializer, to ensure that the type is promoted to non-nullable if
   /// necessary; in this case, [viaInitializer] should be `true`.
-  void write(
-      Variable variable, Type writtenType, Expression? writtenExpression);
+  void write(Expression expression, Variable variable, Type writtenType,
+      Expression? writtenExpression);
 
   /// Prints out a summary of the current state of flow analysis, intended for
   /// debugging use only.
@@ -1249,6 +1358,13 @@
   }
 
   @override
+  void propertyGet(
+      Expression wholeExpression, Expression target, String propertyName) {
+    _wrap('propertyGet($wholeExpression, $target, $propertyName)',
+        () => _wrapped.propertyGet(wholeExpression, target, propertyName));
+  }
+
+  @override
   SsaNode<Variable, Type>? ssaNodeForTesting(Variable variable) {
     return _wrap('ssaNodeForTesting($variable)',
         () => _wrapped.ssaNodeForTesting(variable),
@@ -1274,6 +1390,18 @@
   }
 
   @override
+  void thisOrSuper(Expression expression) {
+    return _wrap(
+        'thisOrSuper($expression)', () => _wrapped.thisOrSuper(expression));
+  }
+
+  @override
+  void thisOrSuperPropertyGet(Expression expression, String propertyName) {
+    _wrap('thisOrSuperPropertyGet($expression, $propertyName)',
+        () => _wrapped.thisOrSuperPropertyGet(expression, propertyName));
+  }
+
+  @override
   void tryCatchStatement_bodyBegin() {
     return _wrap('tryCatchStatement_bodyBegin()',
         () => _wrapped.tryCatchStatement_bodyBegin());
@@ -1350,10 +1478,19 @@
   }
 
   @override
-  void write(
-      Variable variable, Type writtenType, Expression? writtenExpression) {
-    _wrap('write($variable, $writtenType, $writtenExpression)',
-        () => _wrapped.write(variable, writtenType, writtenExpression));
+  Map<Type, NonPromotionReason> whyNotPromoted(Expression target) {
+    return _wrap(
+        'whyNotPromoted($target)', () => _wrapped.whyNotPromoted(target),
+        isQuery: true);
+  }
+
+  @override
+  void write(Expression expression, Variable variable, Type writtenType,
+      Expression? writtenExpression) {
+    _wrap(
+        'write($expression, $variable, $writtenType, $writtenExpression)',
+        () => _wrapped.write(
+            expression, variable, writtenType, writtenExpression));
   }
 
   @override
@@ -1403,10 +1540,14 @@
   /// variable is not in scope anymore.  This should not have any effect on
   /// analysis results for error-free code, because it is an error to refer to a
   /// variable that is no longer in scope.
-  final Map<Variable, VariableModel<Variable, Type> /*!*/ > variableInfo;
+  ///
+  /// `null` is allowed as a special key; it represents the pseudo-variable
+  /// `this`.  (This is needed so that we can explain why `this` is not
+  /// promoted, and why properties of `this` are not promoted).
+  final Map<Variable?, VariableModel<Variable, Type> /*!*/ > variableInfo;
 
   /// The empty map, used to [join] variables.
-  final Map<Variable, VariableModel<Variable, Type>> _emptyVariableMap = {};
+  final Map<Variable?, VariableModel<Variable, Type>> _emptyVariableMap = {};
 
   /// Creates a state object with the given [reachable] status.  All variables
   /// are assumed to be unpromoted and already assigned, so joining another
@@ -1455,13 +1596,13 @@
     Reachability newReachable = afterFinally.reachable.rebaseForward(reachable);
 
     // Consider each variable that is common to all three models.
-    Map<Variable, VariableModel<Variable, Type>> newVariableInfo =
-        <Variable, VariableModel<Variable, Type>>{};
+    Map<Variable?, VariableModel<Variable, Type>> newVariableInfo =
+        <Variable?, VariableModel<Variable, Type>>{};
     bool variableInfoMatchesThis = true;
     bool variableInfoMatchesAfterFinally = true;
-    for (MapEntry<Variable, VariableModel<Variable, Type>> entry
+    for (MapEntry<Variable?, VariableModel<Variable, Type>> entry
         in variableInfo.entries) {
-      Variable variable = entry.key;
+      Variable? variable = entry.key;
       VariableModel<Variable, Type> thisModel = entry.value;
       VariableModel<Variable, Type>? beforeFinallyModel =
           beforeFinally.variableInfo[variable];
@@ -1524,7 +1665,7 @@
     // erroneously think that `newVariableInfo` matches `afterFinally`.  If so,
     // correct that.
     if (variableInfoMatchesAfterFinally) {
-      for (Variable variable in afterFinally.variableInfo.keys) {
+      for (Variable? variable in afterFinally.variableInfo.keys) {
         if (!variableInfo.containsKey(variable)) {
           variableInfoMatchesAfterFinally = false;
           break;
@@ -1567,7 +1708,7 @@
   FlowModel<Variable, Type> conservativeJoin(
       Iterable<Variable> writtenVariables,
       Iterable<Variable> capturedVariables) {
-    Map<Variable, VariableModel<Variable, Type>>? newVariableInfo;
+    Map<Variable?, VariableModel<Variable, Type>>? newVariableInfo;
 
     for (Variable variable in writtenVariables) {
       VariableModel<Variable, Type> info = infoFor(variable);
@@ -1575,7 +1716,7 @@
           info.discardPromotionsAndMarkNotUnassigned();
       if (!identical(info, newInfo)) {
         (newVariableInfo ??=
-            new Map<Variable, VariableModel<Variable, Type>>.from(
+            new Map<Variable?, VariableModel<Variable, Type>>.from(
                 variableInfo))[variable] = newInfo;
       }
     }
@@ -1630,14 +1771,14 @@
   FlowModel<Variable, Type> inheritTested(
       TypeOperations<Variable, Type> typeOperations,
       FlowModel<Variable, Type> other) {
-    Map<Variable, VariableModel<Variable, Type>> newVariableInfo =
-        <Variable, VariableModel<Variable, Type>>{};
-    Map<Variable, VariableModel<Variable, Type>> otherVariableInfo =
+    Map<Variable?, VariableModel<Variable, Type>> newVariableInfo =
+        <Variable?, VariableModel<Variable, Type>>{};
+    Map<Variable?, VariableModel<Variable, Type>> otherVariableInfo =
         other.variableInfo;
     bool changed = false;
-    for (MapEntry<Variable, VariableModel<Variable, Type>> entry
+    for (MapEntry<Variable?, VariableModel<Variable, Type>> entry
         in variableInfo.entries) {
-      Variable variable = entry.key;
+      Variable? variable = entry.key;
       VariableModel<Variable, Type> variableModel = entry.value;
       VariableModel<Variable, Type>? otherVariableModel =
           otherVariableInfo[variable];
@@ -1674,13 +1815,13 @@
     Reachability newReachable = reachable.rebaseForward(base.reachable);
 
     // Consider each variable in the new base model.
-    Map<Variable, VariableModel<Variable, Type>> newVariableInfo =
-        <Variable, VariableModel<Variable, Type>>{};
+    Map<Variable?, VariableModel<Variable, Type>> newVariableInfo =
+        <Variable?, VariableModel<Variable, Type>>{};
     bool variableInfoMatchesThis = true;
     bool variableInfoMatchesBase = true;
-    for (MapEntry<Variable, VariableModel<Variable, Type>> entry
+    for (MapEntry<Variable?, VariableModel<Variable, Type>> entry
         in base.variableInfo.entries) {
-      Variable variable = entry.key;
+      Variable? variable = entry.key;
       VariableModel<Variable, Type> baseModel = entry.value;
       VariableModel<Variable, Type>? thisModel = variableInfo[variable];
       if (thisModel == null) {
@@ -1736,7 +1877,7 @@
     // present in `this` that aren't present in `base`, we may erroneously think
     // that `newVariableInfo` matches `this`.  If so, correct that.
     if (variableInfoMatchesThis) {
-      for (Variable variable in variableInfo.keys) {
+      for (Variable? variable in variableInfo.keys) {
         if (!base.variableInfo.containsKey(variable)) {
           variableInfoMatchesThis = false;
           break;
@@ -1789,13 +1930,13 @@
     Reachability newReachable =
         Reachability.restrict(reachable, other.reachable);
 
-    Map<Variable, VariableModel<Variable, Type>> newVariableInfo =
-        <Variable, VariableModel<Variable, Type>>{};
+    Map<Variable?, VariableModel<Variable, Type>> newVariableInfo =
+        <Variable?, VariableModel<Variable, Type>>{};
     bool variableInfoMatchesThis = true;
     bool variableInfoMatchesOther = true;
-    for (MapEntry<Variable, VariableModel<Variable, Type>> entry
+    for (MapEntry<Variable?, VariableModel<Variable, Type>> entry
         in variableInfo.entries) {
-      Variable variable = entry.key;
+      Variable? variable = entry.key;
       VariableModel<Variable, Type> thisModel = entry.value;
       VariableModel<Variable, Type>? otherModel = other.variableInfo[variable];
       if (otherModel == null) {
@@ -1809,7 +1950,7 @@
       if (!identical(restricted, otherModel)) variableInfoMatchesOther = false;
     }
     if (variableInfoMatchesOther) {
-      for (Variable variable in other.variableInfo.keys) {
+      for (Variable? variable in other.variableInfo.keys) {
         if (!variableInfo.containsKey(variable)) {
           variableInfoMatchesOther = false;
           break;
@@ -1975,7 +2116,12 @@
   /// Updates the state to indicate that an assignment was made to the given
   /// [variable].  The variable is marked as definitely assigned, and any
   /// previous type promotion is removed.
+  ///
+  /// If there is any chance that the write will cause a demotion, the caller
+  /// must pass in a non-null value for [nonPromotionReason] describing the
+  /// reason for any potential demotion.
   FlowModel<Variable, Type> write(
+      NonPromotionReason? nonPromotionReason,
       Variable variable,
       Type writtenType,
       SsaNode<Variable, Type> newSsaNode,
@@ -1983,8 +2129,8 @@
     VariableModel<Variable, Type>? infoForVar = variableInfo[variable];
     if (infoForVar == null) return this;
 
-    VariableModel<Variable, Type> newInfoForVar =
-        infoForVar.write(variable, writtenType, typeOperations, newSsaNode);
+    VariableModel<Variable, Type> newInfoForVar = infoForVar.write(
+        nonPromotionReason, variable, writtenType, typeOperations, newSsaNode);
     if (identical(newInfoForVar, infoForVar)) return this;
 
     return _updateVariableInfo(new VariableReference(variable), newInfoForVar);
@@ -2019,7 +2165,8 @@
     if (promotedType != null) {
       newPromotedTypes =
           VariableModel._addToPromotedTypes(info.promotedTypes, promotedType);
-      if (typeOperations.isNever(promotedType)) {
+      if (reference is VariableReference<Object, Object> &&
+          typeOperations.isNever(promotedType)) {
         newReachable = reachable.setUnreachable();
       }
     }
@@ -2035,7 +2182,8 @@
                 tested: newTested,
                 assigned: info.assigned,
                 unassigned: info.unassigned,
-                ssaNode: info.ssaNode),
+                ssaNode: info.ssaNode,
+                nonPromotionHistory: info.nonPromotionHistory),
             reachable: newReachable);
   }
 
@@ -2045,8 +2193,8 @@
       Reference<Variable, Type> reference, VariableModel<Variable, Type> model,
       {Reachability? reachable}) {
     reachable ??= this.reachable;
-    Map<Variable, VariableModel<Variable, Type>> newVariableInfo =
-        new Map<Variable, VariableModel<Variable, Type>>.from(variableInfo);
+    Map<Variable?, VariableModel<Variable, Type>> newVariableInfo =
+        new Map<Variable?, VariableModel<Variable, Type>>.from(variableInfo);
     reference.storeInfo(newVariableInfo, model);
     return new FlowModel<Variable, Type>.withInfo(reachable, newVariableInfo);
   }
@@ -2065,7 +2213,7 @@
     TypeOperations<Variable, Type> typeOperations,
     FlowModel<Variable, Type>? first,
     FlowModel<Variable, Type>? second,
-    Map<Variable, VariableModel<Variable, Type>> emptyVariableMap,
+    Map<Variable?, VariableModel<Variable, Type>> emptyVariableMap,
   ) {
     if (first == null) return second!;
     if (second == null) return first;
@@ -2082,7 +2230,7 @@
 
     Reachability newReachable =
         Reachability.join(first.reachable, second.reachable);
-    Map<Variable, VariableModel<Variable, Type>> newVariableInfo =
+    Map<Variable?, VariableModel<Variable, Type>> newVariableInfo =
         FlowModel.joinVariableInfo(typeOperations, first.variableInfo,
             second.variableInfo, emptyVariableMap);
 
@@ -2092,25 +2240,25 @@
 
   /// Joins two "variable info" maps.  See [join] for details.
   @visibleForTesting
-  static Map<Variable, VariableModel<Variable, Type>>
+  static Map<Variable?, VariableModel<Variable, Type>>
       joinVariableInfo<Variable extends Object, Type extends Object>(
     TypeOperations<Variable, Type> typeOperations,
-    Map<Variable, VariableModel<Variable, Type>> first,
-    Map<Variable, VariableModel<Variable, Type>> second,
-    Map<Variable, VariableModel<Variable, Type>> emptyMap,
+    Map<Variable?, VariableModel<Variable, Type>> first,
+    Map<Variable?, VariableModel<Variable, Type>> second,
+    Map<Variable?, VariableModel<Variable, Type>> emptyMap,
   ) {
     if (identical(first, second)) return first;
     if (first.isEmpty || second.isEmpty) {
       return emptyMap;
     }
 
-    Map<Variable, VariableModel<Variable, Type>> result =
-        <Variable, VariableModel<Variable, Type>>{};
+    Map<Variable?, VariableModel<Variable, Type>> result =
+        <Variable?, VariableModel<Variable, Type>>{};
     bool alwaysFirst = true;
     bool alwaysSecond = true;
-    for (MapEntry<Variable, VariableModel<Variable, Type>> entry
+    for (MapEntry<Variable?, VariableModel<Variable, Type>> entry
         in first.entries) {
-      Variable variable = entry.key;
+      Variable? variable = entry.key;
       VariableModel<Variable, Type>? secondModel = second[variable];
       if (secondModel == null) {
         alwaysFirst = false;
@@ -2137,7 +2285,7 @@
     TypeOperations<Variable, Type> typeOperations,
     FlowModel<Variable, Type>? first,
     FlowModel<Variable, Type>? second,
-    Map<Variable, VariableModel<Variable, Type>> emptyVariableMap,
+    Map<Variable?, VariableModel<Variable, Type>> emptyVariableMap,
   ) {
     if (first == null) return second!.unsplit();
     if (second == null) return first.unsplit();
@@ -2154,7 +2302,7 @@
 
     Reachability newReachable =
         Reachability.join(first.reachable, second.reachable).unsplit();
-    Map<Variable, VariableModel<Variable, Type>> newVariableInfo =
+    Map<Variable?, VariableModel<Variable, Type>> newVariableInfo =
         FlowModel.joinVariableInfo(typeOperations, first.variableInfo,
             second.variableInfo, emptyVariableMap);
 
@@ -2169,7 +2317,7 @@
           FlowModel<Variable, Type> first,
           FlowModel<Variable, Type> second,
           Reachability newReachable,
-          Map<Variable, VariableModel<Variable, Type>> newVariableInfo) {
+          Map<Variable?, VariableModel<Variable, Type>> newVariableInfo) {
     if (first.reachable == newReachable &&
         identical(first.variableInfo, newVariableInfo)) {
       return first;
@@ -2188,11 +2336,11 @@
   /// The equivalence check is shallow; if two variables' models are not
   /// identical, we return `false`.
   static bool _variableInfosEqual<Variable extends Object, Type extends Object>(
-      Map<Variable, VariableModel<Variable, Type>> p1,
-      Map<Variable, VariableModel<Variable, Type>> p2) {
+      Map<Variable?, VariableModel<Variable, Type>> p1,
+      Map<Variable?, VariableModel<Variable, Type>> p2) {
     if (p1.length != p2.length) return false;
     if (!p1.keys.toSet().containsAll(p2.keys)) return false;
-    for (MapEntry<Variable, VariableModel<Variable, Type>> entry
+    for (MapEntry<Variable?, VariableModel<Variable, Type>> entry
         in p1.entries) {
       VariableModel<Variable, Type> p1Value = entry.value;
       VariableModel<Variable, Type>? p2Value = p2[entry.key];
@@ -2204,6 +2352,62 @@
   }
 }
 
+/// Linked list node representing a set of reasons why a given expression was
+/// not promoted.
+///
+/// We use a linked list representation because it is very efficient to build;
+/// this means that in the "happy path" where no error occurs (so non-promotion
+/// history is not needed) we do a minimal amount of work.
+class NonPromotionHistory<Type> {
+  /// The type that was not promoted to.
+  final Type type;
+
+  /// The reason why the promotion didn't occur.
+  final NonPromotionReason nonPromotionReason;
+
+  /// The previous link in the list.
+  final NonPromotionHistory<Type>? previous;
+
+  NonPromotionHistory(this.type, this.nonPromotionReason, this.previous);
+
+  @override
+  String toString() {
+    List<String> items = <String>[];
+    for (NonPromotionHistory<Type>? link = this;
+        link != null;
+        link = link.previous) {
+      items.add('${link.type}: ${link.nonPromotionReason}');
+    }
+    return items.toString();
+  }
+}
+
+/// Abstract class representing a reason why something was not promoted.
+abstract class NonPromotionReason {
+  /// Short text description of this non-promotion reason; intended for ID
+  /// testing.
+  String get shortName;
+
+  /// Implementation of the visitor pattern for non-promotion reasons.
+  R accept<R, Node extends Object, Expression extends Object,
+          Variable extends Object>(
+      NonPromotionReasonVisitor<R, Node, Expression, Variable> visitor);
+}
+
+/// Implementation of the visitor pattern for non-promotion reasons.
+abstract class NonPromotionReasonVisitor<R, Node extends Object,
+    Expression extends Object, Variable extends Object> {
+  NonPromotionReasonVisitor._() : assert(false, 'Do not extend this class');
+
+  R visitDemoteViaExplicitWrite(
+      DemoteViaExplicitWrite<Variable, Expression> reason);
+
+  R visitDemoteViaForEachVariableWrite(
+      DemoteViaForEachVariableWrite<Variable, Node> reason);
+
+  R visitFieldNotPromoted(FieldNotPromoted reason);
+}
+
 /// Immutable data structure modeling the reachability of the given point in the
 /// source code.  Reachability is tracked relative to checkpoints occurring
 /// previously along the control flow path leading up to the current point in
@@ -2359,16 +2563,27 @@
 
   /// Gets the info for this reference, creating it if it doesn't exist.
   VariableModel<Variable, Type> getInfo(
-          Map<Variable, VariableModel<Variable, Type>> variableInfo) =>
+          Map<Variable?, VariableModel<Variable, Type>> variableInfo) =>
       _getInfo(variableInfo) ?? new VariableModel<Variable, Type>.fresh();
 
+  /// Gets a map of non-promotion reasons associated with this reference.  This
+  /// is the map that will be returned from [FlowAnalysis.whyNotPromoted].
+  Map<Type, NonPromotionReason> getNonPromotionReasons(
+      Map<Variable?, VariableModel<Variable, Type>> variableInfo,
+      TypeOperations<Variable, Type> typeOperations);
+
+  /// Creates a reference representing a get of a property called [propertyName]
+  /// on the reference represented by `this`.
+  Reference<Variable, Type> propertyGet(String propertyName) =>
+      new _PropertyGetReference<Variable, Type>(this, propertyName);
+
   /// Stores info for this reference in [variableInfo].
-  void storeInfo(Map<Variable, VariableModel<Variable, Type>> variableInfo,
+  void storeInfo(Map<Variable?, VariableModel<Variable, Type>> variableInfo,
       VariableModel<Variable, Type> variableModel);
 
   /// Gets the info for this reference, or `null` if it doesn't exist.
   VariableModel<Variable, Type>? _getInfo(
-      Map<Variable, VariableModel<Variable, Type>> variableInfo);
+      Map<Variable?, VariableModel<Variable, Type>> variableInfo);
 }
 
 /// Data structure representing a unique value that a variable might take on
@@ -2423,6 +2638,9 @@
 
 /// Operations on types, abstracted from concrete type interfaces.
 abstract class TypeOperations<Variable extends Object, Type extends Object> {
+  /// Gets the representation of the top type (`Object?`) in the type system.
+  Type get topType;
+
   /// Classifies the given type into one of the three categories defined by
   /// the [TypeClassification] enum.
   TypeClassification classifyType(Type type);
@@ -2510,12 +2728,22 @@
   /// `null` if the variable has been write captured.
   final SsaNode<Variable, Type>? ssaNode;
 
+  /// Non-promotion history of this variable.
+  final NonPromotionHistory<Type>? nonPromotionHistory;
+
+  /// Promotion information for properties of this variable.  We don't actually
+  /// promote properties, but we track the promotions that would occur if we
+  /// did, so that we can report those as non-promotion reasons.
+  final Map<String, VariableModel<Variable, Type>> properties;
+
   VariableModel(
       {required this.promotedTypes,
       required this.tested,
       required this.assigned,
       required this.unassigned,
-      required this.ssaNode}) {
+      required this.ssaNode,
+      this.nonPromotionHistory,
+      this.properties = const {}}) {
     assert(!(assigned && unassigned),
         "Can't be both definitely assigned and unassigned");
     assert(promotedTypes == null || promotedTypes!.isNotEmpty);
@@ -2533,7 +2761,9 @@
       : promotedTypes = null,
         tested = const [],
         unassigned = !assigned,
-        ssaNode = new SsaNode<Variable, Type>(null);
+        ssaNode = new SsaNode<Variable, Type>(null),
+        nonPromotionHistory = null,
+        properties = const {};
 
   /// Indicates whether the variable has been write captured.
   bool get writeCaptured => ssaNode == null;
@@ -2605,6 +2835,18 @@
         newAssigned, newUnassigned, newWriteCaptured ? null : ssaNode);
   }
 
+  /// Updates `this` with a new set of properties.
+  VariableModel<Variable, Type> setProperties(
+          Map<String, VariableModel<Variable, Type>> newProperties) =>
+      new VariableModel<Variable, Type>(
+          promotedTypes: promotedTypes,
+          tested: tested,
+          unassigned: unassigned,
+          assigned: assigned,
+          ssaNode: ssaNode,
+          nonPromotionHistory: nonPromotionHistory,
+          properties: newProperties);
+
   @override
   String toString() {
     List<String> parts = [ssaNode.toString()];
@@ -2623,12 +2865,20 @@
     if (writeCaptured) {
       parts.add('writeCaptured: true');
     }
+    if (nonPromotionHistory != null) {
+      parts.add('nonPromotionHistory: $nonPromotionHistory');
+    }
     return 'VariableModel(${parts.join(', ')})';
   }
 
   /// Returns a new [VariableModel] reflecting the fact that the variable was
   /// just written to.
+  ///
+  /// If there is any chance that the write will cause a demotion, the caller
+  /// must pass in a non-null value for [nonPromotionReason] describing the
+  /// reason for any potential demotion.
   VariableModel<Variable, Type> write(
+      NonPromotionReason? nonPromotionReason,
       Variable variable,
       Type writtenType,
       TypeOperations<Variable, Type> typeOperations,
@@ -2642,14 +2892,15 @@
           ssaNode: null);
     }
 
-    List<Type>? newPromotedTypes = _demoteViaAssignment(
-      writtenType,
-      typeOperations,
-    );
+    _DemotionResult<Type> demotionResult =
+        _demoteViaAssignment(writtenType, typeOperations, nonPromotionReason);
+    List<Type>? newPromotedTypes = demotionResult.promotedTypes;
 
     Type declaredType = typeOperations.variableType(variable);
     newPromotedTypes = _tryPromoteToTypeOfInterest(
         typeOperations, declaredType, newPromotedTypes, writtenType);
+    // TODO(paulberry): remove demotions from demotionResult.nonPromotionHistory
+    // that are no longer in effect due to re-promotion.
     if (identical(promotedTypes, newPromotedTypes) && assigned) {
       return new VariableModel<Variable, Type>(
           promotedTypes: promotedTypes,
@@ -2671,7 +2922,8 @@
         tested: newTested,
         assigned: true,
         unassigned: false,
-        ssaNode: newSsaNode);
+        ssaNode: newSsaNode,
+        nonPromotionHistory: demotionResult.nonPromotionHistory);
   }
 
   /// Returns a new [VariableModel] reflecting the fact that the variable has
@@ -2685,28 +2937,45 @@
         ssaNode: null);
   }
 
-  List<Type>? _demoteViaAssignment(
-    Type writtenType,
-    TypeOperations<Variable, Type> typeOperations,
-  ) {
+  /// Computes the result of demoting this variable due to writing a value of
+  /// type [writtenType].
+  ///
+  /// If there is any chance that the write will cause an actual demotion to
+  /// occur, the caller must pass in a non-null value for [nonPromotionReason]
+  /// describing the reason for the potential demotion.
+  _DemotionResult<Type> _demoteViaAssignment(
+      Type writtenType,
+      TypeOperations<Variable, Type> typeOperations,
+      NonPromotionReason? nonPromotionReason) {
     List<Type>? promotedTypes = this.promotedTypes;
     if (promotedTypes == null) {
-      return null;
+      return new _DemotionResult<Type>(null, nonPromotionHistory);
     }
 
     int numElementsToKeep = promotedTypes.length;
+    NonPromotionHistory<Type>? newNonPromotionHistory = nonPromotionHistory;
+    List<Type>? newPromotedTypes;
     for (;; numElementsToKeep--) {
       if (numElementsToKeep == 0) {
-        return null;
+        break;
       }
       Type promoted = promotedTypes[numElementsToKeep - 1];
       if (typeOperations.isSubtypeOf(writtenType, promoted)) {
         if (numElementsToKeep == promotedTypes.length) {
-          return promotedTypes;
+          newPromotedTypes = promotedTypes;
+          break;
         }
-        return promotedTypes.sublist(0, numElementsToKeep);
+        newPromotedTypes = promotedTypes.sublist(0, numElementsToKeep);
+        break;
+      }
+      if (nonPromotionReason == null) {
+        assert(false, 'Demotion occurred but nonPromotionReason is null');
+      } else {
+        newNonPromotionHistory = new NonPromotionHistory<Type>(
+            promoted, nonPromotionReason, newNonPromotionHistory);
       }
     }
+    return new _DemotionResult<Type>(newPromotedTypes, newNonPromotionHistory);
   }
 
   /// Determines whether a variable with the given [promotedTypes] should be
@@ -3067,14 +3336,36 @@
       typeOperations.variableType(variable);
 
   @override
-  void storeInfo(Map<Variable, VariableModel<Variable, Type>> variableInfo,
+  Map<Type, NonPromotionReason> getNonPromotionReasons(
+      Map<Variable?, VariableModel<Variable, Type>> variableInfo,
+      TypeOperations<Variable, Type> typeOperations) {
+    Map<Type, NonPromotionReason> result = <Type, NonPromotionReason>{};
+    VariableModel<Variable, Type>? currentVariableInfo = variableInfo[variable];
+    if (currentVariableInfo != null) {
+      Type currentType = currentVariableInfo.promotedTypes?.last ??
+          typeOperations.variableType(variable);
+      NonPromotionHistory? nonPromotionHistory =
+          currentVariableInfo.nonPromotionHistory;
+      while (nonPromotionHistory != null) {
+        Type nonPromotedType = nonPromotionHistory.type;
+        if (!typeOperations.isSubtypeOf(currentType, nonPromotedType)) {
+          result[nonPromotedType] ??= nonPromotionHistory.nonPromotionReason;
+        }
+        nonPromotionHistory = nonPromotionHistory.previous;
+      }
+    }
+    return result;
+  }
+
+  @override
+  void storeInfo(Map<Variable?, VariableModel<Variable, Type>> variableInfo,
       VariableModel<Variable, Type> variableModel) {
     variableInfo[variable] = variableModel;
   }
 
   @override
   VariableModel<Variable, Type>? _getInfo(
-          Map<Variable, VariableModel<Variable, Type>> variableInfo) =>
+          Map<Variable?, VariableModel<Variable, Type>> variableInfo) =>
       variableInfo[variable];
 }
 
@@ -3145,6 +3436,19 @@
       'thenInfo: $_thenInfo)';
 }
 
+/// Data structure representing the result of demoting a variable from one type
+/// to another.
+class _DemotionResult<Type extends Object> {
+  /// The new set of promoted types.
+  final List<Type>? promotedTypes;
+
+  /// The new non-promotion history (including the types that the variable is
+  /// no longer promoted to).
+  final NonPromotionHistory<Type>? nonPromotionHistory;
+
+  _DemotionResult(this.promotedTypes, this.nonPromotionHistory);
+}
+
 /// [_FlowContext] representing an equality comparison using `==` or `!=`.
 class _EqualityOpContext<Variable extends Object, Type extends Object>
     extends _BranchContext<Variable, Type> {
@@ -3437,8 +3741,12 @@
             _current.reachable.parent!, _current);
     _stack.add(context);
     if (loopVariable != null) {
-      _current = _current.write(loopVariable, writtenType,
-          new SsaNode<Variable, Type>(null), typeOperations);
+      _current = _current.write(
+          new DemoteViaForEachVariableWrite<Variable, Node>(loopVariable, node),
+          loopVariable,
+          writtenType,
+          new SsaNode<Variable, Type>(null),
+          typeOperations);
     }
   }
 
@@ -3581,8 +3889,8 @@
       // written type is the variable's declared type.
       initializerType = typeOperations.variableType(variable);
     }
-    _current =
-        _current.write(variable, initializerType, newSsaNode, typeOperations);
+    _current = _current.write(
+        null, variable, initializerType, newSsaNode, typeOperations);
   }
 
   @override
@@ -3741,6 +4049,16 @@
   }
 
   @override
+  void propertyGet(
+      Expression wholeExpression, Expression target, String propertyName) {
+    Reference<Variable, Type>? reference = _getExpressionReference(target);
+    if (reference != null) {
+      _storeExpressionReference(
+          wholeExpression, reference.propertyGet(propertyName));
+    }
+  }
+
+  @override
   SsaNode<Variable, Type>? ssaNodeForTesting(Variable variable) =>
       _current.variableInfo[variable]?.ssaNode;
 
@@ -3785,6 +4103,17 @@
   }
 
   @override
+  void thisOrSuper(Expression expression) {
+    _storeExpressionReference(expression, new _ThisReference<Variable, Type>());
+  }
+
+  @override
+  void thisOrSuperPropertyGet(Expression expression, String propertyName) {
+    _storeExpressionReference(expression,
+        new _ThisReference<Variable, Type>().propertyGet(propertyName));
+  }
+
+  @override
   void tryCatchStatement_bodyBegin() {
     _current = _current.split();
     _stack.add(new _TryContext<Variable, Type>(_current));
@@ -3914,15 +4243,31 @@
   }
 
   @override
-  void write(
-      Variable variable, Type writtenType, Expression? writtenExpression) {
+  Map<Type, NonPromotionReason> whyNotPromoted(Expression target) {
+    if (identical(target, _expressionWithReference)) {
+      Reference<Variable, Type>? reference = _expressionReference;
+      if (reference != null) {
+        return reference.getNonPromotionReasons(
+            _current.variableInfo, typeOperations);
+      }
+    }
+    return {};
+  }
+
+  @override
+  void write(Expression expression, Variable variable, Type writtenType,
+      Expression? writtenExpression) {
     ExpressionInfo<Variable, Type>? expressionInfo = writtenExpression == null
         ? null
         : _getExpressionInfo(writtenExpression);
     SsaNode<Variable, Type> newSsaNode = new SsaNode<Variable, Type>(
         expressionInfo is _TrivialExpressionInfo ? null : expressionInfo);
-    _current =
-        _current.write(variable, writtenType, newSsaNode, typeOperations);
+    _current = _current.write(
+        new DemoteViaExplicitWrite<Variable, Expression>(variable, expression),
+        variable,
+        writtenType,
+        newSsaNode,
+        typeOperations);
   }
 
   @override
@@ -4425,6 +4770,10 @@
   }
 
   @override
+  void propertyGet(
+      Expression wholeExpression, Expression target, String propertyName) {}
+
+  @override
   SsaNode<Variable, Type>? ssaNodeForTesting(Variable variable) {
     throw new StateError('ssaNodeForTesting requires null-aware flow analysis');
   }
@@ -4439,6 +4788,12 @@
   void switchStatement_expressionEnd(Statement switchStatement) {}
 
   @override
+  void thisOrSuper(Expression expression) {}
+
+  @override
+  void thisOrSuperPropertyGet(Expression expression, String propertyName) {}
+
+  @override
   void tryCatchStatement_bodyBegin() {}
 
   @override
@@ -4481,8 +4836,13 @@
   void whileStatement_end() {}
 
   @override
-  void write(
-      Variable variable, Type writtenType, Expression? writtenExpression) {
+  Map<Type, NonPromotionReason> whyNotPromoted(Expression target) {
+    return {};
+  }
+
+  @override
+  void write(Expression expression, Variable variable, Type writtenType,
+      Expression? writtenExpression) {
     assert(
         _assignedVariables._anywhere._written.contains(variable),
         "Variable is written to, but was not included in "
@@ -4615,6 +4975,57 @@
       null;
 }
 
+/// [Reference] object representing a property get applied to another reference.
+class _PropertyGetReference<Variable extends Object, Type extends Object>
+    extends Reference<Variable, Type> {
+  /// The target of the property get.  For example a property get of the form
+  /// `a.b`, where `a` is a local variable, has a target which is a reference to
+  /// `a`.
+  final Reference<Variable, Type> target;
+
+  /// The name of the property.
+  final String propertyName;
+
+  _PropertyGetReference(this.target, this.propertyName);
+
+  @override
+  Type getDeclaredType(TypeOperations<Variable, Type> typeOperations) {
+    return typeOperations.topType;
+  }
+
+  @override
+  Map<Type, NonPromotionReason> getNonPromotionReasons(
+      Map<Variable?, VariableModel<Variable, Type>> variableInfo,
+      TypeOperations<Variable, Type> typeOperations) {
+    Map<Type, NonPromotionReason> result = <Type, NonPromotionReason>{};
+    List<Type>? promotedTypes = _getInfo(variableInfo)?.promotedTypes;
+    if (promotedTypes != null) {
+      for (Type type in promotedTypes) {
+        result[type] = new FieldNotPromoted(propertyName);
+      }
+    }
+    return result;
+  }
+
+  @override
+  void storeInfo(Map<Variable?, VariableModel<Variable, Type>> variableInfo,
+      VariableModel<Variable, Type> variableModel) {
+    VariableModel<Variable, Type> targetInfo = target.getInfo(variableInfo);
+    Map<String, VariableModel<Variable, Type>> newProperties =
+        new Map<String, VariableModel<Variable, Type>>.from(
+            targetInfo.properties);
+    newProperties[propertyName] = variableModel;
+    target.storeInfo(variableInfo, targetInfo.setProperties(newProperties));
+  }
+
+  @override
+  VariableModel<Variable, Type>? _getInfo(
+      Map<Variable?, VariableModel<Variable, Type>> variableInfo) {
+    VariableModel<Variable, Type> targetInfo = target.getInfo(variableInfo);
+    return targetInfo.properties[propertyName];
+  }
+}
+
 /// [_FlowContext] representing a language construct for which flow analysis
 /// must store a flow model state to be retrieved later, such as a `try`
 /// statement, function expression, or "if-null" (`??`) expression.
@@ -4650,6 +5061,36 @@
       'checkpoint: $_checkpoint)';
 }
 
+/// [Reference] object representing an implicit or explicit reference to `this`.
+class _ThisReference<Variable extends Object, Type extends Object>
+    extends Reference<Variable, Type> {
+  @override
+  Type getDeclaredType(TypeOperations<Variable, Type> typeOperations) {
+    // TODO(paulberry): can we return the actual type?  Would that have a
+    // user-visible effect?
+    return typeOperations.topType;
+  }
+
+  @override
+  Map<Type, NonPromotionReason> getNonPromotionReasons(
+      Map<Variable?, VariableModel<Variable, Type>> variableInfo,
+      TypeOperations<Variable, Type> typeOperations) {
+    // TODO(paulberry): implement.
+    return {};
+  }
+
+  @override
+  void storeInfo(Map<Variable?, VariableModel<Variable, Type>> variableInfo,
+      VariableModel<Variable, Type> variableModel) {
+    variableInfo[null] = variableModel;
+  }
+
+  @override
+  VariableModel<Variable, Type>? _getInfo(
+          Map<Variable?, VariableModel<Variable, Type>> variableInfo) =>
+      variableInfo[null];
+}
+
 /// Specialization of [ExpressionInfo] for the case where the information we
 /// have about the expression is trivial (meaning we know by construction that
 /// the expression's [after], [ifTrue], and [ifFalse] models are all the same).
diff --git a/pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart b/pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart
index ba1e3a6..d053b4e 100644
--- a/pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart
+++ b/pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart
@@ -4033,6 +4033,29 @@
     tip: r"""Try removing 'this.'.""");
 
 // DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
+const Template<Message Function(String name)> templateFieldNotPromoted =
+    const Template<Message Function(String name)>(
+        messageTemplate:
+            r"""'#name' refers to a property so it could not be promoted.""",
+        withArguments: _withArgumentsFieldNotPromoted);
+
+// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
+const Code<Message Function(String name)> codeFieldNotPromoted =
+    const Code<Message Function(String name)>(
+  "FieldNotPromoted",
+);
+
+// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
+Message _withArgumentsFieldNotPromoted(String name) {
+  if (name.isEmpty) throw 'No name provided';
+  name = demangleMixinApplicationName(name);
+  return new Message(codeFieldNotPromoted,
+      message:
+          """'${name}' refers to a property so it could not be promoted.""",
+      arguments: {'name': name});
+}
+
+// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
 const Code<Null> codeFinalAndCovariant = messageFinalAndCovariant;
 
 // DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
@@ -9621,6 +9644,34 @@
         r"""Try removing the keyword 'var', or replacing it with the name of the return type.""");
 
 // DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
+const Template<
+    Message Function(
+        String
+            name)> templateVariableCouldBeNullDueToWrite = const Template<
+        Message Function(String name)>(
+    messageTemplate:
+        r"""Variable '#name' could be null due to a write occurring here.""",
+    tipTemplate: r"""Try null checking the variable after the write.""",
+    withArguments: _withArgumentsVariableCouldBeNullDueToWrite);
+
+// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
+const Code<Message Function(String name)> codeVariableCouldBeNullDueToWrite =
+    const Code<Message Function(String name)>(
+  "VariableCouldBeNullDueToWrite",
+);
+
+// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
+Message _withArgumentsVariableCouldBeNullDueToWrite(String name) {
+  if (name.isEmpty) throw 'No name provided';
+  name = demangleMixinApplicationName(name);
+  return new Message(codeVariableCouldBeNullDueToWrite,
+      message:
+          """Variable '${name}' could be null due to a write occurring here.""",
+      tip: """Try null checking the variable after the write.""",
+      arguments: {'name': name});
+}
+
+// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
 const Code<Null> codeVerificationErrorOriginContext =
     messageVerificationErrorOriginContext;
 
diff --git a/pkg/_fe_analyzer_shared/test/annotated_code_helper_test.dart b/pkg/_fe_analyzer_shared/test/annotated_code_helper_test.dart
index 9fb1558..755d20c 100644
--- a/pkg/_fe_analyzer_shared/test/annotated_code_helper_test.dart
+++ b/pkg/_fe_analyzer_shared/test/annotated_code_helper_test.dart
@@ -16,6 +16,7 @@
   testDir('pkg/_fe_analyzer_shared/test/flow_analysis/nullability/data');
   testDir('pkg/_fe_analyzer_shared/test/flow_analysis/reachability/data');
   testDir('pkg/_fe_analyzer_shared/test/flow_analysis/type_promotion/data');
+  testDir('pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data');
   testDir('pkg/_fe_analyzer_shared/test/inheritance/data');
 }
 
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 9838684..0c02855 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
@@ -154,6 +154,11 @@
         {required bool isExhaustive}) =>
     new _Switch(expression, cases, isExhaustive);
 
+Expression this_(String type) => new _This(Type(type));
+
+Expression thisOrSuperPropertyGet(String name, {String type = 'Object?'}) =>
+    new _ThisOrSuperPropertyGet(name, type);
+
 Expression throw_(Expression operand) => new _Throw(operand);
 
 Statement tryCatch(List<Statement> body, List<CatchClause> catches) =>
@@ -273,12 +278,24 @@
   /// If `this` is an expression `x`, creates the expression `x || other`.
   Expression or(Expression other) => new _Logical(this, other, isAnd: false);
 
+  /// If `this` is an expression `x`, creates the expression `x.name`.
+  Expression propertyGet(String name, {String type = 'Object?'}) =>
+      new _PropertyGet(this, name, type);
+
   /// If `this` is an expression `x`, creates a pseudo-expression that models
   /// evaluation of `x` followed by execution of [stmt].  This can be used to
   /// test that flow analysis is in the correct state after an expression is
   /// visited.
   Expression thenStmt(Statement stmt) =>
       new _WrappedExpression(null, this, stmt);
+
+  /// Creates an [Expression] that, when analyzed, will behave the same as
+  /// `this`, but after visiting it, will cause [callback] to be passed the
+  /// non-promotion info associated with it.  If the expression has no
+  /// non-promotion info, an empty map will be passed to [callback].
+  Expression whyNotPromoted(
+          void Function(Map<Type, NonPromotionReason>) callback) =>
+      new _WhyNotPromoted(this, callback);
 }
 
 /// Test harness for creating flow analysis tests.  This class implements all
@@ -311,8 +328,10 @@
     'int? <: num?': true,
     'int? <: Object': false,
     'int? <: Object?': true,
+    'Never <: Object?': true,
     'Null <: int': false,
     'Null <: Object': false,
+    'Null <: Object?': true,
     'num <: int': false,
     'num <: Iterable': false,
     'num <: List': false,
@@ -347,6 +366,7 @@
     'Object <: int': false,
     'Object <: int?': false,
     'Object <: List': false,
+    'Object <: Null': false,
     'Object <: num': false,
     'Object <: num?': false,
     'Object <: Object?': true,
@@ -354,6 +374,7 @@
     'Object? <: Object': false,
     'Object? <: int': false,
     'Object? <: int?': false,
+    'Object? <: Null': false,
     'String <: int': false,
     'String <: int?': false,
     'String <: num?': false,
@@ -364,6 +385,8 @@
   static final Map<String, Type> _coreFactors = {
     'Object? - int': Type('Object?'),
     'Object? - int?': Type('Object'),
+    'Object? - Never': Type('Object?'),
+    'Object? - Null': Type('Object'),
     'Object? - num?': Type('Object'),
     'Object? - Object?': Type('Never?'),
     'Object? - String': Type('Object?'),
@@ -410,6 +433,9 @@
 
   Harness({this.allowLocalBooleanVarsToPromote = false, this.legacy = false});
 
+  @override
+  Type get topType => Type('Object?');
+
   /// Updates the harness so that when a [factor] query is invoked on types
   /// [from] and [what], [result] will be returned.
   void addFactor(String from, String what, String result) {
@@ -597,15 +623,30 @@
 /// testing.  This is essentially a thin wrapper around a string representation
 /// of the type.
 class Type {
+  static bool _allowComparisons = false;
+
   final String type;
 
   Type(this.type);
 
   @override
+  int get hashCode {
+    if (!_allowComparisons) {
+      // The flow analysis engine should not hash types using hashCode.  It
+      // should compare them using TypeOperations.
+      fail('Unexpected use of operator== on types');
+    }
+    return type.hashCode;
+  }
+
+  @override
   bool operator ==(Object other) {
-    // The flow analysis engine should not compare types using operator==.  It
-    // should compare them using TypeOperations.
-    fail('Unexpected use of operator== on types');
+    if (!_allowComparisons) {
+      // The flow analysis engine should not compare types using operator==.  It
+      // should compare them using TypeOperations.
+      fail('Unexpected use of operator== on types');
+    }
+    return other is Type && this.type == other.type;
   }
 
   @override
@@ -1424,6 +1465,29 @@
       type;
 }
 
+class _PropertyGet extends Expression {
+  final Expression target;
+
+  final String propertyName;
+
+  final String type;
+
+  _PropertyGet(this.target, this.propertyName, this.type);
+
+  @override
+  void _preVisit(AssignedVariables<Node, Var> assignedVariables) {
+    target._preVisit(assignedVariables);
+  }
+
+  @override
+  Type _visit(
+      Harness h, FlowAnalysis<Node, Statement, Expression, Var, Type> flow) {
+    target._visit(h, flow);
+    flow.propertyGet(this, target, propertyName);
+    return Type(type);
+  }
+}
+
 class _Return extends Statement {
   _Return() : super._();
 
@@ -1481,6 +1545,43 @@
   }
 }
 
+class _This extends Expression {
+  final Type type;
+
+  _This(this.type);
+
+  @override
+  String toString() => 'this';
+
+  @override
+  void _preVisit(AssignedVariables<Node, Var> assignedVariables) {}
+
+  @override
+  Type _visit(
+      Harness h, FlowAnalysis<Node, Statement, Expression, Var, Type> flow) {
+    flow.thisOrSuper(this);
+    return type;
+  }
+}
+
+class _ThisOrSuperPropertyGet extends Expression {
+  final String propertyName;
+
+  final String type;
+
+  _ThisOrSuperPropertyGet(this.propertyName, this.type);
+
+  @override
+  void _preVisit(AssignedVariables<Node, Var> assignedVariables) {}
+
+  @override
+  Type _visit(
+      Harness h, FlowAnalysis<Node, Statement, Expression, Var, Type> flow) {
+    flow.thisOrSuperPropertyGet(this, propertyName);
+    return Type(type);
+  }
+}
+
 class _Throw extends Expression {
   final Expression operand;
 
@@ -1622,6 +1723,37 @@
   }
 }
 
+class _WhyNotPromoted extends Expression {
+  final Expression target;
+
+  final void Function(Map<Type, NonPromotionReason>) callback;
+
+  _WhyNotPromoted(this.target, this.callback);
+
+  @override
+  String toString() => '$target (whyNotPromoted)';
+
+  @override
+  void _preVisit(AssignedVariables<Node, Var> assignedVariables) {
+    target._preVisit(assignedVariables);
+  }
+
+  @override
+  Type _visit(
+      Harness h, FlowAnalysis<Node, Statement, Expression, Var, Type> flow) {
+    var type = target._visit(h, flow);
+    flow.forwardExpression(this, target);
+    assert(!Type._allowComparisons);
+    Type._allowComparisons = true;
+    try {
+      callback(flow.whyNotPromoted(this));
+    } finally {
+      Type._allowComparisons = false;
+    }
+    return type;
+  }
+}
+
 class _WrappedExpression extends Expression {
   final Statement? before;
   final Expression expr;
@@ -1681,7 +1813,7 @@
       Harness h, FlowAnalysis<Node, Statement, Expression, Var, Type> flow) {
     var rhs = this.rhs;
     var type = rhs == null ? variable.type : rhs._visit(h, flow);
-    flow.write(variable, type, rhs);
+    flow.write(this, variable, type, rhs);
     return 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 9621ff7..1657689 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
@@ -499,6 +499,94 @@
       ]);
     });
 
+    test('equalityOp_end on property get preserves target variable', () {
+      // This is a regression test for a mistake made during the implementation
+      // of "why not promoted" functionality: when storing information about an
+      // attempt to promote a field (e.g. `x.y != null`) we need to make sure we
+      // don't wipe out information about the target variable (`x`).
+      var h = Harness();
+      var x = Var('x', 'C');
+      h.run([
+        declare(x, initialized: true),
+        checkAssigned(x, true),
+        if_(x.read.propertyGet('y').notEq(nullLiteral), [
+          checkAssigned(x, true),
+        ], [
+          checkAssigned(x, true),
+        ]),
+      ]);
+    });
+
+    test('equalityOp_end does not set reachability for `this`', () {
+      var h = Harness();
+      h.addSubtype('C', 'Object', true);
+      h.run([
+        if_(this_('C').is_('Null'), [
+          if_(this_('C').eq(nullLiteral), [
+            checkReachable(true),
+          ], [
+            checkReachable(true),
+          ]),
+        ]),
+      ]);
+    });
+
+    group('equalityOp_end does not set reachability for property gets', () {
+      test('on a variable', () {
+        var h = Harness();
+        var x = Var('x', 'C');
+        h.run([
+          declare(x, initialized: true),
+          if_(x.read.propertyGet('f').is_('Null'), [
+            if_(x.read.propertyGet('f').eq(nullLiteral), [
+              checkReachable(true),
+            ], [
+              checkReachable(true),
+            ]),
+          ]),
+        ]);
+      });
+
+      test('on an arbitrary expression', () {
+        var h = Harness();
+        h.run([
+          if_(expr('C').propertyGet('f').is_('Null'), [
+            if_(expr('C').propertyGet('f').eq(nullLiteral), [
+              checkReachable(true),
+            ], [
+              checkReachable(true),
+            ]),
+          ]),
+        ]);
+      });
+
+      test('on explicit this', () {
+        var h = Harness();
+        h.run([
+          if_(this_('C').propertyGet('f').is_('Null'), [
+            if_(this_('C').propertyGet('f').eq(nullLiteral), [
+              checkReachable(true),
+            ], [
+              checkReachable(true),
+            ]),
+          ]),
+        ]);
+      });
+
+      test('on implicit this/super', () {
+        var h = Harness();
+        h.run([
+          if_(thisOrSuperPropertyGet('f').is_('Null'), [
+            if_(thisOrSuperPropertyGet('f').eq(nullLiteral), [
+              checkReachable(true),
+            ], [
+              checkReachable(true),
+            ]),
+          ]),
+        ]);
+      });
+    });
+
     test('finish checks proper nesting', () {
       var h = Harness();
       var e = expr('Null');
@@ -1372,6 +1460,65 @@
       ]);
     });
 
+    test('isExpression_end() does not set reachability for `this`', () {
+      var h = Harness();
+      h.run([
+        if_(this_('C').is_('Never'), [
+          checkReachable(true),
+        ], [
+          checkReachable(true),
+        ]),
+      ]);
+    });
+
+    group('isExpression_end() does not set reachability for property gets', () {
+      test('on a variable', () {
+        var h = Harness();
+        var x = Var('x', 'C');
+        h.run([
+          declare(x, initialized: true),
+          if_(x.read.propertyGet('f').is_('Never'), [
+            checkReachable(true),
+          ], [
+            checkReachable(true),
+          ]),
+        ]);
+      });
+
+      test('on an arbitrary expression', () {
+        var h = Harness();
+        h.run([
+          if_(expr('C').propertyGet('f').is_('Never'), [
+            checkReachable(true),
+          ], [
+            checkReachable(true),
+          ]),
+        ]);
+      });
+
+      test('on explicit this', () {
+        var h = Harness();
+        h.run([
+          if_(this_('C').propertyGet('f').is_('Never'), [
+            checkReachable(true),
+          ], [
+            checkReachable(true),
+          ]),
+        ]);
+      });
+
+      test('on implicit this/super', () {
+        var h = Harness();
+        h.run([
+          if_(thisOrSuperPropertyGet('f').is_('Never'), [
+            checkReachable(true),
+          ], [
+            checkReachable(true),
+          ]),
+        ]);
+      });
+    });
+
     test('labeledBlock without break', () {
       var h = Harness();
       var x = Var('x', 'int?');
@@ -3262,7 +3409,7 @@
         // This should not happen in valid code, but test that we don't crash.
         var h = Harness();
         var s = FlowModel<Var, Type>(Reachability.initial).write(
-            objectQVar, Type('Object?'), new SsaNode<Var, Type>(null), h);
+            null, objectQVar, Type('Object?'), new SsaNode<Var, Type>(null), h);
         expect(s.variableInfo[objectQVar], isNull);
       });
 
@@ -3271,7 +3418,7 @@
         var s1 = FlowModel<Var, Type>(Reachability.initial)
             .declare(objectQVar, true);
         var s2 = s1.write(
-            objectQVar, Type('Object?'), new SsaNode<Var, Type>(null), h);
+            null, objectQVar, Type('Object?'), new SsaNode<Var, Type>(null), h);
         expect(s2, isNot(same(s1)));
         expect(s2.reachable, same(s1.reachable));
         expect(
@@ -3287,8 +3434,8 @@
         var h = Harness();
         var s1 = FlowModel<Var, Type>(Reachability.initial)
             .declare(objectQVar, false);
-        var s2 =
-            s1.write(objectQVar, Type('int?'), new SsaNode<Var, Type>(null), h);
+        var s2 = s1.write(
+            null, objectQVar, Type('int?'), new SsaNode<Var, Type>(null), h);
         expect(s2.reachable.overallReachable, true);
         expect(
             s2.infoFor(objectQVar),
@@ -3306,8 +3453,8 @@
             .tryPromoteForTypeCheck(h, _varRef(objectQVar), Type('int'))
             .ifTrue;
         expect(s1.variableInfo, contains(objectQVar));
-        var s2 =
-            s1.write(objectQVar, Type('int?'), new SsaNode<Var, Type>(null), h);
+        var s2 = s1.write(_MockNonPromotionReason(), objectQVar, Type('int?'),
+            new SsaNode<Var, Type>(null), h);
         expect(s2.reachable.overallReachable, true);
         expect(s2.variableInfo, {
           objectQVar: _matchVariableModel(
@@ -3333,8 +3480,8 @@
               assigned: true,
               unassigned: false)
         });
-        var s2 =
-            s1.write(objectQVar, Type('num'), new SsaNode<Var, Type>(null), h);
+        var s2 = s1.write(_MockNonPromotionReason(), objectQVar, Type('num'),
+            new SsaNode<Var, Type>(null), h);
         expect(s2.reachable.overallReachable, true);
         expect(s2.variableInfo, {
           objectQVar: _matchVariableModel(
@@ -3362,8 +3509,8 @@
               assigned: true,
               unassigned: false)
         });
-        var s2 =
-            s1.write(objectQVar, Type('num'), new SsaNode<Var, Type>(null), h);
+        var s2 = s1.write(_MockNonPromotionReason(), objectQVar, Type('num'),
+            new SsaNode<Var, Type>(null), h);
         expect(s2.reachable.overallReachable, true);
         expect(s2.variableInfo, {
           objectQVar: _matchVariableModel(
@@ -3389,8 +3536,8 @@
               assigned: true,
               unassigned: false)
         });
-        var s2 =
-            s1.write(objectQVar, Type('num'), new SsaNode<Var, Type>(null), h);
+        var s2 = s1.write(
+            null, objectQVar, Type('num'), new SsaNode<Var, Type>(null), h);
         expect(s2.reachable.overallReachable, true);
         expect(s2.variableInfo, isNot(same(s1.variableInfo)));
         expect(s2.variableInfo, {
@@ -3417,8 +3564,8 @@
               assigned: true,
               unassigned: false)
         });
-        var s2 =
-            s1.write(objectQVar, Type('int'), new SsaNode<Var, Type>(null), h);
+        var s2 = s1.write(
+            null, objectQVar, Type('int'), new SsaNode<Var, Type>(null), h);
         expect(s2.reachable.overallReachable, true);
         expect(s2.variableInfo, isNot(same(s1.variableInfo)));
         expect(s2.variableInfo, {
@@ -3440,7 +3587,8 @@
             x: _matchVariableModel(chain: null),
           });
 
-          var s2 = s1.write(x, Type('int'), new SsaNode<Var, Type>(null), h);
+          var s2 =
+              s1.write(null, x, Type('int'), new SsaNode<Var, Type>(null), h);
           expect(s2.variableInfo, {
             x: _matchVariableModel(chain: ['int']),
           });
@@ -3461,7 +3609,8 @@
           });
 
           // 'x' is write-captured, so not promoted
-          var s3 = s2.write(x, Type('int'), new SsaNode<Var, Type>(null), h);
+          var s3 =
+              s2.write(null, x, Type('int'), new SsaNode<Var, Type>(null), h);
           expect(s3.variableInfo, {
             x: _matchVariableModel(chain: null, writeCaptured: true),
           });
@@ -3480,7 +3629,7 @@
             ),
           });
           var s2 = s1.write(
-              objectQVar, Type('int'), new SsaNode<Var, Type>(null), h);
+              null, objectQVar, Type('int'), new SsaNode<Var, Type>(null), h);
           expect(s2.variableInfo, {
             objectQVar: _matchVariableModel(
               chain: ['int?', 'int'],
@@ -3502,7 +3651,7 @@
             ),
           });
           var s2 = s1.write(
-              objectQVar, Type('int'), new SsaNode<Var, Type>(null), h);
+              null, objectQVar, Type('int'), new SsaNode<Var, Type>(null), h);
           expect(s2.variableInfo, {
             objectQVar: _matchVariableModel(
               chain: ['Object', 'int'],
@@ -3524,8 +3673,8 @@
             ofInterest: ['num?'],
           ),
         });
-        var s2 =
-            s1.write(objectQVar, Type('num?'), new SsaNode<Var, Type>(null), h);
+        var s2 = s1.write(_MockNonPromotionReason(), objectQVar, Type('num?'),
+            new SsaNode<Var, Type>(null), h);
         expect(s2.variableInfo, {
           objectQVar: _matchVariableModel(
             chain: ['num?'],
@@ -3548,8 +3697,8 @@
             ofInterest: ['num?', 'int?'],
           ),
         });
-        var s2 =
-            s1.write(objectQVar, Type('int?'), new SsaNode<Var, Type>(null), h);
+        var s2 = s1.write(_MockNonPromotionReason(), objectQVar, Type('int?'),
+            new SsaNode<Var, Type>(null), h);
         expect(s2.variableInfo, {
           objectQVar: _matchVariableModel(
             chain: ['num?', 'int?'],
@@ -3618,7 +3767,8 @@
               ),
             });
 
-            var s2 = s1.write(x, Type('C'), new SsaNode<Var, Type>(null), h);
+            var s2 =
+                s1.write(null, x, Type('C'), new SsaNode<Var, Type>(null), h);
             expect(s2.variableInfo, {
               x: _matchVariableModel(
                 chain: ['Object', 'B'],
@@ -3643,7 +3793,8 @@
               ),
             });
 
-            var s2 = s1.write(x, Type('C'), new SsaNode<Var, Type>(null), h);
+            var s2 =
+                s1.write(null, x, Type('C'), new SsaNode<Var, Type>(null), h);
             expect(s2.variableInfo, {
               x: _matchVariableModel(
                 chain: ['Object', 'B'],
@@ -3668,7 +3819,8 @@
               ),
             });
 
-            var s2 = s1.write(x, Type('B'), new SsaNode<Var, Type>(null), h);
+            var s2 =
+                s1.write(null, x, Type('B'), new SsaNode<Var, Type>(null), h);
             expect(s2.variableInfo, {
               x: _matchVariableModel(
                 chain: ['Object', 'A'],
@@ -3694,7 +3846,7 @@
               ),
             });
             var s2 = s1.write(
-                objectQVar, Type('int'), new SsaNode<Var, Type>(null), h);
+                null, objectQVar, Type('int'), new SsaNode<Var, Type>(null), h);
             // It's ambiguous whether to promote to num? or num*, so we don't
             // promote.
             expect(s2, isNot(same(s1)));
@@ -3721,8 +3873,8 @@
               ofInterest: ['num?', 'num*'],
             ),
           });
-          var s2 = s1.write(
-              objectQVar, Type('num?'), new SsaNode<Var, Type>(null), h);
+          var s2 = s1.write(_MockNonPromotionReason(), objectQVar, Type('num?'),
+              new SsaNode<Var, Type>(null), h);
           // It's ambiguous whether to promote to num? or num*, but since the
           // written type is exactly num?, we use that.
           expect(s2.variableInfo, {
@@ -3754,7 +3906,8 @@
           ),
         });
 
-        var s2 = s1.write(x, Type('double'), new SsaNode<Var, Type>(null), h);
+        var s2 = s1.write(_MockNonPromotionReason(), x, Type('double'),
+            new SsaNode<Var, Type>(null), h);
         expect(s2.variableInfo, {
           x: _matchVariableModel(
             chain: ['num?', 'num'],
@@ -3908,11 +4061,11 @@
             .declare(c, false)
             .declare(d, false);
         var s1 = s0
-            .write(a, Type('int'), new SsaNode<Var, Type>(null), h)
-            .write(b, Type('int'), new SsaNode<Var, Type>(null), h);
+            .write(null, a, Type('int'), new SsaNode<Var, Type>(null), h)
+            .write(null, b, Type('int'), new SsaNode<Var, Type>(null), h);
         var s2 = s0
-            .write(a, Type('int'), new SsaNode<Var, Type>(null), h)
-            .write(c, Type('int'), new SsaNode<Var, Type>(null), h);
+            .write(null, a, Type('int'), new SsaNode<Var, Type>(null), h)
+            .write(null, c, Type('int'), new SsaNode<Var, Type>(null), h);
         var result = s1.rebaseForward(h, s2);
         expect(result.infoFor(a).assigned, true);
         expect(result.infoFor(b).assigned, true);
@@ -3978,7 +4131,8 @@
           var s0 = FlowModel<Var, Type>(Reachability.initial).declare(x, true);
           var s1 = s0;
           if (unsafe) {
-            s1 = s1.write(x, Type('Object?'), new SsaNode<Var, Type>(null), h);
+            s1 = s1.write(
+                null, x, Type('Object?'), new SsaNode<Var, Type>(null), h);
           }
           if (thisType != null) {
             s1 =
@@ -5363,6 +5517,173 @@
       ]);
     });
   });
+
+  group('why not promoted', () {
+    test('due to assignment', () {
+      var h = Harness();
+      var x = Var('x', 'int?');
+      late Expression writeExpression;
+      h.run([
+        declare(x, initialized: true),
+        if_(x.read.eq(nullLiteral), [
+          return_(),
+        ]),
+        checkPromoted(x, 'int'),
+        (writeExpression = x.write(expr('int?'))).stmt,
+        checkNotPromoted(x),
+        x.read.whyNotPromoted((reasons) {
+          expect(reasons.keys, unorderedEquals([Type('int')]));
+          var nonPromotionReason =
+              reasons.values.single as DemoteViaExplicitWrite<Var, Expression>;
+          expect(nonPromotionReason.writeExpression, same(writeExpression));
+        }).stmt,
+      ]);
+    });
+
+    test('due to assignment, multiple demotions', () {
+      var h = Harness();
+      var x = Var('x', 'Object?');
+      late Expression writeExpression;
+      h.run([
+        declare(x, initialized: true),
+        if_(x.read.isNot('int?'), [
+          return_(),
+        ]),
+        if_(x.read.eq(nullLiteral), [
+          return_(),
+        ]),
+        checkPromoted(x, 'int'),
+        (writeExpression = x.write(expr('Object?'))).stmt,
+        checkNotPromoted(x),
+        x.read.whyNotPromoted((reasons) {
+          expect(reasons.keys, unorderedEquals([Type('int'), Type('int?')]));
+          expect(
+              (reasons[Type('int')] as DemoteViaExplicitWrite<Var, Expression>)
+                  .writeExpression,
+              same(writeExpression));
+          expect(
+              (reasons[Type('int?')] as DemoteViaExplicitWrite<Var, Expression>)
+                  .writeExpression,
+              same(writeExpression));
+        }).stmt,
+      ]);
+    });
+
+    test('preserved in join when one branch unreachable', () {
+      var h = Harness();
+      var x = Var('x', 'int?');
+      late Expression writeExpression;
+      h.run([
+        declare(x, initialized: true),
+        if_(x.read.eq(nullLiteral), [
+          return_(),
+        ]),
+        checkPromoted(x, 'int'),
+        (writeExpression = x.write(expr('int?'))).stmt,
+        checkNotPromoted(x),
+        if_(expr('bool'), [
+          return_(),
+        ]),
+        x.read.whyNotPromoted((reasons) {
+          expect(reasons.keys, unorderedEquals([Type('int')]));
+          var nonPromotionReason =
+              reasons.values.single as DemoteViaExplicitWrite<Var, Expression>;
+          expect(nonPromotionReason.writeExpression, same(writeExpression));
+        }).stmt,
+      ]);
+    });
+
+    test('preserved in later promotions', () {
+      var h = Harness();
+      var x = Var('x', 'Object');
+      late Expression writeExpression;
+      h.run([
+        declare(x, initialized: true),
+        if_(x.read.is_('int', isInverted: true), [
+          return_(),
+        ]),
+        checkPromoted(x, 'int'),
+        (writeExpression = x.write(expr('Object'))).stmt,
+        checkNotPromoted(x),
+        if_(x.read.is_('num', isInverted: true), [
+          return_(),
+        ]),
+        checkPromoted(x, 'num'),
+        x.read.whyNotPromoted((reasons) {
+          var nonPromotionReason =
+              reasons[Type('int')] as DemoteViaExplicitWrite;
+          expect(nonPromotionReason.writeExpression, same(writeExpression));
+        }).stmt,
+      ]);
+    });
+
+    test('re-promotion', () {
+      var h = Harness();
+      var x = Var('x', 'int?');
+      h.run([
+        declare(x, initialized: true),
+        if_(x.read.eq(nullLiteral), [
+          return_(),
+        ]),
+        checkPromoted(x, 'int'),
+        x.write(expr('int?')).stmt,
+        checkNotPromoted(x),
+        if_(x.read.eq(nullLiteral), [
+          return_(),
+        ]),
+        checkPromoted(x, 'int'),
+        x.read.whyNotPromoted((reasons) {
+          expect(reasons, isEmpty);
+        }).stmt,
+      ]);
+    });
+
+    group('because field', () {
+      test('via explicit this', () {
+        var h = Harness();
+        h.run([
+          if_(this_('C').propertyGet('field').eq(nullLiteral), [
+            return_(),
+          ]),
+          this_('C').propertyGet('field').whyNotPromoted((reasons) {
+            expect(reasons.keys, unorderedEquals([Type('Object')]));
+            var nonPromotionReason = reasons.values.single;
+            expect(nonPromotionReason, TypeMatcher<FieldNotPromoted>());
+          }).stmt,
+        ]);
+      });
+
+      test('via implicit this/super', () {
+        var h = Harness();
+        h.run([
+          if_(thisOrSuperPropertyGet('field').eq(nullLiteral), [
+            return_(),
+          ]),
+          thisOrSuperPropertyGet('field').whyNotPromoted((reasons) {
+            expect(reasons.keys, unorderedEquals([Type('Object')]));
+            var nonPromotionReason = reasons.values.single;
+            expect(nonPromotionReason, TypeMatcher<FieldNotPromoted>());
+          }).stmt,
+        ]);
+      });
+
+      test('via variable', () {
+        var h = Harness();
+        var x = Var('x', 'C');
+        h.run([
+          declare(x, initialized: true),
+          if_(x.read.propertyGet('field').eq(nullLiteral), [
+            return_(),
+          ]),
+          x.read.propertyGet('field').whyNotPromoted((reasons) {
+            expect(reasons.keys, unorderedEquals([Type('Object')]));
+            var nonPromotionReason = reasons.values.single;
+            expect(nonPromotionReason, TypeMatcher<FieldNotPromoted>());
+          }).stmt,
+        ]);
+      });
+    });
+  });
 }
 
 /// Returns the appropriate matcher for expecting an assertion error to be
@@ -5434,3 +5755,12 @@
 
 Reference<Var, Type> _varRef(Var variable) =>
     new VariableReference<Var, Type>(variable);
+
+class _MockNonPromotionReason extends NonPromotionReason {
+  String get shortName => fail('Unexpected call to shortName');
+
+  R accept<R, Node extends Object, Expression extends Object,
+              Variable extends Object>(
+          NonPromotionReasonVisitor<R, Node, Expression, Variable> visitor) =>
+      fail('Unexpected call to accept');
+}
diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/assignment.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/assignment.dart
new file mode 100644
index 0000000..f867664
--- /dev/null
+++ b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/assignment.dart
@@ -0,0 +1,71 @@
+// 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.
+
+abstract class C {
+  C? operator +(int i);
+  int get cProperty => 0;
+}
+
+direct_assignment(int? i, int? j) {
+  if (i == null) return;
+  /*cfe.update: explicitWrite*/ /*analyzer.explicitWrite*/ i = j;
+  i. /*notPromoted(explicitWrite)*/ isEven;
+}
+
+compound_assignment(C? c, int i) {
+  if (c == null) return;
+  /*cfe.update: explicitWrite*/ /*analyzer.explicitWrite*/ c += i;
+  c. /*notPromoted(explicitWrite)*/ cProperty;
+}
+
+via_postfix_op(C? c) {
+  if (c == null) return;
+  /*cfe.update: explicitWrite*/ /*analyzer.explicitWrite*/ c++;
+  c. /*notPromoted(explicitWrite)*/ cProperty;
+}
+
+via_prefix_op(C? c) {
+  if (c == null) return;
+  /*analyzer.explicitWrite*/ ++ /*cfe.update: explicitWrite*/ c;
+  c. /*notPromoted(explicitWrite)*/ cProperty;
+}
+
+via_for_each_statement(int? i, List<int?> list) {
+  if (i == null) return;
+  for (/*cfe.update: explicitWrite*/ /*analyzer.explicitWrite*/ i in list) {
+    i. /*notPromoted(explicitWrite)*/ isEven;
+  }
+}
+
+via_for_each_list_element(int? i, List<int?> list) {
+  if (i == null) return;
+  [
+    for (/*cfe.update: explicitWrite*/ /*analyzer.explicitWrite*/ i in list)
+      i. /*notPromoted(explicitWrite)*/ isEven
+  ];
+}
+
+via_for_each_set_element(int? i, List<int?> list) {
+  if (i == null) return;
+  ({
+    for (/*cfe.update: explicitWrite*/ /*analyzer.explicitWrite*/ i in list)
+      i. /*notPromoted(explicitWrite)*/ isEven
+  });
+}
+
+via_for_each_map_key(int? i, List<int?> list) {
+  if (i == null) return;
+  ({
+    for (/*cfe.update: explicitWrite*/ /*analyzer.explicitWrite*/ i in list)
+      i. /*notPromoted(explicitWrite)*/ isEven: null
+  });
+}
+
+via_for_each_map_value(int? i, List<int?> list) {
+  if (i == null) return;
+  ({
+    for (/*cfe.update: explicitWrite*/ /*analyzer.explicitWrite*/ i in list)
+      null: i. /*notPromoted(explicitWrite)*/ isEven
+  });
+}
diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/field.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/field.dart
new file mode 100644
index 0000000..0bbec4e
--- /dev/null
+++ b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/field.dart
@@ -0,0 +1,50 @@
+// 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.
+
+class C {
+  int? i;
+  int? j;
+
+  get_field_via_explicit_this() {
+    if (this.i == null) return;
+    this.i. /*notPromoted(fieldNotPromoted(i))*/ isEven;
+  }
+
+  get_field_via_explicit_this_parenthesized() {
+    if ((this).i == null) return;
+    (this).i. /*notPromoted(fieldNotPromoted(i))*/ isEven;
+  }
+
+  get_field_by_implicit_this() {
+    if (i == null) return;
+    i. /*notPromoted(fieldNotPromoted(i))*/ isEven;
+  }
+}
+
+class D extends C {
+  get_field_via_explicit_super() {
+    if (super.i == null) return;
+    super.i. /*notPromoted(fieldNotPromoted(i))*/ isEven;
+  }
+
+  get_field_by_implicit_super() {
+    if (i == null) return;
+    i. /*notPromoted(fieldNotPromoted(i))*/ isEven;
+  }
+}
+
+get_field_via_prefixed_identifier(C c) {
+  if (c.i == null) return;
+  c.i. /*notPromoted(fieldNotPromoted(i))*/ isEven;
+}
+
+get_field_via_prefixed_identifier_mismatched_target(C c1, C c2) {
+  if (c1.i == null) return;
+  c2.i.isEven;
+}
+
+get_field_via_prefixed_identifier_mismatched_property(C c) {
+  if (c.i == null) return;
+  c.j.isEven;
+}
diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/marker.options b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/marker.options
new file mode 100644
index 0000000..1ebb2bc
--- /dev/null
+++ b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/marker.options
@@ -0,0 +1,2 @@
+cfe=pkg/front_end/test/id_tests/why_not_promoted_test.dart
+analyzer=pkg/analyzer/test/id_tests/why_not_promoted_test.dart
\ No newline at end of file
diff --git a/pkg/analyzer/lib/error/listener.dart b/pkg/analyzer/lib/error/listener.dart
index 7de0ed7..e89e8b6 100644
--- a/pkg/analyzer/lib/error/listener.dart
+++ b/pkg/analyzer/lib/error/listener.dart
@@ -94,16 +94,18 @@
   /// Report an error with the given [errorCode] and [arguments].
   /// The [node] is used to compute the location of the error.
   void reportErrorForNode(ErrorCode errorCode, AstNode node,
-      [List<Object?>? arguments]) {
-    reportErrorForOffset(errorCode, node.offset, node.length, arguments);
+      [List<Object?>? arguments, List<DiagnosticMessage>? messages]) {
+    reportErrorForOffset(
+        errorCode, node.offset, node.length, arguments, messages);
   }
 
   /// Report an error with the given [errorCode] and [arguments]. The location
   /// of the error is specified by the given [offset] and [length].
   void reportErrorForOffset(ErrorCode errorCode, int offset, int length,
-      [List<Object?>? arguments]) {
+      [List<Object?>? arguments, List<DiagnosticMessage>? messages]) {
     _convertElements(arguments);
-    var messages = _convertTypeNames(arguments);
+    messages ??= [];
+    messages.addAll(_convertTypeNames(arguments));
     _errorListener.onError(
         AnalysisError(_source, offset, length, errorCode, arguments, messages));
   }
diff --git a/pkg/analyzer/lib/src/dart/resolver/assignment_expression_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/assignment_expression_resolver.dart
index a767e17..d83f418 100644
--- a/pkg/analyzer/lib/src/dart/resolver/assignment_expression_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/assignment_expression_resolver.dart
@@ -26,7 +26,7 @@
 
   AssignmentExpressionResolver({
     required ResolverVisitor resolver,
-  })  : _resolver = resolver,
+  })   : _resolver = resolver,
         _typePropertyResolver = resolver.typePropertyResolver,
         _inferenceHelper = resolver.inferenceHelper,
         _assignmentShared = AssignmentExpressionShared(
@@ -88,7 +88,8 @@
 
     if (flow != null) {
       if (writeElement is PromotableElement) {
-        flow.write(writeElement, node.staticType!, hasRead ? null : right);
+        flow.write(
+            node, writeElement, node.staticType!, hasRead ? null : right);
       }
       if (isIfNull) {
         flow.ifNullExpression_end();
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 1c8de42..38fcc61 100644
--- a/pkg/analyzer/lib/src/dart/resolver/binary_expression_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/binary_expression_resolver.dart
@@ -28,7 +28,7 @@
   BinaryExpressionResolver({
     required ResolverVisitor resolver,
     required TypePromotionManager promoteManager,
-  })  : _resolver = resolver,
+  })   : _resolver = resolver,
         _promoteManager = promoteManager,
         _typePropertyResolver = resolver.typePropertyResolver,
         _inferenceHelper = resolver.inferenceHelper;
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 4a01f6e..27d06a5 100644
--- a/pkg/analyzer/lib/src/dart/resolver/flow_analysis_visitor.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/flow_analysis_visitor.dart
@@ -4,6 +4,7 @@
 
 import 'package:_fe_analyzer_shared/src/flow_analysis/flow_analysis.dart';
 import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/syntactic_entity.dart';
 import 'package:analyzer/dart/ast/token.dart';
 import 'package:analyzer/dart/ast/visitor.dart';
 import 'package:analyzer/dart/element/element.dart';
@@ -40,6 +41,14 @@
   final Map<Declaration,
           AssignedVariablesForTesting<AstNode, PromotableElement>>
       assignedVariables = {};
+
+  /// For each expression that led to an error because it was not promoted, a
+  /// string describing the reason it was not promoted.
+  Map<SyntacticEntity, String> nonPromotionReasons = {};
+
+  /// For each auxiliary AST node pointed to by a non-promotion reason, a string
+  /// describing the non-promotion reason pointing to it.
+  Map<AstNode, String> nonPromotionReasonTargets = {};
 }
 
 /// The helper for performing flow analysis during resolution.
@@ -333,6 +342,9 @@
   TypeSystemTypeOperations(this.typeSystem);
 
   @override
+  DartType get topType => typeSystem.objectQuestion;
+
+  @override
   TypeClassification classifyType(DartType type) {
     if (isSubtypeOf(type, typeSystem.typeProvider.objectType)) {
       return TypeClassification.nonNullable;
diff --git a/pkg/analyzer/lib/src/dart/resolver/function_expression_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/function_expression_resolver.dart
index 830360d..d024131 100644
--- a/pkg/analyzer/lib/src/dart/resolver/function_expression_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/function_expression_resolver.dart
@@ -25,7 +25,7 @@
     required ResolverVisitor resolver,
     required MigrationResolutionHooks? migrationResolutionHooks,
     required TypePromotionManager promoteManager,
-  })  : _resolver = resolver,
+  })   : _resolver = resolver,
         _migrationResolutionHooks = migrationResolutionHooks,
         _inferenceHelper = resolver.inferenceHelper,
         _promoteManager = promoteManager;
diff --git a/pkg/analyzer/lib/src/dart/resolver/postfix_expression_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/postfix_expression_resolver.dart
index 08ebc32..c7af829 100644
--- a/pkg/analyzer/lib/src/dart/resolver/postfix_expression_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/postfix_expression_resolver.dart
@@ -26,7 +26,7 @@
 
   PostfixExpressionResolver({
     required ResolverVisitor resolver,
-  })  : _resolver = resolver,
+  })   : _resolver = resolver,
         _typePropertyResolver = resolver.typePropertyResolver,
         _inferenceHelper = resolver.inferenceHelper,
         _assignmentShared = AssignmentExpressionShared(
@@ -173,7 +173,7 @@
         var element = operand.staticElement;
         if (element is PromotableElement) {
           _resolver.flowAnalysis?.flow
-              ?.write(element, operatorReturnType, null);
+              ?.write(node, element, operatorReturnType, null);
         }
       }
     }
diff --git a/pkg/analyzer/lib/src/dart/resolver/prefix_expression_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/prefix_expression_resolver.dart
index 6901f05..f03bc53 100644
--- a/pkg/analyzer/lib/src/dart/resolver/prefix_expression_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/prefix_expression_resolver.dart
@@ -26,7 +26,7 @@
 
   PrefixExpressionResolver({
     required ResolverVisitor resolver,
-  })  : _resolver = resolver,
+  })   : _resolver = resolver,
         _typePropertyResolver = resolver.typePropertyResolver,
         _inferenceHelper = resolver.inferenceHelper,
         _assignmentShared = AssignmentExpressionShared(
@@ -209,7 +209,8 @@
         if (operand is SimpleIdentifier) {
           var element = operand.staticElement;
           if (element is PromotableElement) {
-            _resolver.flowAnalysis?.flow?.write(element, staticType, null);
+            _resolver.flowAnalysis?.flow
+                ?.write(node, element, staticType, null);
           }
         }
       }
diff --git a/pkg/analyzer/lib/src/dart/resolver/property_element_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/property_element_resolver.dart
index 9a7c23b..00317e0 100644
--- a/pkg/analyzer/lib/src/dart/resolver/property_element_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/property_element_resolver.dart
@@ -143,6 +143,7 @@
     }
 
     return _resolve(
+      node: node,
       target: prefix,
       isCascaded: false,
       isNullAware: false,
@@ -171,6 +172,7 @@
 
     if (target is SuperExpression) {
       return _resolveTargetSuperExpression(
+        node: node,
         target: target,
         propertyName: propertyName,
         hasRead: hasRead,
@@ -179,6 +181,7 @@
     }
 
     return _resolve(
+      node: node,
       target: target,
       isCascaded: node.target == null,
       isNullAware: node.isNullAware,
@@ -198,6 +201,9 @@
     if (hasRead) {
       var readLookup = _resolver.lexicalLookup(node: node, setter: false);
       readElementRequested = readLookup.requested;
+      if (readElementRequested is PropertyAccessorElement) {
+        _resolver.flowAnalysis?.flow?.thisOrSuperPropertyGet(node, node.name);
+      }
       _resolver.checkReadOfNotAssignedLocalVariable(node, readElementRequested);
     }
 
@@ -282,6 +288,7 @@
   }
 
   PropertyElementResolverResult _resolve({
+    required Expression node,
     required Expression target,
     required bool isCascaded,
     required bool isNullAware,
@@ -364,6 +371,8 @@
       nameErrorEntity: propertyName,
     );
 
+    _resolver.flowAnalysis?.flow?.propertyGet(node, target, propertyName.name);
+
     if (hasRead && result.needsGetterError) {
       _errorReporter.reportErrorForNode(
         CompileTimeErrorCode.UNDEFINED_GETTER,
@@ -595,6 +604,7 @@
   }
 
   PropertyElementResolverResult _resolveTargetSuperExpression({
+    required Expression node,
     required SuperExpression target,
     required SimpleIdentifier propertyName,
     required bool hasRead,
@@ -610,6 +620,8 @@
 
     if (targetType is InterfaceTypeImpl) {
       if (hasRead) {
+        _resolver.flowAnalysis?.flow
+            ?.propertyGet(node, target, propertyName.name);
         var name = Name(_definingLibrary.source.uri, propertyName.name);
         readElement = _resolver.inheritance
             .getMember2(targetType.element, name, forSuper: true);
diff --git a/pkg/analyzer/lib/src/dart/resolver/type_property_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/type_property_resolver.dart
index fc603c5..15898fa 100644
--- a/pkg/analyzer/lib/src/dart/resolver/type_property_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/type_property_resolver.dart
@@ -2,18 +2,23 @@
 // 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:_fe_analyzer_shared/src/flow_analysis/flow_analysis.dart';
 import 'package:analyzer/dart/ast/ast.dart';
 import 'package:analyzer/dart/ast/syntactic_entity.dart';
 import 'package:analyzer/dart/element/element.dart';
 import 'package:analyzer/dart/element/type.dart';
+import 'package:analyzer/diagnostic/diagnostic.dart';
 import 'package:analyzer/src/dart/element/element.dart';
 import 'package:analyzer/src/dart/element/inheritance_manager3.dart';
 import 'package:analyzer/src/dart/element/type_provider.dart';
 import 'package:analyzer/src/dart/element/type_system.dart';
 import 'package:analyzer/src/dart/resolver/extension_member_resolver.dart';
+import 'package:analyzer/src/dart/resolver/flow_analysis_visitor.dart';
 import 'package:analyzer/src/dart/resolver/resolution_result.dart';
+import 'package:analyzer/src/diagnostic/diagnostic.dart';
 import 'package:analyzer/src/error/codes.dart';
 import 'package:analyzer/src/generated/resolver.dart';
+import 'package:analyzer/src/generated/source.dart';
 
 /// Helper for resolving properties (getters, setters, or methods).
 class TypePropertyResolver {
@@ -114,9 +119,30 @@
         }
       }
 
+      var whyNotPromoted = receiver == null
+          ? null
+          : _resolver.flowAnalysis?.flow?.whyNotPromoted(receiver);
+      List<DiagnosticMessage> messages = [];
+      if (whyNotPromoted != null) {
+        for (var entry in whyNotPromoted.entries) {
+          var whyNotPromotedVisitor = _WhyNotPromotedVisitor(
+              _resolver.source, _resolver.flowAnalysis!.dataForTesting);
+          if (_typeSystem.isPotentiallyNullable(entry.key)) continue;
+          if (_resolver.flowAnalysis!.dataForTesting != null) {
+            _resolver.flowAnalysis!.dataForTesting!
+                .nonPromotionReasons[nameErrorEntity] = entry.value.shortName;
+          }
+          var message = entry.value.accept(whyNotPromotedVisitor);
+          if (message != null) {
+            messages = [message];
+          }
+          break;
+        }
+      }
+
       _resolver.nullableDereferenceVerifier.report(
           receiverErrorNode, receiverType,
-          errorCode: errorCode, arguments: [name]);
+          errorCode: errorCode, arguments: [name], messages: messages);
       _reportedGetterError = true;
       _reportedSetterError = true;
 
@@ -264,3 +290,71 @@
     );
   }
 }
+
+class _WhyNotPromotedVisitor
+    implements
+        NonPromotionReasonVisitor<DiagnosticMessage?, AstNode, Expression,
+            PromotableElement> {
+  final Source source;
+
+  final FlowAnalysisDataForTesting? _dataForTesting;
+
+  _WhyNotPromotedVisitor(this.source, this._dataForTesting);
+
+  @override
+  DiagnosticMessage? visitDemoteViaExplicitWrite(
+      DemoteViaExplicitWrite<PromotableElement, Expression> reason) {
+    var writeExpression = reason.writeExpression;
+    if (_dataForTesting != null) {
+      _dataForTesting!.nonPromotionReasonTargets[writeExpression] =
+          reason.shortName;
+    }
+    var variableName = reason.variable.name;
+    if (variableName == null) return null;
+    return _contextMessageForWrite(variableName, writeExpression);
+  }
+
+  @override
+  DiagnosticMessage? visitDemoteViaForEachVariableWrite(
+      DemoteViaForEachVariableWrite<PromotableElement, AstNode> reason) {
+    var node = reason.node;
+    var variableName = reason.variable.name;
+    if (variableName == null) return null;
+    ForLoopParts parts;
+    if (node is ForStatement) {
+      parts = node.forLoopParts;
+    } else if (node is ForElement) {
+      parts = node.forLoopParts;
+    } else {
+      assert(false, 'Unexpected node type');
+      return null;
+    }
+    if (parts is ForEachPartsWithIdentifier) {
+      var identifier = parts.identifier;
+      if (_dataForTesting != null) {
+        _dataForTesting!.nonPromotionReasonTargets[identifier] =
+            reason.shortName;
+      }
+      return _contextMessageForWrite(variableName, identifier);
+    } else {
+      assert(false, 'Unexpected parts type');
+      return null;
+    }
+  }
+
+  @override
+  DiagnosticMessage? visitFieldNotPromoted(FieldNotPromoted reason) {
+    // TODO(paulberry): how to report this?
+    return null;
+  }
+
+  DiagnosticMessageImpl _contextMessageForWrite(
+      String variableName, Expression writeExpression) {
+    return DiagnosticMessageImpl(
+        filePath: source.fullName,
+        message:
+            "Variable '$variableName' could be null due to a write occurring here.",
+        offset: writeExpression.offset,
+        length: writeExpression.length);
+  }
+}
diff --git a/pkg/analyzer/lib/src/dart/resolver/variable_declaration_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/variable_declaration_resolver.dart
index 68efd6c..d0829f0 100644
--- a/pkg/analyzer/lib/src/dart/resolver/variable_declaration_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/variable_declaration_resolver.dart
@@ -20,7 +20,7 @@
   VariableDeclarationResolver({
     required ResolverVisitor resolver,
     required bool strictInference,
-  })  : _resolver = resolver,
+  })   : _resolver = resolver,
         _strictInference = strictInference;
 
   void resolve(VariableDeclarationImpl node) {
diff --git a/pkg/analyzer/lib/src/error/nullable_dereference_verifier.dart b/pkg/analyzer/lib/src/error/nullable_dereference_verifier.dart
index e1dd812..1b5f219 100644
--- a/pkg/analyzer/lib/src/error/nullable_dereference_verifier.dart
+++ b/pkg/analyzer/lib/src/error/nullable_dereference_verifier.dart
@@ -4,6 +4,7 @@
 
 import 'package:analyzer/dart/ast/ast.dart';
 import 'package:analyzer/dart/element/type.dart';
+import 'package:analyzer/diagnostic/diagnostic.dart';
 import 'package:analyzer/error/error.dart';
 import 'package:analyzer/error/listener.dart';
 import 'package:analyzer/src/dart/element/type.dart';
@@ -32,13 +33,16 @@
   }
 
   void report(AstNode errorNode, DartType receiverType,
-      {ErrorCode? errorCode, List<String> arguments = const <String>[]}) {
+      {ErrorCode? errorCode,
+      List<String> arguments = const <String>[],
+      List<DiagnosticMessage>? messages}) {
     if (receiverType == _typeSystem.typeProvider.nullType) {
       errorCode = CompileTimeErrorCode.INVALID_USE_OF_NULL_VALUE;
     } else {
       errorCode ??= CompileTimeErrorCode.UNCHECKED_USE_OF_NULLABLE_VALUE;
     }
-    _errorReporter.reportErrorForNode(errorCode, errorNode, arguments);
+    _errorReporter.reportErrorForNode(
+        errorCode, errorNode, arguments, messages);
   }
 
   /// If the [receiverType] is potentially nullable, report it.
diff --git a/pkg/analyzer/lib/src/generated/resolver.dart b/pkg/analyzer/lib/src/generated/resolver.dart
index 14bea1b..e2c8920 100644
--- a/pkg/analyzer/lib/src/generated/resolver.dart
+++ b/pkg/analyzer/lib/src/generated/resolver.dart
@@ -1158,8 +1158,7 @@
     _enclosingFunction = node.declaredElement!;
 
     if (flowAnalysis != null) {
-      flowAnalysis!
-          .topLevelDeclaration_enter(node, node.parameters, node.body);
+      flowAnalysis!.topLevelDeclaration_enter(node, node.parameters, node.body);
       flowAnalysis!.executableDeclaration_enter(node, node.parameters, false);
     } else {
       _promoteManager.enterFunctionBody(node.body!);
@@ -1655,8 +1654,7 @@
     _enclosingFunction = node.declaredElement!;
 
     if (flowAnalysis != null) {
-      flowAnalysis!
-          .topLevelDeclaration_enter(node, node.parameters, node.body);
+      flowAnalysis!.topLevelDeclaration_enter(node, node.parameters, node.body);
       flowAnalysis!.executableDeclaration_enter(node, node.parameters, false);
     } else {
       _promoteManager.enterFunctionBody(node.body);
diff --git a/pkg/analyzer/lib/src/generated/static_type_analyzer.dart b/pkg/analyzer/lib/src/generated/static_type_analyzer.dart
index e7d2de5..009061d 100644
--- a/pkg/analyzer/lib/src/generated/static_type_analyzer.dart
+++ b/pkg/analyzer/lib/src/generated/static_type_analyzer.dart
@@ -284,6 +284,7 @@
 
   @override
   void visitSuperExpression(SuperExpression node) {
+    _resolver.flowAnalysis?.flow?.thisOrSuper(node);
     var thisType = _resolver.thisType;
     if (thisType == null ||
         node.thisOrAncestorOfType<ExtensionDeclaration>() != null) {
@@ -304,6 +305,7 @@
   /// interface of the immediately enclosing class.</blockquote>
   @override
   void visitThisExpression(ThisExpression node) {
+    _resolver.flowAnalysis?.flow?.thisOrSuper(node);
     var thisType = _resolver.thisType;
     if (thisType == null) {
       // TODO(brianwilkerson) Report this error if it hasn't already been
diff --git a/pkg/analyzer/test/id_tests/why_not_promoted_test.dart b/pkg/analyzer/test/id_tests/why_not_promoted_test.dart
new file mode 100644
index 0000000..be0a9c0
--- /dev/null
+++ b/pkg/analyzer/test/id_tests/why_not_promoted_test.dart
@@ -0,0 +1,87 @@
+// 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.
+
+import 'dart:io';
+
+import 'package:_fe_analyzer_shared/src/testing/id.dart' show ActualData, Id;
+import 'package:_fe_analyzer_shared/src/testing/id_testing.dart';
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/element/null_safety_understanding_flag.dart';
+import 'package:analyzer/src/dart/analysis/testing_data.dart';
+import 'package:analyzer/src/dart/resolver/flow_analysis_visitor.dart';
+import 'package:analyzer/src/util/ast_data_extractor.dart';
+
+import '../util/id_testing_helper.dart';
+
+main(List<String> args) async {
+  Directory dataDir = Directory.fromUri(
+      Platform.script.resolve('../../../_fe_analyzer_shared/test/flow_analysis/'
+          'why_not_promoted/data'));
+  await NullSafetyUnderstandingFlag.enableNullSafetyTypes(() {
+    return runTests<String?>(dataDir,
+        args: args,
+        createUriForFileName: createUriForFileName,
+        onFailure: onFailure,
+        runTest: runTestFor(
+            const _WhyNotPromotedDataComputer(), [analyzerNnbdConfig]));
+  });
+}
+
+class _WhyNotPromotedDataComputer extends DataComputer<String?> {
+  const _WhyNotPromotedDataComputer();
+
+  @override
+  DataInterpreter<String?> get dataValidator =>
+      const _WhyNotPromotedDataInterpreter();
+
+  @override
+  bool get supportsErrors => true;
+
+  @override
+  void computeUnitData(TestingData testingData, CompilationUnit unit,
+      Map<Id, ActualData<String?>> actualMap) {
+    var flowResult =
+        testingData.uriToFlowAnalysisData[unit.declaredElement!.source.uri]!;
+    _WhyNotPromotedDataExtractor(
+            unit.declaredElement!.source.uri, actualMap, flowResult)
+        .run(unit);
+  }
+}
+
+class _WhyNotPromotedDataExtractor extends AstDataExtractor<String?> {
+  final FlowAnalysisDataForTesting _flowResult;
+
+  _WhyNotPromotedDataExtractor(
+      Uri uri, Map<Id, ActualData<String?>> actualMap, this._flowResult)
+      : super(uri, actualMap);
+
+  @override
+  String? computeNodeValue(Id id, AstNode node) {
+    String? nonPromotionReason = _flowResult.nonPromotionReasons[node];
+    if (nonPromotionReason != null) {
+      return 'notPromoted($nonPromotionReason)';
+    }
+    return _flowResult.nonPromotionReasonTargets[node];
+  }
+}
+
+class _WhyNotPromotedDataInterpreter implements DataInterpreter<String?> {
+  const _WhyNotPromotedDataInterpreter();
+
+  @override
+  String getText(String? actualData, [String? indentation]) =>
+      actualData.toString();
+
+  @override
+  String? isAsExpected(String? actualData, String? expectedData) {
+    if (actualData == expectedData) {
+      return null;
+    } else {
+      return 'Expected $expectedData, got $actualData';
+    }
+  }
+
+  @override
+  bool isEmpty(String? actualData) => actualData == null;
+}
diff --git a/pkg/analyzer/test/src/lint/lint_rule_test.dart b/pkg/analyzer/test/src/lint/lint_rule_test.dart
index c6a04fb..5db743a 100644
--- a/pkg/analyzer/test/src/lint/lint_rule_test.dart
+++ b/pkg/analyzer/test/src/lint/lint_rule_test.dart
@@ -5,6 +5,7 @@
 import 'package:analyzer/dart/ast/ast.dart';
 import 'package:analyzer/dart/ast/token.dart';
 import 'package:analyzer/dart/element/element.dart';
+import 'package:analyzer/diagnostic/diagnostic.dart';
 import 'package:analyzer/error/error.dart';
 import 'package:analyzer/error/listener.dart';
 import 'package:analyzer/src/dart/ast/ast.dart';
@@ -80,7 +81,7 @@
 
   @override
   void reportErrorForNode(ErrorCode errorCode, AstNode node,
-      [List<Object?>? arguments]) {
+      [List<Object?>? arguments, List<DiagnosticMessage>? messages]) {
     code = errorCode;
   }
 
diff --git a/pkg/analyzer/test/src/workspace/gn_test.dart b/pkg/analyzer/test/src/workspace/gn_test.dart
index 9441a01..d25eb2a 100644
--- a/pkg/analyzer/test/src/workspace/gn_test.dart
+++ b/pkg/analyzer/test/src/workspace/gn_test.dart
@@ -163,8 +163,8 @@
     newFile('/workspace/.fx-build-dir', content: '$buildDir\n');
     newFile(
         '/workspace/out/debug-x87_128/dartlang/gen/some/code/foo_package_config.json');
-    var workspace =
-        GnWorkspace.find(resourceProvider, convertPath('/workspace/some/code'))!;
+    var workspace = GnWorkspace.find(
+        resourceProvider, convertPath('/workspace/some/code'))!;
     expect(workspace.root, convertPath('/workspace'));
   }
 
@@ -189,8 +189,8 @@
     }
   ]
 }''');
-    var workspace =
-        GnWorkspace.find(resourceProvider, convertPath('/workspace/some/code'))!;
+    var workspace = GnWorkspace.find(
+        resourceProvider, convertPath('/workspace/some/code'))!;
     expect(workspace.root, convertPath('/workspace'));
     expect(workspace.packageMap.length, 1);
     expect(workspace.packageMap['flutter']![0].path,
@@ -218,8 +218,8 @@
     }
   ]
 }''');
-    var workspace =
-        GnWorkspace.find(resourceProvider, convertPath('/workspace/some/code'))!;
+    var workspace = GnWorkspace.find(
+        resourceProvider, convertPath('/workspace/some/code'))!;
     expect(workspace.root, convertPath('/workspace'));
     expect(workspace.packageMap.length, 1);
     expect(workspace.packageMap['flutter']![0].path,
@@ -245,8 +245,8 @@
     }
   ]
 }''');
-    var workspace =
-        GnWorkspace.find(resourceProvider, convertPath('/workspace/some/code'))!;
+    var workspace = GnWorkspace.find(
+        resourceProvider, convertPath('/workspace/some/code'))!;
     expect(workspace.root, convertPath('/workspace'));
     expect(workspace.packageMap.length, 1);
     expect(workspace.packageMap['flutter']![0].path,
@@ -273,8 +273,8 @@
     }
   ]
 }''');
-    var workspace =
-        GnWorkspace.find(resourceProvider, convertPath('/workspace/some/code'))!;
+    var workspace = GnWorkspace.find(
+        resourceProvider, convertPath('/workspace/some/code'))!;
     expect(workspace.root, convertPath('/workspace'));
     expect(workspace.packageMap.length, 1);
     expect(workspace.packageMap['flutter']![0].path,
@@ -318,8 +318,8 @@
     }
   ]
 }''');
-    var workspace =
-        GnWorkspace.find(resourceProvider, convertPath('/workspace/some/code'))!;
+    var workspace = GnWorkspace.find(
+        resourceProvider, convertPath('/workspace/some/code'))!;
     expect(workspace.root, convertPath('/workspace'));
     expect(workspace.packageMap.length, 1);
     expect(workspace.packageMap['rettulf']![0].path,
@@ -363,8 +363,8 @@
     }
   ]
 }''');
-    var workspace =
-        GnWorkspace.find(resourceProvider, convertPath('/workspace/some/code'))!;
+    var workspace = GnWorkspace.find(
+        resourceProvider, convertPath('/workspace/some/code'))!;
     expect(workspace.root, convertPath('/workspace'));
     expect(workspace.packageMap.length, 2);
     expect(workspace.packageMap['flutter']![0].path,
diff --git a/pkg/analyzer/tool/update_id_tests.dart b/pkg/analyzer/tool/update_id_tests.dart
index 4f7a5d6..44a9384 100644
--- a/pkg/analyzer/tool/update_id_tests.dart
+++ b/pkg/analyzer/tool/update_id_tests.dart
@@ -17,4 +17,5 @@
   'pkg/analyzer/test/id_tests/nullability_test.dart',
   'pkg/analyzer/test/id_tests/reachability_test.dart',
   'pkg/analyzer/test/id_tests/type_promotion_test.dart',
+  'pkg/analyzer/test/id_tests/why_not_promoted_test.dart',
 ];
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 d0363bb..c2ff740 100644
--- a/pkg/front_end/lib/src/fasta/kernel/inference_visitor.dart
+++ b/pkg/front_end/lib/src/fasta/kernel/inference_visitor.dart
@@ -5,10 +5,13 @@
 // @dart = 2.9
 
 import 'dart:core' hide MapEntry;
+import 'dart:core' as core;
 
+import 'package:_fe_analyzer_shared/src/flow_analysis/flow_analysis.dart';
 import 'package:_fe_analyzer_shared/src/util/link.dart';
 import 'package:front_end/src/api_prototype/lowering_predicates.dart';
-import 'package:kernel/ast.dart';
+import 'package:kernel/ast.dart'
+    hide Reference; // Work around https://github.com/dart-lang/sdk/issues/44667
 import 'package:kernel/src/legacy_erasure.dart';
 import 'package:kernel/type_algebra.dart' show Substitution;
 import 'package:kernel/type_environment.dart';
@@ -4738,12 +4741,31 @@
 
     readResult ??= new ExpressionInferenceResult(readType, read);
     if (!inferrer.isTopLevel && readTarget.isNullable) {
+      Map<DartType, NonPromotionReason> whyNotPromoted =
+          inferrer.flowAnalysis?.whyNotPromoted(receiver);
+      List<LocatedMessage> context;
+      if (whyNotPromoted != null && whyNotPromoted.isNotEmpty) {
+        _WhyNotPromotedVisitor whyNotPromotedVisitor =
+            new _WhyNotPromotedVisitor(inferrer);
+        for (core.MapEntry<DartType, NonPromotionReason> entry
+            in whyNotPromoted.entries) {
+          if (entry.key.isPotentiallyNullable) continue;
+          if (inferrer.dataForTesting != null) {
+            inferrer.dataForTesting.flowAnalysisResult
+                .nonPromotionReasons[read] = entry.value.shortName;
+          }
+          LocatedMessage message = entry.value.accept(whyNotPromotedVisitor);
+          context = [message];
+          break;
+        }
+      }
       readResult = inferrer.wrapExpressionInferenceResultInProblem(
           readResult,
           templateNullablePropertyAccessError.withArguments(
               propertyName.text, receiverType, inferrer.isNonNullableByDefault),
           read.fileOffset,
-          propertyName.text.length);
+          propertyName.text.length,
+          context: context);
     }
     return readResult;
   }
@@ -5709,8 +5731,13 @@
     ExpressionInferenceResult readResult = _computePropertyGet(
         node.fileOffset, receiver, receiverType, node.name, typeContext,
         isThisReceiver: node.receiver is ThisExpression);
-    return inferrer.createNullAwareExpressionInferenceResult(
-        readResult.inferredType, readResult.expression, nullAwareGuards);
+    inferrer.flowAnalysis.propertyGet(node, node.receiver, node.name.name);
+    ExpressionInferenceResult expressionInferenceResult =
+        inferrer.createNullAwareExpressionInferenceResult(
+            readResult.inferredType, readResult.expression, nullAwareGuards);
+    inferrer.flowAnalysis
+        .forwardExpression(expressionInferenceResult.nullAwareAction, node);
+    return expressionInferenceResult;
   }
 
   @override
@@ -5973,6 +6000,7 @@
   @override
   ExpressionInferenceResult visitSuperPropertyGet(
       SuperPropertyGet node, DartType typeContext) {
+    inferrer.flowAnalysis.thisOrSuperPropertyGet(node, node.name.name);
     if (node.interfaceTarget != null) {
       inferrer.instrumentation?.record(
           inferrer.uriForInstrumentation,
@@ -6160,6 +6188,7 @@
 
   ExpressionInferenceResult visitThisExpression(
       ThisExpression node, DartType typeContext) {
+    inferrer.flowAnalysis.thisOrSuper(node);
     return new ExpressionInferenceResult(inferrer.thisType, node);
   }
 
@@ -6280,7 +6309,7 @@
         fileOffset: node.fileOffset,
         isVoidAllowed: declaredOrInferredType is VoidType);
     inferrer.flowAnalysis
-        .write(variable, rhsResult.inferredType, rhsResult.expression);
+        .write(node, variable, rhsResult.inferredType, rhsResult.expression);
     DartType resultType = rhsResult.inferredType;
     Expression resultExpression;
     if (variable.lateSetter != null) {
@@ -6956,6 +6985,44 @@
   }
 }
 
+class _WhyNotPromotedVisitor
+    implements
+        NonPromotionReasonVisitor<LocatedMessage, Node, Expression,
+            VariableDeclaration> {
+  final TypeInferrerImpl inferrer;
+
+  _WhyNotPromotedVisitor(this.inferrer);
+
+  @override
+  LocatedMessage visitDemoteViaExplicitWrite(
+      DemoteViaExplicitWrite<VariableDeclaration, Expression> reason) {
+    if (inferrer.dataForTesting != null) {
+      inferrer.dataForTesting.flowAnalysisResult
+          .nonPromotionReasonTargets[reason.writeExpression] = reason.shortName;
+    }
+    int offset = reason.writeExpression.fileOffset;
+    return templateVariableCouldBeNullDueToWrite
+        .withArguments(reason.variable.name)
+        .withLocation(inferrer.helper.uri, offset, noLength);
+  }
+
+  @override
+  LocatedMessage visitDemoteViaForEachVariableWrite(
+      DemoteViaForEachVariableWrite<VariableDeclaration, Node> reason) {
+    int offset = (reason.node as TreeNode).fileOffset;
+    return templateVariableCouldBeNullDueToWrite
+        .withArguments(reason.variable.name)
+        .withLocation(inferrer.helper.uri, offset, noLength);
+  }
+
+  @override
+  LocatedMessage visitFieldNotPromoted(FieldNotPromoted reason) {
+    return templateFieldNotPromoted
+        .withArguments(reason.propertyName)
+        .withoutLocation();
+  }
+}
+
 class ForInResult {
   final VariableDeclaration variable;
   final Expression iterable;
@@ -7005,7 +7072,8 @@
         isVoidAllowed: true);
 
     variableSet.value = rhs..parent = variableSet;
-    inferrer.flowAnalysis.write(variableSet.variable, rhsType, null);
+    inferrer.flowAnalysis
+        .write(variableSet, variableSet.variable, rhsType, null);
     return variableSet;
   }
 }
diff --git a/pkg/front_end/lib/src/fasta/type_inference/type_inference_engine.dart b/pkg/front_end/lib/src/fasta/type_inference/type_inference_engine.dart
index 9d674c0..f2cb2c4 100644
--- a/pkg/front_end/lib/src/fasta/type_inference/type_inference_engine.dart
+++ b/pkg/front_end/lib/src/fasta/type_inference/type_inference_engine.dart
@@ -257,6 +257,14 @@
 
   /// The assigned variables information that computed for the member.
   AssignedVariablesForTesting<TreeNode, VariableDeclaration> assignedVariables;
+
+  /// For each expression that led to an error because it was not promoted, a
+  /// string describing the reason it was not promoted.
+  final Map<TreeNode, String> nonPromotionReasons = {};
+
+  /// For each auxiliary AST node pointed to by a non-promotion reason, a string
+  /// describing the non-promotion reason pointing to it.
+  final Map<TreeNode, String> nonPromotionReasonTargets = {};
 }
 
 /// CFE-specific implementation of [TypeOperations].
@@ -266,6 +274,9 @@
   TypeOperationsCfe(this.typeEnvironment);
 
   @override
+  DartType get topType => typeEnvironment.objectNullableRawType;
+
+  @override
   TypeClassification classifyType(DartType type) {
     if (type == null) {
       // Note: this can happen during top-level inference.
diff --git a/pkg/front_end/lib/src/fasta/type_inference/type_inferrer.dart b/pkg/front_end/lib/src/fasta/type_inference/type_inferrer.dart
index 0b50fbb..cfdc40c 100644
--- a/pkg/front_end/lib/src/fasta/type_inference/type_inferrer.dart
+++ b/pkg/front_end/lib/src/fasta/type_inference/type_inferrer.dart
@@ -1784,7 +1784,8 @@
     return createNullAwareExpressionInferenceResult(
         result.inferredType,
         helper.wrapInProblem(
-            result.nullAwareAction, message, fileOffset, length),
+            result.nullAwareAction, message, fileOffset, length,
+            context: context),
         result.nullAwareGuards);
   }
 
diff --git a/pkg/front_end/messages.status b/pkg/front_end/messages.status
index 245cd2d..b989951 100644
--- a/pkg/front_end/messages.status
+++ b/pkg/front_end/messages.status
@@ -344,6 +344,8 @@
 FieldNonNullableWithoutInitializerError/example: Fail
 FieldNonNullableWithoutInitializerWarning/analyzerCode: Fail
 FieldNonNullableWithoutInitializerWarning/example: Fail
+FieldNotPromoted/analyzerCode: Fail
+FieldNotPromoted/example: Fail
 FinalAndCovariant/part_wrapped_script2: Fail
 FinalAndCovariant/script2: Fail
 FinalFieldWithoutInitializer/example: Fail
@@ -793,6 +795,8 @@
 ValueForRequiredParameterNotProvidedWarning/example: Fail
 VarAsTypeName/part_wrapped_script1: Fail
 VarAsTypeName/script1: Fail # Too many problems
+VariableCouldBeNullDueToWrite/analyzerCode: Fail
+VariableCouldBeNullDueToWrite/example: Fail
 WeakWithStrongDillLibrary/analyzerCode: Fail
 WeakWithStrongDillLibrary/example: Fail
 WebLiteralCannotBeRepresentedExactly/analyzerCode: Fail
diff --git a/pkg/front_end/messages.yaml b/pkg/front_end/messages.yaml
index 2e197e9..c27c292 100644
--- a/pkg/front_end/messages.yaml
+++ b/pkg/front_end/messages.yaml
@@ -4584,6 +4584,13 @@
   tip: "Use at most one of the 'in', 'out', or 'inout' modifiers."
   analyzerCode: ParserErrorCode.MULTIPLE_VARIANCE_MODIFIERS
 
+VariableCouldBeNullDueToWrite:
+  template: "Variable '#name' could be null due to a write occurring here."
+  tip: "Try null checking the variable after the write."
+
+FieldNotPromoted:
+  template: "'#name' refers to a property so it could not be promoted."
+
 NullablePropertyAccessError:
   template: "Property '#name' cannot be accessed on '#type' because it is potentially null."
   tip: "Try accessing using ?. instead."
diff --git a/pkg/front_end/test/id_tests/why_not_promoted_test.dart b/pkg/front_end/test/id_tests/why_not_promoted_test.dart
new file mode 100644
index 0000000..0b0b6ee
--- /dev/null
+++ b/pkg/front_end/test/id_tests/why_not_promoted_test.dart
@@ -0,0 +1,93 @@
+// 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.
+
+// @dart = 2.9
+
+import 'dart:io' show Directory, Platform;
+
+import 'package:_fe_analyzer_shared/src/testing/id.dart' show ActualData, Id;
+import 'package:_fe_analyzer_shared/src/testing/id_testing.dart'
+    show DataInterpreter, runTests;
+import 'package:_fe_analyzer_shared/src/testing/id_testing.dart';
+import 'package:front_end/src/fasta/builder/member_builder.dart';
+import 'package:front_end/src/fasta/type_inference/type_inference_engine.dart';
+import 'package:front_end/src/testing/id_testing_helper.dart';
+import 'package:front_end/src/testing/id_testing_utils.dart';
+import 'package:kernel/ast.dart' hide Variance, MapEntry;
+
+main(List<String> args) async {
+  Directory dataDir = new Directory.fromUri(
+      Platform.script.resolve('../../../_fe_analyzer_shared/test/flow_analysis/'
+          'why_not_promoted/data'));
+  await runTests<String>(dataDir,
+      args: args,
+      createUriForFileName: createUriForFileName,
+      onFailure: onFailure,
+      runTest: runTestFor(
+          const WhyNotPromotedDataComputer(), [cfeNonNullableOnlyConfig]));
+}
+
+class WhyNotPromotedDataComputer extends DataComputer<String> {
+  const WhyNotPromotedDataComputer();
+
+  @override
+  DataInterpreter<String> get dataValidator =>
+      const _WhyNotPromotedDataInterpreter();
+
+  /// Errors are supported for testing erroneous code. The reported errors are
+  /// not tested.
+  @override
+  bool get supportsErrors => true;
+
+  /// Function that computes a data mapping for [member].
+  ///
+  /// Fills [actualMap] with the data.
+  void computeMemberData(
+      TestConfig config,
+      InternalCompilerResult compilerResult,
+      Member member,
+      Map<Id, ActualData<String>> actualMap,
+      {bool verbose}) {
+    MemberBuilderImpl memberBuilder =
+        lookupMemberBuilder(compilerResult, member);
+    member.accept(new WhyNotPromotedDataExtractor(compilerResult, actualMap,
+        memberBuilder.dataForTesting.inferenceData.flowAnalysisResult));
+  }
+}
+
+class WhyNotPromotedDataExtractor extends CfeDataExtractor<String> {
+  final FlowAnalysisResult _flowResult;
+
+  WhyNotPromotedDataExtractor(InternalCompilerResult compilerResult,
+      Map<Id, ActualData<String>> actualMap, this._flowResult)
+      : super(compilerResult, actualMap);
+
+  @override
+  String computeNodeValue(Id id, TreeNode node) {
+    String nonPromotionReason = _flowResult.nonPromotionReasons[node];
+    if (nonPromotionReason != null) {
+      return 'notPromoted($nonPromotionReason)';
+    }
+    return _flowResult.nonPromotionReasonTargets[node];
+  }
+}
+
+class _WhyNotPromotedDataInterpreter implements DataInterpreter<String> {
+  const _WhyNotPromotedDataInterpreter();
+
+  @override
+  String getText(String actualData, [String indentation]) => actualData;
+
+  @override
+  String isAsExpected(String actualData, String expectedData) {
+    if (actualData == expectedData) {
+      return null;
+    } else {
+      return 'Expected $expectedData, got $actualData';
+    }
+  }
+
+  @override
+  bool isEmpty(String actualData) => actualData == null;
+}
diff --git a/pkg/front_end/test/lint_test.status b/pkg/front_end/test/lint_test.status
index 2728954..aaa6e98 100644
--- a/pkg/front_end/test/lint_test.status
+++ b/pkg/front_end/test/lint_test.status
@@ -20,6 +20,7 @@
 front_end/lib/src/fasta/kernel/body_builder/ImportsTwice: Fail
 front_end/lib/src/fasta/kernel/constant_evaluator/ExplicitType: Pass
 front_end/lib/src/fasta/kernel/expression_generator_helper/ImportsTwice: Fail
+front_end/lib/src/fasta/kernel/inference_visitor/ImportsTwice: Fail
 front_end/lib/src/fasta/kernel/kernel_api/Exports: Fail
 front_end/lib/src/fasta/kernel/kernel_ast_api/Exports: Fail
 front_end/lib/src/fasta/kernel/kernel_builder/Exports: Fail
diff --git a/pkg/front_end/test/spell_checking_list_code.txt b/pkg/front_end/test/spell_checking_list_code.txt
index 572493d..92adf25 100644
--- a/pkg/front_end/test/spell_checking_list_code.txt
+++ b/pkg/front_end/test/spell_checking_list_code.txt
@@ -498,6 +498,7 @@
 hacky
 hadn't
 hang
+happy
 hardcode
 harness
 hashes
@@ -847,6 +848,7 @@
 plugin
 pm
 pn
+pointed
 pointwise
 polluted
 pool
diff --git a/pkg/front_end/tool/update_all.dart b/pkg/front_end/tool/update_all.dart
index 41ba6a1..c25162c 100644
--- a/pkg/front_end/tool/update_all.dart
+++ b/pkg/front_end/tool/update_all.dart
@@ -19,6 +19,7 @@
   'pkg/front_end/test/id_tests/nullability_test.dart',
   'pkg/front_end/test/id_tests/reachability_test.dart',
   'pkg/front_end/test/id_tests/type_promotion_test.dart',
+  'pkg/front_end/test/id_tests/why_not_promoted_test.dart',
   'pkg/front_end/test/language_versioning/language_versioning_test.dart',
   'pkg/front_end/test/patching/patching_test.dart',
   'pkg/front_end/test/static_types/static_type_test.dart',
diff --git a/pkg/nnbd_migration/lib/src/decorated_type_operations.dart b/pkg/nnbd_migration/lib/src/decorated_type_operations.dart
index 4c776c8..0f9328a 100644
--- a/pkg/nnbd_migration/lib/src/decorated_type_operations.dart
+++ b/pkg/nnbd_migration/lib/src/decorated_type_operations.dart
@@ -21,6 +21,14 @@
       this._typeSystem, this._variableRepository, this._graph);
 
   @override
+  DecoratedType get topType {
+    // This is only needed for explaining to the user why fields aren't
+    // promoted, functionality of flow analysis that we don't take advantage of
+    // during migration.  So this method should never be called.
+    throw StateError('Unexpected call to topType');
+  }
+
+  @override
   TypeClassification classifyType(DecoratedType type) {
     if (type.type.isDartCoreNull) {
       return TypeClassification.nullOrEquivalent;
diff --git a/pkg/nnbd_migration/lib/src/edge_builder.dart b/pkg/nnbd_migration/lib/src/edge_builder.dart
index c074a4a..8138239 100644
--- a/pkg/nnbd_migration/lib/src/edge_builder.dart
+++ b/pkg/nnbd_migration/lib/src/edge_builder.dart
@@ -407,7 +407,7 @@
     }
 
     var expressionType = _handleAssignment(node.rightHandSide,
-        destinationExpression: node.leftHandSide,
+        assignmentExpression: node,
         compoundOperatorInfo: isCompound ? node : null,
         questionAssignNode: isQuestionAssign ? node : null,
         sourceIsSetupCall: sourceIsSetupCall);
@@ -1387,7 +1387,7 @@
       if (operand is SimpleIdentifier) {
         var element = getWriteOrReadElement(operand);
         if (element is PromotableElement) {
-          _flowAnalysis.write(element, writeType, null);
+          _flowAnalysis.write(node, element, writeType, null);
         }
       }
       return targetType;
@@ -1438,7 +1438,7 @@
         if (operand is SimpleIdentifier) {
           var element = getWriteOrReadElement(operand);
           if (element is PromotableElement) {
-            _flowAnalysis.write(element, staticType, null);
+            _flowAnalysis.write(node, element, staticType, null);
           }
         }
       }
@@ -2285,26 +2285,28 @@
   /// Creates the necessary constraint(s) for an assignment of the given
   /// [expression] to a destination whose type is [destinationType].
   ///
-  /// Optionally, the caller may supply a [destinationExpression] instead of
+  /// Optionally, the caller may supply an [assignmentExpression] instead of
   /// [destinationType].  In this case, then the type comes from visiting the
-  /// destination expression.  If the destination expression refers to a local
-  /// variable, we mark it as assigned in flow analysis at the proper time.
+  /// LHS of the assignment expression.  If the LHS of the assignment expression
+  /// refers to a local variable, we mark it as assigned in flow analysis at the
+  /// proper time.
   ///
   /// Set [wrapFuture] to true to handle assigning Future<flatten(T)> to R.
   DecoratedType _handleAssignment(Expression expression,
       {DecoratedType destinationType,
-      Expression destinationExpression,
+      AssignmentExpression assignmentExpression,
       AssignmentExpression compoundOperatorInfo,
       AssignmentExpression questionAssignNode,
       bool fromDefaultValue = false,
       bool wrapFuture = false,
       bool sourceIsSetupCall = false}) {
     assert(
-        (destinationExpression == null) != (destinationType == null),
-        'Either destinationExpression or destinationType should be supplied, '
+        (assignmentExpression == null) != (destinationType == null),
+        'Either assignmentExpression or destinationType should be supplied, '
         'but not both');
     PromotableElement destinationLocalVariable;
     if (destinationType == null) {
+      var destinationExpression = assignmentExpression.leftHandSide;
       if (destinationExpression is SimpleIdentifier) {
         var element = getWriteOrReadElement(destinationExpression);
         if (element is PromotableElement) {
@@ -2345,7 +2347,7 @@
               source: destinationType,
               destination: _createNonNullableType(compoundOperatorInfo),
               hard: _postDominatedLocals
-                  .isReferenceInScope(destinationExpression));
+                  .isReferenceInScope(assignmentExpression.leftHandSide));
           DecoratedType compoundOperatorType = getOrComputeElementType(
               compoundOperatorMethod,
               targetType: destinationType);
@@ -2403,8 +2405,8 @@
         }
       }
       if (destinationLocalVariable != null) {
-        _flowAnalysis.write(destinationLocalVariable, sourceType,
-            compoundOperatorInfo == null ? expression : null);
+        _flowAnalysis.write(assignmentExpression, destinationLocalVariable,
+            sourceType, compoundOperatorInfo == null ? expression : null);
       }
       if (questionAssignNode != null) {
         _flowAnalysis.ifNullExpression_end();
@@ -2419,9 +2421,9 @@
         _guards.removeLast();
       }
     }
-    if (destinationExpression != null) {
-      var element =
-          _postDominatedLocals.referencedElement(destinationExpression);
+    if (assignmentExpression != null) {
+      var element = _postDominatedLocals
+          .referencedElement(assignmentExpression.leftHandSide);
       if (element != null) {
         _postDominatedLocals.removeFromAllScopes(element);
         _elementsWrittenToInLocalFunction?.add(element);
diff --git a/tools/generate_package_config.dart b/tools/generate_package_config.dart
index 9369cce..08b7920 100644
--- a/tools/generate_package_config.dart
+++ b/tools/generate_package_config.dart
@@ -48,6 +48,8 @@
         'pkg/_fe_analyzer_shared/test/flow_analysis/reachability/'),
     packageDirectory(
         'pkg/_fe_analyzer_shared/test/flow_analysis/type_promotion/'),
+    packageDirectory(
+        'pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted//'),
     packageDirectory('pkg/_fe_analyzer_shared/test/inheritance/'),
   ];