Check the annotation AST for presence of explicit type args

Assuming `Foo` is declared as `class Foo<T extends Bar> { /* ... */ }`,
there is indeed no way to take apart `Foo` from `Foo<Bar>` once it gets
into the analyzer internals and becomes an `InterfaceType`. The AST still
preserve this information though but we are not looking at the
annotation AST, instead we utilize the `computeContantValue` that gives us
`DartObject` that is easier to work with but now `Foo` and `Foo<Bar>` are
smashed together. It appears to me that there is currently even no public
API to get the AST for the annotation, so I had to cheat with
```dart
(annotation as ElementAnnotationImpl).annotationAst
```
to get the actual AST. When we can use it to just check the presence of
type arguments and pass the results down.

This is still rather unclean, not using the best practice and doesn't work in
all cases where constant evaluation does. For example, with constant evaluation
one can declared `MockSpec` as a constant outside of the annotation and then
use it, but it breaks with direct AST access. I haven't found any other
examples so far. Not being able to use `MockSpec`s defined as constants
doesn't really look like a big deal though: there is no reasonable way to
use a `MockSpec` instance more than once anyway, I think.

On the positive side, this change fixes the bug without changing the API.

Fixes https://github.com/dart-lang/mockito/issues/559 and
https://github.com/dart-lang/mockito/issues/563.

Also opens a way for fixing https://github.com/dart-lang/mockito/issues/562.

PiperOrigin-RevId: 467867637
diff --git a/lib/src/builder.dart b/lib/src/builder.dart
index 11aaab6..345c689 100644
--- a/lib/src/builder.dart
+++ b/lib/src/builder.dart
@@ -14,6 +14,7 @@
 
 import 'dart:collection';
 
+import 'package:analyzer/dart/ast/ast.dart' as ast;
 import 'package:analyzer/dart/constant/value.dart';
 import 'package:analyzer/dart/element/element.dart';
 import 'package:analyzer/dart/element/nullability_suffix.dart';
@@ -22,6 +23,9 @@
 import 'package:analyzer/dart/element/type_system.dart';
 import 'package:analyzer/dart/element/visitor.dart';
 // ignore: implementation_imports
+import 'package:analyzer/src/dart/element/element.dart'
+    show ElementAnnotationImpl;
+// ignore: implementation_imports
 import 'package:analyzer/src/dart/element/inheritance_manager3.dart'
     show InheritanceManager3, Name;
 // ignore: implementation_imports
@@ -373,6 +377,10 @@
 
   final Map<String, ExecutableElement> fallbackGenerators;
 
+  /// Instantiated mock was requested, i.e. `MockSpec<Foo<Bar>>`,
+  /// instead of `MockSpec<Foo>`.
+  final bool hasExplicitTypeArguments;
+
   _MockTarget(
     this.classType,
     this.mockName, {
@@ -380,6 +388,7 @@
     required this.onMissingStub,
     required this.unsupportedMembers,
     required this.fallbackGenerators,
+    this.hasExplicitTypeArguments = false,
   });
 
   InterfaceElement get classElement => classType.element2;
@@ -437,6 +446,27 @@
         entryLib, mockTargets.toList(), inheritanceManager);
   }
 
+  static bool _hasExplicitTypeArgs(ast.CollectionElement mockSpec) {
+    if (mockSpec is! ast.InstanceCreationExpression) {
+      throw InvalidMockitoAnnotationException(
+          'Mockspecs must be constructor calls inside the annotation, '
+          'please inline them if you are using a variable');
+    }
+    return (mockSpec.constructorName.type2.typeArguments?.arguments.firstOrNull
+                as ast.NamedType?)
+            ?.typeArguments !=
+        null;
+  }
+
+  static ast.ListLiteral? _customMocksAst(ast.Annotation annotation) =>
+      (annotation.arguments!.arguments
+                  .firstWhereOrNull((arg) => arg is ast.NamedExpression)
+              as ast.NamedExpression?)
+          ?.expression as ast.ListLiteral?;
+
+  static ast.ListLiteral _niceMocksAst(ast.Annotation annotation) =>
+      annotation.arguments!.arguments.first as ast.ListLiteral;
+
   static Iterable<_MockTarget> _mockTargetsFromGenerateMocks(
       ElementAnnotation annotation, LibraryElement entryLib) {
     final generateMocksValue = annotation.computeConstantValue()!;
@@ -476,15 +506,21 @@
     }
     final customMocksField = generateMocksValue.getField('customMocks');
     if (customMocksField != null && !customMocksField.isNull) {
+      final customMocksAsts =
+          _customMocksAst(annotation.annotationAst)?.elements ??
+              <ast.CollectionElement>[];
       mockTargets.addAll(customMocksField.toListValue()!.mapIndexed(
-          (index, mockSpec) =>
-              _mockTargetFromMockSpec(mockSpec, entryLib, index)));
+          (index, mockSpec) => _mockTargetFromMockSpec(
+              mockSpec, entryLib, index, customMocksAsts.toList())));
     }
     return mockTargets;
   }
 
   static _MockTarget _mockTargetFromMockSpec(
-      DartObject mockSpec, LibraryElement entryLib, int index,
+      DartObject mockSpec,
+      LibraryElement entryLib,
+      int index,
+      List<ast.CollectionElement> mockSpecAsts,
       {bool nice = false}) {
     final mockSpecType = mockSpec.type as analyzer.InterfaceType;
     assert(mockSpecType.typeArguments.length == 1);
@@ -496,8 +532,8 @@
     }
     var type = _determineDartType(typeToMock, entryLib.typeProvider);
 
