Skip to content

Commit

Permalink
Add basic extension types support
Browse files Browse the repository at this point in the history
This includes allowing extension types as both methods' arguments
and return types.

Trying to mock an extension type currently silently produces a mock
of its representation, which is likely undesired, but currently
I don't see a way to detect this. It looks like constant evaluation
always produces erased types.

PiperOrigin-RevId: 609380940
  • Loading branch information
Ilya Yanok authored and copybara-github committed Feb 22, 2024
1 parent 7d6632f commit 59c15f5
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 33 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
* Ignore "must_be_immutable" warning in generated files. Mocks cannot be made
immutable anyway, but this way users aren't prevented from using generated
mocks altogether.
* Require Dart >= 3.3.0.
* Require analyzer 6.4.1.
* Add support for extension types.

## 5.4.4

Expand Down
53 changes: 27 additions & 26 deletions lib/src/builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1631,32 +1631,33 @@ class _MockClassInfo {
}

Expression _dummyValueImplementing(
analyzer.InterfaceType dartType, Expression invocation) {
final elementToFake = dartType.element;
if (elementToFake is EnumElement) {
return _typeReference(dartType).property(
elementToFake.fields.firstWhere((f) => f.isEnumConstant).name);
} else if (elementToFake is ClassElement) {
if (elementToFake.isBase ||
elementToFake.isFinal ||
elementToFake.isSealed) {
// This class can't be faked, so try to call `dummyValue` to get
// a dummy value at run time.
// TODO(yanok): Consider checking subtypes, maybe some of them are
// implementable.
return _dummyValueFallbackToRuntime(dartType, invocation);
}
return _dummyFakedValue(dartType, invocation);
} else if (elementToFake is MixinElement) {
// This is a mixin and not a class. This should not happen in Dart 3,
// since it is not possible to have a value of mixin type. But we
// have to support this for reverse comptatibility.
return _dummyFakedValue(dartType, invocation);
} else {
throw StateError("Interface type '$dartType' which is nether an enum, "
'nor a class, nor a mixin. This case is unknown, please report a bug.');
}
}
analyzer.InterfaceType dartType, Expression invocation) =>
switch (dartType.element) {
EnumElement(:final fields) => _typeReference(dartType)
.property(fields.firstWhere((f) => f.isEnumConstant).name),
ClassElement() && final element
when element.isBase || element.isFinal || element.isSealed =>
// This class can't be faked, so try to call `dummyValue` to get
// a dummy value at run time.
// TODO(yanok): Consider checking subtypes, maybe some of them are
// implementable.
_dummyValueFallbackToRuntime(dartType, invocation),
ClassElement() => _dummyFakedValue(dartType, invocation),
MixinElement() =>
// This is a mixin and not a class. This should not happen in Dart 3,
// since it is not possible to have a value of mixin type. But we
// have to support this for reverse comptatibility.
_dummyFakedValue(dartType, invocation),
ExtensionTypeElement(:final typeErasure)
when !typeErasure.containsPrivateName =>
_dummyValue(typeErasure, invocation),
ExtensionTypeElement() =>
_dummyValueFallbackToRuntime(dartType, invocation),
_ => throw StateError(
"Interface type '$dartType' which is neither an enum, "
'nor a class, nor a mixin, nor an extension type. This case is '
'unknown, please report a bug.')
};

/// Adds a `Fake` implementation of [elementToFake], named [fakeName].
void _addFakeClass(String fakeName, InterfaceElement elementToFake) {
Expand Down
4 changes: 2 additions & 2 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ description: >-
repository: https://github.com/dart-lang/mockito

environment:
sdk: ^3.1.0
sdk: ^3.3.0

dependencies:
analyzer: '>=5.12.0 <7.0.0'
analyzer: '>=6.4.1 <7.0.0'
build: ^2.0.0
code_builder: ^4.5.0
collection: ^1.15.0
Expand Down
26 changes: 24 additions & 2 deletions test/builder/auto_mocks_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ void main() {
final packageConfig = PackageConfig([
Package('foo', Uri.file('/foo/'),
packageUriRoot: Uri.file('/foo/lib/'),
languageVersion: LanguageVersion(3, 0))
languageVersion: LanguageVersion(3, 3))
]);
await testBuilder(buildMocks(BuilderOptions(config)), sourceAssets,
writer: writer, outputs: outputs, packageConfig: packageConfig);
Expand All @@ -98,7 +98,7 @@ void main() {
final packageConfig = PackageConfig([
Package('foo', Uri.file('/foo/'),
packageUriRoot: Uri.file('/foo/lib/'),
languageVersion: LanguageVersion(3, 0))
languageVersion: LanguageVersion(3, 3))
]);

await testBuilder(buildMocks(BuilderOptions({})), sourceAssets,
Expand Down Expand Up @@ -3589,6 +3589,28 @@ void main() {
});
});

