MockBuilder: integrate non-nullability of libraries into types and tests.
The (non-)nullability of types is matched between source classes and mock classes. This should be true for any generated type annotation, including method parameters and method return types.
A method is no longer stubbed if all of its parameters are nullable, or if the source library does not use the non-nullable type system.
PiperOrigin-RevId: 314593229
diff --git a/lib/src/builder.dart b/lib/src/builder.dart
index 1aeb1ef..ff878f3 100644
--- a/lib/src/builder.dart
+++ b/lib/src/builder.dart
@@ -14,7 +14,9 @@
import 'package:analyzer/dart/constant/value.dart';
import 'package:analyzer/dart/element/element.dart';
+import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart' as analyzer;
+import 'package:analyzer/dart/element/type_system.dart';
import 'package:build/build.dart';
import 'package:code_builder/code_builder.dart';
import 'package:dart_style/dart_style.dart';
@@ -37,6 +39,7 @@
@override
Future build(BuildStep buildStep) async {
final entryLib = await buildStep.inputLibrary;
+ final sourceLibIsNonNullable = entryLib.isNonNullableByDefault;
final mockLibraryAsset = buildStep.inputId.changeExtension('.mocks.dart');
final classesToMock = <DartObject>[];
@@ -58,7 +61,9 @@
}
final mockLibrary = Library((b) {
- var mockLibraryInfo = _MockLibraryInfo(classesToMock);
+ var mockLibraryInfo = _MockLibraryInfo(classesToMock,
+ sourceLibIsNonNullable: sourceLibIsNonNullable,
+ typeSystem: entryLib.typeSystem);
b.body.addAll(mockLibraryInfo.fakeClasses);
b.body.addAll(mockLibraryInfo.mockClasses);
});
@@ -68,7 +73,8 @@
return;
}
- final emitter = DartEmitter.scoped();
+ final emitter =
+ DartEmitter.scoped(useNullSafetySyntax: sourceLibIsNonNullable);
final mockLibraryContent =
DartFormatter().format(mockLibrary.accept(emitter).toString());
@@ -82,6 +88,11 @@
}
class _MockLibraryInfo {
+ final bool sourceLibIsNonNullable;
+
+ /// The type system which applies to the source library.
+ final TypeSystem typeSystem;
+
/// Mock classes to be added to the generated library.
final mockClasses = <Class>[];
@@ -97,7 +108,8 @@
/// Build mock classes for [classesToMock], a list of classes obtained from a
/// `@GenerateMocks` annotation.
- _MockLibraryInfo(List<DartObject> classesToMock) {
+ _MockLibraryInfo(List<DartObject> classesToMock,
+ {this.sourceLibIsNonNullable, this.typeSystem}) {
for (final classToMock in classesToMock) {
final dartTypeToMock = classToMock.toTypeValue();
if (dartTypeToMock == null) {
@@ -196,10 +208,20 @@
return true;
}
- // TODO(srawlins): Update this logic to correctly handle non-nullable return
- // types. Right now this logic does not seem to be available on DartType.
+ // Returns whether [method] has at least one parameter whose type is
+ // potentially non-nullable.
+ //
+ // A parameter whose type uses a type variable may be non-nullable on certain
+ // instances. For example:
+ //
+ // class C<T> {
+ // void m(T a) {}
+ // }
+ // final c1 = C<int?>(); // m's parameter's type is nullable.
+ // final c2 = C<int>(); // m's parameter's type is non-nullable.
bool _hasNonNullableParameter(MethodElement method) =>
- method.parameters.isNotEmpty;
+ sourceLibIsNonNullable &&
+ method.parameters.any((p) => typeSystem.isPotentiallyNonNullable(p.type));
/// Build a method which overrides [method], with all non-nullable
/// parameter types widened to be nullable.
@@ -207,7 +229,9 @@
/// This new method just calls `super.noSuchMethod`, optionally passing a
/// return value for methods with a non-nullable return type.
// TODO(srawlins): This method does no widening yet. Widen parameters. Include
- // tests for typedefs, old-style function parameters, and function types.
+ // tests for typedefs, old-style function parameters, function types, type
+ // variables, non-nullable type variables (bounded to Object, I think),
+ // dynamic.
// TODO(srawlins): This method declares no specific non-null return values
// yet.
void _buildOverridingMethod(MethodBuilder builder, MethodElement method) {
@@ -437,6 +461,9 @@
return TypeReference((TypeReferenceBuilder b) {
b
..symbol = type.name
+ // Using the `nullabilitySuffix` rather than `TypeSystem.isNullable`
+ // is more correct for types like `dynamic`.
+ ..isNullable = type.nullabilitySuffix == NullabilitySuffix.question
..url = _typeImport(type)
..types.addAll(type.typeArguments.map(_typeReference));
});
diff --git a/test/builder_test.dart b/test/builder_test.dart
index f67137e..a5d5894 100644
--- a/test/builder_test.dart
+++ b/test/builder_test.dart
@@ -14,9 +14,11 @@
@TestOn('vm')
import 'package:build/build.dart';
+import 'package:build/experiments.dart';
import 'package:build_test/build_test.dart';
import 'package:meta/meta.dart';
import 'package:mockito/src/builder.dart';
+import 'package:package_config/package_config.dart';
import 'package:test/test.dart';
Builder buildMocks(BuilderOptions options) => MockBuilder();
@@ -44,8 +46,7 @@
test(
'generates mock for an imported class but does not override private '
'or static methods or methods w/ zero parameters', () async {
- await testBuilder(
- buildMocks(BuilderOptions({})),
+ await _testWithNonNullable(
{
...annotationsAsset,
...simpleTestAsset,
@@ -74,8 +75,7 @@
test(
'generates mock for an imported class but does not override private '
'or static fields', () async {
- await testBuilder(
- buildMocks(BuilderOptions({})),
+ await _testWithNonNullable(
{
...annotationsAsset,
...simpleTestAsset,
@@ -103,8 +103,7 @@
test(
'generates mock for an imported class but does not override any '
'extension methods', () async {
- await testBuilder(
- buildMocks(BuilderOptions({})),
+ await _testWithNonNullable(
{
...annotationsAsset,
...simpleTestAsset,
@@ -135,8 +134,7 @@
});
test('generates a mock class and overrides methods parameters', () async {
- await testBuilder(
- buildMocks(BuilderOptions({})),
+ await _testWithNonNullable(
{
...annotationsAsset,
...simpleTestAsset,
@@ -181,8 +179,7 @@
});
test('generates multiple mock classes', () async {
- await testBuilder(
- buildMocks(BuilderOptions({})),
+ await _testWithNonNullable(
{
...annotationsAsset,
'foo|lib/foo.dart': dedent(r'''
@@ -226,8 +223,7 @@
});
test('generates generic mock classes', () async {
- await testBuilder(
- buildMocks(BuilderOptions({})),
+ await _testWithNonNullable(
{
...annotationsAsset,
'foo|lib/foo.dart': dedent(r'''
@@ -259,8 +255,7 @@
});
test('generates generic mock classes with type bounds', () async {
- await testBuilder(
- buildMocks(BuilderOptions({})),
+ await _testWithNonNullable(
{
...annotationsAsset,
'foo|lib/foo.dart': dedent(r'''
@@ -302,17 +297,15 @@
});
test('writes non-interface types w/o imports', () async {
- await testBuilder(
- buildMocks(BuilderOptions({})),
+ await _testWithNonNullable(
{
...annotationsAsset,
...simpleTestAsset,
'foo|lib/foo.dart': dedent(r'''
- import 'dart:async';
class Foo<T> {
- void f(dynamic a) {}
- void g(T b) {}
- void h<U>(U c) {}
+ void f(dynamic a, int b) {}
+ void g(T c) {}
+ void h<U>(U d) {}
}
'''),
},
@@ -325,9 +318,9 @@
///
/// See the documentation for Mockito's code generation for more information.
class MockFoo<T> extends _i1.Mock implements _i2.Foo<T> {
- void f(dynamic a) => super.noSuchMethod(Invocation.method(#f, [a]));
- void g(T b) => super.noSuchMethod(Invocation.method(#g, [b]));
- void h<U>(U c) => super.noSuchMethod(Invocation.method(#h, [c]));
+ void f(dynamic a, int b) => super.noSuchMethod(Invocation.method(#f, [a, b]));
+ void g(T c) => super.noSuchMethod(Invocation.method(#g, [c]));
+ void h<U>(U d) => super.noSuchMethod(Invocation.method(#h, [d]));
}
'''),
},
@@ -335,8 +328,7 @@
});
test('imports libraries for external class types', () async {
- await testBuilder(
- buildMocks(BuilderOptions({})),
+ await _testWithNonNullable(
{
...annotationsAsset,
...simpleTestAsset,
@@ -365,8 +357,7 @@
});
test('imports libraries for type aliases with external types', () async {
- await testBuilder(
- buildMocks(BuilderOptions({})),
+ await _testWithNonNullable(
{
...annotationsAsset,
...simpleTestAsset,
@@ -402,8 +393,7 @@
});
test('imports libraries for function types with external types', () async {
- await testBuilder(
- buildMocks(BuilderOptions({})),
+ await _testWithNonNullable(
{
...annotationsAsset,
...simpleTestAsset,
@@ -446,9 +436,86 @@
);
});
+ test('correctly matches nullability of parameters', () async {
+ await _testWithNonNullable(
+ {
+ ...annotationsAsset,
+ ...simpleTestAsset,
+ 'foo|lib/foo.dart': dedent(r'''
+ abstract class Foo {
+ void f(int? a, int b);
+ void g(List<int?> a, List<int> b);
+ void h(int? Function() a, int Function() b);
+ void i(void Function(int?) a, void Function(int) b);
+ void j(int? a(), int b());
+ void k(void a(int? x), void b(int x));
+ void l<T>(T? a, T b);
+ }
+ '''),
+ },
+ outputs: {
+ // TODO(srawlins): The type of l's first parameter should be `T?`.
+ '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 {
+ void f(int? a, int b) => super.noSuchMethod(Invocation.method(#f, [a, b]));
+ void g(List<int?> a, List<int> b) =>
+ super.noSuchMethod(Invocation.method(#g, [a, b]));
+ void h(int? Function() a, int Function() b) =>
+ super.noSuchMethod(Invocation.method(#h, [a, b]));
+ void i(void Function(int?) a, void Function(int) b) =>
+ super.noSuchMethod(Invocation.method(#i, [a, b]));
+ void j(int? Function() a, int Function() b) =>
+ super.noSuchMethod(Invocation.method(#j, [a, b]));
+ void k(void Function(int?) a, void Function(int) b) =>
+ super.noSuchMethod(Invocation.method(#k, [a, b]));
+ void l<T>(T a, T b) => super.noSuchMethod(Invocation.method(#l, [a, b]));
+ }
+ '''),
+ },
+ );
+ });
+
+ test('correctly matches nullability of return types', () async {
+ await _testWithNonNullable(
+ {
+ ...annotationsAsset,
+ ...simpleTestAsset,
+ 'foo|lib/foo.dart': dedent(r'''
+ abstract class Foo {
+ int f();
+ int? g();
+ List<int?> h();
+ List<int> i();
+ }
+ '''),
+ },
+ 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 {
+ int f() => super.noSuchMethod(Invocation.method(#f, []), 0);
+ int? g() => super.noSuchMethod(Invocation.method(#g, []), 0);
+ List<int?> h() => super.noSuchMethod(Invocation.method(#h, []), []);
+ List<int> i() => super.noSuchMethod(Invocation.method(#i, []), []);
+ }
+ '''),
+ },
+ );
+ });
+
test('overrides abstract methods', () async {
- await testBuilder(
- buildMocks(BuilderOptions({})),
+ await _testWithNonNullable(
{
...annotationsAsset,
...simpleTestAsset,
@@ -475,9 +542,62 @@
);
});
+ test('does not override methods with all nullable parameters', () async {
+ await _testWithNonNullable(
+ {
+ ...annotationsAsset,
+ ...simpleTestAsset,
+ 'foo|lib/foo.dart': dedent(r'''
+ class Foo {
+ void a(int? m) {}
+ void b(dynamic n) {}
+ void c(int Function()? o) {}
+ }
+ '''),
+ },
+ 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('overrides methods with a potentially non-nullable parameter', () async {
+ await _testWithNonNullable(
+ {
+ ...annotationsAsset,
+ ...simpleTestAsset,
+ 'foo|lib/foo.dart': dedent(r'''
+ class Foo<T> {
+ void a(T m) {}
+ }
+ '''),
+ },
+ 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<T> extends _i1.Mock implements _i2.Foo<T> {
+ void a(T m) => super.noSuchMethod(Invocation.method(#a, [m]));
+ }
+ '''),
+ },
+ );
+ });
+
test('overrides generic methods', () async {
- await testBuilder(
- buildMocks(BuilderOptions({})),
+ await _testWithNonNullable(
{
...annotationsAsset,
...simpleTestAsset,
@@ -509,8 +629,7 @@
});
test('overrides getters and setters', () async {
- await testBuilder(
- buildMocks(BuilderOptions({})),
+ await _testWithNonNullable(
{
...annotationsAsset,
...simpleTestAsset,
@@ -541,8 +660,7 @@
});
test('overrides operators', () async {
- await testBuilder(
- buildMocks(BuilderOptions({})),
+ await _testWithNonNullable(
{
...annotationsAsset,
...simpleTestAsset,
@@ -574,8 +692,7 @@
});
test('creates dummy non-null return values for known core classes', () async {
- await testBuilder(
- buildMocks(BuilderOptions({})),
+ await _testWithNonNullable(
{
...annotationsAsset,
...simpleTestAsset,
@@ -615,8 +732,7 @@
test('creates dummy non-null return values for Futures of known core classes',
() async {
- await testBuilder(
- buildMocks(BuilderOptions({})),
+ await _testWithNonNullable(
{
...annotationsAsset,
...simpleTestAsset,
@@ -645,8 +761,7 @@
});
test('creates dummy non-null return values for unknown classes', () async {
- await testBuilder(
- buildMocks(BuilderOptions({})),
+ await _testWithNonNullable(
{
...annotationsAsset,
...simpleTestAsset,
@@ -679,8 +794,7 @@
});
test('deduplicates fake classes', () async {
- await testBuilder(
- buildMocks(BuilderOptions({})),
+ await _testWithNonNullable(
{
...annotationsAsset,
...simpleTestAsset,
@@ -715,8 +829,7 @@
});
test('creates dummy non-null return values for enums', () async {
- await testBuilder(
- buildMocks(BuilderOptions({})),
+ await _testWithNonNullable(
{
...annotationsAsset,
...simpleTestAsset,
@@ -747,8 +860,7 @@
});
test('creates dummy non-null return values for functions', () async {
- await testBuilder(
- buildMocks(BuilderOptions({})),
+ await _testWithNonNullable(
{
...annotationsAsset,
...simpleTestAsset,
@@ -844,6 +956,65 @@
message: 'The "classes" argument includes an enum: Foo',
);
});
+
+ test('given a pre-non-nullable library, does not override any members',
+ () async {
+ await _testPreNonNullable(
+ {
+ ...annotationsAsset,
+ ...simpleTestAsset,
+ 'foo|lib/foo.dart': dedent(r'''
+ abstract class Foo {
+ dynamic f(int a) {}
+ }
+ '''),
+ },
+ 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 [MockBuilder] in a package which has not opted into the non-nullable
+/// type system.
+///
+/// Whether the non-nullable experiment is enabled depends on the SDK executing
+/// this test, but that does not affect the opt-in state of the package under
+/// test.
+Future<void> _testPreNonNullable(Map<String, String> sourceAssets,
+ {Map<String, /*String|Matcher<String>*/ dynamic> outputs}) async {
+ var packageConfig = PackageConfig([
+ Package('foo', Uri.file('/foo/'),
+ packageUriRoot: Uri.file('/foo/lib/'),
+ languageVersion: LanguageVersion(2, 7))
+ ]);
+ await testBuilder(buildMocks(BuilderOptions({})), sourceAssets,
+ outputs: outputs, packageConfig: packageConfig);
+}
+
+/// Test [MockBuilder] in a package which has opted into the non-nullable type
+/// system, and with the non-nullable experiment enabled.
+Future<void> _testWithNonNullable(Map<String, String> sourceAssets,
+ {Map<String, /*String|Matcher<String>*/ dynamic> outputs}) async {
+ var packageConfig = PackageConfig([
+ Package('foo', Uri.file('/foo/'),
+ packageUriRoot: Uri.file('/foo/lib/'),
+ languageVersion: LanguageVersion(2, 9))
+ ]);
+ await withEnabledExperiments(
+ () async => await testBuilder(buildMocks(BuilderOptions({})), sourceAssets,
+ outputs: outputs, packageConfig: packageConfig),
+ ['non-nullable'],
+ );
}
/// Expect that [testBuilder], given [assets], throws an