diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md index 6e7b64d9829f..1fef8dbb39d1 100644 --- a/packages/pigeon/CHANGELOG.md +++ b/packages/pigeon/CHANGELOG.md @@ -1,3 +1,7 @@ +## 17.1.2 + +* [swift] Separates message call code generation into separate methods. + ## 17.1.1 * Removes heap allocation in generated C++ code. diff --git a/packages/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart index 00ce3fede820..8540fdaec151 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 = '17.1.1'; +const String pigeonVersion = '17.1.2'; /// Read all the content from [stdin] to a String. String readStdin() { diff --git a/packages/pigeon/lib/swift_generator.dart b/packages/pigeon/lib/swift_generator.dart index 61e2ea20faf3..1004a4f38d5b 100644 --- a/packages/pigeon/lib/swift_generator.dart +++ b/packages/pigeon/lib/swift_generator.dart @@ -263,17 +263,6 @@ class SwiftGenerator extends StructuredGenerator { AstFlutterApi api, { required String dartPackageName, }) { - /// Returns an argument name that can be used in a context where it is possible to collide. - String getEnumSafeArgumentExpression( - Root root, int count, NamedType argument) { - String enumTag = ''; - if (argument.type.isEnum) { - enumTag = argument.type.isNullable ? '?.rawValue' : '.rawValue'; - } - - return '${_getArgumentName(count, argument)}Arg$enumTag'; - } - final bool isCustomCodec = getCodecClasses(api, root).isNotEmpty; if (isCustomCodec) { _writeCodec(indent, api, root); @@ -289,7 +278,15 @@ class SwiftGenerator extends StructuredGenerator { for (final Method func in api.methods) { addDocumentationComments( indent, func.documentationComments, _docCommentSpec); - indent.writeln(_getMethodSignature(func)); + indent.writeln(_getMethodSignature( + name: func.name, + parameters: func.parameters, + returnType: func.returnType, + errorTypeName: 'FlutterError', + isAsynchronous: true, + swiftFunction: func.swiftFunction, + getParameterName: _getSafeArgumentName, + )); } }); @@ -309,65 +306,19 @@ class SwiftGenerator extends StructuredGenerator { indent.writeln('return $codecName.shared'); }); } + for (final Method func in api.methods) { addDocumentationComments( indent, func.documentationComments, _docCommentSpec); - indent.writeScoped('${_getMethodSignature(func)} {', '}', () { - final Iterable enumSafeArgNames = func.parameters - .asMap() - .entries - .map((MapEntry e) => - getEnumSafeArgumentExpression(root, e.key, e.value)); - final String sendArgument = func.parameters.isEmpty - ? 'nil' - : '[${enumSafeArgNames.join(', ')}] as [Any?]'; - const String channel = 'channel'; - indent.writeln( - 'let channelName: String = "${makeChannelName(api, func, dartPackageName)}"'); - indent.writeln( - 'let $channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger$codecArgumentString)'); - indent.write('$channel.sendMessage($sendArgument) '); - - indent.addScoped('{ response in', '}', () { - indent.writeScoped( - 'guard let listResponse = response as? [Any?] else {', '}', () { - indent.writeln( - 'completion(.failure(createConnectionError(withChannelName: channelName)))'); - indent.writeln('return'); - }); - indent.writeScoped('if listResponse.count > 1 {', '} ', () { - indent.writeln('let code: String = listResponse[0] as! String'); - indent.writeln( - 'let message: String? = nilOrValue(listResponse[1])'); - indent.writeln( - 'let details: String? = nilOrValue(listResponse[2])'); - indent.writeln( - 'completion(.failure(FlutterError(code: code, message: message, details: details)))'); - }, addTrailingNewline: false); - if (!func.returnType.isNullable && !func.returnType.isVoid) { - indent.addScoped('else if listResponse[0] == nil {', '} ', () { - indent.writeln( - 'completion(.failure(FlutterError(code: "null-error", message: "Flutter api returned null value for non-null return value.", details: "")))'); - }, addTrailingNewline: false); - } - indent.addScoped('else {', '}', () { - if (func.returnType.isVoid) { - indent.writeln('completion(.success(Void()))'); - } else { - final String fieldType = _swiftTypeForDartType(func.returnType); - - _writeGenericCasting( - indent: indent, - value: 'listResponse[0]', - variableName: 'result', - fieldType: fieldType, - type: func.returnType); - - indent.writeln('completion(.success(result))'); - } - }); - }); - }); + _writeFlutterMethod( + indent, + name: func.name, + channelName: makeChannelName(api, func, dartPackageName), + parameters: func.parameters, + returnType: func.returnType, + codecArgumentString: codecArgumentString, + swiftFunction: func.swiftFunction, + ); } }); } @@ -400,37 +351,16 @@ class SwiftGenerator extends StructuredGenerator { indent.write('protocol $apiName '); indent.addScoped('{', '}', () { for (final Method method in api.methods) { - final _SwiftFunctionComponents components = - _SwiftFunctionComponents.fromMethod(method); - final List argSignature = - components.arguments.map((_SwiftFunctionArgument argument) { - final String? label = argument.label; - final String name = argument.name; - final String type = _nullsafeSwiftTypeForDartType(argument.type); - return '${label == null ? '' : '$label '}$name: $type'; - }).toList(); - - final String returnType = method.returnType.isVoid - ? 'Void' - : _nullsafeSwiftTypeForDartType(method.returnType); - - final String escapeType = - method.returnType.isVoid ? 'Void' : returnType; - addDocumentationComments( indent, method.documentationComments, _docCommentSpec); - - if (method.isAsynchronous) { - argSignature.add( - 'completion: @escaping (Result<$escapeType, Error>) -> Void'); - indent.writeln('func ${components.name}(${argSignature.join(', ')})'); - } else if (method.returnType.isVoid) { - indent.writeln( - 'func ${components.name}(${argSignature.join(', ')}) throws'); - } else { - indent.writeln( - 'func ${components.name}(${argSignature.join(', ')}) throws -> $returnType'); - } + indent.writeln(_getMethodSignature( + name: method.name, + parameters: method.parameters, + returnType: method.returnType, + errorTypeName: 'Error', + isAsynchronous: method.isAsynchronous, + swiftFunction: method.swiftFunction, + )); } }); @@ -453,108 +383,17 @@ class SwiftGenerator extends StructuredGenerator { 'static func setUp(binaryMessenger: FlutterBinaryMessenger, api: $apiName?) '); indent.addScoped('{', '}', () { for (final Method method in api.methods) { - final _SwiftFunctionComponents components = - _SwiftFunctionComponents.fromMethod(method); - - final String channelName = - makeChannelName(api, method, dartPackageName); - final String varChannelName = '${method.name}Channel'; - addDocumentationComments( - indent, method.documentationComments, _docCommentSpec); - - indent.writeln( - 'let $varChannelName = FlutterBasicMessageChannel(name: "$channelName", binaryMessenger: binaryMessenger$codecArgumentString)'); - indent.write('if let api = api '); - indent.addScoped('{', '}', () { - indent.write('$varChannelName.setMessageHandler '); - final String messageVarName = - method.parameters.isNotEmpty ? 'message' : '_'; - indent.addScoped('{ $messageVarName, reply in', '}', () { - final List methodArgument = []; - if (components.arguments.isNotEmpty) { - indent.writeln('let args = message as! [Any?]'); - enumerate(components.arguments, - (int index, _SwiftFunctionArgument arg) { - final String argName = - _getSafeArgumentName(index, arg.namedType); - final String argIndex = 'args[$index]'; - final String fieldType = _swiftTypeForDartType(arg.type); - - _writeGenericCasting( - indent: indent, - value: argIndex, - variableName: argName, - fieldType: fieldType, - type: arg.type); - - if (arg.label == '_') { - methodArgument.add(argName); - } else { - methodArgument.add('${arg.label ?? arg.name}: $argName'); - } - }); - } - final String tryStatement = method.isAsynchronous ? '' : 'try '; - // Empty parens are not required when calling a method whose only - // argument is a trailing closure. - final String argumentString = - methodArgument.isEmpty && method.isAsynchronous - ? '' - : '(${methodArgument.join(', ')})'; - final String call = - '${tryStatement}api.${components.name}$argumentString'; - if (method.isAsynchronous) { - final String resultName = - method.returnType.isVoid ? 'nil' : 'res'; - final String successVariableInit = - method.returnType.isVoid ? '' : '(let res)'; - indent.write('$call '); - - indent.addScoped('{ result in', '}', () { - indent.write('switch result '); - indent.addScoped('{', '}', nestCount: 0, () { - final String nullsafe = - method.returnType.isNullable ? '?' : ''; - final String enumTag = - method.returnType.isEnum ? '$nullsafe.rawValue' : ''; - indent.writeln('case .success$successVariableInit:'); - indent.nest(1, () { - indent.writeln('reply(wrapResult($resultName$enumTag))'); - }); - indent.writeln('case .failure(let error):'); - indent.nest(1, () { - indent.writeln('reply(wrapError(error))'); - }); - }); - }); - } else { - indent.write('do '); - indent.addScoped('{', '}', () { - if (method.returnType.isVoid) { - indent.writeln(call); - indent.writeln('reply(wrapResult(nil))'); - } else { - String enumTag = ''; - if (method.returnType.isEnum) { - enumTag = '.rawValue'; - } - enumTag = - method.returnType.isNullable && method.returnType.isEnum - ? '?$enumTag' - : enumTag; - indent.writeln('let result = $call'); - indent.writeln('reply(wrapResult(result$enumTag))'); - } - }, addTrailingNewline: false); - indent.addScoped(' catch {', '}', () { - indent.writeln('reply(wrapError(error))'); - }); - } - }); - }, addTrailingNewline: false); - indent.addScoped(' else {', '}', () { - indent.writeln('$varChannelName.setMessageHandler(nil)'); - }); + _writeHostMethodMessageHandler( + indent, + name: method.name, + channelName: makeChannelName(api, method, dartPackageName), + parameters: method.parameters, + returnType: method.returnType, + isAsynchronous: method.isAsynchronous, + codecArgumentString: codecArgumentString, + swiftFunction: method.swiftFunction, + documentationComments: method.documentationComments, + ); } }); }); @@ -825,6 +664,199 @@ private func nilOrValue(_ value: Any?) -> T? { _writeIsNullish(indent); _writeNilOrValue(indent); } + + void _writeFlutterMethod( + Indent indent, { + required String name, + required String channelName, + required List parameters, + required TypeDeclaration returnType, + required String codecArgumentString, + required String? swiftFunction, + }) { + final String methodSignature = _getMethodSignature( + name: name, + parameters: parameters, + returnType: returnType, + errorTypeName: 'FlutterError', + isAsynchronous: true, + swiftFunction: swiftFunction, + getParameterName: _getSafeArgumentName, + ); + + /// Returns an argument name that can be used in a context where it is possible to collide. + String getEnumSafeArgumentExpression(int count, NamedType argument) { + String enumTag = ''; + if (argument.type.isEnum) { + enumTag = argument.type.isNullable ? '?.rawValue' : '.rawValue'; + } + + return '${_getArgumentName(count, argument)}Arg$enumTag'; + } + + indent.writeScoped('$methodSignature {', '}', () { + final Iterable enumSafeArgNames = parameters.asMap().entries.map( + (MapEntry e) => + getEnumSafeArgumentExpression(e.key, e.value)); + final String sendArgument = parameters.isEmpty + ? 'nil' + : '[${enumSafeArgNames.join(', ')}] as [Any?]'; + const String channel = 'channel'; + indent.writeln('let channelName: String = "$channelName"'); + indent.writeln( + 'let $channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger$codecArgumentString)'); + indent.write('$channel.sendMessage($sendArgument) '); + + indent.addScoped('{ response in', '}', () { + indent.writeScoped( + 'guard let listResponse = response as? [Any?] else {', '}', () { + indent.writeln( + 'completion(.failure(createConnectionError(withChannelName: channelName)))'); + indent.writeln('return'); + }); + indent.writeScoped('if listResponse.count > 1 {', '} ', () { + indent.writeln('let code: String = listResponse[0] as! String'); + indent.writeln('let message: String? = nilOrValue(listResponse[1])'); + indent.writeln('let details: String? = nilOrValue(listResponse[2])'); + indent.writeln( + 'completion(.failure(FlutterError(code: code, message: message, details: details)))'); + }, addTrailingNewline: false); + if (!returnType.isNullable && !returnType.isVoid) { + indent.addScoped('else if listResponse[0] == nil {', '} ', () { + indent.writeln( + 'completion(.failure(FlutterError(code: "null-error", message: "Flutter api returned null value for non-null return value.", details: "")))'); + }, addTrailingNewline: false); + } + indent.addScoped('else {', '}', () { + if (returnType.isVoid) { + indent.writeln('completion(.success(Void()))'); + } else { + final String fieldType = _swiftTypeForDartType(returnType); + + _writeGenericCasting( + indent: indent, + value: 'listResponse[0]', + variableName: 'result', + fieldType: fieldType, + type: returnType, + ); + + indent.writeln('completion(.success(result))'); + } + }); + }); + }); + } + + void _writeHostMethodMessageHandler( + Indent indent, { + required String name, + required String channelName, + required Iterable parameters, + required TypeDeclaration returnType, + required bool isAsynchronous, + required String codecArgumentString, + required String? swiftFunction, + List documentationComments = const [], + }) { + final _SwiftFunctionComponents components = _SwiftFunctionComponents( + name: name, + parameters: parameters, + returnType: returnType, + swiftFunction: swiftFunction, + ); + + final String varChannelName = '${name}Channel'; + addDocumentationComments(indent, documentationComments, _docCommentSpec); + + indent.writeln( + 'let $varChannelName = FlutterBasicMessageChannel(name: "$channelName", binaryMessenger: binaryMessenger$codecArgumentString)'); + indent.write('if let api = api '); + indent.addScoped('{', '}', () { + indent.write('$varChannelName.setMessageHandler '); + final String messageVarName = parameters.isNotEmpty ? 'message' : '_'; + indent.addScoped('{ $messageVarName, reply in', '}', () { + final List methodArgument = []; + if (components.arguments.isNotEmpty) { + indent.writeln('let args = message as! [Any?]'); + enumerate(components.arguments, + (int index, _SwiftFunctionArgument arg) { + final String argName = _getSafeArgumentName(index, arg.namedType); + final String argIndex = 'args[$index]'; + final String fieldType = _swiftTypeForDartType(arg.type); + + _writeGenericCasting( + indent: indent, + value: argIndex, + variableName: argName, + fieldType: fieldType, + type: arg.type); + + if (arg.label == '_') { + methodArgument.add(argName); + } else { + methodArgument.add('${arg.label ?? arg.name}: $argName'); + } + }); + } + final String tryStatement = isAsynchronous ? '' : 'try '; + // Empty parens are not required when calling a method whose only + // argument is a trailing closure. + final String argumentString = methodArgument.isEmpty && isAsynchronous + ? '' + : '(${methodArgument.join(', ')})'; + final String call = + '${tryStatement}api.${components.name}$argumentString'; + if (isAsynchronous) { + final String resultName = returnType.isVoid ? 'nil' : 'res'; + final String successVariableInit = + returnType.isVoid ? '' : '(let res)'; + indent.write('$call '); + + indent.addScoped('{ result in', '}', () { + indent.write('switch result '); + indent.addScoped('{', '}', nestCount: 0, () { + final String nullsafe = returnType.isNullable ? '?' : ''; + final String enumTag = + returnType.isEnum ? '$nullsafe.rawValue' : ''; + indent.writeln('case .success$successVariableInit:'); + indent.nest(1, () { + indent.writeln('reply(wrapResult($resultName$enumTag))'); + }); + indent.writeln('case .failure(let error):'); + indent.nest(1, () { + indent.writeln('reply(wrapError(error))'); + }); + }); + }); + } else { + indent.write('do '); + indent.addScoped('{', '}', () { + if (returnType.isVoid) { + indent.writeln(call); + indent.writeln('reply(wrapResult(nil))'); + } else { + String enumTag = ''; + if (returnType.isEnum) { + enumTag = '.rawValue'; + } + enumTag = returnType.isNullable && returnType.isEnum + ? '?$enumTag' + : enumTag; + indent.writeln('let result = $call'); + indent.writeln('reply(wrapResult(result$enumTag))'); + } + }, addTrailingNewline: false); + indent.addScoped(' catch {', '}', () { + indent.writeln('reply(wrapError(error))'); + }); + } + }); + }, addTrailingNewline: false); + indent.addScoped(' else {', '}', () { + indent.writeln('$varChannelName.setMessageHandler(nil)'); + }); + } } /// Calculates the name of the codec that will be generated for [api]. @@ -902,28 +934,49 @@ String _nullsafeSwiftTypeForDartType(TypeDeclaration type) { return '${_swiftTypeForDartType(type)}$nullSafe'; } -String _getMethodSignature(Method func) { - final _SwiftFunctionComponents components = - _SwiftFunctionComponents.fromMethod(func); - final String returnType = func.returnType.isVoid - ? 'Void' - : _nullsafeSwiftTypeForDartType(func.returnType); - - if (func.parameters.isEmpty) { - return 'func ${func.name}(completion: @escaping (Result<$returnType, FlutterError>) -> Void)'; +String _getMethodSignature({ + required String name, + required Iterable parameters, + required TypeDeclaration returnType, + required String errorTypeName, + bool isAsynchronous = false, + String? swiftFunction, + String Function(int index, NamedType argument) getParameterName = + _getArgumentName, +}) { + final _SwiftFunctionComponents components = _SwiftFunctionComponents( + name: name, + parameters: parameters, + returnType: returnType, + swiftFunction: swiftFunction, + ); + final String returnTypeString = + returnType.isVoid ? 'Void' : _nullsafeSwiftTypeForDartType(returnType); + + final Iterable types = + parameters.map((NamedType e) => _nullsafeSwiftTypeForDartType(e.type)); + final Iterable labels = indexMap(components.arguments, + (int index, _SwiftFunctionArgument argument) { + return argument.label ?? _getArgumentName(index, argument.namedType); + }); + final Iterable names = indexMap(parameters, getParameterName); + final String parameterSignature = + map3(types, labels, names, (String type, String label, String name) { + return '${label != name ? '$label ' : ''}$name: $type'; + }).join(', '); + + if (isAsynchronous) { + if (parameters.isEmpty) { + return 'func ${components.name}(completion: @escaping (Result<$returnTypeString, $errorTypeName>) -> Void)'; + } else { + return 'func ${components.name}($parameterSignature, completion: @escaping (Result<$returnTypeString, $errorTypeName>) -> Void)'; + } } else { - final Iterable argTypes = func.parameters - .map((NamedType e) => _nullsafeSwiftTypeForDartType(e.type)); - final Iterable argLabels = indexMap(components.arguments, - (int index, _SwiftFunctionArgument argument) { - return argument.label ?? _getArgumentName(index, argument.namedType); - }); - final Iterable argNames = - indexMap(func.parameters, _getSafeArgumentName); - final String argsSignature = map3(argTypes, argLabels, argNames, - (String type, String label, String name) => '$label $name: $type') - .join(', '); - return 'func ${components.name}($argsSignature, completion: @escaping (Result<$returnType, FlutterError>) -> Void)'; + if (returnType.isVoid) { + return 'func ${components.name}($parameterSignature) throws'; + } else { + return 'func ${components.name}($parameterSignature) throws -> $returnTypeString'; + } } } @@ -954,45 +1007,40 @@ class _SwiftFunctionArgument { /// The [returnType] is the return type of the function. /// The [method] is the method that this function signature is generated from. class _SwiftFunctionComponents { - _SwiftFunctionComponents._({ - required this.name, - required this.arguments, - required this.returnType, - required this.method, - }); - /// Constructor that generates a [_SwiftFunctionComponents] from a [Method]. - factory _SwiftFunctionComponents.fromMethod(Method method) { - if (method.swiftFunction.isEmpty) { + factory _SwiftFunctionComponents({ + required String name, + required Iterable parameters, + required TypeDeclaration returnType, + String? swiftFunction, + }) { + if (swiftFunction == null || swiftFunction.isEmpty) { return _SwiftFunctionComponents._( - name: method.name, - returnType: method.returnType, - arguments: method.parameters + name: name, + returnType: returnType, + arguments: parameters .map((NamedType field) => _SwiftFunctionArgument( name: field.name, type: field.type, namedType: field, )) .toList(), - method: method, ); } - final String argsExtractor = - repeat(r'(\w+):', method.parameters.length).join(); + final String argsExtractor = repeat(r'(\w+):', parameters.length).join(); final RegExp signatureRegex = RegExp(r'(\w+) *\(' + argsExtractor + r'\)'); - final RegExpMatch match = signatureRegex.firstMatch(method.swiftFunction)!; + final RegExpMatch match = signatureRegex.firstMatch(swiftFunction)!; final Iterable labels = match - .groups(List.generate( - method.parameters.length, (int index) => index + 2)) + .groups(List.generate(parameters.length, (int index) => index + 2)) .whereType(); return _SwiftFunctionComponents._( name: match.group(1)!, - returnType: method.returnType, + returnType: returnType, arguments: map2( - method.parameters, + parameters, labels, (NamedType field, String label) => _SwiftFunctionArgument( name: field.name, @@ -1001,12 +1049,16 @@ class _SwiftFunctionComponents { namedType: field, ), ).toList(), - method: method, ); } + _SwiftFunctionComponents._({ + required this.name, + required this.arguments, + required this.returnType, + }); + final String name; final List<_SwiftFunctionArgument> arguments; final TypeDeclaration returnType; - final Method method; } diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml index c6b29262ff35..a0cbdbd1f82a 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: 17.1.1 # This must match the version in lib/generator_tools.dart +version: 17.1.2 # This must match the version in lib/generator_tools.dart environment: sdk: ^3.1.0