Skip to content

Commit b693ada

Browse files
Ilya Yanokcopybara-github
authored andcommitted
Add basic extension types support
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: 610676339
1 parent 7d6632f commit b693ada

File tree

7 files changed

+102
-33
lines changed

7 files changed

+102
-33
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
* Ignore "must_be_immutable" warning in generated files. Mocks cannot be made
44
immutable anyway, but this way users aren't prevented from using generated
55
mocks altogether.
6+
* Require Dart >= 3.3.0.
7+
* Require analyzer 6.4.1.
8+
* Add support for extension types.
69

710
## 5.4.4
811

lib/src/builder.dart

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1631,32 +1631,33 @@ class _MockClassInfo {
16311631
}
16321632

16331633
Expression _dummyValueImplementing(
1634-
analyzer.InterfaceType dartType, Expression invocation) {
1635-
final elementToFake = dartType.element;
1636-
if (elementToFake is EnumElement) {
1637-
return _typeReference(dartType).property(
1638-
elementToFake.fields.firstWhere((f) => f.isEnumConstant).name);
1639-
} else if (elementToFake is ClassElement) {
1640-
if (elementToFake.isBase ||
1641-
elementToFake.isFinal ||
1642-
elementToFake.isSealed) {
1643-
// This class can't be faked, so try to call `dummyValue` to get
1644-
// a dummy value at run time.
1645-
// TODO(yanok): Consider checking subtypes, maybe some of them are
1646-
// implementable.
1647-
return _dummyValueFallbackToRuntime(dartType, invocation);
1648-
}
1649-
return _dummyFakedValue(dartType, invocation);
1650-
} else if (elementToFake is MixinElement) {
1651-
// This is a mixin and not a class. This should not happen in Dart 3,
1652-
// since it is not possible to have a value of mixin type. But we
1653-
// have to support this for reverse comptatibility.
1654-
return _dummyFakedValue(dartType, invocation);
1655-
} else {
1656-
throw StateError("Interface type '$dartType' which is nether an enum, "
1657-
'nor a class, nor a mixin. This case is unknown, please report a bug.');
1658-
}
1659-
}
1634+
analyzer.InterfaceType dartType, Expression invocation) =>
1635+
switch (dartType.element) {
1636+
EnumElement(:final fields) => _typeReference(dartType)
1637+
.property(fields.firstWhere((f) => f.isEnumConstant).name),
1638+
ClassElement() && final element
1639+
when element.isBase || element.isFinal || element.isSealed =>
1640+
// This class can't be faked, so try to call `dummyValue` to get
1641+
// a dummy value at run time.
1642+
// TODO(yanok): Consider checking subtypes, maybe some of them are
1643+
// implementable.
1644+
_dummyValueFallbackToRuntime(dartType, invocation),
1645+
ClassElement() => _dummyFakedValue(dartType, invocation),
1646+
MixinElement() =>
1647+
// This is a mixin and not a class. This should not happen in Dart 3,
1648+
// since it is not possible to have a value of mixin type. But we
1649+
// have to support this for reverse comptatibility.
1650+
_dummyFakedValue(dartType, invocation),
1651+
ExtensionTypeElement(:final typeErasure)
1652+
when !typeErasure.containsPrivateName =>
1653+
_dummyValue(typeErasure, invocation),
1654+
ExtensionTypeElement() =>
1655+
_dummyValueFallbackToRuntime(dartType, invocation),
1656+
_ => throw StateError(
1657+
"Interface type '$dartType' which is neither an enum, "
1658+
'nor a class, nor a mixin, nor an extension type. This case is '
1659+
'unknown, please report a bug.')
1660+
};
16601661

16611662
/// Adds a `Fake` implementation of [elementToFake], named [fakeName].
16621663
void _addFakeClass(String fakeName, InterfaceElement elementToFake) {

pubspec.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ description: >-
66
repository: https://github.com/dart-lang/mockito
77

88
environment:
9-
sdk: ^3.1.0
9+
sdk: ^3.3.0
1010

1111
dependencies:
12-
analyzer: '>=5.12.0 <7.0.0'
12+
analyzer: '>=6.4.1 <7.0.0'
1313
build: ^2.0.0
1414
code_builder: ^4.5.0
1515
collection: ^1.15.0

test/builder/auto_mocks_test.dart

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ void main() {
8686
final packageConfig = PackageConfig([
8787
Package('foo', Uri.file('/foo/'),
8888
packageUriRoot: Uri.file('/foo/lib/'),
89-
languageVersion: LanguageVersion(3, 0))
89+
languageVersion: LanguageVersion(3, 3))
9090
]);
9191
await testBuilder(buildMocks(BuilderOptions(config)), sourceAssets,
9292
writer: writer, outputs: outputs, packageConfig: packageConfig);
@@ -98,7 +98,7 @@ void main() {
9898
final packageConfig = PackageConfig([
9999
Package('foo', Uri.file('/foo/'),
100100
packageUriRoot: Uri.file('/foo/lib/'),
101-
languageVersion: LanguageVersion(3, 0))
101+
languageVersion: LanguageVersion(3, 3))
102102
]);
103103

104104
await testBuilder(buildMocks(BuilderOptions({})), sourceAssets,
@@ -3589,6 +3589,28 @@ void main() {
35893589
});
35903590
});
35913591

