diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md index 2d448c384fda..6a901e112875 100644 --- a/packages/pigeon/CHANGELOG.md +++ b/packages/pigeon/CHANGELOG.md @@ -1,3 +1,7 @@ +## 16.0.3 + +* [kotlin] Separates message call code generation into separate methods. + ## 16.0.2 * [dart] Separates message call code generation into separate methods. diff --git a/packages/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart index f32da30f9b41..31a4d0e778b9 100644 --- a/packages/pigeon/lib/generator_tools.dart +++ b/packages/pigeon/lib/generator_tools.dart @@ -13,7 +13,7 @@ import 'ast.dart'; /// The current version of pigeon. /// /// This must match the version in pubspec.yaml. -const String pigeonVersion = '16.0.2'; +const String pigeonVersion = '16.0.3'; /// Read all the content from [stdin] to a String. String readStdin() { diff --git a/packages/pigeon/lib/kotlin_generator.dart b/packages/pigeon/lib/kotlin_generator.dart index 88ffb487406e..a61eb42865dd 100644 --- a/packages/pigeon/lib/kotlin_generator.dart +++ b/packages/pigeon/lib/kotlin_generator.dart @@ -359,77 +359,17 @@ class KotlinGenerator extends StructuredGenerator { }); final String errorClassName = _getErrorClassName(generatorOptions); - for (final Method func in api.methods) { - final String returnType = func.returnType.isVoid - ? 'Unit' - : _nullsafeKotlinTypeForDartType(func.returnType); - String sendArgument; - - addDocumentationComments( - indent, func.documentationComments, _docCommentSpec); - - if (func.parameters.isEmpty) { - indent.write( - 'fun ${func.name}(callback: (Result<$returnType>) -> Unit) '); - sendArgument = 'null'; - } else { - final Iterable argTypes = func.parameters - .map((NamedType e) => _nullsafeKotlinTypeForDartType(e.type)); - final Iterable argNames = - indexMap(func.parameters, _getSafeArgumentName); - final Iterable enumSafeArgNames = indexMap( - func.parameters, - (int count, NamedType type) => - _getEnumSafeArgumentExpression(count, type)); - sendArgument = 'listOf(${enumSafeArgNames.join(', ')})'; - final String argsSignature = map2(argTypes, argNames, - (String type, String name) => '$name: $type').join(', '); - indent.write( - 'fun ${func.name}($argsSignature, callback: (Result<$returnType>) -> Unit) '); - } - indent.addScoped('{', '}', () { - const String channel = 'channel'; - indent.writeln( - 'val channelName = "${makeChannelName(api, func, dartPackageName)}"'); - indent.writeln( - 'val $channel = BasicMessageChannel(binaryMessenger, channelName, codec)'); - indent.writeScoped('$channel.send($sendArgument) {', '}', () { - indent.writeScoped('if (it is List<*>) {', '} ', () { - indent.writeScoped('if (it.size > 1) {', '} ', () { - indent.writeln( - 'callback(Result.failure($errorClassName(it[0] as String, it[1] as String, it[2] as String?)))'); - }, addTrailingNewline: false); - if (!func.returnType.isNullable && !func.returnType.isVoid) { - indent.addScoped('else if (it[0] == null) {', '} ', () { - indent.writeln( - 'callback(Result.failure($errorClassName("null-error", "Flutter api returned null value for non-null return value.", "")))'); - }, addTrailingNewline: false); - } - indent.addScoped('else {', '}', () { - if (func.returnType.isVoid) { - indent.writeln('callback(Result.success(Unit))'); - } else { - const String output = 'output'; - // Nullable enums require special handling. - if (func.returnType.isEnum && func.returnType.isNullable) { - indent.writeScoped( - 'val $output = (it[0] as Int?)?.let {', '}', () { - indent.writeln('${func.returnType.baseName}.ofRaw(it)'); - }); - } else { - indent.writeln( - 'val $output = ${_cast(indent, 'it[0]', type: func.returnType)}'); - } - indent.writeln('callback(Result.success($output))'); - } - }); - }, addTrailingNewline: false); - indent.addScoped('else {', '} ', () { - indent.writeln( - 'callback(Result.failure(createConnectionError(channelName)))'); - }); - }); - }); + for (final Method method in api.methods) { + _writeFlutterMethod( + indent, + name: method.name, + parameters: method.parameters, + returnType: method.returnType, + channelName: makeChannelName(api, method, dartPackageName), + documentationComments: method.documentationComments, + errorClassName: errorClassName, + dartPackageName: dartPackageName, + ); } }); } @@ -469,36 +409,14 @@ class KotlinGenerator extends StructuredGenerator { indent.write('interface $apiName '); indent.addScoped('{', '}', () { for (final Method method in api.methods) { - final List argSignature = []; - if (method.parameters.isNotEmpty) { - final Iterable argTypes = method.parameters - .map((NamedType e) => _nullsafeKotlinTypeForDartType(e.type)); - final Iterable argNames = - method.parameters.map((NamedType e) => e.name); - argSignature.addAll( - map2(argTypes, argNames, (String argType, String argName) { - return '$argName: $argType'; - })); - } - - final String returnType = method.returnType.isVoid - ? '' - : _nullsafeKotlinTypeForDartType(method.returnType); - - final String resultType = - method.returnType.isVoid ? 'Unit' : returnType; - addDocumentationComments( - indent, method.documentationComments, _docCommentSpec); - - if (method.isAsynchronous) { - argSignature.add('callback: (Result<$resultType>) -> Unit'); - indent.writeln('fun ${method.name}(${argSignature.join(', ')})'); - } else if (method.returnType.isVoid) { - indent.writeln('fun ${method.name}(${argSignature.join(', ')})'); - } else { - indent.writeln( - 'fun ${method.name}(${argSignature.join(', ')}): $returnType'); - } + _writeMethodDeclaration( + indent, + name: method.name, + documentationComments: method.documentationComments, + returnType: method.returnType, + parameters: method.parameters, + isAsynchronous: method.isAsynchronous, + ); } indent.newln(); @@ -520,103 +438,16 @@ class KotlinGenerator extends StructuredGenerator { 'fun setUp(binaryMessenger: BinaryMessenger, api: $apiName?) '); indent.addScoped('{', '}', () { for (final Method method in api.methods) { - indent.write('run '); - indent.addScoped('{', '}', () { - String? taskQueue; - if (method.taskQueueType != TaskQueueType.serial) { - taskQueue = 'taskQueue'; - indent.writeln( - 'val $taskQueue = binaryMessenger.makeBackgroundTaskQueue()'); - } - - final String channelName = - makeChannelName(api, method, dartPackageName); - - indent.write( - 'val channel = BasicMessageChannel(binaryMessenger, "$channelName", codec'); - - if (taskQueue != null) { - indent.addln(', $taskQueue)'); - } else { - indent.addln(')'); - } - - indent.write('if (api != null) '); - indent.addScoped('{', '}', () { - final String messageVarName = - method.parameters.isNotEmpty ? 'message' : '_'; - - indent.write('channel.setMessageHandler '); - indent.addScoped('{ $messageVarName, reply ->', '}', () { - final List methodArguments = []; - if (method.parameters.isNotEmpty) { - indent.writeln('val args = message as List'); - enumerate(method.parameters, (int index, NamedType arg) { - final String argName = _getSafeArgumentName(index, arg); - final String argIndex = 'args[$index]'; - indent.writeln( - 'val $argName = ${_castForceUnwrap(argIndex, arg.type, indent)}'); - methodArguments.add(argName); - }); - } - final String call = - 'api.${method.name}(${methodArguments.join(', ')})'; - - if (method.isAsynchronous) { - indent.write('$call '); - final String resultType = method.returnType.isVoid - ? 'Unit' - : _nullsafeKotlinTypeForDartType(method.returnType); - indent.addScoped('{ result: Result<$resultType> ->', '}', - () { - indent.writeln('val error = result.exceptionOrNull()'); - indent.writeScoped('if (error != null) {', '}', () { - indent.writeln('reply.reply(wrapError(error))'); - }, addTrailingNewline: false); - indent.addScoped(' else {', '}', () { - final String enumTagNullablePrefix = - method.returnType.isNullable ? '?' : '!!'; - final String enumTag = method.returnType.isEnum - ? '$enumTagNullablePrefix.raw' - : ''; - if (method.returnType.isVoid) { - indent.writeln('reply.reply(wrapResult(null))'); - } else { - indent.writeln('val data = result.getOrNull()'); - indent - .writeln('reply.reply(wrapResult(data$enumTag))'); - } - }); - }); - } else { - indent.writeln('var wrapped: List'); - indent.write('try '); - indent.addScoped('{', '}', () { - if (method.returnType.isVoid) { - indent.writeln(call); - indent.writeln('wrapped = listOf(null)'); - } else { - String enumTag = ''; - if (method.returnType.isEnum) { - final String safeUnwrap = - method.returnType.isNullable ? '?' : ''; - enumTag = '$safeUnwrap.raw'; - } - indent.writeln('wrapped = listOf($call$enumTag)'); - } - }, addTrailingNewline: false); - indent.add(' catch (exception: Throwable) '); - indent.addScoped('{', '}', () { - indent.writeln('wrapped = wrapError(exception)'); - }); - indent.writeln('reply.reply(wrapped)'); - } - }); - }, addTrailingNewline: false); - indent.addScoped(' else {', '}', () { - indent.writeln('channel.setMessageHandler(null)'); - }); - }); + _writeHostMethodMessageHandler( + indent, + api: api, + name: method.name, + channelName: makeChannelName(api, method, dartPackageName), + taskQueueType: method.taskQueueType, + parameters: method.parameters, + returnType: method.returnType, + isAsynchronous: method.isAsynchronous, + ); } }); }); @@ -756,6 +587,265 @@ class KotlinGenerator extends StructuredGenerator { } _writeErrorClass(generatorOptions, indent); } + + static void _writeMethodDeclaration( + Indent indent, { + required String name, + required TypeDeclaration returnType, + required List parameters, + List documentationComments = const [], + int? minApiRequirement, + bool isAsynchronous = false, + bool isAbstract = false, + String Function(int index, NamedType type) getArgumentName = + _getArgumentName, + }) { + final List argSignature = []; + if (parameters.isNotEmpty) { + final Iterable argTypes = parameters + .map((NamedType e) => _nullsafeKotlinTypeForDartType(e.type)); + final Iterable argNames = indexMap(parameters, getArgumentName); + argSignature.addAll( + map2( + argTypes, + argNames, + (String argType, String argName) { + return '$argName: $argType'; + }, + ), + ); + } + + final String returnTypeString = + returnType.isVoid ? '' : _nullsafeKotlinTypeForDartType(returnType); + + final String resultType = returnType.isVoid ? 'Unit' : returnTypeString; + addDocumentationComments(indent, documentationComments, _docCommentSpec); + + if (minApiRequirement != null) { + indent.writeln( + '@androidx.annotation.RequiresApi(api = $minApiRequirement)', + ); + } + + final String abstractKeyword = isAbstract ? 'abstract ' : ''; + + if (isAsynchronous) { + argSignature.add('callback: (Result<$resultType>) -> Unit'); + indent.writeln('${abstractKeyword}fun $name(${argSignature.join(', ')})'); + } else if (returnType.isVoid) { + indent.writeln('${abstractKeyword}fun $name(${argSignature.join(', ')})'); + } else { + indent.writeln( + '${abstractKeyword}fun $name(${argSignature.join(', ')}): $returnTypeString', + ); + } + } + + void _writeHostMethodMessageHandler( + Indent indent, { + required Api api, + required String name, + required String channelName, + required TaskQueueType taskQueueType, + required List parameters, + required TypeDeclaration returnType, + bool isAsynchronous = false, + String Function(List safeArgNames, {required String apiVarName})? + onCreateCall, + }) { + indent.write('run '); + indent.addScoped('{', '}', () { + String? taskQueue; + if (taskQueueType != TaskQueueType.serial) { + taskQueue = 'taskQueue'; + indent.writeln( + 'val $taskQueue = binaryMessenger.makeBackgroundTaskQueue()'); + } + + indent.write( + 'val channel = BasicMessageChannel(binaryMessenger, "$channelName", codec'); + + if (taskQueue != null) { + indent.addln(', $taskQueue)'); + } else { + indent.addln(')'); + } + + indent.write('if (api != null) '); + indent.addScoped('{', '}', () { + final String messageVarName = parameters.isNotEmpty ? 'message' : '_'; + + indent.write('channel.setMessageHandler '); + indent.addScoped('{ $messageVarName, reply ->', '}', () { + final List methodArguments = []; + if (parameters.isNotEmpty) { + indent.writeln('val args = message as List'); + enumerate(parameters, (int index, NamedType arg) { + final String argName = _getSafeArgumentName(index, arg); + final String argIndex = 'args[$index]'; + indent.writeln( + 'val $argName = ${_castForceUnwrap(argIndex, arg.type, indent)}'); + methodArguments.add(argName); + }); + } + final String call = onCreateCall != null + ? onCreateCall(methodArguments, apiVarName: 'api') + : 'api.$name(${methodArguments.join(', ')})'; + + if (isAsynchronous) { + indent.write('$call '); + final String resultType = returnType.isVoid + ? 'Unit' + : _nullsafeKotlinTypeForDartType(returnType); + indent.addScoped('{ result: Result<$resultType> ->', '}', () { + indent.writeln('val error = result.exceptionOrNull()'); + indent.writeScoped('if (error != null) {', '}', () { + indent.writeln('reply.reply(wrapError(error))'); + }, addTrailingNewline: false); + indent.addScoped(' else {', '}', () { + final String enumTagNullablePrefix = + returnType.isNullable ? '?' : '!!'; + final String enumTag = + returnType.isEnum ? '$enumTagNullablePrefix.raw' : ''; + if (returnType.isVoid) { + indent.writeln('reply.reply(wrapResult(null))'); + } else { + indent.writeln('val data = result.getOrNull()'); + indent.writeln('reply.reply(wrapResult(data$enumTag))'); + } + }); + }); + } else { + indent.writeln('var wrapped: List'); + indent.write('try '); + indent.addScoped('{', '}', () { + if (returnType.isVoid) { + indent.writeln(call); + indent.writeln('wrapped = listOf(null)'); + } else { + String enumTag = ''; + if (returnType.isEnum) { + final String safeUnwrap = returnType.isNullable ? '?' : ''; + enumTag = '$safeUnwrap.raw'; + } + indent.writeln('wrapped = listOf($call$enumTag)'); + } + }, addTrailingNewline: false); + indent.add(' catch (exception: Throwable) '); + indent.addScoped('{', '}', () { + indent.writeln('wrapped = wrapError(exception)'); + }); + indent.writeln('reply.reply(wrapped)'); + } + }); + }, addTrailingNewline: false); + indent.addScoped(' else {', '}', () { + indent.writeln('channel.setMessageHandler(null)'); + }); + }); + } + + void _writeFlutterMethod( + Indent indent, { + required String name, + required List parameters, + required TypeDeclaration returnType, + required String channelName, + required String errorClassName, + required String dartPackageName, + List documentationComments = const [], + int? minApiRequirement, + void Function( + Indent indent, { + required List parameters, + required TypeDeclaration returnType, + required String channelName, + required String errorClassName, + }) onWriteBody = _writeFlutterMethodMessageCall, + }) { + _writeMethodDeclaration( + indent, + name: name, + returnType: returnType, + parameters: parameters, + documentationComments: documentationComments, + isAsynchronous: true, + minApiRequirement: minApiRequirement, + getArgumentName: _getSafeArgumentName, + ); + + indent.addScoped('{', '}', () { + onWriteBody( + indent, + parameters: parameters, + returnType: returnType, + channelName: channelName, + errorClassName: errorClassName, + ); + }); + } + + static void _writeFlutterMethodMessageCall( + Indent indent, { + required List parameters, + required TypeDeclaration returnType, + required String channelName, + required String errorClassName, + }) { + String sendArgument; + + if (parameters.isEmpty) { + sendArgument = 'null'; + } else { + final Iterable enumSafeArgNames = indexMap( + parameters, + (int count, NamedType type) => + _getEnumSafeArgumentExpression(count, type)); + sendArgument = 'listOf(${enumSafeArgNames.join(', ')})'; + } + + const String channel = 'channel'; + indent.writeln('val channelName = "$channelName"'); + indent.writeln( + 'val $channel = BasicMessageChannel(binaryMessenger, channelName, codec)'); + indent.writeScoped('$channel.send($sendArgument) {', '}', () { + indent.writeScoped('if (it is List<*>) {', '} ', () { + indent.writeScoped('if (it.size > 1) {', '} ', () { + indent.writeln( + 'callback(Result.failure($errorClassName(it[0] as String, it[1] as String, it[2] as String?)))'); + }, addTrailingNewline: false); + if (!returnType.isNullable && !returnType.isVoid) { + indent.addScoped('else if (it[0] == null) {', '} ', () { + indent.writeln( + 'callback(Result.failure($errorClassName("null-error", "Flutter api returned null value for non-null return value.", "")))'); + }, addTrailingNewline: false); + } + indent.addScoped('else {', '}', () { + if (returnType.isVoid) { + indent.writeln('callback(Result.success(Unit))'); + } else { + const String output = 'output'; + // Nullable enums require special handling. + if (returnType.isEnum && returnType.isNullable) { + indent.writeScoped('val $output = (it[0] as Int?)?.let {', '}', + () { + indent.writeln('${returnType.baseName}.ofRaw(it)'); + }); + } else { + indent.writeln( + 'val $output = ${_cast(indent, 'it[0]', type: returnType)}'); + } + indent.writeln('callback(Result.success($output))'); + } + }); + }, addTrailingNewline: false); + indent.addScoped('else {', '} ', () { + indent.writeln( + 'callback(Result.failure(createConnectionError(channelName)))'); + }); + }); + } } HostDatatype _getHostDatatype(Root root, NamedType field) { diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml index 7eca1ecc65f7..c6029d4ed308 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%3A%22p%3A+pigeon%22 -version: 16.0.2 # This must match the version in lib/generator_tools.dart +version: 16.0.3 # This must match the version in lib/generator_tools.dart environment: sdk: ">=3.0.0 <4.0.0"