Support nSM Forwarding (#133)

Support nSM Forwarding
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 49ce5db..fc74b07 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,52 @@
+## 3.0.0-alpha+5
+
+* Fix compatibility with new [noSuchMethod Forwarding] feature of Dart 2. This
+  is thankfully a mostly backwards-compatible change. This means that this
+  version of Mockito should continue to work:
+
+  * with Dart `>=2.0.0-dev.16.0`,
+  * with Dart 2 runtime semantics (i.e. with `dart --preview-dart-2`, or with
+    Flutter Beta 3), and
+  * with the new noSuchMethod Forwarding feature, when it lands in CFE, and when
+    it lands in DDC.
+
+  This change, when combined with noSuchMethod Forwarding, will break a few
+  code paths which do not seem to be frequently used. Two examples:
+
+  ```dart
+  class A {
+    int fn(int a, [int b]) => 7;
+  }
+  class MockA extends Mock implements A {}
+
+  var a = new MockA();
+  when(a.fn(typed(any), typed(any))).thenReturn(0);
+  print(a.fn(1));
+  ```
+
+  This used to print `null`, because only one argument was passed, which did
+  not match the two-argument stub. Now it will print `0`, as the real call
+  contains a value for both the required argument, and the optional argument.
+
+  ```dart
+  a.fn(1);
+  a.fn(2, 3);
+  print(verify(a.fn(typed(captureAny), typed(captureAny))).captured);
+  ```
+
+  This used to print `[2, 3]`, because only the second call matched the `verify`
+  call. Now, it will print `[1, null, 2, 3]`, as both real calls contain a value
+  for both the required argument, and the optional argument.
+
+[noSuchMethod Forwarding]: https://github.com/dart-lang/sdk/blob/master/docs/language/informal/nosuchmethod-forwarding.md
+
+## 3.0.0-alpha+4
+
+* Introduce a backward-and-forward compatible API to help users migrate to
+  Mockito 3. See more details in the [upgrading-to-mockito-3] doc.
+
+[upgrading-to-mockito-3]: https://github.com/dart-lang/mockito/blob/master/upgrading-to-mockito-3.md
+
 ## 3.0.0-alpha+3
 
 * `thenReturn` and `thenAnswer` now support generics and infer the correct
diff --git a/lib/src/mock.dart b/lib/src/mock.dart
index b8f6ab5..34978be 100644
--- a/lib/src/mock.dart
+++ b/lib/src/mock.dart
@@ -215,20 +215,10 @@
     invocation.namedArguments.forEach((name, arg) {
       if (arg == null) {
         if (!_storedNamedArgSymbols.contains(name)) {
-          // Incorrect usage of an ArgMatcher, something like:
-          // `when(obj.fn(a: any))`.
-
-          // Clear things out for the next call.
-          _storedArgs.clear();
-          _storedNamedArgs.clear();
-          throw new ArgumentError(
-              'An ArgumentMatcher (or a null value) was passed in as a named '
-              'argument named "$name", but was not passed a value for `named`. '
-              'Each ArgumentMatcher that is passed as a named argument needs '
-              'to specify the `named` argument, and each null value must be '
-              'wrapped in an ArgMatcher. For example: '
-              '`when(obj.fn(x: anyNamed("x")))` or '
-              '`when(obj.fn(x: argThat(isNull, named: "x")))`.');
+          // Either this is a parameter with default value `null`, or a `null`
+          // argument was passed, or an unnamed ArgMatcher was used. Just use
+          // `null`.
+          namedArguments[name] = null;
         }
       } else {
         // Add each real named argument (not wrapped in an ArgMatcher).
@@ -271,13 +261,19 @@
     var positionalArguments = <dynamic>[];
     var nullPositionalArguments =
         invocation.positionalArguments.where((arg) => arg == null);
-    if (_storedArgs.length != nullPositionalArguments.length) {
-      // Clear things out for the next call.
+    if (_storedArgs.length > nullPositionalArguments.length) {
+      // More _positional_ ArgMatchers were stored than were actually passed as
+      // positional arguments. The only way this call was parsed and resolved is
+      // if an ArgMatcher was passed as a named argument, but without a name,
+      // and thus stored in [_storedArgs], something like
+      // `when(obj.fn(a: any))`.
       _storedArgs.clear();
       _storedNamedArgs.clear();
       throw new ArgumentError(
-          'null arguments are not allowed alongside ArgMatchers; use '
-          '"argThat(isNull)"');
+          'An argument matcher (like `any`) was used as a named argument, but '
+          'did not use a Mockito "named" API. Each argument matcher that is '
+          'used as a named argument needs to specify the name of the argument '
+          'it is being used in. For example: `when(obj.fn(x: anyNamed("x")))`.');
     }
     int storedIndex = 0;
     int positionalIndex = 0;
diff --git a/test/capture_test.dart b/test/capture_test.dart
index 13ab95a..8ca3606 100644
--- a/test/capture_test.dart
+++ b/test/capture_test.dart
@@ -16,6 +16,8 @@
 import 'package:mockito/src/mock.dart' show resetMockitoState;
 import 'package:test/test.dart';
 
+import 'utils.dart';
+
 class RealClass {
   RealClass innerObj;
   String methodWithNormalArgs(int x) => 'Real';
@@ -31,6 +33,8 @@
 void main() {
   MockedClass mock;
 
+  var isNsmForwarding = assessNsmForwarding();
+
   setUp(() {
     mock = new MockedClass();
   });
@@ -78,11 +82,12 @@
     test('should capture with matching arguments', () {
       mock.methodWithPositionalArgs(1);
       mock.methodWithPositionalArgs(2, 3);
+      var expectedCaptures = isNsmForwarding ? [1, null, 2, 3] : [2, 3];
       expect(
           verify(mock.methodWithPositionalArgs(
                   typed(captureAny), typed(captureAny)))
               .captured,
-          equals([2, 3]));
+          equals(expectedCaptures));
     });
 
     test('should capture multiple invocations', () {
diff --git a/test/mockito_test.dart b/test/mockito_test.dart
index 49946d7..5794740 100644
--- a/test/mockito_test.dart
+++ b/test/mockito_test.dart
@@ -19,6 +19,8 @@
     show resetMockitoState, throwOnMissingStub;
 import 'package:test/test.dart';
 
+import 'utils.dart';
+
 class RealClass {
   RealClass innerObj;
   String methodWithoutArgs() => "Real";
@@ -72,6 +74,8 @@
 void main() {
   MockedClass mock;
 
+  var isNsmForwarding = assessNsmForwarding();
+
   setUp(() {
     mock = new MockedClass();
   });
@@ -153,7 +157,10 @@
           .thenReturn("x y");
       when(mock.methodWithTwoNamedArgs(any, z: anyNamed('z')))
           .thenReturn("x z");
-      expect(mock.methodWithTwoNamedArgs(42), isNull);
+      if (isNsmForwarding)
+        expect(mock.methodWithTwoNamedArgs(42), "x z");
+      else
+        expect(mock.methodWithTwoNamedArgs(42), isNull);
       expect(mock.methodWithTwoNamedArgs(42, y: 18), equals("x y"));
       expect(mock.methodWithTwoNamedArgs(42, z: 17), equals("x z"));
       expect(mock.methodWithTwoNamedArgs(42, y: 18, z: 17), isNull);
@@ -252,16 +259,6 @@
       }, throwsStateError);
     });
 
-    test("should throw if `null` is passed alongside matchers", () {
-      expect(() {
-        when(mock.methodWithPositionalArgs(argThat(equals(42)), null))
-            .thenReturn("99");
-      }, throwsArgumentError);
-
-      // but doesn't ruin later calls.
-      when(mock.methodWithNormalArgs(43)).thenReturn("43");
-    });
-
     test("thenReturn throws if provided Future", () {
       expect(
           () => when(mock.methodReturningFuture())
@@ -290,20 +287,6 @@
       expect(await mock.methodReturningStream().toList(), ["stub"]);
     });
 
-    test("should throw if `null` is passed as a named arg", () {
-      expect(() {
-        when(mock.methodWithNamedArgs(argThat(equals(42)), y: null))
-            .thenReturn("99");
-      }, throwsArgumentError);
-    });
-
-    test("should throw if named matcher is passed as a positional arg", () {
-      expect(() {
-        when(mock.methodWithNamedArgs(argThat(equals(42), named: "y")))
-            .thenReturn("99");
-      }, throwsArgumentError);
-    });
-
     test("should throw if named matcher is passed as the wrong name", () {
       expect(() {
         when(mock.methodWithNamedArgs(argThat(equals(42)), y: anyNamed("z")))
diff --git a/test/utils.dart b/test/utils.dart
new file mode 100644
index 0000000..a8ecd49
--- /dev/null
+++ b/test/utils.dart
@@ -0,0 +1,21 @@
+import 'package:mockito/mockito.dart';
+
+abstract class NsmForwardingSignal {
+  void fn([int a]);
+}
+
+class MockNsmForwardingSignal extends Mock implements NsmForwardingSignal {}
+
+bool assessNsmForwarding() {
+  var signal = new MockNsmForwardingSignal();
+  signal.fn();
+  try {
+    verify(signal.fn(typed(any)));
+    return true;
+  } catch (_) {
+    // The verify failed, because the default value of 7 was not passed to
+    // noSuchMethod.
+    verify(signal.fn());
+    return false;
+  }
+}
diff --git a/test/verify_test.dart b/test/verify_test.dart
index 9c1bcf1..6945306 100644
--- a/test/verify_test.dart
+++ b/test/verify_test.dart
@@ -16,6 +16,8 @@
 import 'package:mockito/src/mock.dart' show resetMockitoState;
 import 'package:test/test.dart';
 
+import 'utils.dart';
+
 class RealClass {
   RealClass innerObj;
   String methodWithoutArgs() => 'Real';
@@ -73,6 +75,8 @@
 void main() {
   MockedClass mock;
 
+  var isNsmForwarding = assessNsmForwarding();
+
   setUp(() {
     mock = new MockedClass();
   });
@@ -242,8 +246,9 @@
 
     test('and there is one unmatched call without args', () {
       mock.methodWithOptionalArg();
+      var nsmForwardedArgs = isNsmForwarding ? 'null' : '';
       expectFail(
-          'No matching calls. All calls: MockedClass.methodWithOptionalArg()\n'
+          'No matching calls. All calls: MockedClass.methodWithOptionalArg($nsmForwardedArgs)\n'
           '$noMatchingCallsFooter', () {
         verify(mock.methodWithOptionalArg(43));
       });
@@ -263,9 +268,10 @@
 
     test('and unmatched calls have only named args', () {
       mock.methodWithOnlyNamedArgs(y: 1);
+      var nsmForwardedArgs = isNsmForwarding ? '{y: 1, z: null}' : '{y: 1}';
       expectFail(
           'No matching calls. All calls: '
-          'MockedClass.methodWithOnlyNamedArgs({y: 1})\n'
+          'MockedClass.methodWithOnlyNamedArgs($nsmForwardedArgs)\n'
           '$noMatchingCallsFooter', () {
         verify(mock.methodWithOnlyNamedArgs());
       });
@@ -509,6 +515,8 @@
       mock.methodWithLongArgs(null, null,
           c: new LongToString([5, 6], {5: 'g', 6: 'h'}, 'i'),
           d: new LongToString([7, 8], {7: 'j', 8: 'k'}, 'l'));
+      var nsmForwardedNamedArgs =
+          isNsmForwarding ? '>, {c: null, d: null}),' : '>),';
       expectFail(
           'No matching calls. All calls: '
           'MockedClass.methodWithLongArgs(\n'
@@ -521,7 +529,7 @@
           '        aList: [4, 5]\n'
           '        aMap: {3: d, 4: e}\n'
           '        aString: f\n'
-          '    >),\n'
+          '    $nsmForwardedNamedArgs\n'
           'MockedClass.methodWithLongArgs(null, null, {\n'
           '    c: LongToString<\n'
           '        aList: [5, 6]\n'