Track nonNullIntent in unconditional control flow.

In a future CL this will be used to prevent nullability from being
propagated into function parameters where it's clearly not intended.

Change-Id: I34430162db33f99e850ca2c11e5bca8736feb94f
Reviewed-on: https://dart-review.googlesource.com/c/93041
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Dan Rubel <danrubel@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
diff --git a/pkg/analysis_server/lib/src/nullability/constraint_gatherer.dart b/pkg/analysis_server/lib/src/nullability/constraint_gatherer.dart
index 12f4ca5..ec6f294 100644
--- a/pkg/analysis_server/lib/src/nullability/constraint_gatherer.dart
+++ b/pkg/analysis_server/lib/src/nullability/constraint_gatherer.dart
@@ -71,6 +71,12 @@
   /// code that can be proven unreachable by the migration tool.
   final _guards = <ConstraintVariable>[];
 
+  /// Indicates whether the statement or expression being visited is within
+  /// conditional control flow.  If `true`, this means that the enclosing
+  /// function might complete normally without executing the current statement
+  /// or expression.
+  bool _inConditionalControlFlow = false;
+
   ConstraintGatherer(TypeProvider typeProvider, this._variables,
       this._constraints, this._source, this._permissive, this.assumptions)
       : _notNullType = DecoratedType(typeProvider.objectType, null),
