MockBuilder: Refactor exception-throwing paths; add more

* Almost all sanity checking was done in the _MockLibraryInfo constructor.
  Yuck. Move this to a new method.
* Throw when a mock of the same name already exists. Someone is likely
  migrating existing tests, and would be in for a surprise.
* Throw when asked to generate for two unique classes which have the same
  name. The code-generating API does not yet have a mechanism for
  user-supplied names to avoid such a conflict.
* Test that the GenerateMocks annotation can be written multiple times, and
  that mocks from each occurrence are generated.
* Check that `entryLib` is not null. I can't reproduce this in a test, but
  encountered it while testing on a real test: tableview_test.dart.
* Deduplicate classes which are asked to be mocked multiple times.

PiperOrigin-RevId: 315995820
diff --git a/lib/src/builder.dart b/lib/src/builder.dart
index b935030..db31c5a 100644
--- a/lib/src/builder.dart
+++ b/lib/src/builder.dart
@@ -39,9 +39,10 @@
   @override
   Future build(BuildStep buildStep) async {
     final entryLib = await buildStep.inputLibrary;
+    if (entryLib == null) return;
     final sourceLibIsNonNullable = entryLib.isNonNullableByDefault;
     final mockLibraryAsset = buildStep.inputId.changeExtension('.mocks.dart');
-    final classesToMock = <DartObject>[];
+    final objectsToMock = <DartObject>{};
 
     for (final element in entryLib.topLevelElements) {
       final annotation = element.metadata.firstWhere(
@@ -59,9 +60,14 @@
             'The GenerateMocks "classes" argument is missing, includes an '
             'unknown type, or includes an extension');
       }
-      classesToMock.addAll(classesField.toListValue());
+      objectsToMock.addAll(classesField.toListValue());
     }
 
