Flow analysis: implement "why not promoted" for binary/unary operator target.

Bug: https://github.com/dart-lang/sdk/issues/44898
Change-Id: If464a54bdb63fc661db312df5dc10f108049286a
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/196240
Reviewed-by: Johnni Winther <johnniwinther@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/nullable_operator_call_error.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/nullable_operator_call_error.dart
new file mode 100644
index 0000000..c619de5
--- /dev/null
+++ b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/nullable_operator_call_error.dart
@@ -0,0 +1,32 @@
+// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// This test contains a test case for each condition that can lead to the front
+// end's `NullableOperatorCallError` error, for which we wish to report "why not
+// promoted" context information.
+
+class C1 {
+  int? bad;
+}
+
+userDefinableBinaryOpLhs(C1 c) {
+  if (c.bad == null) return;
+  c.bad
+      /*cfe.invoke: notPromoted(propertyNotPromoted(target: member:C1.bad, type: int?))*/
+      /*analyzer.notPromoted(propertyNotPromoted(target: member:C1.bad, type: int?))*/
+      +
+      1;
+}
+
+class C2 {
+  int? bad;
+}
+
+userDefinableUnaryOp(C2 c) {
+  if (c.bad == null) return;
+  /*cfe.invoke: notPromoted(propertyNotPromoted(target: member:C2.bad, type: int?))*/
+  -c.
+      /*analyzer.notPromoted(propertyNotPromoted(target: member:C2.bad, type: int?))*/
+      bad;
+}
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 20a67a7..69a9aa7 100644
--- a/pkg/front_end/lib/src/fasta/kernel/inference_visitor.dart
+++ b/pkg/front_end/lib/src/fasta/kernel/inference_visitor.dart
@@ -666,7 +666,8 @@
         read,
         readType,
         node.binaryName,
-        node.rhs);
+        node.rhs,
+        null);
 
     Expression binary = binaryResult.expression;
     DartType binaryType = binaryResult.inferredType;
@@ -2906,7 +2907,8 @@
         read,
         readType,
         node.binaryName,
-        node.rhs);
+        node.rhs,
+        null);
     DartType binaryType = binaryResult.inferredType;
 
     Expression binary =
@@ -3999,7 +4001,8 @@
       Expression left,
       DartType leftType,
       Name binaryName,
-      Expression right) {
+      Expression right,
+      Map<DartType, NonPromotionReason> Function() whyNotPromoted) {
     assert(binaryName != equalsName);
 
     ObjectAccessTarget binaryTarget = inferrer.findInterfaceMember(
@@ -4203,6 +4206,10 @@
     }
 
     if (!inferrer.isTopLevel && binaryTarget.isNullable) {
+      List<LocatedMessage> context = inferrer.getWhyNotPromotedContext(
+          whyNotPromoted?.call(),
+          binary,
+          (type) => !type.isPotentiallyNullable);
       return new ExpressionInferenceResult(
           binaryType,
           inferrer.helper.wrapInProblem(
@@ -4210,7 +4217,8 @@
               templateNullableOperatorCallError.withArguments(
                   binaryName.text, leftType, inferrer.isNonNullableByDefault),
               binary.fileOffset,
-              binaryName.text.length));
+              binaryName.text.length,
+              context: context));
     }
     return new ExpressionInferenceResult(binaryType, binary);
   }
@@ -4220,8 +4228,12 @@
   ///
   /// [fileOffset] is used as the file offset for created nodes.
   /// [expressionType] is the already inferred type of the [expression].
-  ExpressionInferenceResult _computeUnaryExpression(int fileOffset,
-      Expression expression, DartType expressionType, Name unaryName) {
+  ExpressionInferenceResult _computeUnaryExpression(
+      int fileOffset,
+      Expression expression,
+      DartType expressionType,
+      Name unaryName,
+      Map<DartType, NonPromotionReason> Function() whyNotPromoted) {
     ObjectAccessTarget unaryTarget = inferrer.findInterfaceMember(
         expressionType, unaryName, fileOffset,
         includeExtensionMethods: true);
@@ -4352,6 +4364,8 @@
     }
 
     if (!inferrer.isTopLevel && unaryTarget.isNullable) {
+      List<LocatedMessage> context = inferrer.getWhyNotPromotedContext(
+          whyNotPromoted?.call(), unary, (type) => !type.isPotentiallyNullable);
       // TODO(johnniwinther): Special case 'unary-' in messages. It should
       // probably be referred to as "Unary operator '-' ...".
       return new ExpressionInferenceResult(
@@ -4361,7 +4375,8 @@
               templateNullableOperatorCallError.withArguments(unaryName.text,
                   expressionType, inferrer.isNonNullableByDefault),
               unary.fileOffset,
-              unaryName == unaryMinusName ? 1 : unaryName.text.length));
+              unaryName == unaryMinusName ? 1 : unaryName.text.length,
+              context: context));
     }
     return new ExpressionInferenceResult(unaryType, unary);
   }
