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)));
}