Migration: add support for cascade expressions.

Change-Id: Iae52bef6a18c9536db3187102566423bc2f1dba8
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/105325
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
diff --git a/pkg/analysis_server/lib/src/nullability/graph_builder.dart b/pkg/analysis_server/lib/src/nullability/graph_builder.dart
index f467a40..232ec55 100644
--- a/pkg/analysis_server/lib/src/nullability/graph_builder.dart
+++ b/pkg/analysis_server/lib/src/nullability/graph_builder.dart
@@ -213,6 +213,13 @@
   }
 
   @override
+  DecoratedType visitCascadeExpression(CascadeExpression node) {
+    var type = node.target.accept(this);
+    node.cascadeSections.accept(this);
+    return type;
+  }
+
+  @override
   DecoratedType visitClassDeclaration(ClassDeclaration node) {
     node.members.accept(this);
     return null;
@@ -313,8 +320,9 @@
   @override
   DecoratedType visitIndexExpression(IndexExpression node) {
     DecoratedType targetType;
-    if (node.target != null) {
-      targetType = _handleAssignment(_notNullType, node.target);
+    var target = node.realTarget;
+    if (target != null) {
+      targetType = _handleAssignment(_notNullType, target);
     }
     var callee = node.staticElement;
     if (callee == null) {
@@ -353,12 +361,17 @@
   @override
   DecoratedType visitMethodInvocation(MethodInvocation node) {
     DecoratedType targetType;
-    if (node.target != null) {
-      if (node.operator.type != TokenType.PERIOD) {
-        throw new UnimplementedError('TODO(paulberry)');
+    var target = node.realTarget;
+    if (target != null) {
+      switch (node.operator.type) {
+        case TokenType.PERIOD:
+        case TokenType.PERIOD_PERIOD:
+          _checkNonObjectMember(node.methodName.name); // TODO(paulberry)
+          targetType = _handleAssignment(_notNullType, target);
+          break;
+        default:
+          throw new UnimplementedError('TODO(paulberry)');
       }
-      _checkNonObjectMember(node.methodName.name); // TODO(paulberry)
-      targetType = _handleAssignment(_notNullType, node.target);
     }
     var callee = node.methodName.staticElement;
     if (callee == null) {
@@ -423,7 +436,8 @@
 
   @override
   DecoratedType visitPropertyAccess(PropertyAccess node) {
-    return _handlePropertyAccess(node.target, node.operator, node.propertyName);
+    return _handlePropertyAccess(
+        node.realTarget, node.operator, node.propertyName);
   }
 
   @override
@@ -568,21 +582,24 @@
 
   DecoratedType _handlePropertyAccess(
       Expression target, Token operator, SimpleIdentifier propertyName) {
-    if (operator.type != TokenType.PERIOD) {
-      throw new UnimplementedError('TODO(paulberry)');
-    }
-    _checkNonObjectMember(propertyName.name); // TODO(paulberry)
-    var targetType = _handleAssignment(_notNullType, target);
-    var callee = propertyName.staticElement;
-    if (callee == null) {
-      throw new UnimplementedError('TODO(paulberry)');
-    }
-    var calleeType = getOrComputeElementType(callee, targetType: targetType);
-    // TODO(paulberry): substitute if necessary
-    if (propertyName.inSetterContext()) {
-      return calleeType.positionalParameters[0];
-    } else {
-      return calleeType.returnType;
+    switch (operator.type) {
+      case TokenType.PERIOD:
+      case TokenType.PERIOD_PERIOD:
+        _checkNonObjectMember(propertyName.name); // TODO(paulberry)
+        var targetType = _handleAssignment(_notNullType, target);
+        var callee = propertyName.staticElement;
+        if (callee == null) {
+          throw new UnimplementedError('TODO(paulberry)');
+        }
+        var calleeType =
+            getOrComputeElementType(callee, targetType: targetType);
+        // TODO(paulberry): substitute if necessary
+        if (propertyName.inSetterContext()) {
+          return calleeType.positionalParameters[0];
+        }
+        return calleeType.returnType;
+      default:
+        throw new UnimplementedError('TODO(paulberry)');
     }
   }
 
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 daa4bed..b17cff6 100644
--- a/pkg/analysis_server/test/src/nullability/migration_visitor_test.dart
+++ b/pkg/analysis_server/test/src/nullability/migration_visitor_test.dart
@@ -122,6 +122,20 @@
         hard: true);
   }
 
+  test_assignmentExpression_field_cascaded() async {
+    await analyze('''
+class C {
+  int x = 0;
+}
+void f(C c, int i) {
+  c..x = i;
+}
+''');
+    assertEdge(decoratedTypeAnnotation('int i').node,
+        decoratedTypeAnnotation('int x').node,
+        hard: true);
+  }
+
   test_assignmentExpression_field_target_check() async {
     await analyze('''
 class C {
@@ -135,6 +149,19 @@
         checkExpression('c.x'), decoratedTypeAnnotation('C c').node);
   }
 
+  test_assignmentExpression_field_target_check_cascaded() async {
+    await analyze('''
+class C {
+  int x = 0;
+}
+void f(C c, int i) {
+  c..x = i;
+}
+''');
+    assertNullCheck(
+        checkExpression('c..x'), decoratedTypeAnnotation('C c').node);
+  }
+
   test_assignmentExpression_indexExpression_index() async {
     await analyze('''
 class C {
@@ -318,6 +345,18 @@
     assertNoUpstreamNullability(decoratedTypeAnnotation('bool').node);
   }
 
+  test_cascadeExpression() async {
+    await analyze('''
+class C {
+  int x = 0;
+}
+C f(C c, int i) => c..x = i;
+''');
+    assertEdge(decoratedTypeAnnotation('C c').node,
+        decoratedTypeAnnotation('C f').node,
+        hard: false);
+  }
+
   test_conditionalExpression_condition_check() async {
     await analyze('''
 int f(bool b, int i, int j) {
@@ -680,6 +719,18 @@
         hard: true);
   }
 
+  test_indexExpression_index_cascaded() async {
+    await analyze('''
+class C {
+  int operator[](int i) => 1;
+}
+C f(C c, int j) => c..[j];
+''');
+    assertEdge(decoratedTypeAnnotation('int j').node,
+        decoratedTypeAnnotation('int i').node,
+        hard: true);
+  }
+
   test_indexExpression_return_type() async {
     await analyze('''
 class C {
@@ -702,6 +753,37 @@
     assertNullCheck(checkExpression('c['), decoratedTypeAnnotation('C c').node);
   }
 
+  test_indexExpression_target_check_cascaded() async {
+    await analyze('''
+class C {
+  int operator[](int i) => 1;
+}
+C f(C c) => c..[0];
+''');
+    assertNullCheck(
+        checkExpression('c..['), decoratedTypeAnnotation('C c').node);
+  }
+
+  test_indexExpression_target_demonstrates_non_null_intent() async {
+    await analyze('''
+class C {
+  int operator[](int i) => 1;
+}
+int f(C c) => c[0];
+''');
+    assertEdge(decoratedTypeAnnotation('C c').node, never, hard: true);
+  }
+
+  test_indexExpression_target_demonstrates_non_null_intent_cascaded() async {
+    await analyze('''
+class C {
+  int operator[](int i) => 1;
+}
+C f(C c) => c..[0];
+''');
+    assertEdge(decoratedTypeAnnotation('C c').node, never, hard: true);
+  }
+
   test_intLiteral() async {
     await analyze('''
 int f() {
@@ -809,6 +891,20 @@
         checkExpression('c.m'), decoratedTypeAnnotation('C c').node);
   }
 
+  test_methodInvocation_target_check_cascaded() async {
+    await analyze('''
+class C {
+  void m() {}
+}
+void test(C c) {
+  c..m();
+}
+''');
+
+    assertNullCheck(
+        checkExpression('c..m'), decoratedTypeAnnotation('C c').node);
+  }
+
   test_methodInvocation_target_demonstrates_non_null_intent() async {
     await analyze('''
 class C {
@@ -822,6 +918,19 @@
     assertEdge(decoratedTypeAnnotation('C c').node, never, hard: true);
   }
 
+  test_methodInvocation_target_demonstrates_non_null_intent_cascaded() async {
+    await analyze('''
+class C {
+  void m() {}
+}
+void test(C c) {
+  c..m();
+}
+''');
+
+    assertEdge(decoratedTypeAnnotation('C c').node, never, hard: true);
+  }
+
   test_never() async {
     await analyze('');
 
diff --git a/pkg/analysis_server/test/src/nullability/provisional_api_test.dart b/pkg/analysis_server/test/src/nullability/provisional_api_test.dart
index 99c27ce37..ea1081f 100644
--- a/pkg/analysis_server/test/src/nullability/provisional_api_test.dart
+++ b/pkg/analysis_server/test/src/nullability/provisional_api_test.dart
@@ -244,6 +244,26 @@
     await _checkSingleFileChanges(content, expected);
   }
 
+  test_data_flow_assignment_field_in_cascade() async {
+    var content = '''
+class C {
+  int x = 0;
+}
+void f(C c) {
+  c..x = null;
+}
+''';
+    var expected = '''
+class C {
+  int? x = 0;
+}
+void f(C c) {
+  c..x = null;
+}
+''';
+    await _checkSingleFileChanges(content, expected);
+  }
+
   test_data_flow_assignment_local() async {
     var content = '''
 void main() {
@@ -386,6 +406,26 @@
     await _checkSingleFileChanges(content, expected);
   }
 
+  test_data_flow_indexed_set_index_value_in_cascade() async {
+    var content = '''
+class C {
+  void operator[]=(int i, int j) {}
+}
+void f(C c) {
+  c..[null] = 0;
+}
+''';
+    var expected = '''
+class C {
+  void operator[]=(int? i, int j) {}
+}
+void f(C c) {
+  c..[null] = 0;
+}
+''';
+    await _checkSingleFileChanges(content, expected);
+  }
+
   test_data_flow_indexed_set_value() async {
     var content = '''
 class C {
@@ -488,6 +528,26 @@
     await _checkSingleFileChanges(content, expected);
   }
 
+  test_data_flow_method_call_in_cascade() async {
+    var content = '''
+class C {
+  void m(int x) {}
+}
+void f(C c) {
+  c..m(null);
+}
+''';
+    var expected = '''
+class C {
+  void m(int? x) {}
+}
+void f(C c) {
+  c..m(null);
+}
+''';
+    await _checkSingleFileChanges(content, expected);
+  }
+
   test_data_flow_outward() async {
     var content = '''
 int f(int i) => null;
@@ -880,6 +940,96 @@
     await _checkSingleFileChanges(content, expected);
   }
 
+  test_unconditional_cascaded_indexed_set_implies_non_null_intent() async {
+    var content = '''
+class C {
+  operator[]=(int i, int j) {}
+}
+void f(C c) {
+  c..[1] = 2;
+}
+void g(bool b, C c) {
+  if (b) f(c);
+}
+main() {
+  g(false, null);
+}
+''';
+    var expected = '''
+class C {
+  operator[]=(int i, int j) {}
+}
+void f(C c) {
+  c..[1] = 2;
+}
+void g(bool b, C? c) {
+  if (b) f(c!);
+}
+main() {
+  g(false, null);
+}
+''';
+    await _checkSingleFileChanges(content, expected);
+  }
+
+  test_unconditional_cascaded_method_call_implies_non_null_intent() async {
+    var content = '''
+void f(int i) {
+  i..abs();
+}
+void g(bool b, int i) {
+  if (b) f(i);
+}
+main() {
+  g(false, null);
+}
+''';
+    var expected = '''
+void f(int i) {
+  i..abs();
+}
+void g(bool b, int? i) {
+  if (b) f(i!);
+}
+main() {
+  g(false, null);
+}
+''';
+    await _checkSingleFileChanges(content, expected);
+  }
+
+  test_unconditional_cascaded_property_set_implies_non_null_intent() async {
+    var content = '''
+class C {
+  int x = 0;
+}
+void f(C c) {
+  c..x = 1;
+}
+void g(bool b, C c) {
+  if (b) f(c);
+}
+main() {
+  g(false, null);
+}
+''';
+    var expected = '''
+class C {
+  int x = 0;
+}
+void f(C c) {
+  c..x = 1;
+}
+void g(bool b, C? c) {
+  if (b) f(c!);
+}
+main() {
+  g(false, null);
+}
+''';
+    await _checkSingleFileChanges(content, expected);
+  }
+
   test_unconditional_method_call_implies_non_null_intent() async {
     var content = '''
 void f(int i) {