diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md index 8c7d93abc70c..a8c1255b7eeb 100644 --- a/packages/pigeon/CHANGELOG.md +++ b/packages/pigeon/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.19 + +* Implements nullable return types. + ## 1.0.18 * [front-end] Fix error caused by parsing `copyrightHeaders` passed to options in `@ConfigurePigeon`. diff --git a/packages/pigeon/README.md b/packages/pigeon/README.md index 9dfcec7910aa..b74f4fea9aae 100644 --- a/packages/pigeon/README.md +++ b/packages/pigeon/README.md @@ -64,8 +64,8 @@ denotes APIs that live in Flutter but are invoked from the host platform. `void`. 1) Generics are supported, but can currently only be used with nullable types (example: `List`). -1) Arguments and return values to methods must be non-nullable. Fields on - classes can be nullable or non-nullable. +1) Arguments must be non-nullable. Fields on classes and return types can be + nullable or non-nullable. ## Supported Datatypes @@ -124,11 +124,11 @@ Pigeon supports generating null-safe code, but it doesn't yet support: 1) Nullable method parameters 1) Nullable generics type arguments -1) Nullable return values It does support: 1) Nullable and Non-nullable class fields. +1) Nullable return values The default is to generate null-safe code but in order to generate non-null-safe code run Pigeon with the extra argument `--no-dart_null_safety`. For example: diff --git a/packages/pigeon/bin/run_tests.dart b/packages/pigeon/bin/run_tests.dart index 927c5178ca7a..a506b78fda92 100644 --- a/packages/pigeon/bin/run_tests.dart +++ b/packages/pigeon/bin/run_tests.dart @@ -144,6 +144,8 @@ Future _runFlutterUnitTests() async { '$flutterUnitTestsPath/lib/multiple_arity.gen.dart', 'pigeons/non_null_fields.dart': '$flutterUnitTestsPath/lib/non_null_fields.gen.dart', + 'pigeons/nullable_returns.dart': + '$flutterUnitTestsPath/lib/nullable_returns.gen.dart', }); if (generateCode != 0) { return generateCode; diff --git a/packages/pigeon/lib/dart_generator.dart b/packages/pigeon/lib/dart_generator.dart index fe40d43ff79b..2f735b45c728 100644 --- a/packages/pigeon/lib/dart_generator.dart +++ b/packages/pigeon/lib/dart_generator.dart @@ -186,7 +186,7 @@ final BinaryMessenger$nullTag _binaryMessenger; argSignature = _getMethodArgumentsSignature(func, argNameFunc, nullTag); } indent.write( - 'Future<${_addGenericTypes(func.returnType, nullTag)}> ${func.name}($argSignature) async ', + 'Future<${_addGenericTypesNullable(func.returnType, nullTag)}> ${func.name}($argSignature) async ', ); indent.scoped('{', '}', () { final String channelName = makeChannelName(api, func); @@ -200,16 +200,18 @@ final BinaryMessenger$nullTag _binaryMessenger; final String returnType = _makeGenericTypeArguments(func.returnType, nullTag); final String castCall = _makeGenericCastCall(func.returnType, nullTag); + const String accessor = 'replyMap[\'${Keys.result}\']'; + final String unwrapper = + func.returnType.isNullable ? '' : unwrapOperator; final String returnStatement = func.returnType.isVoid ? 'return;' - : 'return (replyMap[\'${Keys.result}\'] as $returnType$nullTag)$unwrapOperator$castCall;'; + : 'return ($accessor as $returnType$nullTag)$unwrapper$castCall;'; indent.format(''' final Map$nullTag replyMap =\n\t\tawait channel.send($sendArgument) as Map$nullTag; if (replyMap == null) { \tthrow PlatformException( \t\tcode: 'channel-error', \t\tmessage: 'Unable to establish connection on channel.', -\t\tdetails: null, \t); } else if (replyMap['error'] != null) { \tfinal Map error = (replyMap['${Keys.error}'] as Map$nullTag)$unwrapOperator; @@ -217,7 +219,19 @@ if (replyMap == null) { \t\tcode: (error['${Keys.errorCode}'] as String$nullTag)$unwrapOperator, \t\tmessage: error['${Keys.errorMessage}'] as String$nullTag, \t\tdetails: error['${Keys.errorDetails}'], -\t); +\t);'''); + // On iOS we can return nil from functions to accommodate error + // handling. Returning a nil value and not returning an error is an + // exception. + if (!func.returnType.isNullable && !func.returnType.isVoid) { + indent.format(''' +} else if (replyMap['${Keys.result}'] == null) { +\tthrow PlatformException( +\t\tcode: 'null-error', +\t\tmessage: 'Host platform returned null value for non-null return value.', +\t);'''); + } + indent.format(''' } else { \t$returnStatement }'''); @@ -255,8 +269,8 @@ void _writeFlutterApi( for (final Method func in api.methods) { final bool isAsync = func.isAsynchronous; final String returnType = isAsync - ? 'Future<${_addGenericTypes(func.returnType, nullTag)}>' - : _addGenericTypes(func.returnType, nullTag); + ? 'Future<${_addGenericTypesNullable(func.returnType, nullTag)}>' + : _addGenericTypesNullable(func.returnType, nullTag); final String argSignature = _getMethodArgumentsSignature( func, _getArgumentName, @@ -294,7 +308,7 @@ void _writeFlutterApi( ); indent.scoped('{', '});', () { final String returnType = - _addGenericTypes(func.returnType, nullTag); + _addGenericTypesNullable(func.returnType, nullTag); final bool isAsync = func.isAsynchronous; final String emptyReturnStatement = isMockHandler ? 'return {};' @@ -387,9 +401,9 @@ String _addGenericTypes(TypeDeclaration type, String nullTag) { } } -String _addGenericTypesNullable(NamedType field, String nullTag) { - final String genericdType = _addGenericTypes(field.type, nullTag); - return field.type.isNullable ? '$genericdType$nullTag' : genericdType; +String _addGenericTypesNullable(TypeDeclaration type, String nullTag) { + final String genericdType = _addGenericTypes(type, nullTag); + return type.isNullable ? '$genericdType$nullTag' : genericdType; } /// Generates Dart source code for the given AST represented by [root], @@ -495,7 +509,8 @@ pigeonMap['${field.name}'] != null '(pigeonMap[\'${field.name}\'] as $genericType$nullTag)$castCallPrefix$castCall', ); } else { - final String genericdType = _addGenericTypesNullable(field, nullTag); + final String genericdType = + _addGenericTypesNullable(field.type, nullTag); if (field.type.isNullable) { indent.add( 'pigeonMap[\'${field.name}\'] as $genericdType', @@ -532,7 +547,7 @@ pigeonMap['${field.name}'] != null writeConstructor(); indent.addln(''); for (final NamedType field in klass.fields) { - final String datatype = _addGenericTypesNullable(field, nullTag); + final String datatype = _addGenericTypesNullable(field.type, nullTag); indent.writeln('$datatype ${field.name};'); } if (klass.fields.isNotEmpty) { diff --git a/packages/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart index 850e6b3a6b49..9a7487ab4de8 100644 --- a/packages/pigeon/lib/generator_tools.dart +++ b/packages/pigeon/lib/generator_tools.dart @@ -8,7 +8,7 @@ import 'dart:mirrors'; import 'ast.dart'; /// The current version of pigeon. This must match the version in pubspec.yaml. -const String pigeonVersion = '1.0.18'; +const String pigeonVersion = '1.0.19'; /// Read all the content from [stdin] to a String. String readStdin() { diff --git a/packages/pigeon/lib/java_generator.dart b/packages/pigeon/lib/java_generator.dart index dd9c391913ae..ef17076233cf 100644 --- a/packages/pigeon/lib/java_generator.dart +++ b/packages/pigeon/lib/java_generator.dart @@ -118,95 +118,85 @@ void _writeCodec(Indent indent, Api api, Root root) { void _writeHostApi(Indent indent, Api api) { assert(api.location == ApiLocation.host); - indent.writeln( - '/** Generated interface from Pigeon that represents a handler of messages from Flutter.*/'); - indent.write('public interface ${api.name} '); - indent.scoped('{', '}', () { - for (final Method method in api.methods) { - final String returnType = method.isAsynchronous - ? 'void' + /// Write a method in the interface. + /// Example: + /// int add(int x, int y); + void writeInterfaceMethod(final Method method) { + final String returnType = method.isAsynchronous + ? 'void' + : _nullsafeJavaTypeForDartType(method.returnType); + final List argSignature = []; + if (method.arguments.isNotEmpty) { + final Iterable argTypes = + method.arguments.map((NamedType e) => _javaTypeForDartType(e.type)); + final Iterable argNames = + method.arguments.map((NamedType e) => e.name); + argSignature + .addAll(map2(argTypes, argNames, (String argType, String argName) { + return '$argType $argName'; + })); + } + if (method.isAsynchronous) { + final String resultType = method.returnType.isVoid + ? 'Void' : _javaTypeForDartType(method.returnType); - final List argSignature = []; - if (method.arguments.isNotEmpty) { - final Iterable argTypes = - method.arguments.map((NamedType e) => _javaTypeForDartType(e.type)); - final Iterable argNames = - method.arguments.map((NamedType e) => e.name); - argSignature - .addAll(map2(argTypes, argNames, (String argType, String argName) { - return '$argType $argName'; - })); - } - if (method.isAsynchronous) { - final String returnType = method.returnType.isVoid - ? 'Void' - : _javaTypeForDartType(method.returnType); - argSignature.add('Result<$returnType> result'); - } - indent.writeln('$returnType ${method.name}(${argSignature.join(', ')});'); + argSignature.add('Result<$resultType> result'); } - indent.addln(''); - final String codecName = _getCodecName(api); - indent.format(''' -/** The codec used by ${api.name}. */ -static MessageCodec getCodec() { -\treturn $codecName.INSTANCE; -} -'''); - indent.writeln( - '/** Sets up an instance of `${api.name}` to handle messages through the `binaryMessenger`. */'); - indent.write( - 'static void setup(BinaryMessenger binaryMessenger, ${api.name} api) '); + indent.writeln('$returnType ${method.name}(${argSignature.join(', ')});'); + } + + /// Write a static setup function in the interface. + /// Example: + /// static void setup(BinaryMessenger binaryMessenger, Foo api) {...} + void writeMethodSetup(final Method method) { + final String channelName = makeChannelName(api, method); + indent.write(''); indent.scoped('{', '}', () { - for (final Method method in api.methods) { - final String channelName = makeChannelName(api, method); - indent.write(''); - indent.scoped('{', '}', () { - indent.writeln('BasicMessageChannel channel ='); - indent.inc(); - indent.inc(); - indent.writeln( - 'new BasicMessageChannel<>(binaryMessenger, "$channelName", getCodec());'); - indent.dec(); - indent.dec(); - indent.write('if (api != null) '); - indent.scoped('{', '} else {', () { - indent.write('channel.setMessageHandler((message, reply) -> '); - indent.scoped('{', '});', () { - final String returnType = method.returnType.isVoid - ? 'Void' - : _javaTypeForDartType(method.returnType); - indent.writeln('Map wrapped = new HashMap<>();'); - indent.write('try '); - indent.scoped('{', '}', () { - final List methodArgument = []; - if (method.arguments.isNotEmpty) { + indent.writeln('BasicMessageChannel channel ='); + indent.inc(); + indent.inc(); + indent.writeln( + 'new BasicMessageChannel<>(binaryMessenger, "$channelName", getCodec());'); + indent.dec(); + indent.dec(); + indent.write('if (api != null) '); + indent.scoped('{', '} else {', () { + indent.write('channel.setMessageHandler((message, reply) -> '); + indent.scoped('{', '});', () { + final String returnType = method.returnType.isVoid + ? 'Void' + : _javaTypeForDartType(method.returnType); + indent.writeln('Map wrapped = new HashMap<>();'); + indent.write('try '); + indent.scoped('{', '}', () { + final List methodArgument = []; + if (method.arguments.isNotEmpty) { + indent.writeln( + 'ArrayList args = (ArrayList)message;'); + enumerate(method.arguments, (int index, NamedType arg) { + // The StandardMessageCodec can give us [Integer, Long] for + // a Dart 'int'. To keep things simple we just use 64bit + // longs in Pigeon with Java. + final bool isInt = arg.type.baseName == 'int'; + final String argType = + isInt ? 'Number' : _javaTypeForDartType(arg.type); + final String argCast = isInt ? '.longValue()' : ''; + final String argName = _getSafeArgumentName(index, arg); + indent + .writeln('$argType $argName = ($argType)args.get($index);'); + indent.write('if ($argName == null) '); + indent.scoped('{', '}', () { indent.writeln( - 'ArrayList args = (ArrayList)message;'); - enumerate(method.arguments, (int index, NamedType arg) { - // The StandardMessageCodec can give us [Integer, Long] for - // a Dart 'int'. To keep things simple we just use 64bit - // longs in Pigeon with Java. - final bool isInt = arg.type.baseName == 'int'; - final String argType = - isInt ? 'Number' : _javaTypeForDartType(arg.type); - final String argCast = isInt ? '.longValue()' : ''; - final String argName = _getSafeArgumentName(index, arg); - indent.writeln( - '$argType $argName = ($argType)args.get($index);'); - indent.write('if ($argName == null) '); - indent.scoped('{', '}', () { - indent.writeln( - 'throw new NullPointerException("$argName unexpectedly null.");'); - }); - methodArgument.add('$argName$argCast'); - }); - } - if (method.isAsynchronous) { - final String resultValue = - method.returnType.isVoid ? 'null' : 'result'; - const String resultName = 'resultCallback'; - indent.format(''' + 'throw new NullPointerException("$argName unexpectedly null.");'); + }); + methodArgument.add('$argName$argCast'); + }); + } + if (method.isAsynchronous) { + final String resultValue = + method.returnType.isVoid ? 'null' : 'result'; + const String resultName = 'resultCallback'; + indent.format(''' Result<$returnType> $resultName = new Result<$returnType>() { \tpublic void success($returnType result) { \t\twrapped.put("${Keys.result}", $resultValue); @@ -218,38 +208,58 @@ Result<$returnType> $resultName = new Result<$returnType>() { \t} }; '''); - methodArgument.add(resultName); - } - final String call = - 'api.${method.name}(${methodArgument.join(', ')})'; - if (method.isAsynchronous) { - indent.writeln('$call;'); - } else if (method.returnType.isVoid) { - indent.writeln('$call;'); - indent.writeln('wrapped.put("${Keys.result}", null);'); - } else { - indent.writeln('$returnType output = $call;'); - indent.writeln('wrapped.put("${Keys.result}", output);'); - } - }); - indent.write('catch (Error | RuntimeException exception) '); - indent.scoped('{', '}', () { - indent.writeln( - 'wrapped.put("${Keys.error}", wrapError(exception));'); - if (method.isAsynchronous) { - indent.writeln('reply.reply(wrapped);'); - } - }); - if (!method.isAsynchronous) { - indent.writeln('reply.reply(wrapped);'); - } - }); + methodArgument.add(resultName); + } + final String call = + 'api.${method.name}(${methodArgument.join(', ')})'; + if (method.isAsynchronous) { + indent.writeln('$call;'); + } else if (method.returnType.isVoid) { + indent.writeln('$call;'); + indent.writeln('wrapped.put("${Keys.result}", null);'); + } else { + indent.writeln('$returnType output = $call;'); + indent.writeln('wrapped.put("${Keys.result}", output);'); + } }); - indent.scoped(null, '}', () { - indent.writeln('channel.setMessageHandler(null);'); + indent.write('catch (Error | RuntimeException exception) '); + indent.scoped('{', '}', () { + indent + .writeln('wrapped.put("${Keys.error}", wrapError(exception));'); + if (method.isAsynchronous) { + indent.writeln('reply.reply(wrapped);'); + } }); + if (!method.isAsynchronous) { + indent.writeln('reply.reply(wrapped);'); + } }); - } + }); + indent.scoped(null, '}', () { + indent.writeln('channel.setMessageHandler(null);'); + }); + }); + } + + indent.writeln( + '/** Generated interface from Pigeon that represents a handler of messages from Flutter.*/'); + indent.write('public interface ${api.name} '); + indent.scoped('{', '}', () { + api.methods.forEach(writeInterfaceMethod); + indent.addln(''); + final String codecName = _getCodecName(api); + indent.format(''' +/** The codec used by ${api.name}. */ +static MessageCodec getCodec() { +\treturn $codecName.INSTANCE; +} +'''); + indent.writeln( + '/** Sets up an instance of `${api.name}` to handle messages through the `binaryMessenger`. */'); + indent.write( + 'static void setup(BinaryMessenger binaryMessenger, ${api.name} api) '); + indent.scoped('{', '}', () { + api.methods.forEach(writeMethodSetup); }); }); } @@ -393,6 +403,11 @@ String _javaTypeForDartType(TypeDeclaration type) { return _javaTypeForBuiltinDartType(type) ?? type.baseName; } +String _nullsafeJavaTypeForDartType(TypeDeclaration type) { + final String nullSafe = type.isNullable ? '@Nullable' : '@NonNull'; + return '$nullSafe ${_javaTypeForDartType(type)}'; +} + /// Casts variable named [varName] to the correct host datatype for [field]. /// This is for use in codecs where we may have a map representation of an /// object. diff --git a/packages/pigeon/lib/objc_generator.dart b/packages/pigeon/lib/objc_generator.dart index 8ef398692216..87a8d2385c37 100644 --- a/packages/pigeon/lib/objc_generator.dart +++ b/packages/pigeon/lib/objc_generator.dart @@ -69,7 +69,7 @@ String _className(String? prefix, String className) { String _callbackForType(TypeDeclaration type, _ObjcPtr objcType) { return type.isVoid ? 'void(^)(NSError *_Nullable)' - : 'void(^)(${objcType.ptr.trim()}, NSError *_Nullable)'; + : 'void(^)(${objcType.ptr.trim()}_Nullable, NSError *_Nullable)'; } /// Represents an ObjC pointer (ex 'id', 'NSString *'). @@ -427,6 +427,9 @@ void _writeHostApiDeclaration(Indent indent, Api api, ObjcOptions options) { lastArgType = 'FlutterError *_Nullable *_Nonnull'; lastArgName = 'error'; } + if (!func.returnType.isNullable) { + indent.writeln('/// @return `nil` only when `error != nil`.'); + } indent.writeln(_makeObjcSignature( func: func, options: options, diff --git a/packages/pigeon/lib/pigeon_lib.dart b/packages/pigeon/lib/pigeon_lib.dart index 428056c03c2e..71082dd37ecc 100644 --- a/packages/pigeon/lib/pigeon_lib.dart +++ b/packages/pigeon/lib/pigeon_lib.dart @@ -474,13 +474,6 @@ List _validateAst(Root root, String source) { } for (final Api api in root.apis) { for (final Method method in api.methods) { - if (method.returnType.isNullable) { - result.add(Error( - message: - 'Nullable return types types aren\'t supported for Pigeon methods: "${method.returnType.baseName}" in API: "${api.name}" method: "${method.name}"', - lineNumber: _calculateLineNumberNullable(source, method.offset), - )); - } if (method.arguments.isNotEmpty && method.arguments.any((NamedType element) => customEnums.contains(element.type.baseName))) { diff --git a/packages/pigeon/pigeons/nullable_returns.dart b/packages/pigeon/pigeons/nullable_returns.dart new file mode 100644 index 000000000000..caa33efd56fe --- /dev/null +++ b/packages/pigeon/pigeons/nullable_returns.dart @@ -0,0 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This file is an example pigeon file that is used in compilation, unit, mock +// handler, and e2e tests. + +import 'package:pigeon/pigeon.dart'; + +@HostApi() +abstract class NonNullHostApi { + int? doit(); +} + +@FlutterApi() +abstract class NonNullFlutterApi { + int? doit(); +} diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/all_datatypes.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/all_datatypes.dart index d71bddcfe7a8..f318dc58e7ee 100644 --- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/all_datatypes.dart +++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/all_datatypes.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 (v1.0.15), do not edit directly. +// Autogenerated from Pigeon (v1.0.19), 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 @@ -130,7 +130,6 @@ class HostEverything { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -140,6 +139,11 @@ class HostEverything { 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 Everything?)!; } @@ -155,7 +159,6 @@ class HostEverything { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -165,6 +168,11 @@ class HostEverything { 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 Everything?)!; } diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/multiple_arity.gen.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/multiple_arity.gen.dart index 6421b9f7277f..8c8060301f36 100644 --- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/multiple_arity.gen.dart +++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/multiple_arity.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 (v1.0.15), do not edit directly. +// Autogenerated from Pigeon (v1.0.19), 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 @@ -37,7 +37,6 @@ class MultipleArityHostApi { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -47,6 +46,11 @@ class MultipleArityHostApi { 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 int?)!; } 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 c8942847a2f0..97ea6a48ab8f 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 (v1.0.15), do not edit directly. +// Autogenerated from Pigeon (v1.0.19), 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 @@ -28,7 +28,7 @@ class SearchRequest { static SearchRequest decode(Object message) { final Map pigeonMap = message as Map; return SearchRequest( - query: (pigeonMap['query'] as String?)!, + query: pigeonMap['query']! as String, ); } } @@ -55,8 +55,8 @@ class SearchReply { static SearchReply decode(Object message) { final Map pigeonMap = message as Map; return SearchReply( - result: (pigeonMap['result'] as String?)!, - error: (pigeonMap['error'] as String?)!, + result: pigeonMap['result']! as String, + error: pigeonMap['error']! as String, indices: (pigeonMap['indices'] as List?)!.cast(), ); } @@ -113,7 +113,6 @@ class NonNullHostApi { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -123,6 +122,11 @@ class NonNullHostApi { 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 SearchReply?)!; } diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_safe_pigeon.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_safe_pigeon.dart index 5b3c5c0d00fa..5cce6081fa51 100644 --- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_safe_pigeon.dart +++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_safe_pigeon.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 (v1.0.15), do not edit directly. +// Autogenerated from Pigeon (v1.0.19), 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 @@ -162,7 +162,6 @@ class Api { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -172,6 +171,11 @@ class Api { 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 SearchReply?)!; } @@ -187,7 +191,6 @@ class Api { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -197,6 +200,11 @@ class Api { 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 SearchReplies?)!; } @@ -212,7 +220,6 @@ class Api { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -222,6 +229,11 @@ class Api { 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 SearchRequests?)!; } @@ -237,7 +249,6 @@ class Api { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -247,6 +258,11 @@ class Api { 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 int?)!; } 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 new file mode 100644 index 000000000000..f80c7d36bba8 --- /dev/null +++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/nullable_returns.gen.dart @@ -0,0 +1,80 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Autogenerated from Pigeon (v1.0.19), 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 +import 'dart:async'; +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 NonNullHostApi { + /// Constructor for [NonNullHostApi]. 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}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _NonNullHostApiCodec(); + + Future doit() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NonNullHostApi.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 int?); + } + } +} + +class _NonNullFlutterApiCodec extends StandardMessageCodec { + const _NonNullFlutterApiCodec(); +} + +abstract class NonNullFlutterApi { + static const MessageCodec codec = _NonNullFlutterApiCodec(); + + int? doit(); + static void setup(NonNullFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NonNullFlutterApi.doit', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + // ignore message + final int? output = api.doit(); + return output; + }); + } + } + } +} diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/primitive.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/primitive.dart index 780b4125c1a7..b71adc31435e 100644 --- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/primitive.dart +++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/primitive.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 (v1.0.15), do not edit directly. +// Autogenerated from Pigeon (v1.0.19), 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 @@ -37,7 +37,6 @@ class PrimitiveHostApi { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -47,6 +46,11 @@ class PrimitiveHostApi { 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 int?)!; } @@ -62,7 +66,6 @@ class PrimitiveHostApi { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -72,6 +75,11 @@ class PrimitiveHostApi { 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 bool?)!; } @@ -87,7 +95,6 @@ class PrimitiveHostApi { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -97,6 +104,11 @@ class PrimitiveHostApi { 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 String?)!; } @@ -112,7 +124,6 @@ class PrimitiveHostApi { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -122,6 +133,11 @@ class PrimitiveHostApi { 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 double?)!; } @@ -137,7 +153,6 @@ class PrimitiveHostApi { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -147,6 +162,11 @@ class PrimitiveHostApi { 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 Map?)!; } @@ -162,7 +182,6 @@ class PrimitiveHostApi { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -172,6 +191,11 @@ class PrimitiveHostApi { 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?)!; } @@ -187,7 +211,6 @@ class PrimitiveHostApi { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -197,6 +220,11 @@ class PrimitiveHostApi { 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 Int32List?)!; } @@ -212,7 +240,6 @@ class PrimitiveHostApi { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -222,6 +249,11 @@ class PrimitiveHostApi { 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(); } @@ -237,7 +269,6 @@ class PrimitiveHostApi { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -247,6 +278,11 @@ class PrimitiveHostApi { 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 Map?)! .cast(); 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 b00362ca49c3..e412b788cd6f 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 @@ -77,4 +77,16 @@ void main() { final int result = await api.anInt(1); expect(result, 1); }); + + test('return null to nonnull', () async { + final BinaryMessenger mockMessenger = MockBinaryMessenger(); + const String channel = 'dev.flutter.pigeon.Api.anInt'; + when(mockMessenger.send(channel, any)) + .thenAnswer((Invocation realInvocation) async { + return Api.codec.encodeMessage({'result': null}); + }); + final Api api = Api(binaryMessenger: mockMessenger); + expect(() async => api.anInt(1), + throwsA(const TypeMatcher())); + }); } diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj index 0a81cb2836d6..ed58c5c40153 100644 --- a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj @@ -3,10 +3,11 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 46; objects = { /* Begin PBXBuildFile section */ + 0D02163D27BC7B48009BD76F /* nullable_returns.gen.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D02163C27BC7B48009BD76F /* nullable_returns.gen.m */; }; 0D50127523FF75B100CD5B95 /* RunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D50127423FF75B100CD5B95 /* RunnerTests.m */; }; 0D6FD3C526A76D400046D8BD /* primitive.gen.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D6FD3C426A76D400046D8BD /* primitive.gen.m */; }; 0D6FD3C726A777C00046D8BD /* PrimitiveTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D6FD3C626A777C00046D8BD /* PrimitiveTest.m */; }; @@ -64,6 +65,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0D02163B27BC7B48009BD76F /* nullable_returns.gen.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = nullable_returns.gen.h; sourceTree = ""; }; + 0D02163C27BC7B48009BD76F /* nullable_returns.gen.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = nullable_returns.gen.m; sourceTree = ""; }; 0D50127223FF75B100CD5B95 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 0D50127423FF75B100CD5B95 /* RunnerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerTests.m; sourceTree = ""; }; 0D50127623FF75B100CD5B95 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -193,6 +196,8 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + 0D02163B27BC7B48009BD76F /* nullable_returns.gen.h */, + 0D02163C27BC7B48009BD76F /* nullable_returns.gen.m */, 0DBD8C40279B741800E4FDBA /* non_null_fields.gen.h */, 0DBD8C3F279B741800E4FDBA /* non_null_fields.gen.m */, 0DA5DFD426CC39D600D2354B /* multiple_arity.gen.h */, @@ -411,6 +416,7 @@ 0DD2E6BE2684031300A7D764 /* message.gen.m in Sources */, 0DD2E6BA2684031300A7D764 /* void_arg_host.gen.m in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 0D02163D27BC7B48009BD76F /* nullable_returns.gen.m in Sources */, 0DD2E6BF2684031300A7D764 /* enum.gen.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml index e3dba645e4a2..686205b3bb83 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: 1.0.18 # This must match the version in lib/generator_tools.dart +version: 1.0.19 # This must match the version in lib/generator_tools.dart environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/pigeon/run_tests.sh b/packages/pigeon/run_tests.sh index a44a34bc5c3c..60f39718659a 100755 --- a/packages/pigeon/run_tests.sh +++ b/packages/pigeon/run_tests.sh @@ -241,6 +241,7 @@ run_ios_unittests() { gen_ios_unittests_code ./pigeons/message.dart "" gen_ios_unittests_code ./pigeons/multiple_arity.dart "" gen_ios_unittests_code ./pigeons/non_null_fields.dart "NNF" + gen_ios_unittests_code ./pigeons/nullable_returns.dart "NR" gen_ios_unittests_code ./pigeons/primitive.dart "" gen_ios_unittests_code ./pigeons/void_arg_flutter.dart "VAF" gen_ios_unittests_code ./pigeons/void_arg_host.dart "VAH" @@ -300,6 +301,7 @@ run_android_unittests() { gen_android_unittests_code ./pigeons/message.dart MessagePigeon gen_android_unittests_code ./pigeons/multiple_arity.dart MultipleArity gen_android_unittests_code ./pigeons/non_null_fields.dart NonNullFields + gen_android_unittests_code ./pigeons/nullable_returns.dart NullableReturns gen_android_unittests_code ./pigeons/primitive.dart Primitive gen_android_unittests_code ./pigeons/void_arg_flutter.dart VoidArgFlutter gen_android_unittests_code ./pigeons/void_arg_host.dart VoidArgHost diff --git a/packages/pigeon/test/dart_generator_test.dart b/packages/pigeon/test/dart_generator_test.dart index 8a9dc5169861..647376b91b24 100644 --- a/packages/pigeon/test/dart_generator_test.dart +++ b/packages/pigeon/test/dart_generator_test.dart @@ -945,4 +945,123 @@ void main() { 'final List? arg_foo = (args[0] as List?)?.cast()')); expect(code, contains('final List output = api.doit(arg_foo!)')); }); + + test('return nullable host', () { + final Root root = Root( + apis: [ + Api(name: 'Api', location: ApiLocation.host, methods: [ + Method( + name: 'doit', + returnType: const TypeDeclaration( + baseName: 'int', + isNullable: true, + ), + arguments: []) + ]) + ], + classes: [], + enums: [], + ); + final StringBuffer sink = StringBuffer(); + generateDart(const DartOptions(isNullSafe: true), root, sink); + final String code = sink.toString(); + expect(code, contains('Future doit()')); + expect(code, contains('return (replyMap[\'result\'] as int?);')); + }); + + test('return nullable async host', () { + final Root root = Root( + apis: [ + Api(name: 'Api', location: ApiLocation.host, methods: [ + Method( + name: 'doit', + returnType: const TypeDeclaration( + baseName: 'int', + isNullable: true, + ), + arguments: [], + isAsynchronous: true) + ]) + ], + classes: [], + enums: [], + ); + final StringBuffer sink = StringBuffer(); + generateDart(const DartOptions(isNullSafe: true), root, sink); + final String code = sink.toString(); + expect(code, contains('Future doit()')); + expect(code, contains('return (replyMap[\'result\'] as int?);')); + }); + + test('return nullable flutter', () { + final Root root = Root( + apis: [ + Api(name: 'Api', location: ApiLocation.flutter, methods: [ + Method( + name: 'doit', + returnType: const TypeDeclaration( + baseName: 'int', + isNullable: true, + ), + arguments: []) + ]) + ], + classes: [], + enums: [], + ); + final StringBuffer sink = StringBuffer(); + generateDart(const DartOptions(isNullSafe: true), root, sink); + final String code = sink.toString(); + expect(code, contains('int? doit();')); + expect(code, contains('final int? output = api.doit();')); + }); + + test('return nullable async flutter', () { + final Root root = Root( + apis: [ + Api(name: 'Api', location: ApiLocation.flutter, methods: [ + Method( + name: 'doit', + returnType: const TypeDeclaration( + baseName: 'int', + isNullable: true, + ), + arguments: [], + isAsynchronous: true) + ]) + ], + classes: [], + enums: [], + ); + final StringBuffer sink = StringBuffer(); + generateDart(const DartOptions(isNullSafe: true), root, sink); + final String code = sink.toString(); + expect(code, contains('Future doit();')); + expect(code, contains('final int? output = await api.doit();')); + }); + + test('platform error for return nil on nonnull', () { + final Root root = Root( + apis: [ + Api(name: 'Api', location: ApiLocation.host, methods: [ + Method( + name: 'doit', + returnType: const TypeDeclaration( + baseName: 'int', + isNullable: false, + ), + arguments: []) + ]) + ], + classes: [], + enums: [], + ); + final StringBuffer sink = StringBuffer(); + generateDart(const DartOptions(isNullSafe: true), root, sink); + final String code = sink.toString(); + expect( + code, + contains( + 'Host platform returned null value for non-null return value.')); + }); } diff --git a/packages/pigeon/test/java_generator_test.dart b/packages/pigeon/test/java_generator_test.dart index f162d3c299e0..95f4652f267f 100644 --- a/packages/pigeon/test/java_generator_test.dart +++ b/packages/pigeon/test/java_generator_test.dart @@ -874,4 +874,52 @@ void main() { contains( 'channel.send(new ArrayList(Arrays.asList(xArg, yArg)), channelReply ->')); }); + + test('return nullable host', () { + final Root root = Root( + apis: [ + Api(name: 'Api', location: ApiLocation.host, methods: [ + Method( + name: 'doit', + returnType: const TypeDeclaration( + baseName: 'int', + isNullable: true, + ), + arguments: []) + ]) + ], + classes: [], + enums: [], + ); + final StringBuffer sink = StringBuffer(); + const JavaOptions javaOptions = JavaOptions(className: 'Messages'); + generateJava(javaOptions, root, sink); + final String code = sink.toString(); + expect(code, contains('@Nullable Long doit();')); + }); + + test('return nullable host async', () { + final Root root = Root( + apis: [ + Api(name: 'Api', location: ApiLocation.host, methods: [ + Method( + name: 'doit', + returnType: const TypeDeclaration( + baseName: 'int', + isNullable: true, + ), + isAsynchronous: true, + arguments: []) + ]) + ], + classes: [], + enums: [], + ); + final StringBuffer sink = StringBuffer(); + const JavaOptions javaOptions = JavaOptions(className: 'Messages'); + generateJava(javaOptions, root, sink); + final String code = sink.toString(); + // Java doesn't accept nullability annotations in type arguments. + expect(code, contains('Result')); + }); } diff --git a/packages/pigeon/test/objc_generator_test.dart b/packages/pigeon/test/objc_generator_test.dart index f53444166065..c03738c50773 100644 --- a/packages/pigeon/test/objc_generator_test.dart +++ b/packages/pigeon/test/objc_generator_test.dart @@ -187,6 +187,7 @@ void main() { expect(code, contains('@interface Input')); expect(code, contains('@interface Output')); expect(code, contains('@protocol Api')); + expect(code, contains('/// @return `nil` only when `error != nil`.')); expect(code, matches('nullable Output.*doSomething.*Input.*FlutterError')); expect(code, matches('ApiSetup.*.*_Nullable')); }); @@ -748,7 +749,7 @@ void main() { expect( code, contains( - '(void)doSomethingWithCompletion:(void(^)(ABCOutput *, NSError *_Nullable))completion')); + '(void)doSomethingWithCompletion:(void(^)(ABCOutput *_Nullable, NSError *_Nullable))completion')); }); test('gen flutter void arg source', () { @@ -775,7 +776,7 @@ void main() { expect( code, contains( - '(void)doSomethingWithCompletion:(void(^)(ABCOutput *, NSError *_Nullable))completion')); + '(void)doSomethingWithCompletion:(void(^)(ABCOutput *_Nullable, NSError *_Nullable))completion')); expect(code, contains('channel sendMessage:nil')); }); @@ -1484,7 +1485,7 @@ void main() { expect( code, contains( - '- (void)addX:(NSNumber *)x y:(NSNumber *)y completion:(void(^)(NSNumber *, NSError *_Nullable))completion;')); + '- (void)addX:(NSNumber *)x y:(NSNumber *)y completion:(void(^)(NSNumber *_Nullable, NSError *_Nullable))completion;')); } { final StringBuffer sink = StringBuffer(); @@ -1494,7 +1495,7 @@ void main() { expect( code, contains( - '- (void)addX:(NSNumber *)arg_x y:(NSNumber *)arg_y completion:(void(^)(NSNumber *, NSError *_Nullable))completion {')); + '- (void)addX:(NSNumber *)arg_x y:(NSNumber *)arg_y completion:(void(^)(NSNumber *_Nullable, NSError *_Nullable))completion {')); expect(code, contains('[channel sendMessage:@[arg_x, arg_y] reply:')); } }); @@ -1582,4 +1583,73 @@ void main() { expect(code, contains('@interface Foobar')); expect(code, contains('@property(nonatomic, copy) NSString * field1')); }); + + test('return nullable flutter header', () { + final Root root = Root( + apis: [ + Api(name: 'Api', location: ApiLocation.flutter, methods: [ + Method( + name: 'doit', + returnType: const TypeDeclaration( + baseName: 'int', + isNullable: true, + ), + arguments: []) + ]) + ], + classes: [], + enums: [], + ); + final StringBuffer sink = StringBuffer(); + generateObjcHeader(const ObjcOptions(), root, sink); + final String code = sink.toString(); + expect( + code, + matches( + r'doitWithCompletion.*void.*NSNumber \*_Nullable.*NSError.*completion;')); + }); + + test('return nullable flutter source', () { + final Root root = Root( + apis: [ + Api(name: 'Api', location: ApiLocation.flutter, methods: [ + Method( + name: 'doit', + returnType: const TypeDeclaration( + baseName: 'int', + isNullable: true, + ), + arguments: []) + ]) + ], + classes: [], + enums: [], + ); + final StringBuffer sink = StringBuffer(); + generateObjcSource(const ObjcOptions(), root, sink); + final String code = sink.toString(); + expect(code, matches(r'doitWithCompletion.*NSNumber \*_Nullable')); + }); + + test('return nullable host header', () { + final Root root = Root( + apis: [ + Api(name: 'Api', location: ApiLocation.host, methods: [ + Method( + name: 'doit', + returnType: const TypeDeclaration( + baseName: 'int', + isNullable: true, + ), + arguments: []) + ]) + ], + classes: [], + enums: [], + ); + final StringBuffer sink = StringBuffer(); + generateObjcHeader(const ObjcOptions(), root, sink); + final String code = sink.toString(); + expect(code, matches(r'nullable NSNumber.*doitWithError')); + }); } diff --git a/packages/pigeon/test/pigeon_lib_test.dart b/packages/pigeon/test/pigeon_lib_test.dart index 512b165dbbd4..1d21f466e3b0 100644 --- a/packages/pigeon/test/pigeon_lib_test.dart +++ b/packages/pigeon/test/pigeon_lib_test.dart @@ -581,23 +581,6 @@ abstract class Api { expect(results.errors[0].message, contains('Nullable')); }); - test('nullable api return', () { - const String code = ''' -class Foo { - int? x; -} - -@HostApi() -abstract class Api { - Foo? doit(Foo foo); -} -'''; - final ParseResults results = _parseSource(code); - expect(results.errors.length, 1); - expect(results.errors[0].lineNumber, 7); - expect(results.errors[0].message, contains('Nullable')); - }); - test('test invalid import', () { const String code = 'import \'foo.dart\';\n'; final ParseResults results = _parseSource(code); @@ -1049,4 +1032,17 @@ class Message { final PigeonOptions options = PigeonOptions.fromMap(results.pigeonOptions!); expect(options.objcOptions!.copyrightHeader, ['A', 'Header']); }); + + test('return nullable', () { + const String code = ''' +@HostApi() +abstract class Api { + int? calc(); +} +'''; + + final ParseResults results = _parseSource(code); + expect(results.errors.length, 0); + expect(results.root.apis[0].methods[0].returnType.isNullable, isTrue); + }); }