Mockito generated API: Create fake classes for unknown dummy return types.

This supports unknown classes, enums, and function types.

PiperOrigin-RevId: 281594923
diff --git a/lib/src/builder.dart b/lib/src/builder.dart
index b74768e..4f04ad6 100644
--- a/lib/src/builder.dart
+++ b/lib/src/builder.dart
@@ -41,24 +41,29 @@
 
     final mockLibraryAsset = buildStep.inputId.changeExtension('.mocks.dart');
 
-    final mockLibrary = Library((lBuilder) {
-      for (final element in entryLib.topLevelElements) {
-        final annotation = element.metadata.firstWhere(
-            (annotation) =>
-                annotation.element is ConstructorElement &&
-                annotation.element.enclosingElement.name == 'GenerateMocks',
-            orElse: () => null);
-        if (annotation == null) continue;
-        final generateMocksValue = annotation.computeConstantValue();
-        // TODO(srawlins): handle `generateMocksValue == null`?
-        final classesToMock = generateMocksValue.getField('classes');
-        if (classesToMock.isNull) {
-          throw InvalidMockitoAnnotationException(
-              'The "classes" argument has unknown types');
-        }
+    final classesToMock = <DartObject>[];
 
-        _buildMockClasses(classesToMock.toListValue(), lBuilder);
+    for (final element in entryLib.topLevelElements) {
+      final annotation = element.metadata.firstWhere(
+          (annotation) =>
+              annotation.element is ConstructorElement &&
+              annotation.element.enclosingElement.name == 'GenerateMocks',
+          orElse: () => null);
+      if (annotation == null) continue;
+      final generateMocksValue = annotation.computeConstantValue();
+      // TODO(srawlins): handle `generateMocksValue == null`?
+      final classesField = generateMocksValue.getField('classes');
+      if (classesField.isNull) {
+        throw InvalidMockitoAnnotationException(
+            'The "classes" argument has unknown types');
       }
+      classesToMock.addAll(classesField.toListValue());
+    }
+
+    final mockLibrary = Library((b) {
+      var mockLibraryInfo = _MockLibraryInfo(classesToMock);
+      b.body.addAll(mockLibraryInfo.fakeClasses);
+      b.body.addAll(mockLibraryInfo.mockClasses);
     });
 
     if (mockLibrary.body.isEmpty) {
@@ -73,10 +78,29 @@
     await buildStep.writeAsString(mockLibraryAsset, mockLibraryContent);
   }
 
+  @override
+  final buildExtensions = const {
+    '.dart': ['.mocks.dart']
+  };
+}
+
+class _MockLibraryInfo {
+  /// Mock classes to be added to the generated library.
+  final mockClasses = <Class>[];
+
+  /// Fake classes to be added to the library.
+  ///
+  /// A fake class is only generated when it is needed for non-nullable return
+  /// values.
+  final fakeClasses = <Class>[];
+
+  /// [ClassElement]s which are used in non-nullable return types, for which
+  /// fake classes are added to the generated library.
+  final fakedClassElements = <ClassElement>[];
+
   /// Build mock classes for [classesToMock], a list of classes obtained from a
   /// `@GenerateMocks` annotation.
-  void _buildMockClasses(
-      List<DartObject> classesToMock, LibraryBuilder lBuilder) {
+  _MockLibraryInfo(List<DartObject> classesToMock) {
     for (final classToMock in classesToMock) {
       final dartTypeToMock = classToMock.toTypeValue();
       if (dartTypeToMock == null) {
@@ -93,7 +117,7 @@
         }
         // TODO(srawlins): Catch when someone tries to generate mocks for an
         // un-subtypable class, like bool, String, FutureOr, etc.
-        lBuilder.body.add(_buildCodeForClass(dartTypeToMock, elementToMock));
+        mockClasses.add(_buildMockClass(dartTypeToMock, elementToMock));
       } else if (elementToMock is GenericFunctionTypeElement &&
           elementToMock.enclosingElement is FunctionTypeAliasElement) {
         throw InvalidMockitoAnnotationException(
@@ -107,8 +131,7 @@
     }
   }
 
-  Class _buildCodeForClass(
-      analyzer.DartType dartType, ClassElement classToMock) {
+  Class _buildMockClass(analyzer.DartType dartType, ClassElement classToMock) {
     final className = dartType.displayName;
 
     return Class((cBuilder) {
@@ -272,24 +295,92 @@
     } else if (type.isDartCoreString) {
       return literalString('');
     } else {
-      // TODO(srawlins): Returning null for now, but really this should only
-      // ever get to a state where we have to make a Fake class which implements
-      // the type, and return a no-op constructor call to that Fake class here.
-      return literalNull;
+      // This class is unknown; we must likely generate a fake class, and return
+      // an instance here.
+      return _dummyValueImplementing(type);
     }
   }
 
-  /// Returns a [Parameter] which matches [parameter].
-  Parameter _matchingParameter(ParameterElement parameter) =>
-      Parameter((pBuilder) {
-        pBuilder
-          ..name = parameter.displayName
-          ..type = _typeReference(parameter.type);
-        if (parameter.isNamed) pBuilder.named = true;
-        if (parameter.defaultValueCode != null) {
-          pBuilder.defaultTo = Code(parameter.defaultValueCode);
+  Expression _dummyValueImplementing(analyzer.DartType dartType) {
+    // For each type parameter on [classToMock], the Mock class needs a type
+    // parameter with same type variables, and a mirrored type argument for
+    // the "implements" clause.
+    var typeArguments = <Reference>[];
+    var elementToFake = dartType.element;
+    if (elementToFake is ClassElement) {
+      if (elementToFake.isEnum) {
+        return _typeReference(dartType).property(
+            elementToFake.fields.firstWhere((f) => f.isEnumConstant).name);
+      } else {
+        var fakeName = '_Fake${dartType.name}';
+        // Only make one fake class for each class that needs to be faked.
+        if (!fakedClassElements.contains(elementToFake)) {
+          fakeClasses.add(Class((cBuilder) {
+            cBuilder
+              ..name = fakeName
+              ..extend = refer('Fake', 'package:mockito/mockito.dart');
+            if (elementToFake.typeParameters != null) {
+              for (var typeParameter in elementToFake.typeParameters) {
+                cBuilder.types.add(_typeParameterReference(typeParameter));
+                typeArguments.add(refer(typeParameter.name));
+              }
+            }
+            cBuilder.implements.add(TypeReference((b) {
+              b
+                ..symbol = dartType.name
+                ..url = _typeImport(dartType)
+                ..types.addAll(typeArguments);
+            }));
+          }));
+          fakedClassElements.add(elementToFake);
         }
-      });
+        return refer(fakeName).newInstance([]);
+      }
+    } else if (dartType is analyzer.FunctionType) {
+      return Method((b) {
+        // The positional parameters in a FunctionType have no names. This
+        // counter lets us create unique dummy names.
+        var counter = 0;
+        for (final parameter in dartType.parameters) {
+          if (parameter.isRequiredPositional) {
+            b.requiredParameters
+                .add(_matchingParameter(parameter, defaultName: '__p$counter'));
+            counter++;
+          } else if (parameter.isOptionalPositional) {
+            b.optionalParameters
+                .add(_matchingParameter(parameter, defaultName: '__p$counter'));
+            counter++;
+          } else if (parameter.isNamed) {
+            b.optionalParameters.add(_matchingParameter(parameter));
+          }
+        }
+        if (dartType.returnType.isVoid) {
+          b.body = Code('');
+        } else {
+          b.body = _dummyValue(dartType.returnType).code;
+        }
+      }).closure;
+    }
+
+    // We shouldn't get here.
+    return literalNull;
+  }
+
+  /// Returns a [Parameter] which matches [parameter].
+  Parameter _matchingParameter(ParameterElement parameter,
+      {String defaultName}) {
+    String name =
+        parameter.name?.isEmpty ?? false ? defaultName : parameter.name;
+    return Parameter((pBuilder) {
+      pBuilder
+        ..name = name
+        ..type = _typeReference(parameter.type);
+      if (parameter.isNamed) pBuilder.named = true;
+      if (parameter.defaultValueCode != null) {
+        pBuilder.defaultTo = Code(parameter.defaultValueCode);
+      }
+    });
+  }
 
   /// Build a setter which overrides [setter], widening the single parameter
   /// type to be nullable if it is non-nullable.
@@ -340,18 +431,17 @@
   /// This creates proper references for:
   /// * [InterfaceType]s (classes, generic classes),
   /// * FunctionType parameters (like `void callback(int i)`),
-  /// * type aliases (typedefs), both new- and old-style.
+  /// * type aliases (typedefs), both new- and old-style,
+  /// * enums.
   // TODO(srawlins): Contribute this back to a common location, like
   // package:source_gen?
   Reference _typeReference(analyzer.DartType type) {
     if (type is analyzer.InterfaceType) {
-      return TypeReference((TypeReferenceBuilder trBuilder) {
-        trBuilder
+      return TypeReference((TypeReferenceBuilder b) {
+        b
           ..symbol = type.name
-          ..url = _typeImport(type);
-        for (var typeArgument in type.typeArguments) {
-          trBuilder.types.add(_typeReference(typeArgument));
-        }
+          ..url = _typeImport(type)
+          ..types.addAll(type.typeArguments.map(_typeReference));
       });
     } else if (type is analyzer.FunctionType) {
       GenericFunctionTypeElement element = type.element;
@@ -394,11 +484,6 @@
     // URIs.
     return library.source.uri.toString();
   }
-
-  @override
-  final buildExtensions = const {
-    '.dart': ['.mocks.dart']
-  };
 }
 
 /// An exception which is thrown when Mockito encounters an invalid annotation.
diff --git a/test/builder_test.dart b/test/builder_test.dart
index 4d2d00a..336ebd7 100644
--- a/test/builder_test.dart
+++ b/test/builder_test.dart
@@ -548,6 +548,145 @@
     );
   });
 
+  test('creates dummy non-null return values for unknown classes', () async {
+    await testBuilder(
+      buildMocks(BuilderOptions({})),
+      {
+        ...annotationsAsset,
+        ...simpleTestAsset,
+        'foo|lib/foo.dart': dedent(r'''
+        class Foo {
+          Bar m1() => Bar('name');
+        }
+        class Bar {
+          final String name;
+          Bar(this.name);
+        }
+        '''),
+      },
+      outputs: {
+        'foo|test/foo_test.mocks.dart': dedent(r'''
+        import 'package:mockito/mockito.dart' as _i1;
+        import 'package:foo/foo.dart' as _i2;
+
+        class _FakeBar extends _i1.Fake implements _i2.Bar {}
+
+        /// A class which mocks [Foo].
+        ///
+        /// See the documentation for Mockito's code generation for more information.
+        class MockFoo extends _i1.Mock implements _i2.Foo {
+          _i2.Bar m1() => super.noSuchMethod(Invocation.method(#m1, []), _FakeBar());
+        }
+        '''),
+      },
+    );
+  });
+
+  test('deduplicates fake classes', () async {
+    await testBuilder(
+      buildMocks(BuilderOptions({})),
+      {
+        ...annotationsAsset,
+        ...simpleTestAsset,
+        'foo|lib/foo.dart': dedent(r'''
+        class Foo {
+          Bar m1() => Bar('name1');
+          Bar m2() => Bar('name2');
+        }
+        class Bar {
+          final String name;
+          Bar(this.name);
+        }
+        '''),
+      },
+      outputs: {
+        'foo|test/foo_test.mocks.dart': dedent(r'''
+        import 'package:mockito/mockito.dart' as _i1;
+        import 'package:foo/foo.dart' as _i2;
+
+        class _FakeBar extends _i1.Fake implements _i2.Bar {}
+
+        /// A class which mocks [Foo].
+        ///
+        /// See the documentation for Mockito's code generation for more information.
+        class MockFoo extends _i1.Mock implements _i2.Foo {
+          _i2.Bar m1() => super.noSuchMethod(Invocation.method(#m1, []), _FakeBar());
+          _i2.Bar m2() => super.noSuchMethod(Invocation.method(#m2, []), _FakeBar());
+        }
+        '''),
+      },
+    );
+  });
+
+  test('creates dummy non-null return values for enums', () async {
+    await testBuilder(
+      buildMocks(BuilderOptions({})),
+      {
+        ...annotationsAsset,
+        ...simpleTestAsset,
+        'foo|lib/foo.dart': dedent(r'''
+        class Foo {
+          Bar m1() => Bar('name');
+        }
+        enum Bar {
+          one,
+          two,
+        }
+        '''),
+      },
+      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 {
+          _i2.Bar m1() => super.noSuchMethod(Invocation.method(#m1, []), _i2.Bar.one);
+        }
+        '''),
+      },
+    );
+  });
+
+  test('creates dummy non-null return values for functions', () async {
+    await testBuilder(
+      buildMocks(BuilderOptions({})),
+      {
+        ...annotationsAsset,
+        ...simpleTestAsset,
+        'foo|lib/foo.dart': dedent(r'''
+        class Foo {
+          void Function(int, [String]) m1() => (int i, [String s]) {};
+          void Function(Foo, {bool b}) m2() => (Foo f, {bool b}) {};
+          Foo Function() m3() => () => Foo();
+        }
+        '''),
+      },
+      outputs: {
+        'foo|test/foo_test.mocks.dart': dedent(r'''
+        import 'package:mockito/mockito.dart' as _i1;
+        import 'package:foo/foo.dart' as _i2;
+
+        class _FakeFoo extends _i1.Fake implements _i2.Foo {}
+
+        /// 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 Function(int, [String]) m1() => super
+              .noSuchMethod(Invocation.method(#m1, []), (int __p0, [String __p1]) {});
+          void Function(_i2.Foo, {bool b}) m2() => super
+              .noSuchMethod(Invocation.method(#m2, []), (_i2.Foo __p0, {bool b}) {});
+          _i2.Foo Function() m3() =>
+              super.noSuchMethod(Invocation.method(#m3, []), () => _FakeFoo());
+        }
+        '''),
+      },
+    );
+  });
+
   test('throws when GenerateMocks references an unresolved type', () async {
     expectBuilderThrows(
       assets: {