-    if (!type.hasExplicitTypeArguments) {
-      // We assume the type was given without explicit type arguments. In
+    if (!_hasExplicitTypeArgs(mockSpecAsts[index])) {
+      // 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.
@@ -581,6 +617,7 @@
       onMissingStub: onMissingStub,
       unsupportedMembers: unsupportedMembers,
       fallbackGenerators: _extractFallbackGenerators(fallbackGeneratorObjects),
+      hasExplicitTypeArguments: _hasExplicitTypeArgs(mockSpecAsts[index]),
     );
   }
 
@@ -592,8 +629,11 @@
       throw InvalidMockitoAnnotationException(
           'The GenerateNiceMocks "mockSpecs" argument is missing');
     }
+    final mockSpecAsts = _niceMocksAst(annotation.annotationAst).elements;
     return mockSpecsField.toListValue()!.mapIndexed((index, mockSpec) =>
-        _mockTargetFromMockSpec(mockSpec, entryLib, index, nice: true));
+        _mockTargetFromMockSpec(
+            mockSpec, entryLib, index, mockSpecAsts.toList(),
+            nice: true));
   }
 
   static Map<String, ExecutableElement> _extractFallbackGenerators(
@@ -1006,7 +1046,7 @@
       // parameter with same type variables, and a mirrored type argument for
       // the "implements" clause.
       var typeArguments = <Reference>[];
-      if (typeToMock.hasExplicitTypeArguments) {
+      if (mockTarget.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:
@@ -1968,38 +2008,6 @@
   }
 }
 
-extension on analyzer.InterfaceType {
-  bool get hasExplicitTypeArguments {
-    // 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 = element2.typeParameters[i].bound;
-      if (!typeArgument.isDynamic && typeArgument != bound) return true;
-    }
-    return false;
-  }
-}
-
 extension on TypeSystem {
   bool _returnTypeIsNonNullable(ExecutableElement method) =>
       isPotentiallyNonNullable(method.returnType);
@@ -2037,3 +2045,11 @@
 
 bool _needsOverrideForVoidStub(ExecutableElement method) =>
     method.returnType.isVoid || method.returnType.isFutureOfVoid;
+
+/// This casts `ElementAnnotation` to the internal `ElementAnnotationImpl`
+/// class, since analyzer doesn't provide public interface to access
+/// the annotation AST currently.
+extension on ElementAnnotation {
+  ast.Annotation get annotationAst =>
+      (this as ElementAnnotationImpl).annotationAst;
+}
diff --git a/test/builder/custom_mocks_test.dart b/test/builder/custom_mocks_test.dart
index 8702d9b..8636cfe 100644
--- a/test/builder/custom_mocks_test.dart
+++ b/test/builder/custom_mocks_test.dart
@@ -199,6 +199,28 @@
             'class MockFooOfIntBar extends _i1.Mock implements _i2.Foo<int, _i2.Bar>'));
   });
 
+  test('generates a generic mock class with lower bound type arguments',
+      () async {
+    var mocksContent = await buildWithNonNullable({
+      ...annotationsAsset,
+      'foo|lib/foo.dart': dedent(r'''
+        class Foo<T, U extends Bar> {}
+        class Bar {}
+        '''),
+      'foo|test/foo_test.dart': '''
+        import 'package:foo/foo.dart';
+        import 'package:mockito/annotations.dart';
+        @GenerateMocks(
+            [], customMocks: [MockSpec<Foo<dynamic, Bar>>(as: #MockFoo)])
+        void main() {}
+        '''
+    });
+    expect(
+        mocksContent,
+        contains(
+            'class MockFoo extends _i1.Mock implements _i2.Foo<dynamic, _i2.Bar>'));
+  });
+
   test('generates a generic mock class with nullable type arguments', () async {
     var mocksContent = await buildWithNonNullable({
       ...annotationsAsset,