Migration: add support for null-aware accesses.
Change-Id: Ie034c0410e667f2e55fd2b9d9e9b2b3b3726de30
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/105465
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
diff --git a/pkg/nnbd_migration/lib/src/decorated_type.dart b/pkg/nnbd_migration/lib/src/decorated_type.dart
index 580164e..7dd5b69 100644
--- a/pkg/nnbd_migration/lib/src/decorated_type.dart
+++ b/pkg/nnbd_migration/lib/src/decorated_type.dart
@@ -117,6 +117,13 @@
}
}
+ /// Creates a shallow copy of `this`, replacing the nullability node.
+ DecoratedType withNode(NullabilityNode node) => DecoratedType(type, node,
+ returnType: returnType,
+ positionalParameters: positionalParameters,
+ namedParameters: namedParameters,
+ typeArguments: typeArguments);
+
/// Internal implementation of [_substitute], used as a recursion target.
DecoratedType _substitute(
Map<TypeParameterElement, DecoratedType> substitution,
diff --git a/pkg/nnbd_migration/lib/src/graph_builder.dart b/pkg/nnbd_migration/lib/src/graph_builder.dart
index 85ef317..0bd0aa8 100644
--- a/pkg/nnbd_migration/lib/src/graph_builder.dart
+++ b/pkg/nnbd_migration/lib/src/graph_builder.dart
@@ -74,6 +74,8 @@
/// or expression.
bool _inConditionalControlFlow = false;
+ NullabilityNode _lastConditionalNode;
+
GraphBuilder(TypeProvider typeProvider, this._variables, this._graph,
this._source, this.listener)
: _notNullType =
@@ -164,7 +166,15 @@
throw UnimplementedError('TODO(paulberry)');
}
var leftType = node.leftHandSide.accept(this);
- return _handleAssignment(leftType, node.rightHandSide);
+ var conditionalNode = _lastConditionalNode;
+ _lastConditionalNode = null;
+ var expressionType = _handleAssignment(leftType, node.rightHandSide);
+ if (_isConditionalExpression(node.leftHandSide)) {
+ expressionType = expressionType.withNode(
+ NullabilityNode.forLUB(conditionalNode, expressionType.node));
+ _variables.recordDecoratedExpressionType(node, expressionType);
+ }
+ return expressionType;
}
@override
@@ -363,15 +373,13 @@
DecoratedType visitMethodInvocation(MethodInvocation node) {
DecoratedType targetType;
var target = node.realTarget;
+ bool isConditional = _isConditionalExpression(node);
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)');
+ if (isConditional) {
+ targetType = target.accept(this);
+ } else {
+ _checkNonObjectMember(node.methodName.name); // TODO(paulberry)
+ targetType = _handleAssignment(_notNullType, target);
}
}
var callee = node.methodName.staticElement;
@@ -400,7 +408,13 @@
if (suppliedNamedParameters.contains(entry.key)) continue;
entry.value.node.recordNamedParameterNotSupplied(_guards, _graph);
}
- return calleeType.returnType;
+ var expressionType = calleeType.returnType;
+ if (isConditional) {
+ expressionType = expressionType.withNode(
+ NullabilityNode.forLUB(targetType.node, expressionType.node));
+ _variables.recordDecoratedExpressionType(node, expressionType);
+ }
+ return expressionType;
}
@override
@@ -435,14 +449,13 @@
if (node.prefix.staticElement is ImportElement) {
throw new UnimplementedError('TODO(paulberry)');
} else {
- return _handlePropertyAccess(node.prefix, node.period, node.identifier);
+ return _handlePropertyAccess(node, node.prefix, node.identifier);
}
}
@override
DecoratedType visitPropertyAccess(PropertyAccess node) {
- return _handlePropertyAccess(
- node.realTarget, node.operator, node.propertyName);
+ return _handlePropertyAccess(node, node.realTarget, node.propertyName);
}
@override
@@ -586,23 +599,53 @@
}
DecoratedType _handlePropertyAccess(
- Expression target, Token operator, SimpleIdentifier propertyName) {
- switch (operator.type) {
+ Expression node, Expression target, SimpleIdentifier propertyName) {
+ DecoratedType targetType;
+ bool isConditional = _isConditionalExpression(node);
+ if (isConditional) {
+ targetType = target.accept(this);
+ } else {
+ _checkNonObjectMember(propertyName.name); // TODO(paulberry)
+ 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()) {
+ if (isConditional) {
+ _lastConditionalNode = targetType.node;
+ }
+ return calleeType.positionalParameters[0];
+ } else {
+ var expressionType = calleeType.returnType;
+ if (isConditional) {
+ expressionType = expressionType.withNode(
+ NullabilityNode.forLUB(targetType.node, expressionType.node));
+ _variables.recordDecoratedExpressionType(node, expressionType);
+ }
+ return expressionType;
+ }
+ }
+
+ bool _isConditionalExpression(Expression expression) {
+ Token token;
+ if (expression is MethodInvocation) {
+ token = expression.operator;
+ if (token == null) return false;
+ } else if (expression is PropertyAccess) {
+ token = expression.operator;
+ } else {
+ return false;
+ }
+ switch (token.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;
+ return false;
+ case TokenType.QUESTION_PERIOD:
+ return true;
default:
throw new UnimplementedError('TODO(paulberry)');
}
diff --git a/pkg/nnbd_migration/test/api_test.dart b/pkg/nnbd_migration/test/api_test.dart
index cf2d0d1..8a15fbf 100644
--- a/pkg/nnbd_migration/test/api_test.dart
+++ b/pkg/nnbd_migration/test/api_test.dart
@@ -844,6 +844,82 @@
await _checkSingleFileChanges(content, expected);
}
+ test_null_aware_getter_invocation() async {
+ var content = '''
+bool f(int i) => i?.isEven;
+main() {
+ f(null);
+}
+''';
+ var expected = '''
+bool? f(int? i) => i?.isEven;
+main() {
+ f(null);
+}
+''';
+ await _checkSingleFileChanges(content, expected);
+ }
+
+ test_null_aware_method_invocation() async {
+ var content = '''
+int f(int i) => i?.abs();
+main() {
+ f(null);
+}
+''';
+ var expected = '''
+int? f(int? i) => i?.abs();
+main() {
+ f(null);
+}
+''';
+ await _checkSingleFileChanges(content, expected);
+ }
+
+ test_null_aware_setter_invocation_null_target() async {
+ var content = '''
+class C {
+ void set x(int value);
+}
+int f(C c) => c?.x = 1;
+main() {
+ f(null);
+}
+''';
+ var expected = '''
+class C {
+ void set x(int value);
+}
+int? f(C? c) => c?.x = 1;
+main() {
+ f(null);
+}
+''';
+ await _checkSingleFileChanges(content, expected);
+ }
+
+ test_null_aware_setter_invocation_null_value() async {
+ var content = '''
+class C {
+ void set x(int value);
+}
+int f(C c) => c?.x = 1;
+main() {
+ f(null);
+}
+''';
+ var expected = '''
+class C {
+ void set x(int value);
+}
+int? f(C? c) => c?.x = 1;
+main() {
+ f(null);
+}
+''';
+ await _checkSingleFileChanges(content, expected);
+ }
+
test_single_file_multiple_changes() async {
var content = '''
int f() => null;
diff --git a/pkg/nnbd_migration/test/migration_visitor_test.dart b/pkg/nnbd_migration/test/migration_visitor_test.dart
index 8ff7c98..ab5c5e3 100644
--- a/pkg/nnbd_migration/test/migration_visitor_test.dart
+++ b/pkg/nnbd_migration/test/migration_visitor_test.dart
@@ -251,6 +251,20 @@
hard: true);
}
+ test_assignmentExpression_setter_null_aware() async {
+ await analyze('''
+class C {
+ void set s(int value) {}
+}
+int f(C c, int i) => (c?.s = i);
+''');
+ var lubNode =
+ decoratedExpressionType('(c?.s = i)').node as NullabilityNodeForLUB;
+ expect(lubNode.left, same(decoratedTypeAnnotation('C c').node));
+ expect(lubNode.right, same(decoratedTypeAnnotation('int i').node));
+ assertEdge(lubNode, decoratedTypeAnnotation('int f').node, hard: false);
+ }
+
test_assignmentExpression_setter_target_check() async {
await analyze('''
class C {
@@ -877,6 +891,20 @@
hard: false);
}
+ test_methodInvocation_return_type_null_aware() async {
+ await analyze('''
+class C {
+ bool m() => true;
+}
+bool f(C c) => (c?.m());
+''');
+ var lubNode =
+ decoratedExpressionType('(c?.m())').node as NullabilityNodeForLUB;
+ expect(lubNode.left, same(decoratedTypeAnnotation('C c').node));
+ expect(lubNode.right, same(decoratedTypeAnnotation('bool m').node));
+ assertEdge(lubNode, decoratedTypeAnnotation('bool f').node, hard: false);
+ }
+
test_methodInvocation_target_check() async {
await analyze('''
class C {
@@ -1011,6 +1039,20 @@
hard: false);
}
+ test_propertyAccess_return_type_null_aware() async {
+ await analyze('''
+class C {
+ bool get b => true;
+}
+bool f(C c) => (c?.b);
+''');
+ var lubNode =
+ decoratedExpressionType('(c?.b)').node as NullabilityNodeForLUB;
+ expect(lubNode.left, same(decoratedTypeAnnotation('C c').node));
+ expect(lubNode.right, same(decoratedTypeAnnotation('bool get b').node));
+ assertEdge(lubNode, decoratedTypeAnnotation('bool f').node, hard: false);
+ }
+
test_propertyAccess_target_check() async {
await analyze('''
class C {