MockBuilder: integrate non-nullability of libraries into types and tests.

The (non-)nullability of types is matched between source classes and mock classes. This should be true for any generated type annotation, including method parameters and method return types.

A method is no longer stubbed if all of its parameters are nullable, or if the source library does not use the non-nullable type system.

PiperOrigin-RevId: 314593229
diff --git a/lib/src/builder.dart b/lib/src/builder.dart
index 1aeb1ef..ff878f3 100644
--- a/lib/src/builder.dart
+++ b/lib/src/builder.dart
@@ -14,7 +14,9 @@
 
 import 'package:analyzer/dart/constant/value.dart';
 import 'package:analyzer/dart/element/element.dart';
+import 'package:analyzer/dart/element/nullability_suffix.dart';
 import 'package:analyzer/dart/element/type.dart' as analyzer;
+import 'package:analyzer/dart/element/type_system.dart';
 import 'package:build/build.dart';
 import 'package:code_builder/code_builder.dart';
 import 'package:dart_style/dart_style.dart';
@@ -37,6 +39,7 @@
   @override
   Future build(BuildStep buildStep) async {
     final entryLib = await buildStep.inputLibrary;
+    final sourceLibIsNonNullable = entryLib.isNonNullableByDefault;
     final mockLibraryAsset = buildStep.inputId.changeExtension('.mocks.dart');
     final classesToMock = <DartObject>[];
 
@@ -58,7 +61,9 @@
     }
 
     final mockLibrary = Library((b) {
-      var mockLibraryInfo = _MockLibraryInfo(classesToMock);
+      var mockLibraryInfo = _MockLibraryInfo(classesToMock,
+          sourceLibIsNonNullable: sourceLibIsNonNullable,
+          typeSystem: entryLib.typeSystem);
       b.body.addAll(mockLibraryInfo.fakeClasses);
       b.body.addAll(mockLibraryInfo.mockClasses);
     });
@@ -68,7 +73,8 @@
       return;
     }
 
-    final emitter = DartEmitter.scoped();
+    final emitter =
+        DartEmitter.scoped(useNullSafetySyntax: sourceLibIsNonNullable);
     final mockLibraryContent =
         DartFormatter().format(mockLibrary.accept(emitter).toString());
 
