[dart2js] Check non-nullable native call values

When `--null-assertions` is enabled, values returned from @Native
methods and getters with non-nullable return types are checked.

Change-Id: I4b38e651a9a6b370ebed5420a3cfac0a53cfeb60
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/158527
Commit-Queue: Stephen Adams <sra@google.com>
Reviewed-by: Sigmund Cherem <sigmund@google.com>
Reviewed-by: Srujan Gaddam <srujzs@google.com>
diff --git a/pkg/compiler/lib/src/options.dart b/pkg/compiler/lib/src/options.dart
index 2871956..f4bc3a6 100644
--- a/pkg/compiler/lib/src/options.dart
+++ b/pkg/compiler/lib/src/options.dart
@@ -250,6 +250,10 @@
   /// operate with a stronger guarantee.
   bool enableNullAssertions = false;
 
+  /// Whether to generate code asserting that non-nullable return values of
+  /// `@Native` methods are checked for being non-null.
+  bool enableNativeReturnNullAssertions = false;
+
   /// Whether to generate a source-map file together with the output program.
   bool generateSourceMap = true;
 
@@ -616,6 +620,11 @@
 
     if (_deferClassTypes) deferClassTypes = true;
     if (_noDeferClassTypes) deferClassTypes = false;
+
+    if (enableNullAssertions) {
+      // TODO(sra): Add a command-line flag to control this independently.
+      enableNativeReturnNullAssertions = true;
+    }
   }
 
   /// Returns `true` if warnings and hints are shown for all packages.
diff --git a/pkg/compiler/lib/src/ssa/builder_kernel.dart b/pkg/compiler/lib/src/ssa/builder_kernel.dart
index 24104e5..dfc216c 100644
--- a/pkg/compiler/lib/src/ssa/builder_kernel.dart
+++ b/pkg/compiler/lib/src/ssa/builder_kernel.dart
@@ -1540,8 +1540,18 @@
 
       push(HInvokeExternal(targetElement, inputs, returnType, nativeBehavior,
           sourceInformation: null));
-      // TODO(johnniwinther): Provide source information.
       HInstruction value = pop();
+      // TODO(johnniwinther): Provide source information.
+      if (options.enableNativeReturnNullAssertions) {
+        if (_isNonNullableByDefault(functionNode)) {
+          DartType type = _getDartTypeIfValid(functionNode.returnType);
+          if (dartTypes.isNonNullableIfSound(type)) {
+            push(HNullCheck(value, _abstractValueDomain.excludeNull(returnType),
+                sticky: true));
+            value = pop();
+          }
+        }
+      }
       if (targetElement.isSetter) {
         _closeAndGotoExit(HGoto(_abstractValueDomain));
       } else {
diff --git a/pkg/compiler/lib/src/ssa/nodes.dart b/pkg/compiler/lib/src/ssa/nodes.dart
index 9e5cdc5..72631d5 100644
--- a/pkg/compiler/lib/src/ssa/nodes.dart
+++ b/pkg/compiler/lib/src/ssa/nodes.dart
@@ -1548,18 +1548,20 @@
       : super(inputs, type);
 }
 
