Use proper generic types in a generated mock class which comes from a "custom mock" annotation with implicit type arguments.

Given a method which references
type variables defined on their enclosing class (for example, `T` in
`class Foo<T>`), mockito will now correctly reference `T` in generated code.

Fixes https://github.com/dart-lang/mockito/issues/422

PiperOrigin-RevId: 377571931
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7554929..2975682 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,11 @@
 * Override `toString` implementation on generated Fakes in order to match the
   signature of an overriding method which adds optional parameters.
   [#371](https://github.com/dart-lang/mockito/issues/371)
+  Properly type methods in a generated mock class which comes from a "custom
+  mock" annotation referencing an implicit type. Given a method which references
+  type variables defined on their enclosing class (for example, `T` in
+  `class Foo<T>`), mockito will now correctly reference `T` in generated code.
+  [#422](https://github.com/dart-lang/mockito/issues/422)
 
 ## 5.0.9
 
diff --git a/lib/src/builder.dart b/lib/src/builder.dart
index a4d64a9..9d29c31 100644
--- a/lib/src/builder.dart
+++ b/lib/src/builder.dart
@@ -389,8 +389,10 @@
             'Mockito cannot mock `dynamic`');
       }
       final type = _determineDartType(typeToMock, entryLib.typeProvider);
-      // [type] is `Foo<dynamic>` for generic classes. Switch to declaration,
-      // which will yield `Foo<T>`.
+      // For a generic class like `Foo<T>` or `Foo<T extends num>`, a type
+      // literal (`Foo`) cannot express type arguments. The type argument(s) on
+      // `type` have been instantiated to bounds here. Switch to the
+      // declaration, which will be an uninstantiated type.
       final declarationType =
           (type.element.declaration as ClassElement).thisType;
       final mockName = 'Mock${declarationType.element.name}';
@@ -409,6 +411,14 @@
               'arguments on MockSpec(), in @GenerateMocks.');
         }
         var type = _determineDartType(typeToMock, entryLib.typeProvider);
+
+        if (!type.hasExplicitTypeArguments) {
+          // We assume the type was given without explicit type arguments. In
+          // this case the type argument(s) on `type` have been instantiated to
+          // bounds. Switch to the declaration, which will be an uninstantiated
+          // type.
+          type = (type.element.declaration as ClassElement).thisType;
+        }
         final mockName = mockSpec.getField('mockName')!.toSymbolValue() ??
             'Mock${type.element.name}';
         final returnNullOnMissingStub =
@@ -734,39 +744,6 @@
     }
   }
 
-  bool _hasExplicitTypeArguments(analyzer.InterfaceType type) {
-    if (type.typeArguments == null) return false;
-
-    // If it appears that one type argument was given, then they all were. This
-    // returns the wrong result when the type arguments given are all `dynamic`,
-    // or are each equal to the bound of the corresponding type parameter. There
-    // may not be a way to get around this.
-    for (var i = 0; i < type.typeArguments.length; i++) {
-      var typeArgument = type.typeArguments[i];
-      // If [typeArgument] is a type parameter, this indicates that no type
-      // arguments were passed. This likely came from the 'classes' argument of
-      // GenerateMocks, and [type] is the declaration type (`Foo<T>` vs
-      // `Foo<dynamic>`).
-      if (typeArgument is analyzer.TypeParameterType) return false;
-
-      // If [type] was given to @GenerateMocks as a Type, and no explicit type
-      // argument is given, [typeArgument] is `dynamic` (_not_ the bound, as one
-      // might think). We determine that an explicit type argument was given if
-      // it is not `dynamic`.
-      if (typeArgument.isDynamic) continue;
-
-      // If, on the other hand, [type] was given to @GenerateMock as a type
-      // argument to `Of()`, and no type argument is given, [typeArgument] is
-      // the bound of the corresponding type paramter (dynamic or otherwise). We
-      // determine that an explicit type argument was given if [typeArgument] is
-      // not [bound].
-      var bound =
-          type.element.typeParameters[i].bound ?? typeProvider.dynamicType;
-      if (!typeArgument.isDynamic && typeArgument != bound) return true;
-    }
-    return false;
-  }
-
   Class _buildMockClass(_MockTarget mockTarget) {
     final typeToMock = mockTarget.classType;
     final classToMock = mockTarget.classElement;
@@ -790,7 +767,7 @@
       // parameter with same type variables, and a mirrored type argument for
       // the "implements" clause.
       var typeArguments = <Reference>[];
-      if (_hasExplicitTypeArguments(typeToMock)) {
+      if (typeToMock.hasExplicitTypeArguments) {
         // [typeToMock] is a reference to a type with type arguments (for
         // example: `Foo<int>`). Generate a non-generic mock class which
         // implements the mock target with said type arguments. For example:
@@ -1561,3 +1538,37 @@
         name == 'Uint64List';
   }
 }
+
+extension on analyzer.InterfaceType {
+  bool get hasExplicitTypeArguments {
+    if (typeArguments == null) return false;
+
+    // If it appears that one type argument was given, then they all were. This
+    // returns the wrong result when the type arguments given are all `dynamic`,
+    // or are each equal to the bound of the corresponding type parameter. There
+    // may not be a way to get around this.
+    for (var i = 0; i < typeArguments.length; i++) {
+      final typeArgument = typeArguments[i];
+      // If [typeArgument] is a type parameter, this indicates that no type
+      // arguments were passed. This likely came from the 'classes' argument of
+      // GenerateMocks, and [type] is the declaration type (`Foo<T>` vs
+      // `Foo<dynamic>`).
+      if (typeArgument is analyzer.TypeParameterType) return false;
+
+      // If [type] was given to @GenerateMocks as a Type, and no explicit type
+      // argument is given, [typeArgument] is `dynamic` (_not_ the bound, as one
+      // might think). We determine that an explicit type argument was given if
+      // it is not `dynamic`.
+      if (typeArgument.isDynamic) continue;
+
+      // If, on the other hand, [type] was given to @GenerateMock as a type
+      // argument to `MockSpec()`, and no type argument is given, [typeArgument]
+      // is the bound of the corresponding type paramter (`dynamic` or
+      // otherwise). We determine that an explicit type argument was given if
+      // [typeArgument] is not equal to [bound].
+      final bound = element.typeParameters[i].bound;
+      if (!typeArgument.isDynamic && typeArgument != bound) return true;
+    }
+    return false;
+  }
+}
diff --git a/test/builder/custom_mocks_test.dart b/test/builder/custom_mocks_test.dart
index f4c6340..1bf1c54 100644
--- a/test/builder/custom_mocks_test.dart
+++ b/test/builder/custom_mocks_test.dart
@@ -121,6 +121,24 @@
         contains('class MockFoo<T> extends _i1.Mock implements _i2.Foo<T>'));
   });
 
+  test('without type arguments, generates generic method types', () async {
+    var mocksContent = await buildWithNonNullable({
+      ...annotationsAsset,
+      'foo|lib/foo.dart': dedent(r'''
+        class Foo<T> {
+          List<T> f;
+        }
+        '''),
+      'foo|test/foo_test.dart': '''
+        import 'package:foo/foo.dart';
+        import 'package:mockito/annotations.dart';
+        @GenerateMocks([], customMocks: [MockSpec<Foo>(as: #MockFoo)])
+        void main() {}
+        '''
+    });
+    expect(mocksContent, contains('List<T> get f =>'));
+  });
+
   test('generates a generic mock class with type arguments', () async {
     var mocksContent = await buildWithNonNullable({
       ...annotationsAsset,