group('Extension types', () {
test('are supported as arguments', () async {
await expectSingleNonNullableOutput(dedent('''
extension type E(int v) {}
class Foo {
int m(E e);
}
'''), _containsAllOf('int m(_i2.E? e)'));
});

test('are supported as return types', () async {
await expectSingleNonNullableOutput(
dedent('''
extension type E(int v) {}
class Foo {
E get v;
}
'''),
decodedMatches(
allOf(contains('E get v'), contains('returnValue: 0'))));
});
});
group('build_extensions support', () {
test('should export mocks to different directory', () async {
await testWithNonNullable({
Expand Down
2 changes: 1 addition & 1 deletion test/builder/custom_mocks_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ void main() {
final packageConfig = PackageConfig([
Package('foo', Uri.file('/foo/'),
packageUriRoot: Uri.file('/foo/lib/'),
languageVersion: LanguageVersion(3, 0))
languageVersion: LanguageVersion(3, 3))
]);
await testBuilder(buildMocks(BuilderOptions({})), sourceAssets,
writer: writer, packageConfig: packageConfig);
Expand Down
11 changes: 11 additions & 0 deletions test/end2end/foo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,14 @@ mixin HasPrivateMixin implements HasPrivate {
@override
Object? _p;
}

extension type Ext(int x) {}

extension type ExtOfPrivate(_Private private) {}

class UsesExtTypes {
bool extTypeArg(Ext _) => true;
Ext extTypeReturn(int _) => Ext(42);
bool privateExtTypeArg(ExtOfPrivate _) => true;
ExtOfPrivate privateExtTypeReturn(int _) => ExtOfPrivate(private);
}
36 changes: 34 additions & 2 deletions test/end2end/generated_mocks_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@ import 'generated_mocks_test.mocks.dart';
// ignore: deprecated_member_use_from_same_package
MockSpec<HasPrivate>(mixingIn: [HasPrivateMixin]),
])
@GenerateNiceMocks(
[MockSpec<Foo>(as: #MockFooNice), MockSpec<Bar>(as: #MockBarNice)])
@GenerateNiceMocks([
MockSpec<Foo>(as: #MockFooNice),
MockSpec<Bar>(as: #MockBarNice),
MockSpec<UsesExtTypes>()
])
void main() {
group('for a generated mock,', () {
late MockFoo<Object> foo;
Expand Down Expand Up @@ -334,6 +337,35 @@ void main() {
expect(await foo.returnsFuture(MockBar()), bar);
});
});

group('for a class using extension types', () {
late MockUsesExtTypes usesExtTypes;

setUp(() {
usesExtTypes = MockUsesExtTypes();
});

test(
'a method using extension type as an argument can be stubbed with any',
() {
when(usesExtTypes.extTypeArg(any)).thenReturn(true);
expect(usesExtTypes.extTypeArg(Ext(42)), isTrue);
});

test(
'a method using extension type as an argument can be stubbed with a '
'specific value', () {
when(usesExtTypes.extTypeArg(Ext(42))).thenReturn(true);
expect(usesExtTypes.extTypeArg(Ext(0)), isFalse);
expect(usesExtTypes.extTypeArg(Ext(42)), isTrue);
});

test('a method using extension type as a return type can be stubbed', () {
when(usesExtTypes.extTypeReturn(2)).thenReturn(Ext(42));
expect(usesExtTypes.extTypeReturn(2), equals(Ext(42)));
expect(usesExtTypes.extTypeReturn(42), equals(Ext(0)));
});
});
});

test('a generated mock can be used as a stub argument', () {
Expand Down

0 comments on commit 59c15f5

Please sign in to comment.