Add tests reproducing language issue 4127.

These tests exercise the current (unintended) behavior described
https://github.com/dart-lang/language/issues/4127, which was
previously not well tested.

Adding these tests acts as a safeguard to make sure that we don't
change the current behavior by accident.  If/when we decide to fix
https://github.com/dart-lang/language/issues/4127, the test
expectations will need to be updated.

Bug: https://github.com/dart-lang/language/issues/4127
Change-Id: I02fd1d393038a304401d11cf2c19e97755ba90a0
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/389584
Reviewed-by: Kallen Tu <kallentu@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
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 4dc13f8..5987070 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
@@ -7249,6 +7249,54 @@
         ]);
       });
     });
+
+    group('and equality:', () {
+      test('promoted type ignored on LHS', () {
+        // Normally flow analysis understands when an `if` test is guaranteed to
+        // succeed (or fail) based on the static types of the LHS and RHS. But
+        // due to https://github.com/dart-lang/language/issues/4127, this
+        // doesn't fully work when the LHS or RHS is a property reference; in
+        // that case, the unpromoted type is used.
+
+        // This test is here to make sure we don't change the existing behavior
+        // by accident; if/when we fix #4127, this test should be changed
+        // accordingly.
+        h.addMember('C', 'f', 'Object?', promotable: true);
+        h.thisType = 'C';
+        h.run([
+          if_(thisProperty('f').isNot('Null'), [return_()]),
+          checkPromoted(thisProperty('f'), 'Null'),
+          if_(thisProperty('f').eq(nullLiteral), [
+            checkReachable(true),
+          ], [
+            checkReachable(true),
+          ]),
+        ]);
+      });
+
+      test('promoted type ignored on RHS', () {
+        // Normally flow analysis understands when an `if` test is guaranteed to
+        // succeed (or fail) based on the static types of the LHS and RHS. But
+        // due to https://github.com/dart-lang/language/issues/4127, this
+        // doesn't fully work when the LHS or RHS is a property reference; in
+        // that case, the unpromoted type is used.
+
+        // This test is here to make sure we don't change the existing behavior
+        // by accident; if/when we fix #4127, this test should be changed
+        // accordingly.
+        h.addMember('C', 'f', 'Object?', promotable: true);
+        h.thisType = 'C';
+        h.run([
+          if_(thisProperty('f').isNot('Null'), [return_()]),
+          checkPromoted(thisProperty('f'), 'Null'),
+          if_(nullLiteral.eq(thisProperty('f')), [
+            checkReachable(true),
+          ], [
+            checkReachable(true),
+          ]),
+        ]);
+      });
+    });
   });
 
   group('Patterns:', () {
diff --git a/tests/language/inference_update_2/promoted_field_type_in_equality_test.dart b/tests/language/inference_update_2/promoted_field_type_in_equality_test.dart
new file mode 100644
index 0000000..0998a0e
--- /dev/null
+++ b/tests/language/inference_update_2/promoted_field_type_in_equality_test.dart
@@ -0,0 +1,265 @@
+// Copyright (c) 2024, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// Tests the behavior described in
+// https://github.com/dart-lang/language/issues/4127, namely the fact that when
+// deciding whether an `==` or `!=` comparison is guaranteed to evaluate to
+// `true` or `false`, flow analysis considers promoted fields to have their base
+// type rather than their promoted type.
+
+// This test is here to make sure we don't change the existing behavior by
+// accident; if/when we fix #4127, this test should be changed accordingly.
+
+import '../static_type_helper.dart';
+
+class C {
+  final Object? _f;
+  C(this._f);
+
+  void testImplicitThisReferenceOnLhsOfEquals() {
+    int? x = 0;
+    int? y = 0;
+    x.expectStaticType<Exactly<int>>();
+    y.expectStaticType<Exactly<int>>();
+    if (_f is! Null) return;
+    _f.expectStaticType<Exactly<Null>>();
+    if (_f == null) {
+      x = null;
+    } else {
+      y = null;
+    }
+    // In analyzing the `==` check, flow analysis assumes that `_f` has its
+    // unpromoted type (`Object?`), so both branches of the `if` are
+    // reachable. Therefore both `x` and `y` should both be demoted here.
+    x.expectStaticType<Exactly<int?>>();
+    y.expectStaticType<Exactly<int?>>();
+  }
+
+  void testImplicitThisReferenceOnRhsOfEquals() {
+    int? x = 0;
+    int? y = 0;
+    x.expectStaticType<Exactly<int>>();
+    y.expectStaticType<Exactly<int>>();
+    if (_f is! Null) return;
+    _f.expectStaticType<Exactly<Null>>();
+    if (null == _f) {
+      x = null;
+    } else {
+      y = null;
+    }
+    // In analyzing the `==` check, flow analysis assumes that `_f` has its
+    // unpromoted type (`Object?`), so both branches of the `if` are
+    // reachable. Therefore both `x` and `y` should both be demoted here.
+    x.expectStaticType<Exactly<int?>>();
+    y.expectStaticType<Exactly<int?>>();
+  }
+
+  void testImplicitThisReferenceOnLhsOfNotEquals() {
+    int? x = 0;
+    int? y = 0;
+    x.expectStaticType<Exactly<int>>();
+    y.expectStaticType<Exactly<int>>();
+    if (_f is! Null) return;
+    _f.expectStaticType<Exactly<Null>>();
+    if (_f != null) {
+      x = null;
+    } else {
+      y = null;
+    }
+    // In analyzing the `!=` check, flow analysis assumes that `_f` has its
+    // unpromoted type (`Object?`), so both branches of the `if` are
+    // reachable. Therefore both `x` and `y` should both be demoted here.
+    x.expectStaticType<Exactly<int?>>();
+    y.expectStaticType<Exactly<int?>>();
+  }
+
+  void testImplicitThisReferenceOnRhsOfNotEquals() {
+    int? x = 0;
+    int? y = 0;
+    x.expectStaticType<Exactly<int>>();
+    y.expectStaticType<Exactly<int>>();
+    if (_f is! Null) return;
+    _f.expectStaticType<Exactly<Null>>();
+    if (null != _f) {
+      x = null;
+    } else {
+      y = null;
+    }
+    // In analyzing the `!=` check, flow analysis assumes that `_f` has its
+    // unpromoted type (`Object?`), so both branches of the `if` are
+    // reachable. Therefore both `x` and `y` should both be demoted here.
+    x.expectStaticType<Exactly<int?>>();
+    y.expectStaticType<Exactly<int?>>();
+  }
+
+  void testExplicitThisReferenceOnLhsOfEquals() {
+    int? x = 0;
+    int? y = 0;
+    x.expectStaticType<Exactly<int>>();
+    y.expectStaticType<Exactly<int>>();
+    if (this._f is! Null) return;
+    this._f.expectStaticType<Exactly<Null>>();
+    if (this._f == null) {
+      x = null;
+    } else {
+      y = null;
+    }
+    // In analyzing the `==` check, flow analysis assumes that `this._f` has its
+    // unpromoted type (`Object?`), so both branches of the `if` are
+    // reachable. Therefore both `x` and `y` should both be demoted here.
+    x.expectStaticType<Exactly<int?>>();
+    y.expectStaticType<Exactly<int?>>();
+  }
+
+  void testExplicitThisReferenceOnRhsOfEquals() {
+    int? x = 0;
+    int? y = 0;
+    x.expectStaticType<Exactly<int>>();
+    y.expectStaticType<Exactly<int>>();
+    if (this._f is! Null) return;
+    this._f.expectStaticType<Exactly<Null>>();
+    if (null == this._f) {
+      x = null;
+    } else {
+      y = null;
+    }
+    // In analyzing the `==` check, flow analysis assumes that `this._f` has its
+    // unpromoted type (`Object?`), so both branches of the `if` are
+    // reachable. Therefore both `x` and `y` should both be demoted here.
+    x.expectStaticType<Exactly<int?>>();
+    y.expectStaticType<Exactly<int?>>();
+  }
+
+  void testExplicitThisReferenceOnLhsOfNotEquals() {
+    int? x = 0;
+    int? y = 0;
+    x.expectStaticType<Exactly<int>>();
+    y.expectStaticType<Exactly<int>>();
+    if (this._f is! Null) return;
+    this._f.expectStaticType<Exactly<Null>>();
+    if (this._f != null) {
+      x = null;
+    } else {
+      y = null;
+    }
+    // In analyzing the `!=` check, flow analysis assumes that `this._f` has its
+    // unpromoted type (`Object?`), so both branches of the `if` are
+    // reachable. Therefore both `x` and `y` should both be demoted here.
+    x.expectStaticType<Exactly<int?>>();
+    y.expectStaticType<Exactly<int?>>();
+  }
+
+  void testExplicitThisReferenceOnRhsOfNotEquals() {
+    int? x = 0;
+    int? y = 0;
+    x.expectStaticType<Exactly<int>>();
+    y.expectStaticType<Exactly<int>>();
+    if (this._f is! Null) return;
+    this._f.expectStaticType<Exactly<Null>>();
+    if (null != this._f) {
+      x = null;
+    } else {
+      y = null;
+    }
+    // In analyzing the `!=` check, flow analysis assumes that `this._f` has its
+    // unpromoted type (`Object?`), so both branches of the `if` are
+    // reachable. Therefore both `x` and `y` should both be demoted here.
+    x.expectStaticType<Exactly<int?>>();
+    y.expectStaticType<Exactly<int?>>();
+  }
+}
+
+void testExplicitPropertyReferenceOnLhsOfEquals(C c) {
+  int? x = 0;
+  int? y = 0;
+  x.expectStaticType<Exactly<int>>();
+  y.expectStaticType<Exactly<int>>();
+  if (c._f is! Null) return;
+  c._f.expectStaticType<Exactly<Null>>();
+  if (c._f == null) {
+    x = null;
+  } else {
+    y = null;
+  }
+  // In analyzing the `==` check, flow analysis assumes that `c._f` has its
+  // unpromoted type (`Object?`), so both branches of the `if` are
+  // reachable. Therefore both `x` and `y` should both be demoted here.
+  x.expectStaticType<Exactly<int?>>();
+  y.expectStaticType<Exactly<int?>>();
+}
+
+void testExplicitPropertyReferenceOnRhsOfEquals(C c) {
+  int? x = 0;
+  int? y = 0;
+  x.expectStaticType<Exactly<int>>();
+  y.expectStaticType<Exactly<int>>();
+  if (c._f is! Null) return;
+  c._f.expectStaticType<Exactly<Null>>();
+  if (null == c._f) {
+    x = null;
+  } else {
+    y = null;
+  }
+  // In analyzing the `==` check, flow analysis assumes that `c._f` has its
+  // unpromoted type (`Object?`), so both branches of the `if` are
+  // reachable. Therefore both `x` and `y` should both be demoted here.
+  x.expectStaticType<Exactly<int?>>();
+  y.expectStaticType<Exactly<int?>>();
+}
+
+void testExplicitPropertyReferenceOnLhsOfNotEquals(C c) {
+  int? x = 0;
+  int? y = 0;
+  x.expectStaticType<Exactly<int>>();
+  y.expectStaticType<Exactly<int>>();
+  if (c._f is! Null) return;
+  c._f.expectStaticType<Exactly<Null>>();
+  if (c._f != null) {
+    x = null;
+  } else {
+    y = null;
+  }
+  // In analyzing the `!=` check, flow analysis assumes that `c._f` has its
+  // unpromoted type (`Object?`), so both branches of the `if` are
+  // reachable. Therefore both `x` and `y` should both be demoted here.
+  x.expectStaticType<Exactly<int?>>();
+  y.expectStaticType<Exactly<int?>>();
+}
+
+void testExplicitPropertyReferenceOnRhsOfNotEquals(C c) {
+  int? x = 0;
+  int? y = 0;
+  x.expectStaticType<Exactly<int>>();
+  y.expectStaticType<Exactly<int>>();
+  if (c._f is! Null) return;
+  c._f.expectStaticType<Exactly<Null>>();
+  if (null != c._f) {
+    x = null;
+  } else {
+    y = null;
+  }
+  // In analyzing the `!=` check, flow analysis assumes that `c._f` has its
+  // unpromoted type (`Object?`), so both branches of the `if` are
+  // reachable. Therefore both `x` and `y` should both be demoted here.
+  x.expectStaticType<Exactly<int?>>();
+  y.expectStaticType<Exactly<int?>>();
+}
+
+main() {
+  for (var value in [null, '']) {
+    var c = C(value);
+    c.testImplicitThisReferenceOnLhsOfEquals();
+    c.testImplicitThisReferenceOnRhsOfEquals();
+    c.testImplicitThisReferenceOnLhsOfNotEquals();
+    c.testImplicitThisReferenceOnRhsOfNotEquals();
+    c.testExplicitThisReferenceOnLhsOfEquals();
+    c.testExplicitThisReferenceOnRhsOfEquals();
+    c.testExplicitThisReferenceOnLhsOfNotEquals();
+    c.testExplicitThisReferenceOnRhsOfNotEquals();
+    testExplicitPropertyReferenceOnLhsOfEquals(c);
+    testExplicitPropertyReferenceOnRhsOfEquals(c);
+    testExplicitPropertyReferenceOnLhsOfNotEquals(c);
+    testExplicitPropertyReferenceOnRhsOfNotEquals(c);
+  }
+}