+    var classesToMock =
+        _mapAnnotationValuesToClasses(objectsToMock, entryLib.typeProvider);
+
+    _checkClassesToMockAreValid(classesToMock, entryLib);
+
     final mockLibrary = Library((b) {
       var mockLibraryInfo = _MockLibraryInfo(classesToMock,
           sourceLibIsNonNullable: sourceLibIsNonNullable,
@@ -84,6 +90,99 @@
     await buildStep.writeAsString(mockLibraryAsset, mockLibraryContent);
   }
 
+  /// Map the values passed to the GenerateMocks annotation to the classes which
+  /// they represent.
+  ///
+  /// This function is responsible for ensuring that each value is an
+  /// appropriate target for mocking. It will throw an
+  /// [InvalidMockitoAnnotationException] under various conditions.
+  List<analyzer.DartType> _mapAnnotationValuesToClasses(
+      Iterable<DartObject> objectsToMock, TypeProvider typeProvider) {
+    var classesToMock = <analyzer.DartType>[];
+
+    for (final objectToMock in objectsToMock) {
+      final typeToMock = objectToMock.toTypeValue();
+      if (typeToMock == null) {
+        throw InvalidMockitoAnnotationException(
+            'The "classes" argument includes a non-type: $objectToMock');
+      }
+
+      final elementToMock = typeToMock.element;
+      if (elementToMock is ClassElement) {
+        if (elementToMock.isEnum) {
+          throw InvalidMockitoAnnotationException(
+              'The "classes" argument includes an enum: '
+              '${elementToMock.displayName}');
+        }
+        if (typeProvider.nonSubtypableClasses.contains(elementToMock)) {
+          throw InvalidMockitoAnnotationException(
+              'The "classes" argument includes a non-subtypable type: '
+              '${elementToMock.displayName}. It is illegal to subtype this '
+              'type.');
+        }
+        classesToMock.add(typeToMock);
+      } else if (elementToMock is GenericFunctionTypeElement &&
+          elementToMock.enclosingElement is FunctionTypeAliasElement) {
+        throw InvalidMockitoAnnotationException(
+            'The "classes" argument includes a typedef: '
+            '${elementToMock.enclosingElement.displayName}');
+      } else {
+        throw InvalidMockitoAnnotationException(
+            'The "classes" argument includes a non-class: '
+            '${elementToMock.displayName}');
+      }
+    }
+    return classesToMock;
+  }
+
+  void _checkClassesToMockAreValid(
+      List<analyzer.DartType> classesToMock, LibraryElement entryLib) {
+    var classesInEntryLib = entryLib.topLevelElements.whereType<ClassElement>();
+    var classNamesToMock = <String, ClassElement>{};
+    for (var class_ in classesToMock) {
+      var name = class_.element.name;
+      if (classNamesToMock.containsKey(name)) {
+        var firstSource = classNamesToMock[name].source.fullName;
+        var secondSource = class_.element.source.fullName;
+        // TODO(srawlins): Support an optional @GenerateMocks API that allows
+        // users to choose names. One class might be named MockFoo and the other
+        // named MockPbFoo, for example.
+        throw InvalidMockitoAnnotationException(
+            'The GenerateMocks "classes" argument contains two classes with '
+            'the same name: $name. One declared in $firstSource, the other in '
+            '$secondSource.');
+      }
+      classNamesToMock[name] = class_.element as ClassElement;
+    }
+    var classNamesToGenerate = classNamesToMock.keys.map((name) => 'Mock$name');
+    classNamesToMock.forEach((name, element) {
+      var conflictingClass = classesInEntryLib.firstWhere(
+          (c) => c.name == 'Mock${element.name}',
+          orElse: () => null);
+      if (conflictingClass != null) {
+        throw InvalidMockitoAnnotationException(
+            'The GenerateMocks "classes" argument contains a class which '
+            'conflicts with another class declared in this library: '
+            '${conflictingClass.name}');
+      }
+      var preexistingMock = classesInEntryLib.firstWhere(
+          (c) =>
+              c.interfaces.map((type) => type.element).contains(element) &&
+              _isMockClass(c.supertype),
+          orElse: () => null);
+      if (preexistingMock != null) {
+        throw InvalidMockitoAnnotationException(
+            'The GenerateMocks "classes" argument contains a class which '
+            'appears to already be mocked inline: ${preexistingMock.name}');
+      }
+    });
+  }
+
+  /// Return whether [type] is the Mock class declared by mockito.
+  bool _isMockClass(analyzer.InterfaceType type) =>
+      type.element.name == 'Mock' &&
+      type.element.source.fullName.endsWith('lib/src/mock.dart');
+
   @override
   final buildExtensions = const {
     '.dart': ['.mocks.dart']
@@ -114,44 +213,16 @@
 
   /// Build mock classes for [classesToMock], a list of classes obtained from a
   /// `@GenerateMocks` annotation.
-  _MockLibraryInfo(List<DartObject> classesToMock,
+  _MockLibraryInfo(List<analyzer.DartType> classesToMock,
       {this.sourceLibIsNonNullable, this.typeProvider, this.typeSystem}) {
     for (final classToMock in classesToMock) {
-      final dartTypeToMock = classToMock.toTypeValue();
-      if (dartTypeToMock == null) {
-        throw InvalidMockitoAnnotationException(
-            'The "classes" argument includes a non-type: $classToMock');
-      }
-
-      final elementToMock = dartTypeToMock.element;
-      if (elementToMock is ClassElement) {
-        if (elementToMock.isEnum) {
-          throw InvalidMockitoAnnotationException(
-              'The "classes" argument includes an enum: '
-              '${elementToMock.displayName}');
-        }
-        if (typeProvider.nonSubtypableClasses.contains(elementToMock)) {
-          throw InvalidMockitoAnnotationException(
-              'The "classes" argument includes a non-subtypable type: '
-              '${elementToMock.displayName}. It is illegal to subtype this '
-              'type.');
-        }
-        mockClasses.add(_buildMockClass(dartTypeToMock, elementToMock));
-      } else if (elementToMock is GenericFunctionTypeElement &&
-          elementToMock.enclosingElement is FunctionTypeAliasElement) {
-        throw InvalidMockitoAnnotationException(
-            'The "classes" argument includes a typedef: '
-            '${elementToMock.enclosingElement.displayName}');
-      } else {
-        throw InvalidMockitoAnnotationException(
-            'The "classes" argument includes a non-class: '
-            '${elementToMock.displayName}');
-      }
+      mockClasses.add(_buildMockClass(classToMock));
     }
   }
 
-  Class _buildMockClass(analyzer.DartType dartType, ClassElement classToMock) {
-    final className = dartType.displayName;
+  Class _buildMockClass(analyzer.DartType dartType) {
+    final classToMock = dartType.element as ClassElement;
+    final className = dartType.name;
 
     return Class((cBuilder) {
       cBuilder
@@ -330,6 +401,9 @@
         return _typeReference(dartType).property(
             elementToFake.fields.firstWhere((f) => f.isEnumConstant).name);
       } else {
+        // There is a potential for these names to collide. If one mock class
+        // requires a fake for a certain Foo, and another mock class requires a
+        // fake for a different Foo, they will collide.
         var fakeName = '_Fake${dartType.name}';
         // Only make one fake class for each class that needs to be faked.
         if (!fakedClassElements.contains(elementToFake)) {
diff --git a/test/builder_test.dart b/test/builder_test.dart
index 409a23c..b703f40 100644
--- a/test/builder_test.dart
+++ b/test/builder_test.dart
@@ -33,6 +33,15 @@
 '''
 };
 
+const mockitoAssets = {
+  'mockito|lib/mockito.dart': '''
+export 'src/mock.dart';
+''',
+  'mockito|lib/src/mock.dart': '''
+class Mock {}
+'''
+};
+
 const simpleTestAsset = {
   'foo|test/foo_test.dart': '''
 import 'package:foo/foo.dart';
@@ -176,6 +185,60 @@
     );
   });
 
+  test('deduplicates classes listed multiply in GenerateMocks', () async {
+    await _testWithNonNullable(
+      {
+        ...annotationsAsset,
+        'foo|lib/foo.dart': dedent(r'''
+        class Foo {}
+        '''),
+        'foo|test/foo_test.dart': '''
+        import 'package:foo/foo.dart';
+        import 'package:mockito/annotations.dart';
+        @GenerateMocks([Foo, Foo])
+        void main() {}
+        '''
+      },
+      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('generates mock classes from multiple annotations', () async {
+    await _testWithNonNullable(
+      {
+        ...annotationsAsset,
+        'foo|lib/foo.dart': dedent(r'''
+        class Foo {}
+        class Bar {}
+        '''),
+        'foo|test/foo_test.dart': '''
+        import 'package:foo/foo.dart';
+        import 'package:mockito/annotations.dart';
+        @GenerateMocks([Foo])
+        void fooTests() {}
+        @GenerateMocks([Bar])
+        void barTests() {}
+        '''
+      },
+      outputs: {
+        'foo|test/foo_test.mocks.dart': _containsAllOf(
+          'class MockFoo extends _i1.Mock implements _i2.Foo {}',
+          'class MockBar extends _i1.Mock implements _i2.Bar {}',
+        ),
+      },
+    );
+  });
+
   test('generates generic mock classes', () async {
     await _expectSingleNonNullableOutput(
       dedent(r'''
@@ -1040,6 +1103,74 @@
     );
   });
 
+  test('throws when two distinct classes with the same name are mocked',
+      () async {
+    _expectBuilderThrows(
+      assets: {
+        ...annotationsAsset,
+        'foo|lib/a.dart': dedent(r'''
+        class Foo {}
+        '''),
+        'foo|lib/b.dart': dedent(r'''
+        class Foo {}
+        '''),
+        'foo|test/foo_test.dart': dedent('''
+        import 'package:foo/a.dart' as a;
+        import 'package:foo/b.dart' as b;
+        import 'package:mockito/annotations.dart';
+        @GenerateMocks([a.Foo, b.Foo])
+        void main() {}
+        '''),
+      },
+      message: contains(
+          'contains two classes with the same name: Foo. One declared in '
+          '/foo/lib/a.dart, the other in /foo/lib/b.dart'),
+    );
+  });
+
+  test('throws when a mock class of the same name already exists', () async {
+    _expectBuilderThrows(
+      assets: {
+        ...annotationsAsset,
+        'foo|lib/foo.dart': dedent(r'''
+        class Foo {}
+        '''),
+        'foo|test/foo_test.dart': dedent('''
+        import 'package:foo/foo.dart';
+        import 'package:mockito/annotations.dart';
+        @GenerateMocks([Foo])
+        void main() {}
+        class MockFoo {}
+        '''),
+      },
+      message: contains(
+          'contains a class which conflicts with another class declared in '
+          'this library: MockFoo'),
+    );
+  });
+
+  test('throws when a mock class of class-to-mock already exists', () async {
+    _expectBuilderThrows(
+      assets: {
+        ...annotationsAsset,
+        ...mockitoAssets,
+        'foo|lib/foo.dart': dedent(r'''
+        class Foo {}
+        '''),
+        'foo|test/foo_test.dart': dedent('''
+        import 'package:foo/foo.dart';
+        import 'package:mockito/annotations.dart';
+        import 'package:mockito/mockito.dart';
+        @GenerateMocks([Foo])
+        void main() {}
+        class FakeFoo extends Mock implements Foo {}
+        '''),
+      },
+      message: contains(
+          'contains a class which appears to already be mocked inline: FakeFoo'),
+    );
+  });
+
   test('throws when GenerateMocks references a non-type', () async {
     _expectBuilderThrows(
       assets: {