@@ -82,6 +88,11 @@
 }
 
 class _MockLibraryInfo {
+  final bool sourceLibIsNonNullable;
+
+  /// The type system which applies to the source library.
+  final TypeSystem typeSystem;
+
   /// Mock classes to be added to the generated library.
   final mockClasses = <Class>[];
 
@@ -97,7 +108,8 @@
 
   /// Build mock classes for [classesToMock], a list of classes obtained from a
   /// `@GenerateMocks` annotation.
-  _MockLibraryInfo(List<DartObject> classesToMock) {
+  _MockLibraryInfo(List<DartObject> classesToMock,
+      {this.sourceLibIsNonNullable, this.typeSystem}) {
     for (final classToMock in classesToMock) {
       final dartTypeToMock = classToMock.toTypeValue();
       if (dartTypeToMock == null) {
@@ -196,10 +208,20 @@
     return true;
   }
 
-  // TODO(srawlins): Update this logic to correctly handle non-nullable return
-  // types. Right now this logic does not seem to be available on DartType.
+  // Returns whether [method] has at least one parameter whose type is
+  // potentially non-nullable.
+  //
+  // A parameter whose type uses a type variable may be non-nullable on certain
+  // instances. For example:
+  //
+  //     class C<T> {
+  //       void m(T a) {}
+  //     }
+  //     final c1 = C<int?>(); // m's parameter's type is nullable.
+  //     final c2 = C<int>(); // m's parameter's type is non-nullable.
   bool _hasNonNullableParameter(MethodElement method) =>
-      method.parameters.isNotEmpty;
+      sourceLibIsNonNullable &&
+      method.parameters.any((p) => typeSystem.isPotentiallyNonNullable(p.type));
 
   /// Build a method which overrides [method], with all non-nullable
   /// parameter types widened to be nullable.
@@ -207,7 +229,9 @@
   /// This new method just calls `super.noSuchMethod`, optionally passing a
   /// return value for methods with a non-nullable return type.
   // TODO(srawlins): This method does no widening yet. Widen parameters. Include
-  // tests for typedefs, old-style function parameters, and function types.
+  // tests for typedefs, old-style function parameters, function types, type
+  // variables, non-nullable type variables (bounded to Object, I think),
+  // dynamic.
   // TODO(srawlins): This method declares no specific non-null return values
   // yet.
   void _buildOverridingMethod(MethodBuilder builder, MethodElement method) {
@@ -437,6 +461,9 @@
       return TypeReference((TypeReferenceBuilder b) {
         b
           ..symbol = type.name
+          // Using the `nullabilitySuffix` rather than `TypeSystem.isNullable`
+          // is more correct for types like `dynamic`.
+          ..isNullable = type.nullabilitySuffix == NullabilitySuffix.question
           ..url = _typeImport(type)
           ..types.addAll(type.typeArguments.map(_typeReference));
       });
diff --git a/test/builder_test.dart b/test/builder_test.dart
index f67137e..a5d5894 100644
--- a/test/builder_test.dart
+++ b/test/builder_test.dart
@@ -14,9 +14,11 @@
 
 @TestOn('vm')
 import 'package:build/build.dart';
+import 'package:build/experiments.dart';
 import 'package:build_test/build_test.dart';
 import 'package:meta/meta.dart';
 import 'package:mockito/src/builder.dart';
+import 'package:package_config/package_config.dart';
 import 'package:test/test.dart';
 
 Builder buildMocks(BuilderOptions options) => MockBuilder();
@@ -44,8 +46,7 @@
   test(
       'generates mock for an imported class but does not override private '
       'or static methods or methods w/ zero parameters', () async {
-    await testBuilder(
-      buildMocks(BuilderOptions({})),
+    await _testWithNonNullable(
       {
         ...annotationsAsset,
         ...simpleTestAsset,
@@ -74,8 +75,7 @@
   test(
       'generates mock for an imported class but does not override private '
       'or static fields', () async {
-    await testBuilder(
-      buildMocks(BuilderOptions({})),
+    await _testWithNonNullable(
       {
         ...annotationsAsset,
         ...simpleTestAsset,
@@ -103,8 +103,7 @@
   test(
       'generates mock for an imported class but does not override any '
       'extension methods', () async {
-    await testBuilder(
-      buildMocks(BuilderOptions({})),
+    await _testWithNonNullable(
       {
         ...annotationsAsset,
         ...simpleTestAsset,
@@ -135,8 +134,7 @@
   });
 
   test('generates a mock class and overrides methods parameters', () async {
-    await testBuilder(
-      buildMocks(BuilderOptions({})),
+    await _testWithNonNullable(
       {
         ...annotationsAsset,
         ...simpleTestAsset,
@@ -181,8 +179,7 @@
   });
 
   test('generates multiple mock classes', () async {
-    await testBuilder(
-      buildMocks(BuilderOptions({})),
+    await _testWithNonNullable(
       {
         ...annotationsAsset,
         'foo|lib/foo.dart': dedent(r'''
@@ -226,8 +223,7 @@
   });
 
   test('generates generic mock classes', () async {
-    await testBuilder(
-      buildMocks(BuilderOptions({})),
+    await _testWithNonNullable(
       {
         ...annotationsAsset,
         'foo|lib/foo.dart': dedent(r'''
@@ -259,8 +255,7 @@
   });
 
   test('generates generic mock classes with type bounds', () async {
-    await testBuilder(
-      buildMocks(BuilderOptions({})),
+    await _testWithNonNullable(
       {
         ...annotationsAsset,
         'foo|lib/foo.dart': dedent(r'''
@@ -302,17 +297,15 @@
   });
 
   test('writes non-interface types w/o imports', () async {
-    await testBuilder(
-      buildMocks(BuilderOptions({})),
+    await _testWithNonNullable(
       {
         ...annotationsAsset,
         ...simpleTestAsset,
         'foo|lib/foo.dart': dedent(r'''
-        import 'dart:async';
         class Foo<T> {
-          void f(dynamic a) {}
-          void g(T b) {}
-          void h<U>(U c) {}
+          void f(dynamic a, int b) {}
+          void g(T c) {}
+          void h<U>(U d) {}
         }
         '''),
       },
@@ -325,9 +318,9 @@
         ///
         /// See the documentation for Mockito's code generation for more information.
         class MockFoo<T> extends _i1.Mock implements _i2.Foo<T> {
-          void f(dynamic a) => super.noSuchMethod(Invocation.method(#f, [a]));
-          void g(T b) => super.noSuchMethod(Invocation.method(#g, [b]));
-          void h<U>(U c) => super.noSuchMethod(Invocation.method(#h, [c]));
+          void f(dynamic a, int b) => super.noSuchMethod(Invocation.method(#f, [a, b]));
+          void g(T c) => super.noSuchMethod(Invocation.method(#g, [c]));
+          void h<U>(U d) => super.noSuchMethod(Invocation.method(#h, [d]));
         }
         '''),
       },
@@ -335,8 +328,7 @@
   });
 
   test('imports libraries for external class types', () async {
-    await testBuilder(
-      buildMocks(BuilderOptions({})),
+    await _testWithNonNullable(
       {
         ...annotationsAsset,
         ...simpleTestAsset,
@@ -365,8 +357,7 @@
   });
 
   test('imports libraries for type aliases with external types', () async {
-    await testBuilder(
-      buildMocks(BuilderOptions({})),
+    await _testWithNonNullable(
       {
         ...annotationsAsset,
         ...simpleTestAsset,
@@ -402,8 +393,7 @@
   });
 
   test('imports libraries for function types with external types', () async {
-    await testBuilder(
-      buildMocks(BuilderOptions({})),
+    await _testWithNonNullable(
       {
         ...annotationsAsset,
         ...simpleTestAsset,
@@ -446,9 +436,86 @@
     );
   });
 
+  test('correctly matches nullability of parameters', () async {
+    await _testWithNonNullable(
+      {
+        ...annotationsAsset,
+        ...simpleTestAsset,
+        'foo|lib/foo.dart': dedent(r'''
+        abstract class Foo {
+          void f(int? a, int b);
+          void g(List<int?> a, List<int> b);
+          void h(int? Function() a, int Function() b);
+          void i(void Function(int?) a, void Function(int) b);
+          void j(int? a(), int b());
+          void k(void a(int? x), void b(int x));
+          void l<T>(T? a, T b);
+        }
+        '''),
+      },
+      outputs: {
+        // TODO(srawlins): The type of l's first parameter should be `T?`.
+        'foo|test/foo_test.mocks.dart': dedent(r'''
+        import 'package:mockito/mockito.dart' as _i1;
+        import 'package:foo/foo.dart' as _i2;
+
+        /// A class which mocks [Foo].
+        ///
+        /// See the documentation for Mockito's code generation for more information.
+        class MockFoo extends _i1.Mock implements _i2.Foo {
+          void f(int? a, int b) => super.noSuchMethod(Invocation.method(#f, [a, b]));
+          void g(List<int?> a, List<int> b) =>
+              super.noSuchMethod(Invocation.method(#g, [a, b]));
+          void h(int? Function() a, int Function() b) =>
+              super.noSuchMethod(Invocation.method(#h, [a, b]));
+          void i(void Function(int?) a, void Function(int) b) =>
+              super.noSuchMethod(Invocation.method(#i, [a, b]));
+          void j(int? Function() a, int Function() b) =>
+              super.noSuchMethod(Invocation.method(#j, [a, b]));
+          void k(void Function(int?) a, void Function(int) b) =>
+              super.noSuchMethod(Invocation.method(#k, [a, b]));
+          void l<T>(T a, T b) => super.noSuchMethod(Invocation.method(#l, [a, b]));
+        }
+        '''),
+      },
+    );
+  });
+
+  test('correctly matches nullability of return types', () async {
+    await _testWithNonNullable(
+      {
+        ...annotationsAsset,
+        ...simpleTestAsset,
+        'foo|lib/foo.dart': dedent(r'''
+        abstract class Foo {
+          int f();
+          int? g();
+          List<int?> h();
+          List<int> i();
+        }
+        '''),
+      },
+      outputs: {
+        'foo|test/foo_test.mocks.dart': dedent(r'''
+        import 'package:mockito/mockito.dart' as _i1;
+        import 'package:foo/foo.dart' as _i2;
+
+        /// A class which mocks [Foo].
+        ///
+        /// See the documentation for Mockito's code generation for more information.
+        class MockFoo extends _i1.Mock implements _i2.Foo {
+          int f() => super.noSuchMethod(Invocation.method(#f, []), 0);
+          int? g() => super.noSuchMethod(Invocation.method(#g, []), 0);
+          List<int?> h() => super.noSuchMethod(Invocation.method(#h, []), []);
+          List<int> i() => super.noSuchMethod(Invocation.method(#i, []), []);
+        }
+        '''),
+      },
+    );
+  });
+
   test('overrides abstract methods', () async {
-    await testBuilder(
-      buildMocks(BuilderOptions({})),
+    await _testWithNonNullable(
       {
         ...annotationsAsset,
         ...simpleTestAsset,
@@ -475,9 +542,62 @@
     );
   });
 
+  test('does not override methods with all nullable parameters', () async {
+    await _testWithNonNullable(
+      {
+        ...annotationsAsset,
+        ...simpleTestAsset,
+        'foo|lib/foo.dart': dedent(r'''
+          class Foo {
+            void a(int? m) {}
+            void b(dynamic n) {}
+            void c(int Function()? o) {}
+          }
+          '''),
+      },
+      outputs: {
+        'foo|test/foo_test.mocks.dart': dedent(r'''
+          import 'package:mockito/mockito.dart' as _i1;
+          import 'package:foo/foo.dart' as _i2;
+
+          /// A class which mocks [Foo].
+          ///
+          /// See the documentation for Mockito's code generation for more information.
+          class MockFoo extends _i1.Mock implements _i2.Foo {}
+          '''),
+      },
+    );
+  });
+
+  test('overrides methods with a potentially non-nullable parameter', () async {
+    await _testWithNonNullable(
+      {
+        ...annotationsAsset,
+        ...simpleTestAsset,
+        'foo|lib/foo.dart': dedent(r'''
+        class Foo<T> {
+          void a(T m) {}
+        }
+        '''),
+      },
+      outputs: {
+        'foo|test/foo_test.mocks.dart': dedent(r'''
+        import 'package:mockito/mockito.dart' as _i1;
+        import 'package:foo/foo.dart' as _i2;
+
+        /// A class which mocks [Foo].
+        ///
+        /// See the documentation for Mockito's code generation for more information.
+        class MockFoo<T> extends _i1.Mock implements _i2.Foo<T> {
+          void a(T m) => super.noSuchMethod(Invocation.method(#a, [m]));
+        }
+        '''),
+      },
+    );
+  });
+
   test('overrides generic methods', () async {
-    await testBuilder(
-      buildMocks(BuilderOptions({})),
+    await _testWithNonNullable(
       {
         ...annotationsAsset,
         ...simpleTestAsset,
@@ -509,8 +629,7 @@
   });
 
   test('overrides getters and setters', () async {
-    await testBuilder(
-      buildMocks(BuilderOptions({})),
+    await _testWithNonNullable(
       {
         ...annotationsAsset,
         ...simpleTestAsset,
@@ -541,8 +660,7 @@
   });
 
   test('overrides operators', () async {
-    await testBuilder(
-      buildMocks(BuilderOptions({})),
+    await _testWithNonNullable(
       {
         ...annotationsAsset,
         ...simpleTestAsset,
@@ -574,8 +692,7 @@
   });
 
   test('creates dummy non-null return values for known core classes', () async {
-    await testBuilder(
-      buildMocks(BuilderOptions({})),
+    await _testWithNonNullable(
       {
         ...annotationsAsset,
         ...simpleTestAsset,
@@ -615,8 +732,7 @@
 
   test('creates dummy non-null return values for Futures of known core classes',
       () async {
-    await testBuilder(
-      buildMocks(BuilderOptions({})),
+    await _testWithNonNullable(
       {
         ...annotationsAsset,
         ...simpleTestAsset,
@@ -645,8 +761,7 @@
   });
 
   test('creates dummy non-null return values for unknown classes', () async {
-    await testBuilder(
-      buildMocks(BuilderOptions({})),
+    await _testWithNonNullable(
       {
         ...annotationsAsset,
         ...simpleTestAsset,
@@ -679,8 +794,7 @@
   });
 
   test('deduplicates fake classes', () async {
-    await testBuilder(
-      buildMocks(BuilderOptions({})),
+    await _testWithNonNullable(
       {
         ...annotationsAsset,
         ...simpleTestAsset,
@@ -715,8 +829,7 @@
   });
 
   test('creates dummy non-null return values for enums', () async {
-    await testBuilder(
-      buildMocks(BuilderOptions({})),
+    await _testWithNonNullable(
       {
         ...annotationsAsset,
         ...simpleTestAsset,
@@ -747,8 +860,7 @@
   });
 
   test('creates dummy non-null return values for functions', () async {
-    await testBuilder(
-      buildMocks(BuilderOptions({})),
+    await _testWithNonNullable(
       {
         ...annotationsAsset,
         ...simpleTestAsset,
@@ -844,6 +956,65 @@
       message: 'The "classes" argument includes an enum: Foo',
     );
   });
+
+  test('given a pre-non-nullable library, does not override any members',
+      () async {
+    await _testPreNonNullable(
+      {
+        ...annotationsAsset,
+        ...simpleTestAsset,
+        'foo|lib/foo.dart': dedent(r'''
+        abstract class Foo {
+          dynamic f(int a) {}
+        }
+        '''),
+      },
+      outputs: {
+        'foo|test/foo_test.mocks.dart': dedent(r'''
+        import 'package:mockito/mockito.dart' as _i1;
+        import 'package:foo/foo.dart' as _i2;
+
+        /// A class which mocks [Foo].
+        ///
+        /// See the documentation for Mockito's code generation for more information.
+        class MockFoo extends _i1.Mock implements _i2.Foo {}
+        '''),
+      },
+    );
+  });
+}
+
+/// Test [MockBuilder] in a package which has not opted into the non-nullable
+/// type system.
+///
+/// Whether the non-nullable experiment is enabled depends on the SDK executing
+/// this test, but that does not affect the opt-in state of the package under
+/// test.
+Future<void> _testPreNonNullable(Map<String, String> sourceAssets,
+    {Map<String, /*String|Matcher<String>*/ dynamic> outputs}) async {
+  var packageConfig = PackageConfig([
+    Package('foo', Uri.file('/foo/'),
+        packageUriRoot: Uri.file('/foo/lib/'),
+        languageVersion: LanguageVersion(2, 7))
+  ]);
+  await testBuilder(buildMocks(BuilderOptions({})), sourceAssets,
+      outputs: outputs, packageConfig: packageConfig);
+}
+
+/// Test [MockBuilder] in a package which has opted into the non-nullable type
+/// system, and with the non-nullable experiment enabled.
+Future<void> _testWithNonNullable(Map<String, String> sourceAssets,
+    {Map<String, /*String|Matcher<String>*/ dynamic> outputs}) async {
+  var packageConfig = PackageConfig([
+    Package('foo', Uri.file('/foo/'),
+        packageUriRoot: Uri.file('/foo/lib/'),
+        languageVersion: LanguageVersion(2, 9))
+  ]);
+  await withEnabledExperiments(
+    () async => await testBuilder(buildMocks(BuilderOptions({})), sourceAssets,
+        outputs: outputs, packageConfig: packageConfig),
+    ['non-nullable'],
+  );
 }
 
 /// Expect that [testBuilder], given [assets], throws an