Migration: infer non-null intent for `this` in extensions.
Since `this` is essentially a variable in extensions, it should be
treated similarly to variables in terms of inference of non-null
intent; for example, if an extension declaration contains a a method
that unconditionall dereferences `this` (either implicitly or
explicitly), that's a good indication that the user probably doesn't
intend for the extension to be applied to nullable types.
Fixes #44675.
Bug: https://github.com/dart-lang/sdk/issues/44675
Change-Id: I004328f5b1fd6710954363c07f4e9db6bc6ac2cd
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/180268
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
diff --git a/pkg/nnbd_migration/lib/instrumentation.dart b/pkg/nnbd_migration/lib/instrumentation.dart
index 5295242..a5fa5ce 100644
--- a/pkg/nnbd_migration/lib/instrumentation.dart
+++ b/pkg/nnbd_migration/lib/instrumentation.dart
@@ -274,6 +274,7 @@
implicitMixinSuperCall,
implicitNullInitializer,
implicitNullReturn,
+ implicitThis,
inferredTypeParameterInstantiation,
instanceCreation,
instantiateToBounds,
diff --git a/pkg/nnbd_migration/lib/src/edge_builder.dart b/pkg/nnbd_migration/lib/src/edge_builder.dart
index 572ea5d..5f30b51 100644
--- a/pkg/nnbd_migration/lib/src/edge_builder.dart
+++ b/pkg/nnbd_migration/lib/src/edge_builder.dart
@@ -10,6 +10,7 @@
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/dart/element/type_provider.dart';
import 'package:analyzer/dart/element/type_system.dart';
+import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/dart/element/inheritance_manager3.dart';
import 'package:analyzer/src/dart/element/type.dart';
import 'package:analyzer/src/dart/element/type_system.dart' show TypeSystemImpl;
@@ -1268,6 +1269,7 @@
}
} else if (target == null && callee.enclosingElement is ClassElement) {
targetType = _thisOrSuper(node);
+ _checkThisNotNull(targetType, node);
}
DecoratedType expressionType;
DecoratedType calleeType;
@@ -1575,6 +1577,7 @@
@override
DecoratedType visitSimpleIdentifier(SimpleIdentifier node) {
+ DecoratedType targetType;
DecoratedType result;
var staticElement = getWriteOrReadElement(node);
if (staticElement is PromotableElement) {
@@ -1594,15 +1597,16 @@
} else if (staticElement is FunctionElement ||
staticElement is MethodElement ||
staticElement is ConstructorElement) {
- result = getOrComputeElementType(staticElement,
- targetType: staticElement.enclosingElement is ClassElement
- ? _thisOrSuper(node)
- : null);
+ if (staticElement.enclosingElement is ClassElement) {
+ targetType = _thisOrSuper(node);
+ }
+ result = getOrComputeElementType(staticElement, targetType: targetType);
} else if (staticElement is PropertyAccessorElement) {
- var elementType = getOrComputeElementType(staticElement,
- targetType: staticElement.enclosingElement is ClassElement
- ? _thisOrSuper(node)
- : null);
+ if (staticElement.enclosingElement is ClassElement) {
+ targetType = _thisOrSuper(node);
+ }
+ var elementType =
+ getOrComputeElementType(staticElement, targetType: targetType);
result = staticElement.isGetter
? elementType.returnType
: elementType.positionalParameters[0];
@@ -1621,6 +1625,9 @@
_unimplemented(node,
'Simple identifier with a static element of type ${staticElement.runtimeType}');
}
+ if (targetType != null) {
+ _checkThisNotNull(targetType, node);
+ }
return result;
}
@@ -1986,6 +1993,18 @@
return sourceType;
}
+ /// Generates the appropriate edge to assert that the value of `this` is
+ /// non-null.
+ void _checkThisNotNull(DecoratedType thisType, AstNode node) {
+ // `this` can only be `null` in extensions, so if we're not in an extension,
+ // there's nothing to do.
+ if (_currentExtendedType == null) return;
+ var origin = ImplicitThisOrigin(source, node);
+ var hard =
+ _postDominatedLocals.isInScope(_postDominatedLocals.extensionThis);
+ _graph.makeNonNullable(thisType.node, origin, hard: hard, guards: _guards);
+ }
+
@override
void _connect(NullabilityNode source, NullabilityNode destination,
EdgeOrigin origin, FixReasonTarget edgeTarget,
@@ -2452,6 +2471,9 @@
_addParametersToFlowAnalysis(parameters);
// Push a scope of post-dominated declarations on the stack.
_postDominatedLocals.pushScope(elements: declaredElement.parameters);
+ if (declaredElement.enclosingElement is ExtensionElement) {
+ _postDominatedLocals.add(_postDominatedLocals.extensionThis);
+ }
try {
_dispatchList(initializers);
if (declaredElement is ConstructorElement &&
@@ -3659,6 +3681,10 @@
///
/// Contains helpers for dealing with expressions as if they were elements.
class _ScopedLocalSet extends ScopedSet<Element> {
+ /// The synthetic element we use as a stand-in for `this` when analyzing
+ /// extension methods.
+ Element get extensionThis => DynamicElementImpl.instance;
+
bool isReferenceInScope(Expression expression) {
var element = referencedElement(expression);
return element != null && isInScope(element);
@@ -3670,6 +3696,8 @@
expression = expression.unParenthesized;
if (expression is SimpleIdentifier) {
return expression.staticElement;
+ } else if (expression is ThisExpression || expression is SuperExpression) {
+ return extensionThis;
} else {
return null;
}
diff --git a/pkg/nnbd_migration/lib/src/edge_origin.dart b/pkg/nnbd_migration/lib/src/edge_origin.dart
index f1d39f6..cff0b7b 100644
--- a/pkg/nnbd_migration/lib/src/edge_origin.dart
+++ b/pkg/nnbd_migration/lib/src/edge_origin.dart
@@ -324,6 +324,18 @@
ReturnStatement get node => super.node as ReturnStatement;
}
+/// Edge origin used for edges that arise from an implicit use of `this`, e.g.
+/// during a method call from an extension.
+class ImplicitThisOrigin extends EdgeOrigin {
+ ImplicitThisOrigin(Source source, AstNode node) : super(source, node);
+
+ @override
+ String get description => 'implicit use of `this`';
+
+ @override
+ EdgeOriginKind get kind => EdgeOriginKind.implicitThis;
+}
+
/// Edge origin resulting from the inference of a type parameter, which
/// can affects the nullability of that type parameter's bound.
class InferredTypeParameterInstantiationOrigin extends EdgeOrigin {
diff --git a/pkg/nnbd_migration/test/api_test.dart b/pkg/nnbd_migration/test/api_test.dart
index 7d9e21f..7089d06 100644
--- a/pkg/nnbd_migration/test/api_test.dart
+++ b/pkg/nnbd_migration/test/api_test.dart
@@ -2116,6 +2116,51 @@
await _checkSingleFileChanges(content, expected, removeViaComments: true);
}
+ Future<void> test_extension_extended_type_nullability_intent() async {
+ var content = '''
+extension E on C {
+ String foo() => this.bar();
+}
+
+class C {
+ String bar() => null;
+}
+
+void test(C c, bool b) {
+ if (b) {
+ c.foo();
+ }
+}
+
+main() {
+ test(null, false);
+}
+''';
+ // The call to `bar` from `foo` should be taken as a demonstration that the
+ // extension E is not intended to apply to nullable types, so the call to
+ // `foo` should be null checked.
+ var expected = '''
+extension E on C {
+ String? foo() => this.bar();
+}
+
+class C {
+ String? bar() => null;
+}
+
+void test(C? c, bool b) {
+ if (b) {
+ c!.foo();
+ }
+}
+
+main() {
+ test(null, false);
+}
+''';
+ await _checkSingleFileChanges(content, expected);
+ }
+
Future<void> test_extension_null_check_non_nullable() async {
var content = '''
class C {}
diff --git a/pkg/nnbd_migration/test/edge_builder_test.dart b/pkg/nnbd_migration/test/edge_builder_test.dart
index e943de9..9558d4e 100644
--- a/pkg/nnbd_migration/test/edge_builder_test.dart
+++ b/pkg/nnbd_migration/test/edge_builder_test.dart
@@ -2952,6 +2952,87 @@
// adding assertion(s).
}
+ Future<void> test_extension_this_non_null_intent_explicit_direct() async {
+ await analyze('''
+extension on int {
+ f() => g(this);
+}
+void g(int i) {}
+''');
+ assertEdge(decoratedTypeAnnotation('int {').node,
+ decoratedTypeAnnotation('int i').node,
+ hard: true);
+ }
+
+ Future<void> test_extension_this_non_null_intent_explicit_method() async {
+ await analyze('''
+extension on int {
+ f() => this.abs();
+}
+''');
+ assertEdge(decoratedTypeAnnotation('int').node, never, hard: true);
+ }
+
+ Future<void>
+ test_extension_this_non_null_intent_explicit_property_get() async {
+ await analyze('''
+extension on int {
+ f() => this.isEven;
+}
+''');
+ assertEdge(decoratedTypeAnnotation('int').node, never, hard: true);
+ }
+
+ Future<void>
+ test_extension_this_non_null_intent_explicit_property_set() async {
+ await analyze('''
+class C {
+ int x;
+}
+extension on C /*reference*/ {
+ f() {
+ this.x = 0;
+ }
+}
+''');
+ assertEdge(decoratedTypeAnnotation('C /*reference*/').node, never,
+ hard: true);
+ }
+
+ Future<void> test_extension_this_non_null_intent_implicit_method() async {
+ await analyze('''
+extension on int {
+ f() => abs();
+}
+''');
+ assertEdge(decoratedTypeAnnotation('int').node, never, hard: true);
+ }
+
+ Future<void> test_extension_this_non_null_intent_implicit_property() async {
+ await analyze('''
+extension on int {
+ f() => isEven;
+}
+''');
+ assertEdge(decoratedTypeAnnotation('int').node, never, hard: true);
+ }
+
+ Future<void>
+ test_extension_this_non_null_intent_implicit_property_set() async {
+ await analyze('''
+class C {
+ int x;
+}
+extension on C /*reference*/ {
+ f() {
+ x = 0;
+ }
+}
+''');
+ assertEdge(decoratedTypeAnnotation('C /*reference*/').node, never,
+ hard: true);
+ }
+
Future<void> test_field_final_does_not_override_setter() async {
await analyze('''
abstract class A {
@@ -7712,11 +7793,11 @@
}
''');
expect(
- assertEdge(anyNode, decoratedTypeAnnotation('int f1').node, hard: false)
+ assertEdge(anyNode, decoratedTypeAnnotation('int f1').node, hard: true)
.sourceNode,
isNot(never));
expect(
- assertEdge(anyNode, decoratedTypeAnnotation('int f2').node, hard: false)
+ assertEdge(anyNode, decoratedTypeAnnotation('int f2').node, hard: true)
.sourceNode,
never);
expect(hasNullCheckHint(findNode.this_('this/*!*/')), isTrue);