3592+
group('Extension types', () {
3593+
test('are supported as arguments', () async {
3594+
await expectSingleNonNullableOutput(dedent('''
3595+
extension type E(int v) {}
3596+
class Foo {
3597+
int m(E e);
3598+
}
3599+
'''), _containsAllOf('int m(_i2.E? e)'));
3600+
});
3601+
3602+
test('are supported as return types', () async {
3603+
await expectSingleNonNullableOutput(
3604+
dedent('''
3605+
extension type E(int v) {}
3606+
class Foo {
3607+
E get v;
3608+
}
3609+
'''),
3610+
decodedMatches(
3611+
allOf(contains('E get v'), contains('returnValue: 0'))));
3612+
});
3613+
});
35923614
group('build_extensions support', () {
35933615
test('should export mocks to different directory', () async {
35943616
await testWithNonNullable({

test/builder/custom_mocks_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ void main() {
9595
final packageConfig = PackageConfig([
9696
Package('foo', Uri.file('/foo/'),
9797
packageUriRoot: Uri.file('/foo/lib/'),
98-
languageVersion: LanguageVersion(3, 0))
98+
languageVersion: LanguageVersion(3, 3))
9999
]);
100100
await testBuilder(buildMocks(BuilderOptions({})), sourceAssets,
101101
writer: writer, packageConfig: packageConfig);

test/end2end/foo.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,14 @@ mixin HasPrivateMixin implements HasPrivate {
6464
@override
6565
Object? _p;
6666
}
67+
68+
extension type Ext(int x) {}
69+
70+
extension type ExtOfPrivate(_Private private) {}
71+
72+
class UsesExtTypes {
73+
bool extTypeArg(Ext _) => true;
74+
Ext extTypeReturn(int _) => Ext(42);
75+
bool privateExtTypeArg(ExtOfPrivate _) => true;
76+
ExtOfPrivate privateExtTypeReturn(int _) => ExtOfPrivate(private);
77+
}

test/end2end/generated_mocks_test.dart

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,11 @@ import 'generated_mocks_test.mocks.dart';
2727
// ignore: deprecated_member_use_from_same_package
2828
MockSpec<HasPrivate>(mixingIn: [HasPrivateMixin]),
2929
])
30-
@GenerateNiceMocks(
31-
[MockSpec<Foo>(as: #MockFooNice), MockSpec<Bar>(as: #MockBarNice)])
30+
@GenerateNiceMocks([
31+
MockSpec<Foo>(as: #MockFooNice),
32+
MockSpec<Bar>(as: #MockBarNice),
33+
MockSpec<UsesExtTypes>()
34+
])
3235
void main() {
3336
group('for a generated mock,', () {
3437
late MockFoo<Object> foo;
@@ -334,6 +337,35 @@ void main() {
334337
expect(await foo.returnsFuture(MockBar()), bar);
335338
});
336339
});
340+
341+
group('for a class using extension types', () {
342+
late MockUsesExtTypes usesExtTypes;
343+
344+
setUp(() {
345+
usesExtTypes = MockUsesExtTypes();
346+
});
347+
348+
test(
349+
'a method using extension type as an argument can be stubbed with any',
350+
() {
351+
when(usesExtTypes.extTypeArg(any)).thenReturn(true);
352+
expect(usesExtTypes.extTypeArg(Ext(42)), isTrue);
353+
});
354+
355+
test(
356+
'a method using extension type as an argument can be stubbed with a '
357+
'specific value', () {
358+
when(usesExtTypes.extTypeArg(Ext(42))).thenReturn(true);
359+
expect(usesExtTypes.extTypeArg(Ext(0)), isFalse);
360+
expect(usesExtTypes.extTypeArg(Ext(42)), isTrue);
361+
});
362+
363+
test('a method using extension type as a return type can be stubbed', () {
364+
when(usesExtTypes.extTypeReturn(2)).thenReturn(Ext(42));
365+
expect(usesExtTypes.extTypeReturn(2), equals(Ext(42)));
366+
expect(usesExtTypes.extTypeReturn(42), equals(Ext(0)));
367+
});
368+
});
337369
});
338370

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

0 commit comments

Comments
 (0)