MockBuilder: Match parameter default values.

When overriding a class, the default value of a parameter must be repeated; it is not inferred, and it cannot be changed.

While default values of optional parameters must be const, they can still be complex. They can be literals, like strings or numbers, or `const`-instantiated objects, using named constructors, and constructor arguments. To reproduce all of this code, and include the proper imports, we use source_gen's Revivable.

PiperOrigin-RevId: 322854095
diff --git a/lib/src/builder.dart b/lib/src/builder.dart
index cb3ff7d..67a3b77 100644
--- a/lib/src/builder.dart
+++ b/lib/src/builder.dart
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import 'package:analyzer/dart/constant/value.dart';
 import 'package:analyzer/dart/element/element.dart';
 import 'package:analyzer/dart/element/type.dart' as analyzer;
 import 'package:analyzer/dart/element/type_provider.dart';
@@ -20,6 +21,7 @@
 import 'package:code_builder/code_builder.dart';
 import 'package:dart_style/dart_style.dart';
 import 'package:meta/meta.dart';
+import 'package:source_gen/source_gen.dart';
 
 /// For a source Dart library, generate the mocks referenced therein.
 ///
@@ -752,11 +754,97 @@
         ..type = _typeReference(parameter.type, forceNullable: forceNullable);
       if (parameter.isNamed) pBuilder.named = true;
       if (parameter.defaultValueCode != null) {
-        pBuilder.defaultTo = Code(parameter.defaultValueCode);
+        try {
+          pBuilder.defaultTo =
+              _expressionFromDartObject(parameter.computeConstantValue()).code;
+        } on _ReviveError catch (e) {
+          final method = parameter.enclosingElement;
+          final clazz = method.enclosingElement;
+          throw InvalidMockitoAnnotationException(
+              'Mockito cannot generate a valid stub for method '
+              "'${clazz.displayName}.${method.displayName}'; parameter "
+              "'${parameter.displayName}' causes a problem: ${e.message}");
+        }
       }
     });
   }
 
+  /// Creates a code_builder [Expression] from [object], a constant object from
+  /// analyzer.
+  ///
+  /// This is very similar to Angular's revive code, in
+  /// angular_compiler/analyzer/di/injector.dart.
+  Expression _expressionFromDartObject(DartObject object) {
+    final constant = ConstantReader(object);
+    if (constant.isNull) {
+      return literalNull;
+    } else if (constant.isBool) {
+      return literalBool(constant.boolValue);
+    } else if (constant.isDouble) {
+      return literalNum(constant.doubleValue);
+    } else if (constant.isInt) {
+      return literalNum(constant.intValue);
+    } else if (constant.isString) {
+      return literalString(constant.stringValue, raw: true);
+    } else if (constant.isList) {
+      return literalConstList([
+        for (var element in constant.listValue)
+          _expressionFromDartObject(element)
+      ]);
+    } else if (constant.isMap) {
+      return literalConstMap({
+        for (var pair in constant.mapValue.entries)
+          _expressionFromDartObject(pair.key):
+              _expressionFromDartObject(pair.value)
+      });
+    } else if (constant.isSet) {
+      return literalConstSet({
+        for (var element in constant.setValue)
+          _expressionFromDartObject(element)
+      });
+    } else if (constant.isType) {
+      // TODO(srawlins): It seems like this might be revivable, but Angular
+      // does not revive Types; we should investigate this if users request it.
+      throw _ReviveError('default value is a Type: ${object.toTypeValue()}.');
+    } else {
+      // If [constant] is not null, a literal, or a type, then it must be an
+      // object constructed with `const`. Revive it.
+      var revivable = constant.revive();
+      if (revivable.isPrivate) {
+        final privateReference = revivable.accessor?.isNotEmpty == true
+            ? '${revivable.source}::${revivable.accessor}'
+            : '${revivable.source}';
+        throw _ReviveError(
+            'default value has a private type: $privateReference.');
+      }
+      if (revivable.source.fragment.isEmpty) {
+        // We can create this invocation by referring to a const field.
+        return refer(revivable.accessor, _typeImport(object.type));
+      }
+
+      final name = revivable.source.fragment;
+      final positionalArgs = [
+        for (var argument in revivable.positionalArguments)
+          _expressionFromDartObject(argument)
+      ];
+      final namedArgs = {
+        for (var pair in revivable.namedArguments.entries)
+          pair.key: _expressionFromDartObject(pair.value)
+      };
+      final type = refer(name, _typeImport(object.type));
+      if (revivable.accessor.isNotEmpty) {
+        return type.constInstanceNamed(
+          revivable.accessor,
+          positionalArgs,
+          namedArgs,
+          // No type arguments. See
+          // https://github.com/dart-lang/source_gen/issues/478.
+        );
+      }
+      return type.constInstance(positionalArgs, namedArgs);
+    }
+  }
+
   /// Build a getter which overrides [getter].
   ///
   /// This new method just calls `super.noSuchMethod`, optionally passing a
