diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md index d848818b241f..d3d130aa573d 100644 --- a/packages/pigeon/CHANGELOG.md +++ b/packages/pigeon/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.0.2 + +* Fixes non-nullable classes and enums as fields. +* Fixes nullable collections as return types. + ## 3.0.1 * Enables NNBD for the Pigeon tool itself. diff --git a/packages/pigeon/lib/dart_generator.dart b/packages/pigeon/lib/dart_generator.dart index 982044cf1194..3625b369ab69 100644 --- a/packages/pigeon/lib/dart_generator.dart +++ b/packages/pigeon/lib/dart_generator.dart @@ -192,10 +192,11 @@ final BinaryMessenger? _binaryMessenger; final String returnType = _makeGenericTypeArguments(func.returnType); final String castCall = _makeGenericCastCall(func.returnType); const String accessor = 'replyMap[\'${Keys.result}\']'; - final String unwrapper = func.returnType.isNullable ? '' : '!'; + final String nullHandler = + func.returnType.isNullable ? (castCall.isEmpty ? '' : '?') : '!'; final String returnStatement = func.returnType.isVoid ? 'return;' - : 'return ($accessor as $returnType?)$unwrapper$castCall;'; + : 'return ($accessor as $returnType?)$nullHandler$castCall;'; indent.format(''' final Map? replyMap =\n\t\tawait channel.send($sendArgument) as Map?; if (replyMap == null) { @@ -459,13 +460,14 @@ void generateDart(DartOptions opt, Root root, StringSink sink) { ); for (final NamedType field in klass.fields) { indent.write('pigeonMap[\'${field.name}\'] = '); + final String conditional = field.type.isNullable ? '?' : ''; if (customClassNames.contains(field.type.baseName)) { indent.addln( - '${field.name}?.encode();', + '${field.name}$conditional.encode();', ); } else if (customEnumNames.contains(field.type.baseName)) { indent.addln( - '${field.name}?.index;', + '${field.name}$conditional.index;', ); } else { indent.addln('${field.name};'); @@ -478,15 +480,29 @@ void generateDart(DartOptions opt, Root root, StringSink sink) { void writeDecode() { void writeValueDecode(NamedType field) { if (customClassNames.contains(field.type.baseName)) { - indent.format(''' + final String nonNullValue = + "${field.type.baseName}.decode(pigeonMap['${field.name}']!)"; + indent.format( + field.type.isNullable + ? ''' pigeonMap['${field.name}'] != null -\t\t? ${field.type.baseName}.decode(pigeonMap['${field.name}']!) -\t\t: null''', leadingSpace: false, trailingNewline: false); +\t\t? $nonNullValue +\t\t: null''' + : nonNullValue, + leadingSpace: false, + trailingNewline: false); } else if (customEnumNames.contains(field.type.baseName)) { - indent.format(''' + final String nonNullValue = + "${field.type.baseName}.values[pigeonMap['${field.name}']! as int]"; + indent.format( + field.type.isNullable + ? ''' pigeonMap['${field.name}'] != null -\t\t? ${field.type.baseName}.values[pigeonMap['${field.name}']! as int] -\t\t: null''', leadingSpace: false, trailingNewline: false); +\t\t? $nonNullValue +\t\t: null''' + : nonNullValue, + leadingSpace: false, + trailingNewline: false); } else if (field.type.typeArguments.isNotEmpty) { final String genericType = _makeGenericTypeArguments(field.type); final String castCall = _makeGenericCastCall(field.type); diff --git a/packages/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart index 65397d9ee1bc..a22c7e479c42 100644 --- a/packages/pigeon/lib/generator_tools.dart +++ b/packages/pigeon/lib/generator_tools.dart @@ -7,8 +7,8 @@ import 'dart:io'; import 'dart:mirrors'; import 'ast.dart'; -/// The current version of pigeon. This must match the version in pubspec.yaml. -const String pigeonVersion = '3.0.1'; +/// The current version of pigeon. This must match the version in pubspec.yaml.\ +const String pigeonVersion = '3.0.2'; /// Read all the content from [stdin] to a String. String readStdin() { diff --git a/packages/pigeon/pigeons/non_null_fields.dart b/packages/pigeon/pigeons/non_null_fields.dart index bbce3367b66a..aa96b8736274 100644 --- a/packages/pigeon/pigeons/non_null_fields.dart +++ b/packages/pigeon/pigeons/non_null_fields.dart @@ -12,11 +12,21 @@ class SearchRequest { String query; } +class ExtraData { + ExtraData({required this.detailA, required this.detailB}); + String detailA; + String detailB; +} + +enum ReplyType { success, error } + class SearchReply { - SearchReply(this.result, this.error, this.indices); + SearchReply(this.result, this.error, this.indices, this.extraData, this.type); String result; String error; List indices; + ExtraData extraData; + ReplyType type; } @HostApi() diff --git a/packages/pigeon/pigeons/nullable_returns.dart b/packages/pigeon/pigeons/nullable_returns.dart index 150d5d3e91fe..548e159452a9 100644 --- a/packages/pigeon/pigeons/nullable_returns.dart +++ b/packages/pigeon/pigeons/nullable_returns.dart @@ -8,12 +8,12 @@ import 'package:pigeon/pigeon.dart'; @HostApi() -abstract class NonNullHostApi { +abstract class NullableReturnHostApi { int? doit(); } @FlutterApi() -abstract class NonNullFlutterApi { +abstract class NullableReturnFlutterApi { int? doit(); } @@ -26,3 +26,23 @@ abstract class NullableArgHostApi { abstract class NullableArgFlutterApi { int doit(int? x); } + +@HostApi() +abstract class NullableCollectionReturnHostApi { + List? doit(); +} + +@FlutterApi() +abstract class NullableCollectionReturnFlutterApi { + List? doit(); +} + +@HostApi() +abstract class NullableCollectionArgHostApi { + List doit(List? x); +} + +@FlutterApi() +abstract class NullableCollectionArgFlutterApi { + List doit(List? x); +} diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/non_null_fields.gen.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/non_null_fields.gen.dart index 9dbcebeedaf1..0b7e51334720 100644 --- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/non_null_fields.gen.dart +++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/non_null_fields.gen.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // -// Autogenerated from Pigeon (v2.0.4), do not edit directly. +// Autogenerated from Pigeon (v3.0.2), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name // @dart = 2.12 @@ -12,6 +12,11 @@ import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; import 'package:flutter/services.dart'; +enum ReplyType { + success, + error, +} + class SearchRequest { SearchRequest({ required this.query, @@ -33,22 +38,53 @@ class SearchRequest { } } +class ExtraData { + ExtraData({ + required this.detailA, + required this.detailB, + }); + + String detailA; + String detailB; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['detailA'] = detailA; + pigeonMap['detailB'] = detailB; + return pigeonMap; + } + + static ExtraData decode(Object message) { + final Map pigeonMap = message as Map; + return ExtraData( + detailA: pigeonMap['detailA']! as String, + detailB: pigeonMap['detailB']! as String, + ); + } +} + class SearchReply { SearchReply({ required this.result, required this.error, required this.indices, + required this.extraData, + required this.type, }); String result; String error; List indices; + ExtraData extraData; + ReplyType type; Object encode() { final Map pigeonMap = {}; pigeonMap['result'] = result; pigeonMap['error'] = error; pigeonMap['indices'] = indices; + pigeonMap['extraData'] = extraData.encode(); + pigeonMap['type'] = type.index; return pigeonMap; } @@ -58,6 +94,8 @@ class SearchReply { result: pigeonMap['result']! as String, error: pigeonMap['error']! as String, indices: (pigeonMap['indices'] as List?)!.cast(), + extraData: ExtraData.decode(pigeonMap['extraData']!), + type: ReplyType.values[pigeonMap['type']! as int], ); } } @@ -66,12 +104,15 @@ class _NonNullHostApiCodec extends StandardMessageCodec { const _NonNullHostApiCodec(); @override void writeValue(WriteBuffer buffer, Object? value) { - if (value is SearchReply) { + if (value is ExtraData) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else if (value is SearchRequest) { + } else if (value is SearchReply) { buffer.putUint8(129); writeValue(buffer, value.encode()); + } else if (value is SearchRequest) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -81,9 +122,12 @@ class _NonNullHostApiCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 128: - return SearchReply.decode(readValue(buffer)!); + return ExtraData.decode(readValue(buffer)!); case 129: + return SearchReply.decode(readValue(buffer)!); + + case 130: return SearchRequest.decode(readValue(buffer)!); default: @@ -137,12 +181,15 @@ class _NonNullFlutterApiCodec extends StandardMessageCodec { const _NonNullFlutterApiCodec(); @override void writeValue(WriteBuffer buffer, Object? value) { - if (value is SearchReply) { + if (value is ExtraData) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else if (value is SearchRequest) { + } else if (value is SearchReply) { buffer.putUint8(129); writeValue(buffer, value.encode()); + } else if (value is SearchRequest) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -152,9 +199,12 @@ class _NonNullFlutterApiCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 128: - return SearchReply.decode(readValue(buffer)!); + return ExtraData.decode(readValue(buffer)!); case 129: + return SearchReply.decode(readValue(buffer)!); + + case 130: return SearchRequest.decode(readValue(buffer)!); default: diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/nullable_returns.gen.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/nullable_returns.gen.dart index cf07297eaf0c..52cf1e57d907 100644 --- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/nullable_returns.gen.dart +++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/nullable_returns.gen.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // -// Autogenerated from Pigeon (v2.0.4), do not edit directly. +// Autogenerated from Pigeon (v3.0.2), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name // @dart = 2.12 @@ -12,24 +12,24 @@ import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; import 'package:flutter/services.dart'; -class _NonNullHostApiCodec extends StandardMessageCodec { - const _NonNullHostApiCodec(); +class _NullableReturnHostApiCodec extends StandardMessageCodec { + const _NullableReturnHostApiCodec(); } -class NonNullHostApi { - /// Constructor for [NonNullHostApi]. The [binaryMessenger] named argument is +class NullableReturnHostApi { + /// Constructor for [NullableReturnHostApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - NonNullHostApi({BinaryMessenger? binaryMessenger}) + NullableReturnHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _NonNullHostApiCodec(); + static const MessageCodec codec = _NullableReturnHostApiCodec(); Future doit() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.NonNullHostApi.doit', codec, + 'dev.flutter.pigeon.NullableReturnHostApi.doit', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = await channel.send(null) as Map?; @@ -52,19 +52,19 @@ class NonNullHostApi { } } -class _NonNullFlutterApiCodec extends StandardMessageCodec { - const _NonNullFlutterApiCodec(); +class _NullableReturnFlutterApiCodec extends StandardMessageCodec { + const _NullableReturnFlutterApiCodec(); } -abstract class NonNullFlutterApi { - static const MessageCodec codec = _NonNullFlutterApiCodec(); +abstract class NullableReturnFlutterApi { + static const MessageCodec codec = _NullableReturnFlutterApiCodec(); int? doit(); - static void setup(NonNullFlutterApi? api, + static void setup(NullableReturnFlutterApi? api, {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.NonNullFlutterApi.doit', codec, + 'dev.flutter.pigeon.NullableReturnFlutterApi.doit', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); @@ -153,3 +153,150 @@ abstract class NullableArgFlutterApi { } } } + +class _NullableCollectionReturnHostApiCodec extends StandardMessageCodec { + const _NullableCollectionReturnHostApiCodec(); +} + +class NullableCollectionReturnHostApi { + /// Constructor for [NullableCollectionReturnHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + NullableCollectionReturnHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = + _NullableCollectionReturnHostApiCodec(); + + Future?> doit() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NullableCollectionReturnHostApi.doit', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as List?)?.cast(); + } + } +} + +class _NullableCollectionReturnFlutterApiCodec extends StandardMessageCodec { + const _NullableCollectionReturnFlutterApiCodec(); +} + +abstract class NullableCollectionReturnFlutterApi { + static const MessageCodec codec = + _NullableCollectionReturnFlutterApiCodec(); + + List? doit(); + static void setup(NullableCollectionReturnFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NullableCollectionReturnFlutterApi.doit', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + // ignore message + final List? output = api.doit(); + return output; + }); + } + } + } +} + +class _NullableCollectionArgHostApiCodec extends StandardMessageCodec { + const _NullableCollectionArgHostApiCodec(); +} + +class NullableCollectionArgHostApi { + /// Constructor for [NullableCollectionArgHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + NullableCollectionArgHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = + _NullableCollectionArgHostApiCodec(); + + Future> doit(List? arg_x) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NullableCollectionArgHostApi.doit', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_x]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as List?)!.cast(); + } + } +} + +class _NullableCollectionArgFlutterApiCodec extends StandardMessageCodec { + const _NullableCollectionArgFlutterApiCodec(); +} + +abstract class NullableCollectionArgFlutterApi { + static const MessageCodec codec = + _NullableCollectionArgFlutterApiCodec(); + + List doit(List? x); + static void setup(NullableCollectionArgFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NullableCollectionArgFlutterApi.doit', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.NullableCollectionArgFlutterApi.doit was null.'); + final List args = (message as List?)!; + final List? arg_x = + (args[0] as List?)?.cast(); + final List output = api.doit(arg_x); + return output; + }); + } + } + } +} diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/pubspec.yaml b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/pubspec.yaml index 9b2123478d91..dad6b7c34e5c 100644 --- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/pubspec.yaml +++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/pubspec.yaml @@ -10,7 +10,7 @@ dependencies: sdk: flutter dev_dependencies: - build_runner: ^1.11.0 + build_runner: ^2.1.10 flutter_test: sdk: flutter mockito: ^5.0.4 diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.dart index 9c0ab20ccf42..5cd468c12c8f 100644 --- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.dart +++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.dart @@ -13,7 +13,13 @@ import 'package:mockito/mockito.dart'; import 'null_safe_test.mocks.dart'; import 'test_util.dart'; -@GenerateMocks([BinaryMessenger, NullableArgFlutterApi]) +@GenerateMocks([ + BinaryMessenger, + NullableArgFlutterApi, + NullableReturnFlutterApi, + NullableCollectionArgFlutterApi, + NullableCollectionReturnFlutterApi, +]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -105,6 +111,21 @@ void main() { expect(await api.doit(null), 123); }); + test('send null collection parameter', () async { + final BinaryMessenger mockMessenger = MockBinaryMessenger(); + const String channel = + 'dev.flutter.pigeon.NullableCollectionArgHostApi.doit'; + when(mockMessenger.send(channel, any)) + .thenAnswer((Invocation realInvocation) async { + return Api.codec.encodeMessage({ + 'result': ['123'] + }); + }); + final NullableCollectionArgHostApi api = + NullableCollectionArgHostApi(binaryMessenger: mockMessenger); + expect(await api.doit(null), ['123']); + }); + test('receive null parameters', () { final MockNullableArgFlutterApi mockFlutterApi = MockNullableArgFlutterApi(); @@ -115,8 +136,9 @@ void main() { final Completer resultCompleter = Completer(); // Null check operator is used because ServicesBinding.instance is nullable // in earlier versions of Flutter. - // ignore: unnecessary_non_null_assertion - ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage( + _ambiguate(ServicesBinding.instance)! + .defaultBinaryMessenger + .handlePlatformMessage( 'dev.flutter.pigeon.NullableArgFlutterApi.doit', NullableArgFlutterApi.codec.encodeMessage([null]), (ByteData? data) { @@ -131,4 +153,121 @@ void main() { // Removes message handlers from global default binary messenger. NullableArgFlutterApi.setup(null); }); + + test('receive null collection parameters', () { + final MockNullableCollectionArgFlutterApi mockFlutterApi = + MockNullableCollectionArgFlutterApi(); + when(mockFlutterApi.doit(null)).thenReturn(['14']); + + NullableCollectionArgFlutterApi.setup(mockFlutterApi); + + final Completer> resultCompleter = Completer>(); + // Null check operator is used because ServicesBinding.instance is nullable + // in earlier versions of Flutter. + _ambiguate(ServicesBinding.instance)! + .defaultBinaryMessenger + .handlePlatformMessage( + 'dev.flutter.pigeon.NullableCollectionArgFlutterApi.doit', + NullableCollectionArgFlutterApi.codec.encodeMessage([null]), + (ByteData? data) { + resultCompleter.complete( + (NullableCollectionArgFlutterApi.codec.decodeMessage(data)! + as List) + .cast(), + ); + }, + ); + + expect(resultCompleter.future, completion(['14'])); + + // Removes message handlers from global default binary messenger. + NullableArgFlutterApi.setup(null); + }); + + test('receive null return', () async { + final BinaryMessenger mockMessenger = MockBinaryMessenger(); + const String channel = 'dev.flutter.pigeon.NullableReturnHostApi.doit'; + when(mockMessenger.send(channel, any)) + .thenAnswer((Invocation realInvocation) async { + return NullableReturnHostApi.codec + .encodeMessage({'result': null}); + }); + final NullableReturnHostApi api = + NullableReturnHostApi(binaryMessenger: mockMessenger); + expect(await api.doit(), null); + }); + + test('receive null collection return', () async { + final BinaryMessenger mockMessenger = MockBinaryMessenger(); + const String channel = + 'dev.flutter.pigeon.NullableCollectionReturnHostApi.doit'; + when(mockMessenger.send(channel, any)) + .thenAnswer((Invocation realInvocation) async { + return NullableCollectionReturnHostApi.codec + .encodeMessage({'result': null}); + }); + final NullableCollectionReturnHostApi api = + NullableCollectionReturnHostApi(binaryMessenger: mockMessenger); + expect(await api.doit(), null); + }); + + test('send null return', () async { + final MockNullableReturnFlutterApi mockFlutterApi = + MockNullableReturnFlutterApi(); + when(mockFlutterApi.doit()).thenReturn(null); + + NullableReturnFlutterApi.setup(mockFlutterApi); + + final Completer resultCompleter = Completer(); + // Null check operator is used because ServicesBinding.instance is nullable + // in earlier versions of Flutter. + _ambiguate(ServicesBinding.instance)! + .defaultBinaryMessenger + .handlePlatformMessage( + 'dev.flutter.pigeon.NullableReturnFlutterApi.doit', + NullableReturnFlutterApi.codec.encodeMessage([]), + (ByteData? data) { + resultCompleter.complete(null); + }, + ); + + expect(resultCompleter.future, completion(null)); + + // Removes message handlers from global default binary messenger. + NullableArgFlutterApi.setup(null); + }); + + test('send null collection return', () async { + final MockNullableCollectionReturnFlutterApi mockFlutterApi = + MockNullableCollectionReturnFlutterApi(); + when(mockFlutterApi.doit()).thenReturn(null); + + NullableCollectionReturnFlutterApi.setup(mockFlutterApi); + + final Completer?> resultCompleter = + Completer?>(); + // Null check operator is used because ServicesBinding.instance is nullable + // in earlier versions of Flutter. + _ambiguate(ServicesBinding.instance)! + .defaultBinaryMessenger + .handlePlatformMessage( + 'dev.flutter.pigeon.NullableCollectionReturnFlutterApi.doit', + NullableCollectionReturnFlutterApi.codec.encodeMessage([]), + (ByteData? data) { + resultCompleter.complete(null); + }, + ); + + expect(resultCompleter.future, completion(null)); + + // Removes message handlers from global default binary messenger. + NullableArgFlutterApi.setup(null); + }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +// TODO(stuartmorgan): Remove this once 2.13 or later is on the stable channel. +T? _ambiguate(T? value) => value; diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.mocks.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.mocks.dart index 5f8f1292d56d..9b5bfcbcf5c8 100644 --- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.mocks.dart +++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.mocks.dart @@ -56,3 +56,38 @@ class MockNullableArgFlutterApi extends _i1.Mock (super.noSuchMethod(Invocation.method(#doit, [x]), returnValue: 0) as int); } + +/// A class which mocks [NullableReturnFlutterApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockNullableReturnFlutterApi extends _i1.Mock + implements _i6.NullableReturnFlutterApi { + MockNullableReturnFlutterApi() { + _i1.throwOnMissingStub(this); + } +} + +/// A class which mocks [NullableCollectionArgFlutterApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockNullableCollectionArgFlutterApi extends _i1.Mock + implements _i6.NullableCollectionArgFlutterApi { + MockNullableCollectionArgFlutterApi() { + _i1.throwOnMissingStub(this); + } + + @override + List doit(List? x) => (super + .noSuchMethod(Invocation.method(#doit, [x]), returnValue: []) + as List); +} + +/// A class which mocks [NullableCollectionReturnFlutterApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockNullableCollectionReturnFlutterApi extends _i1.Mock + implements _i6.NullableCollectionReturnFlutterApi { + MockNullableCollectionReturnFlutterApi() { + _i1.throwOnMissingStub(this); + } +} diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml index bd17ad5c9ef1..a08046ac0ec6 100644 --- a/packages/pigeon/pubspec.yaml +++ b/packages/pigeon/pubspec.yaml @@ -2,7 +2,7 @@ name: pigeon description: Code generator tool to make communication between Flutter and the host platform type-safe and easier. repository: https://github.com/flutter/packages/tree/main/packages/pigeon issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3Apigeon -version: 3.0.1 # This must match the version in lib/generator_tools.dart +version: 3.0.2 # This must match the version in lib/generator_tools.dart environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/pigeon/test/dart_generator_test.dart b/packages/pigeon/test/dart_generator_test.dart index 4f03de674657..a7c2bef57d43 100644 --- a/packages/pigeon/test/dart_generator_test.dart +++ b/packages/pigeon/test/dart_generator_test.dart @@ -204,6 +204,50 @@ void main() { ); }); + test('nested non-nullable class', () { + final Root root = Root(apis: [], classes: [ + Class( + name: 'Input', + fields: [ + NamedType( + type: const TypeDeclaration( + baseName: 'String', + isNullable: false, + ), + name: 'input', + offset: null) + ], + ), + Class( + name: 'Nested', + fields: [ + NamedType( + type: const TypeDeclaration( + baseName: 'Input', + isNullable: false, + ), + name: 'nested', + offset: null) + ], + ) + ], enums: []); + final StringBuffer sink = StringBuffer(); + generateDart(const DartOptions(), root, sink); + final String code = sink.toString(); + expect( + code, + contains( + 'pigeonMap[\'nested\'] = nested.encode()', + ), + ); + expect( + code.replaceAll('\n', ' ').replaceAll(' ', ''), + contains( + 'nested: Input.decode(pigeonMap[\'nested\']!)', + ), + ); + }); + test('flutterapi', () { final Root root = Root(apis: [ Api(name: 'Api', location: ApiLocation.flutter, methods: [ @@ -401,6 +445,51 @@ void main() { expect(code, contains('EnumClass doSomething(EnumClass arg0);')); }); + test('flutter non-nullable enum argument with enum class', () { + final Root root = Root(apis: [ + Api(name: 'Api', location: ApiLocation.flutter, methods: [ + Method( + name: 'doSomething', + arguments: [ + NamedType( + type: const TypeDeclaration( + baseName: 'EnumClass', + isNullable: false, + ), + name: '', + offset: null) + ], + returnType: + const TypeDeclaration(baseName: 'EnumClass', isNullable: false), + isAsynchronous: false, + ) + ]) + ], classes: [ + Class(name: 'EnumClass', fields: [ + NamedType( + type: const TypeDeclaration( + baseName: 'Enum', + isNullable: false, + ), + name: 'enum1', + offset: null) + ]), + ], enums: [ + Enum( + name: 'Enum', + members: [ + 'one', + 'two', + ], + ) + ]); + final StringBuffer sink = StringBuffer(); + generateDart(const DartOptions(), root, sink); + final String code = sink.toString(); + expect(code, contains('pigeonMap[\'enum1\'] = enum1.index;')); + expect(code, contains('enum1: Enum.values[pigeonMap[\'enum1\']! as int]')); + }); + test('host void argument', () { final Root root = Root(apis: [ Api(name: 'Api', location: ApiLocation.host, methods: [ @@ -893,6 +982,34 @@ void main() { expect(code, contains('return (replyMap[\'result\'] as int?);')); }); + test('return nullable collection host', () { + final Root root = Root( + apis: [ + Api(name: 'Api', location: ApiLocation.host, methods: [ + Method( + name: 'doit', + returnType: const TypeDeclaration( + baseName: 'List', + isNullable: true, + typeArguments: [ + TypeDeclaration(baseName: 'int', isNullable: true) + ]), + arguments: []) + ]) + ], + classes: [], + enums: [], + ); + final StringBuffer sink = StringBuffer(); + generateDart(const DartOptions(), root, sink); + final String code = sink.toString(); + expect(code, contains('Future?> doit()')); + expect( + code, + contains( + 'return (replyMap[\'result\'] as List?)?.cast();')); + }); + test('return nullable async host', () { final Root root = Root( apis: [