diff --git a/CHANGELOG.md b/CHANGELOG.md index 42533c00..435e30b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/src/builder.dart b/lib/src/builder.dart index 35d8ed74..ee71999f 100644 --- a/lib/src/builder.dart +++ b/lib/src/builder.dart @@ -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) { diff --git a/pubspec.yaml b/pubspec.yaml index e5585b58..5cd36f6b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 diff --git a/test/builder/auto_mocks_test.dart b/test/builder/auto_mocks_test.dart index 2b7f6bb1..1e6a0f36 100644 --- a/test/builder/auto_mocks_test.dart +++ b/test/builder/auto_mocks_test.dart @@ -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); @@ -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, @@ -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({ diff --git a/test/builder/custom_mocks_test.dart b/test/builder/custom_mocks_test.dart index b03ce4ca..2b987cf3 100644 --- a/test/builder/custom_mocks_test.dart +++ b/test/builder/custom_mocks_test.dart @@ -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); diff --git a/test/end2end/foo.dart b/test/end2end/foo.dart index 857321da..09f676fe 100644 --- a/test/end2end/foo.dart +++ b/test/end2end/foo.dart @@ -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); +} diff --git a/test/end2end/generated_mocks_test.dart b/test/end2end/generated_mocks_test.dart index a68e0e1c..ca891e9b 100644 --- a/test/end2end/generated_mocks_test.dart +++ b/test/end2end/generated_mocks_test.dart @@ -27,8 +27,11 @@ import 'generated_mocks_test.mocks.dart'; // ignore: deprecated_member_use_from_same_package MockSpec(mixingIn: [HasPrivateMixin]), ]) -@GenerateNiceMocks( - [MockSpec(as: #MockFooNice), MockSpec(as: #MockBarNice)]) +@GenerateNiceMocks([ + MockSpec(as: #MockFooNice), + MockSpec(as: #MockBarNice), + MockSpec() +]) void main() { group('for a generated mock,', () { late MockFoo foo; @@ -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', () {