@@ -903,6 +991,17 @@
   }
 }
 
+/// An exception thrown when reviving a potentially deep value in a constant.
+///
+/// This exception should always be caught within this library. An
+/// [InvalidMockitoAnnotationException] can be presented to the user after
+/// catching this exception.
+class _ReviveError implements Exception {
+  final String message;
+
+  _ReviveError(this.message);
+}
+
 /// An exception which is thrown when Mockito encounters an invalid annotation.
 class InvalidMockitoAnnotationException implements Exception {
   final String message;
diff --git a/pubspec.yaml b/pubspec.yaml
index c44c970..a52743b 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -15,6 +15,7 @@
   dart_style: ^1.3.6
   matcher: ^0.12.3
   meta: '>=1.0.4 <1.2.0'
+  source_gen: ^0.9.7
   test_api: ^0.2.1
 
 dev_dependencies:
diff --git a/test/builder/auto_mocks_test.dart b/test/builder/auto_mocks_test.dart
index 1ebeb63..0a6d762 100644
--- a/test/builder/auto_mocks_test.dart
+++ b/test/builder/auto_mocks_test.dart
@@ -132,11 +132,23 @@
     );
   });
 
+  test('overrides methods, matching required positional parameters', () async {
+    await _expectSingleNonNullableOutput(
+      dedent(r'''
+      class Foo {
+        void m(int a) {}
+      }
+      '''),
+      _containsAllOf('void m(int? a) =>',
+          'super.noSuchMethod(Invocation.method(#m, [a]));'),
+    );
+  });
+
   test('overrides methods, matching optional positional parameters', () async {
     await _expectSingleNonNullableOutput(
       dedent(r'''
       class Foo {
-          void m(int a, [int b, int c = 0]) {}
+        void m(int a, [int b, int c = 0]) {}
       }
       '''),
       _containsAllOf('void m(int? a, [int? b, int? c = 0]) =>',
@@ -148,7 +160,7 @@
     await _expectSingleNonNullableOutput(
       dedent(r'''
       class Foo {
-          void m(int a, {int b, int c = 0}) {}
+        void m(int a, {int b, int c = 0}) {}
       }
       '''),
       _containsAllOf('void m(int? a, {int? b, int? c = 0}) =>',
@@ -156,6 +168,275 @@
     );
   });
 
+  test('matches parameter default values', () async {
+    await _expectSingleNonNullableOutput(
+      dedent(r'''
+      class Foo {
+        void m([int a, int b = 0]) {}
+      }
+      '''),
+      _containsAllOf('void m([int? a, int? b = 0]) =>',
+          'super.noSuchMethod(Invocation.method(#m, [a, b]));'),
+    );
+  });
+
+  test('matches boolean literal parameter default values', () async {
+    await _expectSingleNonNullableOutput(
+      dedent(r'''
+      class Foo {
+        void m([bool a = true, bool b = false]) {}
+      }
+      '''),
+      _containsAllOf('void m([bool? a = true, bool? b = false]) =>',
+          'super.noSuchMethod(Invocation.method(#m, [a, b]));'),
+    );
+  });
+
+  test('matches number literal parameter default values', () async {
+    await _expectSingleNonNullableOutput(
+      dedent(r'''
+      class Foo {
+        void m([int a = 0, double b = 0.5]) {}
+      }
+      '''),
+      _containsAllOf('void m([int? a = 0, double? b = 0.5]) =>',
+          'super.noSuchMethod(Invocation.method(#m, [a, b]));'),
+    );
+  });
+
+  test('matches string literal parameter default values', () async {
+    await _expectSingleNonNullableOutput(
+      dedent(r'''
+      class Foo {
+        void m([String a = 'Hello', String b = 'Hello ' r"World"]) {}
+      }
+      '''),
+      _containsAllOf(
+          "void m([String? a = r'Hello', String? b = r'Hello World']) =>",
+          'super.noSuchMethod(Invocation.method(#m, [a, b]));'),
+    );
+  });
+
+  test('matches empty collection literal parameter default values', () async {
+    await _expectSingleNonNullableOutput(
+      dedent(r'''
+      class Foo {
+        void m([List<int> a = const [], Map<int, int> b = const {}]) {}
+      }
+      '''),
+      _containsAllOf(
+          'void m([List<int>? a = const [], Map<int, int>? b = const {}]) =>',
+          'super.noSuchMethod(Invocation.method(#m, [a, b]));'),
+    );
+  });
+
+  test('matches non-empty list literal parameter default values', () async {
+    await _expectSingleNonNullableOutput(
+      dedent(r'''
+      class Foo {
+        void m([List<int> a = const [1, 2, 3]]) {}
+      }
+      '''),
+      _containsAllOf('void m([List<int>? a = const [1, 2, 3]]) =>',
+          'super.noSuchMethod(Invocation.method(#m, [a]));'),
+    );
+  });
+
+  test('matches non-empty map literal parameter default values', () async {
+    await _expectSingleNonNullableOutput(
+      dedent(r'''
+      class Foo {
+        void m([Map<int, String> a = const {1: 'a', 2: 'b'}]) {}
+      }
+      '''),
+      _containsAllOf(
+          "void m([Map<int, String>? a = const {1: r'a', 2: r'b'}]) =>",
+          'super.noSuchMethod(Invocation.method(#m, [a]));'),
+    );
+  });
+
+  test('matches non-empty map literal parameter default values', () async {
+    await _expectSingleNonNullableOutput(
+      dedent(r'''
+      class Foo {
+        void m([Map<int, String> a = const {1: 'a', 2: 'b'}]) {}
+      }
+      '''),
+      _containsAllOf(
+          "void m([Map<int, String>? a = const {1: r'a', 2: r'b'}]) =>",
+          'super.noSuchMethod(Invocation.method(#m, [a]));'),
+    );
+  });
+
+  test('matches parameter default values constructed from a local class',
+      () async {
+    await _expectSingleNonNullableOutput(
+      dedent(r'''
+      class Foo {
+        void m([Bar a = const Bar()]) {}
+      }
+      class Bar {
+        const Bar();
+      }
+      '''),
+      _containsAllOf('void m([_i2.Bar? a = const _i2.Bar()]) =>',
+          'super.noSuchMethod(Invocation.method(#m, [a]));'),
+    );
+  });
+
+  test('matches parameter default values constructed from a Dart SDK class',
+      () async {
+    await _expectSingleNonNullableOutput(
+      dedent(r'''
+      class Foo {
+        void m([Duration a = const Duration(days: 1)]) {}
+      }
+      '''),
+      _containsAllOf('void m([Duration? a = const Duration(days: 1)]) =>',
+          'super.noSuchMethod(Invocation.method(#m, [a]));'),
+    );
+  });
+
+  test('matches parameter default values constructed from a named constructor',
+      () async {
+    await _expectSingleNonNullableOutput(
+      dedent(r'''
+      class Foo {
+        void m([Bar a = const Bar.named()]) {}
+      }
+      class Bar {
+        const Bar.named();
+      }
+      '''),
+      _containsAllOf('void m([_i2.Bar? a = const _i2.Bar.named()]) =>',
+          'super.noSuchMethod(Invocation.method(#m, [a]));'),
+    );
+  });
+
+  test('matches parameter default values constructed with positional arguments',
+      () async {
+    await _expectSingleNonNullableOutput(
+      dedent(r'''
+      class Foo {
+        void m([Bar a = const Bar(7)]) {}
+      }
+      class Bar {
+        final int i;
+        const Bar(this.i);
+      }
+      '''),
+      _containsAllOf('void m([_i2.Bar? a = const _i2.Bar(7)]) =>',
+          'super.noSuchMethod(Invocation.method(#m, [a]));'),
+    );
+  });
+
+  test('matches parameter default values constructed with named arguments',
+      () async {
+    await _expectSingleNonNullableOutput(
+      dedent(r'''
+      class Foo {
+        void m([Bar a = const Bar(i: 7)]) {}
+      }
+      class Bar {
+        final int i;
+        const Bar({this.i});
+      }
+      '''),
+      _containsAllOf('void m([_i2.Bar? a = const _i2.Bar(i: 7)]) =>',
+          'super.noSuchMethod(Invocation.method(#m, [a]));'),
+    );
+  });
+
+  test('matches parameter default values constructed with top-level variable',
+      () async {
+    await _expectSingleNonNullableOutput(
+      dedent(r'''
+      class Foo {
+        void m([int a = x]) {}
+      }
+      const x = 1;
+      '''),
+      _containsAllOf('void m([int? a = 1]) =>',
+          'super.noSuchMethod(Invocation.method(#m, [a]));'),
+    );
+  });
+
+  test('matches parameter default values constructed with static field',
+      () async {
+    await _expectSingleNonNullableOutput(
+      dedent(r'''
+      class Foo {
+        static const x = 1;
+        void m([int a = x]) {}
+      }
+      '''),
+      _containsAllOf('void m([int? a = 1]) =>',
+          'super.noSuchMethod(Invocation.method(#m, [a]));'),
+    );
+  });
+
+  test('throws when given a parameter default value using a private type', () {
+    _expectBuilderThrows(
+      assets: {
+        ...annotationsAsset,
+        ...simpleTestAsset,
+        'foo|lib/foo.dart': dedent(r'''
+      class Foo {
+        void m([Bar a = const _Bar()]) {}
+      }
+      class Bar {}
+      class _Bar implements Bar {
+        const _Bar();
+      }
+      '''),
+      },
+      message: contains(
+          "Mockito cannot generate a valid stub for method 'Foo.m'; parameter "
+          "'a' causes a problem: default value has a private type: "
+          'asset:foo/lib/foo.dart#_Bar'),
+    );
+  });
+
+  test(
+      'throws when given a parameter default value using a private constructor',
+      () {
+    _expectBuilderThrows(
+      assets: {
+        ...annotationsAsset,
+        ...simpleTestAsset,
+        'foo|lib/foo.dart': dedent(r'''
+        class Foo {
+          void m([Bar a = const Bar._named()]) {}
+        }
+        class Bar {
+          const Bar._named();
+        }
+        '''),
+      },
+      message: contains(
+          "Mockito cannot generate a valid stub for method 'Foo.m'; parameter "
+          "'a' causes a problem: default value has a private type: "
+          'asset:foo/lib/foo.dart#Bar::_named'),
+    );
+  });
+
+  test('throws when given a parameter default value which is a type', () {
+    _expectBuilderThrows(
+      assets: {
+        ...annotationsAsset,
+        ...simpleTestAsset,
+        'foo|lib/foo.dart': dedent(r'''
+        class Foo {
+          void m([Type a = int]) {}
+        }
+        '''),
+      },
+      message: contains(
+          "Mockito cannot generate a valid stub for method 'Foo.m'; parameter "
+          "'a' causes a problem: default value is a Type: int"),
+    );
+  });
+
   test('overrides async methods legally', () async {
     await _expectSingleNonNullableOutput(
       dedent(r'''
@@ -1807,8 +2088,19 @@
 void _expectBuilderThrows(
     {@required Map<String, String> assets,
     @required dynamic /*String|Matcher<List<int>>*/ message}) {
+  var packageConfig = PackageConfig([
+    Package('foo', Uri.file('/foo/'),
+        packageUriRoot: Uri.file('/foo/lib/'),
+        languageVersion: LanguageVersion(2, 9))
+  ]);
+
   expect(
-      () async => await testBuilder(buildMocks(BuilderOptions({})), assets),
+      () async => await withEnabledExperiments(
+            () async => await testBuilder(
+                buildMocks(BuilderOptions({})), assets,
+                packageConfig: packageConfig),
+            ['non-nullable'],
+          ),
       throwsA(TypeMatcher<InvalidMockitoAnnotationException>()
           .having((e) => e.message, 'message', message)));
 }