@@ -5121,7 +5136,8 @@
         left,
         readType,
         node.binaryName,
-        node.rhs);
+        node.rhs,
+        null);
     Expression binary = binaryResult.expression;
     DartType binaryType = binaryResult.inferredType;
 
@@ -5263,7 +5279,8 @@
         left,
         readType,
         node.binaryName,
-        node.rhs);
+        node.rhs,
+        null);
     Expression binary = binaryResult.expression;
     DartType binaryType = binaryResult.inferredType;
 
@@ -5417,7 +5434,8 @@
         left,
         readType,
         node.binaryName,
-        node.rhs);
+        node.rhs,
+        null);
     Expression binary = binaryResult.expression;
     DartType binaryType = binaryResult.inferredType;
 
@@ -5596,7 +5614,8 @@
         left,
         readType,
         node.binaryName,
-        node.rhs);
+        node.rhs,
+        null);
 
     Expression binary = binaryResult.expression;
     DartType binaryType = binaryResult.inferredType;
@@ -6922,13 +6941,16 @@
       BinaryExpression node, DartType typeContext) {
     ExpressionInferenceResult leftResult =
         inferrer.inferExpression(node.left, const UnknownType(), true);
+    Map<DartType, NonPromotionReason> Function() whyNotPromoted =
+        inferrer.flowAnalysis?.whyNotPromoted(leftResult.expression);
     return _computeBinaryExpression(
         node.fileOffset,
         typeContext,
         leftResult.expression,
         leftResult.inferredType,
         node.binaryName,
-        node.right);
+        node.right,
+        whyNotPromoted);
   }
 
   ExpressionInferenceResult visitUnary(
@@ -7001,8 +7023,10 @@
       expressionResult =
           inferrer.inferExpression(node.expression, const UnknownType(), true);
     }
+    Map<DartType, NonPromotionReason> Function() whyNotPromoted =
+        inferrer.flowAnalysis?.whyNotPromoted(expressionResult.expression);
     return _computeUnaryExpression(node.fileOffset, expressionResult.expression,
-        expressionResult.inferredType, node.unaryName);
+        expressionResult.inferredType, node.unaryName, whyNotPromoted);
   }
 
   ExpressionInferenceResult visitParenthesized(
diff --git a/tests/language/why_not_promoted/nullable_operator_call_error.dart b/tests/language/why_not_promoted/nullable_operator_call_error.dart
new file mode 100644
index 0000000..a145f34
--- /dev/null
+++ b/tests/language/why_not_promoted/nullable_operator_call_error.dart
@@ -0,0 +1,37 @@
+// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// This test contains a test case for each condition that can lead to the front
+// end's `NullableOperatorCallError` error, for which we wish to report "why not
+// promoted" context information.
+
+class C1 {
+  int? bad;
+  //   ^^^
+  // [context 2] 'bad' refers to a property so it couldn't be promoted.  See http://dart.dev/go/non-promo-property
+  // [context 3] 'bad' refers to a property so it couldn't be promoted.
+}
+
+userDefinableBinaryOpLhs(C1 c) {
+  if (c.bad == null) return;
+  c.bad + 1;
+  //    ^
+  // [analyzer 2] COMPILE_TIME_ERROR.UNCHECKED_USE_OF_NULLABLE_VALUE
+  // [cfe 3] Operator '+' cannot be called on 'int?' because it is potentially null.
+}
+
+class C2 {
+  int? bad;
+  //   ^^^
+  // [context 1] 'bad' refers to a property so it couldn't be promoted.  See http://dart.dev/go/non-promo-property
+  // [context 4] 'bad' refers to a property so it couldn't be promoted.
+}
+
+userDefinableUnaryOp(C2 c) {
+  if (c.bad == null) return;
+  -c.bad;
+//^
+// [analyzer 1] COMPILE_TIME_ERROR.UNCHECKED_USE_OF_NULLABLE_VALUE
+// [cfe 4] Operator 'unary-' cannot be called on 'int?' because it is potentially null.
+}