@@ -124,9 +130,9 @@
   DecoratedType visitAssertStatement(AssertStatement node) {
     _handleAssignment(_notNullType, node.condition);
     if (identical(_conditionInfo?.condition, node.condition)) {
-      // TODO(paulberry): should only do this if in unconditional control flow.
-      if (_conditionInfo.trueChecksNonNull != null) {
-        _recordFact(_conditionInfo.trueChecksNonNull);
+      if (!_inConditionalControlFlow &&
+          _conditionInfo.trueDemonstratesNonNullIntent != null) {
+        _recordFact(_conditionInfo.trueDemonstratesNonNullIntent);
       }
     }
     node.message?.accept(this);
@@ -137,6 +143,7 @@
   DecoratedType visitBinaryExpression(BinaryExpression node) {
     switch (node.operator.type) {
       case TokenType.EQ_EQ:
+      case TokenType.BANG_EQ:
         assert(node.leftOperand is! NullLiteral); // TODO(paulberry)
         var leftType = node.leftOperand.accept(this);
         node.rightOperand.accept(this);
@@ -145,10 +152,13 @@
           // TODO(paulberry): only set falseChecksNonNull in unconditional
           // control flow
           bool isPure = node.leftOperand is SimpleIdentifier;
-          _conditionInfo = _ConditionInfo(node,
+          var conditionInfo = _ConditionInfo(node,
               isPure: isPure,
               trueGuard: leftType.nullable,
-              falseChecksNonNull: leftType.nullAsserts);
+              falseDemonstratesNonNullIntent: leftType.nonNullIntent);
+          _conditionInfo = node.operator.type == TokenType.EQ_EQ
+              ? conditionInfo
+              : conditionInfo.not(node);
         }
         return _nonNullableBoolType;
       case TokenType.PLUS:
@@ -234,6 +244,7 @@
     // TODO(paulberry): should the use of a boolean in an if-statement be
     // treated like an implicit `assert(b != null)`?  Probably.
     _handleAssignment(_notNullType, node.condition);
+    _inConditionalControlFlow = true;
     ConstraintVariable trueGuard;
     ConstraintVariable falseGuard;
     if (identical(_conditionInfo?.condition, node.condition)) {
@@ -514,16 +525,24 @@
 
   /// If not `null`, the [ConstraintVariable] whose value should be set to
   /// `true` if [condition] is asserted to be `true`.
-  final ConstraintVariable trueChecksNonNull;
+  final ConstraintVariable trueDemonstratesNonNullIntent;
 
   /// If not `null`, the [ConstraintVariable] whose value should be set to
   /// `true` if [condition] is asserted to be `false`.
-  final ConstraintVariable falseChecksNonNull;
+  final ConstraintVariable falseDemonstratesNonNullIntent;
 
   _ConditionInfo(this.condition,
       {@required this.isPure,
       this.trueGuard,
       this.falseGuard,
-      this.trueChecksNonNull,
-      this.falseChecksNonNull});
+      this.trueDemonstratesNonNullIntent,
+      this.falseDemonstratesNonNullIntent});
+
+  /// Returns a new [_ConditionInfo] describing the boolean "not" of `this`.
+  _ConditionInfo not(Expression condition) => _ConditionInfo(condition,
+      isPure: isPure,
+      trueGuard: falseGuard,
+      falseGuard: trueGuard,
+      trueDemonstratesNonNullIntent: falseDemonstratesNonNullIntent,
+      falseDemonstratesNonNullIntent: trueDemonstratesNonNullIntent);
 }
diff --git a/pkg/analysis_server/lib/src/nullability/constraint_variable_gatherer.dart b/pkg/analysis_server/lib/src/nullability/constraint_variable_gatherer.dart
index 1715526..d3a89538 100644
--- a/pkg/analysis_server/lib/src/nullability/constraint_variable_gatherer.dart
+++ b/pkg/analysis_server/lib/src/nullability/constraint_variable_gatherer.dart
@@ -117,6 +117,8 @@
   DecoratedType visitSimpleFormalParameter(SimpleFormalParameter node) {
     var type = decorateType(node.type);
     var declaredElement = node.declaredElement;
+    assert(type.nonNullIntent == null);
+    type.nonNullIntent = NonNullIntent(node.offset);
     _variables.recordDecoratedElementType(declaredElement, type);
     if (declaredElement.isNamed) {
       _currentFunctionType.namedParameters[declaredElement.name] = type;
@@ -147,11 +149,9 @@
     var nullable = node.question == null
         ? TypeIsNullable(node.end)
         : ConstraintVariable.always;
-    // TODO(paulberry): decide whether to assign a variable for nullAsserts
-    var nullAsserts = null;
     var decoratedType = DecoratedTypeAnnotation(
         type, nullable, _source, node.end,
-        nullAsserts: nullAsserts, typeArguments: typeArguments);
+        typeArguments: typeArguments);
     _variables.recordDecoratedTypeAnnotation(node, decoratedType);
     return decoratedType;
   }
diff --git a/pkg/analysis_server/lib/src/nullability/decorated_type.dart b/pkg/analysis_server/lib/src/nullability/decorated_type.dart
index fff240f..53c023b 100644
--- a/pkg/analysis_server/lib/src/nullability/decorated_type.dart
+++ b/pkg/analysis_server/lib/src/nullability/decorated_type.dart
@@ -23,10 +23,11 @@
   /// migrated) forces this type to be non-nullable.
   final ConstraintVariable nullable;
 
-  /// [ConstraintVariable] whose value will be set to `true` if migration
-  /// determines that a `null` value stored in an expression having this type
-  /// will unconditionally lead to an assertion failure.
-  final ConstraintVariable nullAsserts;
+  /// [ConstraintVariable] whose value will be set to `true` if the usage of
+  /// this type suggests that it is intended to be non-null (because of the
+  /// presence of a statement or expression that would unconditionally lead to
+  /// an exception being thrown in the case of a `null` value at runtime).
+  ConstraintVariable nonNullIntent;
 
   /// If `this` is a function type, the [DecoratedType] of its return type.
   final DecoratedType returnType;
@@ -57,8 +58,7 @@
   final List<DecoratedType> typeArguments;
 
   DecoratedType(this.type, this.nullable,
-      {this.nullAsserts,
-      this.returnType,
+      {this.returnType,
       this.positionalParameters = const [],
       this.namedParameters = const {},
       this.namedParameterOptionalVariables = const {},
@@ -156,20 +156,14 @@
         newPositionalParameters.add(positionalParameters[i]
             ._substitute(constraints, substitution, undecoratedParameterType));
       }
-      // TODO(paulberry): what do we do for nullAsserts here?
-      var nullAsserts = null;
       return DecoratedType(undecoratedResult, nullable,
-          nullAsserts: nullAsserts,
           returnType: returnType._substitute(
               constraints, substitution, undecoratedResult.returnType),
           positionalParameters: newPositionalParameters);
     } else if (type is TypeParameterType) {
       var inner = substitution[type.element];
-      // TODO(paulberry): what do we do for nullAsserts here?
-      var nullAsserts = null;
       return DecoratedType(undecoratedResult,
-          ConstraintVariable.or(constraints, inner?.nullable, nullable),
-          nullAsserts: nullAsserts);
+          ConstraintVariable.or(constraints, inner?.nullable, nullable));
     } else if (type is VoidType) {
       return this;
     }
@@ -191,10 +185,8 @@
 
   DecoratedTypeAnnotation(
       DartType type, ConstraintVariable nullable, this.source, this._offset,
-      {ConstraintVariable nullAsserts,
-      List<DecoratedType> typeArguments = const []})
-      : super(type, nullable,
-            nullAsserts: nullAsserts, typeArguments: typeArguments);
+      {List<DecoratedType> typeArguments = const []})
+      : super(type, nullable, typeArguments: typeArguments);
 
   @override
   bool get isEmpty =>