-/// A [HCheck] instruction is an instruction that might do a dynamic
-/// check at runtime on another instruction. To have proper instruction
-/// dependencies in the graph, instructions that depend on the check
-/// being done reference the [HCheck] instruction instead of the
-/// instruction itself.
+/// A [HCheck] instruction is an instruction that might do a dynamic check at
+/// runtime on an input instruction. To have proper instruction dependencies in
+/// the graph, instructions that depend on the check being done reference the
+/// [HCheck] instruction instead of the input instruction.
 abstract class HCheck extends HInstruction {
   HCheck(inputs, type) : super(inputs, type) {
     setUseGvn();
   }
+
   HInstruction get checkedInput => inputs[0];
+
   @override
   bool isJsStatement() => true;
+
   @override
   bool canThrow(AbstractValueDomain domain) => true;
 
@@ -3607,10 +3609,12 @@
 /// field getter or setter when the receiver might be null. In these cases, the
 /// [selector] and [field] members are assigned.
 class HNullCheck extends HCheck {
+  // A sticky check is not optimized away on the basis of the input type.
+  final bool sticky;
   Selector selector;
   FieldEntity field;
 
-  HNullCheck(HInstruction input, AbstractValue type)
+  HNullCheck(HInstruction input, AbstractValue type, {this.sticky = false})
       : super(<HInstruction>[input], type);
 
   @override
@@ -3632,6 +3636,7 @@
   bool dataEquals(HNullCheck other) => true;
 
   bool isRedundant(JClosedWorld closedWorld) {
+    if (sticky) return false;
     AbstractValueDomain abstractValueDomain = closedWorld.abstractValueDomain;
     AbstractValue inputType = checkedInput.instructionType;
     return abstractValueDomain.isNull(inputType).isDefinitelyFalse;
diff --git a/pkg/compiler/lib/src/ssa/optimize.dart b/pkg/compiler/lib/src/ssa/optimize.dart
index 9ff7388..0d00347 100644
--- a/pkg/compiler/lib/src/ssa/optimize.dart
+++ b/pkg/compiler/lib/src/ssa/optimize.dart
@@ -908,7 +908,7 @@
         AbstractValueFactory.fromNativeBehavior(nativeBehavior, _closedWorld);
     HInstruction receiver = node.inputs.last; // Drop interceptor.
     receiver = maybeGuardWithNullCheck(receiver, node, null);
-    HInvokeExternal result = HInvokeExternal(
+    HInstruction result = HInvokeExternal(
         method, [receiver], returnType, nativeBehavior,
         sourceInformation: node.sourceInformation);
     _registry.registerStaticUse(StaticUse.methodInlining(method, null));
@@ -919,7 +919,25 @@
     result.sideEffects.setDependsOnSomething();
     result.sideEffects.clearAllSideEffects();
     result.setUseGvn();
-    return result;
+
+    return maybeAddNativeReturnNullCheck(node, result, method);
+  }
+
+  HInstruction maybeAddNativeReturnNullCheck(
+      HInstruction node, HInstruction replacement, FunctionEntity method) {
+    if (_options.enableNativeReturnNullAssertions) {
+      if (method.library.isNonNullableByDefault) {
+        FunctionType type =
+            _closedWorld.elementEnvironment.getFunctionType(method);
+        if (_closedWorld.dartTypes.isNonNullableIfSound(type.returnType)) {
+          node.block.addBefore(node, replacement);
+          replacement = HNullCheck(replacement,
+              _abstractValueDomain.excludeNull(replacement.instructionType),
+              sticky: true);
+        }
+      }
+    }
+    return replacement;
   }
 
   // Try to 'inline' an instance setter call to a known native or js-interop
@@ -1025,14 +1043,15 @@
         AbstractValueFactory.fromNativeBehavior(nativeBehavior, _closedWorld);
     HInstruction receiver = inputs[1];
     receiver = maybeGuardWithNullCheck(receiver, node, null);
-    HInvokeExternal result = HInvokeExternal(
+    HInstruction result = HInvokeExternal(
         method,
         [receiver, ...inputs.skip(2)], // '2': Drop interceptor and receiver.
         returnType,
         nativeBehavior,
         sourceInformation: node.sourceInformation);
     _registry.registerStaticUse(StaticUse.methodInlining(method, null));
-    return result;
+
+    return maybeAddNativeReturnNullCheck(node, result, method);
   }
 
   @override
diff --git a/tests/dart2js/native/null_assertions_opt_in_lib.dart b/tests/dart2js/native/null_assertions_opt_in_lib.dart
new file mode 100644
index 0000000..e08c788
--- /dev/null
+++ b/tests/dart2js/native/null_assertions_opt_in_lib.dart
@@ -0,0 +1,58 @@
+// Copyright (c) 2020, 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.
+
+import 'native_testing.dart';
+
+abstract class Interface {
+  int get size;
+  String get name;
+  String? get optName;
+  int method1();
+  String method2();
+}
+
+@Native("AAA")
+class AAA implements Interface {
+  int get size native;
+  String get name native;
+  String? get optName native;
+  int method1() native;
+  String method2() native;
+}
+
+/// Returns an 'AAA' object that satisfies the interface.
+AAA makeA() native;
+
+/// Returns an 'AAA' object where each method breaks the interface's contract.
+AAA makeAX() native;
+
+void setup() {
+  JS('', r"""
+(function(){
+  function AAA(s,n,m1,m2) {
+    this.size = s;
+    this.name = n;
+    this.optName = n;
+    this._m1 = m1;
+    this._m2 = m2;
+  }
+  AAA.prototype.method1 = function(){return this._m1};
+  AAA.prototype.method2 = function(){return this._m2};
+
+  makeA = function() {return new AAA(100, 'Albert', 200, 'amazing!')};
+  makeAX = function() {return new AAA(void 0, void 0, void 0, void 0)};
+
+  self.nativeConstructor(AAA);
+})()""");
+}
+
+class BBB implements Interface {
+  int get size => 300;
+  String get name => 'Brenda';
+  String? get optName => name;
+  int method1() => 400;
+  String method2() => 'brilliant!';
+}
+
+List<Interface> items = [makeA(), BBB()];
diff --git a/tests/dart2js/native/null_assertions_test.dart b/tests/dart2js/native/null_assertions_test.dart
new file mode 100644
index 0000000..0a07785
--- /dev/null
+++ b/tests/dart2js/native/null_assertions_test.dart
@@ -0,0 +1,74 @@
+// Copyright (c) 2020, 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.
+
+// Requirements=nnbd-weak
+// dart2jsOptions=--null-assertions
+
+// @dart=2.8
+
+// Test that `--null-assertions` injects null-checks on the returned value of
+// native methods with a non-nullable return type in an opt-in library.
+
+import 'native_testing.dart';
+import 'null_assertions_opt_in_lib.dart' as lib;
+
+// The 'Interface' version of the code is passed both native and Dart objects,
+// so there will be an interceptor dispatch to the method. This tests that the
+// null-check exists in the forwarding method.
+//
+// The 'AAA' version of the code is passed only objects of a single native
+// class, so the native method can be inlined (which happens in the optimizer).
+// This tests that the null-check exists in the 'inlined' code.
+
+@pragma('dart2js:noInline')
+String describeInterface(lib.Interface o) {
+  return '${o.name} ${o.method2()} ${o.size} ${o.method1()}';
+}
+
+@pragma('dart2js:noInline')
+String describeAAA(lib.AAA o) {
+  return '${o.name} ${o.method2()} ${o.size} ${o.method1()}';
+}
+
+@pragma('dart2js:noInline')
+void checkOptNameInterface(lib.Interface o, dynamic expected) {
+  Expect.equals(expected, o.optName);
+}
+
+@pragma('dart2js:noInline')
+void checkOptNameAAA(lib.AAA o, dynamic expected) {
+  Expect.equals(expected, o.optName);
+}
+
+const expectedA = 'Albert amazing! 100 200';
+const expectedB = 'Brenda brilliant! 300 400';
+
+void main() {
+  nativeTesting();
+  lib.setup();
+  lib.AAA a = lib.makeA();
+  lib.BBB b = lib.BBB();
+
+  Expect.equals(expectedA, describeInterface(a));
+  Expect.equals(expectedB, describeInterface(b));
+
+  Expect.equals(expectedA, describeAAA(a));
+
+  lib.AAA x = lib.makeAX(); // This object returns `null`!
+  Expect.throws(() => describeInterface(x));
+  Expect.throws(() => describeAAA(x));
+
+  Expect.throws(() => x.name);
+  Expect.throws(() => x.size);
+  Expect.throws(() => x.method1());
+  Expect.throws(() => x.method2());
+
+  // Now test that a nullable return type does not have a check.
+  checkOptNameInterface(a, 'Albert');
+  checkOptNameInterface(b, 'Brenda');
+  checkOptNameInterface(x, null);
+
+  checkOptNameAAA(a, 'Albert');
+  checkOptNameAAA(x, null);
+}