@@ -205,6 +197,17 @@
       isEmpty ? [] : [SourceEdit(_offset, 0, '?')];
 }
 
+/// Type of a [ConstraintVariable] representing the fact that a type is intended
+/// to be non-null.
+class NonNullIntent extends ConstraintVariable {
+  final int _offset;
+
+  NonNullIntent(this._offset);
+
+  @override
+  toString() => 'nonNullIntent($_offset)';
+}
+
 /// Type of a [ConstraintVariable] representing the fact that a type is
 /// nullable.
 class TypeIsNullable extends ConstraintVariable {
diff --git a/pkg/analysis_server/test/src/nullability/migration_visitor_test.dart b/pkg/analysis_server/test/src/nullability/migration_visitor_test.dart
index e64f358..88c6a34 100644
--- a/pkg/analysis_server/test/src/nullability/migration_visitor_test.dart
+++ b/pkg/analysis_server/test/src/nullability/migration_visitor_test.dart
@@ -71,6 +71,16 @@
     assert(ConstraintVariable.always.value, isTrue);
   }
 
+  test_assert_demonstrates_non_null_intent() async {
+    await analyze('''
+void f(int i) {
+  assert(i != null);
+}
+''');
+
+    assertConstraint([], decoratedTypeAnnotation('int i').nonNullIntent);
+  }
+
   test_binaryExpression_add_left_check() async {
     await analyze('''
 int f(int i, int j) => i + j;
@@ -416,6 +426,34 @@
         checkExpression('b) {}').nullCheck);
   }
 
+  test_if_conditional_control_flow_after() async {
+    // Asserts after ifs don't demonstrate non-null intent.
+    // TODO(paulberry): if both branches complete normally, they should.
+    await analyze('''
+void f(bool b, int i) {
+  if (b) return;
+  assert(i != null);
+}
+''');
+
+    assertNoConstraints(decoratedTypeAnnotation('int i').nonNullIntent);
+  }
+
+  test_if_conditional_control_flow_within() async {
+    // Asserts inside ifs don't demonstrate non-null intent.
+    await analyze('''
+void f(bool b, int i) {
+  if (b) {
+    assert(i != null);
+  } else {
+    assert(i != null);
+  }
+}
+''');
+
+    assertNoConstraints(decoratedTypeAnnotation('int i').nonNullIntent);
+  }
+
   test_if_guard_equals_null() async {
     await analyze('''
 int f(int i, int j, int k) {
@@ -737,6 +775,7 @@
     expect(decoratedFunctionType('f').positionalParameters[0],
         same(decoratedType));
     expect(decoratedType.nullable, isNotNull);
+    expect(decoratedType.nonNullIntent, isNotNull);
   }
 
   test_topLevelFunction_returnType_implicit_dynamic() async {