diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md index 6be42dca77c6..c63fd4685f5c 100644 --- a/packages/pigeon/CHANGELOG.md +++ b/packages/pigeon/CHANGELOG.md @@ -5,6 +5,7 @@ * [front-end] Added more errors for incorrect usage of Pigeon (previously they were just ignored). * Moved Pigeon to using a custom codec which allows collection types to contain custom classes. * Started allowing primitive data types as arguments and return types. +* Started supporting generics' type arguments for fields in classes. ## 0.3.0 diff --git a/packages/pigeon/lib/ast.dart b/packages/pigeon/lib/ast.dart index bfc438fe8de5..b9b316b09e32 100644 --- a/packages/pigeon/lib/ast.dart +++ b/packages/pigeon/lib/ast.dart @@ -20,10 +20,8 @@ class Method extends Node { Method({ required this.name, required this.returnType, - required this.argType, - this.isArgNullable = false, + required this.arguments, this.isAsynchronous = false, - this.isReturnNullable = false, this.offset, }); @@ -31,16 +29,10 @@ class Method extends Node { String name; /// The data-type of the return value. - String returnType; - - /// True if the method can return a null value. - bool isReturnNullable; + Field returnType; /// The data-type of the argument. - String argType; - - /// True if the argument has a null tag `?`. - bool isArgNullable; + List arguments; /// Whether the receiver of this method is expected to return synchronously or not. bool isAsynchronous; @@ -50,7 +42,7 @@ class Method extends Node { @override String toString() { - return '(Api name:$name returnType:$returnType argType:$argType isAsynchronous:$isAsynchronous)'; + return '(Method name:$name returnType:$returnType argType:$arguments isAsynchronous:$isAsynchronous)'; } } @@ -82,8 +74,47 @@ class Api extends Node { } } +/// An entity that represents a typed concept, like a [TypeArgument] or [Field]. +abstract class TypedEntity { + /// The data-type of the entity (ex 'String' or 'int'). + String get dataType; + + /// The type arguments to the entity. + List? get typeArguments; + + /// True if the type is nullable. + bool get isNullable; +} + +/// A parameter to a generic entity. For example, "String" to "List". +class TypeArgument implements TypedEntity { + /// Constructor for [TypeArgument]. + TypeArgument({ + required this.dataType, + required this.isNullable, + this.typeArguments, + }); + + /// A string representation of the base datatype. + @override + final String dataType; + + /// The type arguments to this [TypeArgument]. + @override + final List? typeArguments; + + /// True if the type is nullable. + @override + final bool isNullable; + + @override + String toString() { + return '(TypeArgument dataType:$dataType isNullable:$isNullable typeArguments:$typeArguments)'; + } +} + /// Represents a field on a [Class]. -class Field extends Node { +class Field extends Node implements TypedEntity { /// Parametric constructor for [Field]. Field({ required this.name, @@ -97,20 +128,23 @@ class Field extends Node { String name; /// The data-type of the field (ex 'String' or 'int'). + @override String dataType; /// The offset in the source file where the field appears. int? offset; /// True if the datatype is nullable (ex `int?`). + @override bool isNullable; /// Type parameters used for generics. - List? typeArguments; + @override + List? typeArguments; @override String toString() { - return '(Field name:$name dataType:$dataType)'; + return '(Field name:$name dataType:$dataType typeArguments:$typeArguments)'; } } diff --git a/packages/pigeon/lib/dart_generator.dart b/packages/pigeon/lib/dart_generator.dart index c2311352b8a4..d544ee33410a 100644 --- a/packages/pigeon/lib/dart_generator.dart +++ b/packages/pigeon/lib/dart_generator.dart @@ -92,6 +92,18 @@ void _writeCodec(Indent indent, String codecName, Api api) { }); } +String _makeGenericTypeArguments(TypedEntity entity, String nullTag) { + return entity.typeArguments != null + ? '${entity.dataType}<${entity.typeArguments!.map((TypeArgument e) => 'Object$nullTag').reduce((String value, String element) => '$value, $element')}>' + : _addGenericTypes(entity, nullTag); +} + +String _makeGenericCastCall(TypedEntity entity, String nullTag) { + return entity.typeArguments != null + ? '.cast<${_flattenTypeArguments(entity.typeArguments!, nullTag)}>()' + : ''; +} + void _writeHostApi(DartOptions opt, Indent indent, Api api) { assert(api.location == ApiLocation.host); final String codecName = _getCodecName(api); @@ -121,12 +133,12 @@ final BinaryMessenger$nullTag _binaryMessenger; } String argSignature = ''; String sendArgument = 'null'; - if (func.argType != 'void') { - argSignature = '${func.argType} arg'; + if (func.arguments.isNotEmpty) { + argSignature = '${_addGenericTypes(func.arguments[0], nullTag)} arg'; sendArgument = 'arg'; } indent.write( - 'Future<${func.returnType}> ${func.name}($argSignature) async ', + 'Future<${_addGenericTypes(func.returnType, nullTag)}> ${func.name}($argSignature) async ', ); indent.scoped('{', '}', () { final String channelName = makeChannelName(api, func); @@ -137,9 +149,12 @@ final BinaryMessenger$nullTag _binaryMessenger; '\'$channelName\', codec, binaryMessenger: _binaryMessenger);', ); }); - final String returnStatement = func.returnType == 'void' + final String returnType = + _makeGenericTypeArguments(func.returnType, nullTag); + final String castCall = _makeGenericCastCall(func.returnType, nullTag); + final String returnStatement = func.returnType.dataType == 'void' ? '// noop' - : 'return (replyMap[\'${Keys.result}\'] as ${func.returnType}$nullTag)$unwrapOperator;'; + : 'return (replyMap[\'${Keys.result}\'] as $returnType$nullTag)$unwrapOperator$castCall;'; indent.format(''' final Map$nullTag replyMap =\n\t\tawait channel.send($sendArgument) as Map$nullTag; if (replyMap == null) { @@ -181,10 +196,12 @@ void _writeFlutterApi( indent.addln(''); for (final Method func in api.methods) { final bool isAsync = func.isAsynchronous; - final String returnType = - isAsync ? 'Future<${func.returnType}>' : func.returnType; - final String argSignature = - func.argType == 'void' ? '' : '${func.argType} arg'; + final String returnType = isAsync + ? 'Future<${_addGenericTypes(func.returnType, nullTag)}>' + : _addGenericTypes(func.returnType, nullTag); + final String argSignature = func.arguments.isEmpty + ? '' + : '${_addGenericTypes(func.arguments[0], nullTag)} arg'; indent.writeln('$returnType ${func.name}($argSignature);'); } indent.write('static void setup(${api.name}$nullTag api) '); @@ -215,19 +232,21 @@ void _writeFlutterApi( 'channel.$messageHandlerSetter((Object$nullTag message) async ', ); indent.scoped('{', '});', () { - final String argType = func.argType; - final String returnType = func.returnType; + final String returnType = + _addGenericTypes(func.returnType, nullTag); final bool isAsync = func.isAsynchronous; final String emptyReturnStatement = isMockHandler ? 'return {};' - : func.returnType == 'void' + : func.returnType.dataType == 'void' ? 'return;' : 'return null;'; String call; - if (argType == 'void') { + if (func.arguments.isEmpty) { indent.writeln('// ignore message'); call = 'api.${func.name}()'; } else { + final String argType = + _addGenericTypes(func.arguments[0], nullTag); indent.writeln( 'assert(message != null, \'Argument for $channelName was null. Expected $argType.\');', ); @@ -263,17 +282,37 @@ void _writeFlutterApi( }); } -String _addGenericTypes(String dataType, String nullTag) { - switch (dataType) { +/// Converts a [List] of [TypeArgument]s to a comma separated [String] to be +/// used in Dart code. +String _flattenTypeArguments(List args, String nullTag) { + return args + .map((TypeArgument arg) => arg.typeArguments == null + ? '${arg.dataType}$nullTag' + : '${arg.dataType}<${_flattenTypeArguments(arg.typeArguments!, nullTag)}>$nullTag') + .reduce((String value, String element) => '$value, $element'); +} + +/// Creates the type declaration for use in Dart code from a [Field] making sure +/// that type arguments are used for primitive generic types. +String _addGenericTypes(TypedEntity entity, String nullTag) { + switch (entity.dataType) { case 'List': - return 'List$nullTag'; + return (entity.typeArguments == null) + ? 'List' + : 'List<${_flattenTypeArguments(entity.typeArguments!, nullTag)}>'; case 'Map': - return 'Map$nullTag'; + return (entity.typeArguments == null) + ? 'Map' + : 'Map<${_flattenTypeArguments(entity.typeArguments!, nullTag)}>'; default: - return '$dataType$nullTag'; + return entity.dataType; } } +String _addGenericTypesNullable(Field field, String nullTag) { + return '${_addGenericTypes(field, nullTag)}$nullTag'; +} + /// Generates Dart source code for the given AST represented by [root], /// outputting the code to [sink]. void generateDart(DartOptions opt, Root root, StringSink sink) { @@ -315,7 +354,7 @@ void generateDart(DartOptions opt, Root root, StringSink sink) { indent.write('class ${klass.name} '); indent.scoped('{', '}', () { for (final Field field in klass.fields) { - final String datatype = _addGenericTypes(field.dataType, nullTag); + final String datatype = _addGenericTypesNullable(field, nullTag); indent.writeln('$datatype ${field.name};'); } if (klass.fields.isNotEmpty) { @@ -365,9 +404,16 @@ pigeonMap['${field.name}'] != null pigeonMap['${field.name}'] != null \t\t? ${field.dataType}.values[pigeonMap['${field.name}']$unwrapOperator as int] \t\t: null''', leadingSpace: false, trailingNewline: false); + } else if (field.typeArguments != null) { + final String genericType = + _makeGenericTypeArguments(field, nullTag); + final String castCall = _makeGenericCastCall(field, nullTag); + indent.add( + '(pigeonMap[\'${field.name}\'] as $genericType$nullTag)$nullTag$castCall', + ); } else { indent.add( - 'pigeonMap[\'${field.name}\'] as ${_addGenericTypes(field.dataType, nullTag)}', + 'pigeonMap[\'${field.name}\'] as ${_addGenericTypesNullable(field, nullTag)}', ); } indent.addln(index == klass.fields.length - 1 ? ';' : ''); diff --git a/packages/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart index 560bcd184fbd..e2b391757439 100644 --- a/packages/pigeon/lib/generator_tools.dart +++ b/packages/pigeon/lib/generator_tools.dart @@ -159,9 +159,9 @@ class HostDatatype { /// `builtinResolver` will return the host datatype for the Dart datatype for /// builtin types. `customResolver` can modify the datatype of custom types. HostDatatype getHostDatatype(Field field, List classes, List enums, - String? Function(String) builtinResolver, + String? Function(Field) builtinResolver, {String Function(String)? customResolver}) { - final String? datatype = builtinResolver(field.dataType); + final String? datatype = builtinResolver(field); if (datatype == null) { if (classes.map((Class x) => x.name).contains(field.dataType)) { final String customName = customResolver != null @@ -285,8 +285,10 @@ const int _minimumCodecFieldKey = 128; Iterable getCodecClasses(Api api) sync* { final Set names = {}; for (final Method method in api.methods) { - names.add(method.returnType); - names.add(method.argType); + names.add(method.returnType.dataType); + if (method.arguments.isNotEmpty) { + names.add(method.arguments[0].dataType); + } } final List sortedNames = names .where((String element) => diff --git a/packages/pigeon/lib/java_generator.dart b/packages/pigeon/lib/java_generator.dart index eae2cea23567..88bffad05e01 100644 --- a/packages/pigeon/lib/java_generator.dart +++ b/packages/pigeon/lib/java_generator.dart @@ -5,19 +5,6 @@ import 'ast.dart'; import 'generator_tools.dart'; -const Map _javaTypeForDartTypeMap = { - 'bool': 'Boolean', - 'int': 'Long', - 'String': 'String', - 'double': 'Double', - 'Uint8List': 'byte[]', - 'Int32List': 'int[]', - 'Int64List': 'long[]', - 'Float64List': 'double[]', - 'List': 'List', - 'Map': 'Map', -}; - /// Options that control how Java code will be generated. class JavaOptions { /// Creates a [JavaOptions] object @@ -113,22 +100,6 @@ void _writeCodec(Indent indent, Api api) { }); } -/// This performs Dart to Java type conversions. If performs a passthrough of -/// the input if it can't be converted. -// TODO(gaaclarke): Remove this method and unify it with `_javaTypeForDartType`. -String _javaTypeForDartTypePassthrough(String type) { - const Map map = { - 'int': 'Integer', - 'bool': 'Boolean', - 'double': 'Double', - 'Int32List': 'int[]', - 'Uint8List': 'byte[]', - 'Int64List': 'long[]', - 'Float64List': 'double[]', - }; - return map[type] ?? type; -} - void _writeHostApi(Indent indent, Api api) { assert(api.location == ApiLocation.host); @@ -137,17 +108,21 @@ void _writeHostApi(Indent indent, Api api) { indent.write('public interface ${api.name} '); indent.scoped('{', '}', () { for (final Method method in api.methods) { - final String argType = _javaTypeForDartTypePassthrough(method.argType); final String returnType = method.isAsynchronous ? 'void' - : _javaTypeForDartTypePassthrough(method.returnType); + : _javaTypeForDartType(method.returnType) ?? + method.returnType.dataType; final List argSignature = []; - if (method.argType != 'void') { + if (method.arguments.isNotEmpty) { + final String argType = _javaTypeForDartType(method.arguments[0]) ?? + method.arguments[0].dataType; argSignature.add('$argType arg'); } if (method.isAsynchronous) { - final String returnType = - method.returnType == 'void' ? 'Void' : method.returnType; + final String returnType = method.returnType.dataType == 'void' + ? 'Void' + : _javaTypeForDartType(method.returnType) ?? + method.returnType.dataType; argSignature.add('Result<$returnType> result'); } indent.writeln('$returnType ${method.name}(${argSignature.join(', ')});'); @@ -180,15 +155,17 @@ static MessageCodec getCodec() { indent.scoped('{', '} else {', () { indent.write('channel.setMessageHandler((message, reply) -> '); indent.scoped('{', '});', () { - final String argType = - _javaTypeForDartTypePassthrough(method.argType); final String returnType = - _javaTypeForDartTypePassthrough(method.returnType); + _javaTypeForDartType(method.returnType) ?? + method.returnType.dataType; indent.writeln('Map wrapped = new HashMap<>();'); indent.write('try '); indent.scoped('{', '}', () { final List methodArgument = []; - if (argType != 'void') { + if (method.arguments.isNotEmpty) { + final String argType = + _javaTypeForDartType(method.arguments[0]) ?? + method.arguments[0].dataType; indent.writeln('@SuppressWarnings("ConstantConditions")'); indent.writeln('$argType input = ($argType)message;'); indent.write('if (input == null) '); @@ -200,7 +177,7 @@ static MessageCodec getCodec() { } if (method.isAsynchronous) { final String resultValue = - method.returnType == 'void' ? 'null' : 'result'; + method.returnType.dataType == 'void' ? 'null' : 'result'; methodArgument.add( 'result -> { ' 'wrapped.put("${Keys.result}", $resultValue); ' @@ -212,7 +189,7 @@ static MessageCodec getCodec() { 'api.${method.name}(${methodArgument.join(', ')})'; if (method.isAsynchronous) { indent.writeln('$call;'); - } else if (method.returnType == 'void') { + } else if (method.returnType.dataType == 'void') { indent.writeln('$call;'); indent.writeln('wrapped.put("${Keys.result}", null);'); } else { @@ -265,15 +242,16 @@ static MessageCodec getCodec() { '''); for (final Method func in api.methods) { final String channelName = makeChannelName(api, func); - final String returnType = func.returnType == 'void' + final String returnType = func.returnType.dataType == 'void' ? 'Void' - : _javaTypeForDartTypePassthrough(func.returnType); - final String argType = _javaTypeForDartTypePassthrough(func.argType); + : _javaTypeForDartType(func.returnType) ?? func.returnType.dataType; String sendArgument; - if (func.argType == 'void') { + if (func.arguments.isEmpty) { indent.write('public void ${func.name}(Reply<$returnType> callback) '); sendArgument = 'null'; } else { + final String argType = _javaTypeForDartType(func.arguments[0]) ?? + func.arguments[0].dataType; indent.write( 'public void ${func.name}($argType argInput, Reply<$returnType> callback) '); sendArgument = 'argInput'; @@ -288,7 +266,7 @@ static MessageCodec getCodec() { indent.dec(); indent.write('channel.send($sendArgument, channelReply -> '); indent.scoped('{', '});', () { - if (func.returnType == 'void') { + if (func.returnType.dataType == 'void') { indent.writeln('callback.reply(null);'); } else { indent.writeln('@SuppressWarnings("ConstantConditions")'); @@ -313,8 +291,37 @@ String _makeSetter(Field field) { return 'set$uppercased'; } -String? _javaTypeForDartType(String datatype) { - return _javaTypeForDartTypeMap[datatype]; +/// Converts a [List] of [TypeArgument]s to a comma separated [String] to be +/// used in Java code. +String _flattenTypeArguments(List args) { + return args + .map((TypeArgument e) => _javaTypeForDartType(e) ?? e.dataType) + .reduce((String value, String element) => '$value, $element'); +} + +String? _javaTypeForDartType(TypedEntity field) { + const Map javaTypeForDartTypeMap = { + 'bool': 'Boolean', + 'int': 'Long', + 'String': 'String', + 'double': 'Double', + 'Uint8List': 'byte[]', + 'Int32List': 'int[]', + 'Int64List': 'long[]', + 'Float64List': 'double[]', + 'Map': 'Map', + }; + if (javaTypeForDartTypeMap.containsKey(field.dataType)) { + return javaTypeForDartTypeMap[field.dataType]; + } else if (field.dataType == 'List') { + if (field.typeArguments == null) { + return 'List'; + } else { + return 'List<${_flattenTypeArguments(field.typeArguments!)}>'; + } + } else { + return null; + } } String _castObject( diff --git a/packages/pigeon/lib/java_generator.dart.orig b/packages/pigeon/lib/java_generator.dart.orig new file mode 100644 index 000000000000..f49900e9dfa9 --- /dev/null +++ b/packages/pigeon/lib/java_generator.dart.orig @@ -0,0 +1,486 @@ +// 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. + +import 'ast.dart'; +import 'generator_tools.dart'; + +/// Options that control how Java code will be generated. +class JavaOptions { + /// Creates a [JavaOptions] object + const JavaOptions({ + this.className, + this.package, + this.copyrightHeader, + }); + + /// The name of the class that will house all the generated classes. + final String? className; + + /// The package where the generated class will live. + final String? package; + + /// A copyright header that will get prepended to generated code. + final Iterable? copyrightHeader; + + /// Creates a [JavaOptions] from a Map representation where: + /// `x = JavaOptions.fromMap(x.toMap())`. + static JavaOptions fromMap(Map map) { + return JavaOptions( + className: map['className'] as String?, + package: map['package'] as String?, + copyrightHeader: map['copyrightHeader'] as Iterable?, + ); + } + + /// Converts a [JavaOptions] to a Map representation where: + /// `x = JavaOptions.fromMap(x.toMap())`. + Map toMap() { + final Map result = { + if (className != null) 'className': className!, + if (package != null) 'package': package!, + if (copyrightHeader != null) 'copyrightHeader': copyrightHeader!, + }; + return result; + } + + /// Overrides any non-null parameters from [options] into this to make a new + /// [JavaOptions]. + JavaOptions merge(JavaOptions options) { + return JavaOptions.fromMap(mergeMaps(toMap(), options.toMap())); + } +} + +String _getCodecName(Api api) => '${api.name}Codec'; + +void _writeCodec(Indent indent, Api api) { + final String codecName = _getCodecName(api); + indent.write('private static class $codecName extends StandardMessageCodec '); + indent.scoped('{', '}', () { + indent + .writeln('public static final $codecName INSTANCE = new $codecName();'); + indent.writeln('private $codecName() {}'); + if (getCodecClasses(api).isNotEmpty) { + indent.writeln('@Override'); + indent.write( + 'protected Object readValueOfType(byte type, ByteBuffer buffer) '); + indent.scoped('{', '}', () { + indent.write('switch (type) '); + indent.scoped('{', '}', () { + for (final EnumeratedClass customClass in getCodecClasses(api)) { + indent.write('case (byte)${customClass.enumeration}: '); + indent.writeScoped('', '', () { + indent.writeln( + 'return ${customClass.name}.fromMap((Map) readValue(buffer));'); + }); + } + indent.write('default:'); + indent.writeScoped('', '', () { + indent.writeln('return super.readValueOfType(type, buffer);'); + }); + }); + }); + indent.writeln('@Override'); + indent.write( + 'protected void writeValue(ByteArrayOutputStream stream, Object value) '); + indent.writeScoped('{', '}', () { + for (final EnumeratedClass customClass in getCodecClasses(api)) { + indent.write('if (value instanceof ${customClass.name}) '); + indent.scoped('{', '} else ', () { + indent.writeln('stream.write(${customClass.enumeration});'); + indent.writeln( + 'writeValue(stream, ((${customClass.name}) value).toMap());'); + }); + } + indent.scoped('{', '}', () { + indent.writeln('super.writeValue(stream, value);'); + }); + }); + } + }); +} + +String _boxedType(String type) { + const Map map = { + 'int': 'Integer', + 'bool': 'Boolean', + 'double': 'Double', + 'Int32List': 'int[]', + 'Uint8List': 'byte[]', + 'Int64List': 'long[]', + 'Float64List': 'double[]', + }; + return map[type] ?? type; +} + +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' + : _boxedType(method.returnType.dataType); + final List argSignature = []; + if (method.arguments.isNotEmpty) { + final String argType = _boxedType(method.arguments[0].dataType); + argSignature.add('$argType arg'); + } + if (method.isAsynchronous) { + final String returnType = method.returnType.dataType == 'void' + ? 'Void' + : method.returnType.dataType; + argSignature.add('Result<$returnType> result'); + } + indent.writeln('$returnType ${method.name}(${argSignature.join(', ')});'); + } + 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('{', '}', () { + 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 = _boxedType(method.returnType.dataType); + indent.writeln('Map wrapped = new HashMap<>();'); + indent.write('try '); + indent.scoped('{', '}', () { + final List methodArgument = []; + if (method.arguments.isNotEmpty) { + final String argType = _boxedType(method.arguments[0].dataType); + indent.writeln('@SuppressWarnings("ConstantConditions")'); + indent.writeln('$argType input = ($argType)message;'); + indent.write('if (input == null) '); + indent.scoped('{', '}', () { + indent.writeln( + 'throw new NullPointerException("Message unexpectedly null.");'); + }); + methodArgument.add('input'); + } + if (method.isAsynchronous) { + final String resultValue = + method.returnType.dataType == 'void' ? 'null' : 'result'; + methodArgument.add( + 'result -> { ' + 'wrapped.put("${Keys.result}", $resultValue); ' + 'reply.reply(wrapped); ' + '}', + ); + } + final String call = + 'api.${method.name}(${methodArgument.join(', ')})'; + if (method.isAsynchronous) { + indent.writeln('$call;'); + } else if (method.returnType.dataType == 'void') { + 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);'); + } + }); + }); + indent.scoped(null, '}', () { + indent.writeln('channel.setMessageHandler(null);'); + }); + }); + } + }); + }); +} + +void _writeFlutterApi(Indent indent, Api api) { + assert(api.location == ApiLocation.flutter); + indent.writeln( + '/** Generated class from Pigeon that represents Flutter messages that can be called from Java.*/'); + indent.write('public static class ${api.name} '); + indent.scoped('{', '}', () { + indent.writeln('private final BinaryMessenger binaryMessenger;'); + indent.write('public ${api.name}(BinaryMessenger argBinaryMessenger)'); + indent.scoped('{', '}', () { + indent.writeln('this.binaryMessenger = argBinaryMessenger;'); + }); + indent.write('public interface Reply '); + indent.scoped('{', '}', () { + indent.writeln('void reply(T reply);'); + }); + final String codecName = _getCodecName(api); + indent.format(''' +static MessageCodec getCodec() { +\treturn $codecName.INSTANCE; +} +'''); + for (final Method func in api.methods) { + final String channelName = makeChannelName(api, func); + final String returnType = func.returnType.dataType == 'void' + ? 'Void' + : _boxedType(func.returnType.dataType); + String sendArgument; + if (func.arguments.isEmpty) { + indent.write('public void ${func.name}(Reply<$returnType> callback) '); + sendArgument = 'null'; + } else { + final String argType = _boxedType(func.arguments[0].dataType); + indent.write( + 'public void ${func.name}($argType argInput, Reply<$returnType> callback) '); + sendArgument = 'argInput'; + } + indent.scoped('{', '}', () { + indent.writeln('BasicMessageChannel channel ='); + indent.inc(); + indent.inc(); + indent.writeln( + 'new BasicMessageChannel<>(binaryMessenger, "$channelName", getCodec());'); + indent.dec(); + indent.dec(); + indent.write('channel.send($sendArgument, channelReply -> '); + indent.scoped('{', '});', () { + if (func.returnType.dataType == 'void') { + indent.writeln('callback.reply(null);'); + } else { + indent.writeln('@SuppressWarnings("ConstantConditions")'); + indent.writeln('$returnType output = ($returnType)channelReply;'); + indent.writeln('callback.reply(output);'); + } + }); + }); + } + }); +} + +String _makeGetter(Field field) { + final String uppercased = + field.name.substring(0, 1).toUpperCase() + field.name.substring(1); + return 'get$uppercased'; +} + +String _makeSetter(Field field) { + final String uppercased = + field.name.substring(0, 1).toUpperCase() + field.name.substring(1); + return 'set$uppercased'; +} + +String _flattenTypeArguments(List args) { + return args + .map((TypeArgument e) => e.typeArguments == null + ? _boxedType(e.dataType) + : '${_boxedType(e.dataType)}<${_flattenTypeArguments(e.typeArguments!)}>') + .reduce((String value, String element) => '$value, $element'); +} + +String? _javaTypeForDartType(Field field) { + const Map javaTypeForDartTypeMap = { + 'bool': 'Boolean', + 'int': 'Long', + 'String': 'String', + 'double': 'Double', + 'Uint8List': 'byte[]', + 'Int32List': 'int[]', + 'Int64List': 'long[]', + 'Float64List': 'double[]', + 'Map': 'Map', + }; + if (javaTypeForDartTypeMap.containsKey(field.dataType)) { + return javaTypeForDartTypeMap[field.dataType]; + } else if (field.dataType == 'List') { + if (field.typeArguments == null) { + return 'List'; + } else { + return 'List<${_flattenTypeArguments(field.typeArguments!)}>'; + } + } else { + return null; + } +} + +String _castObject( + Field field, List classes, List enums, String varName) { + final HostDatatype hostDatatype = + getHostDatatype(field, classes, enums, _javaTypeForDartType); + if (field.dataType == 'int') { + return '($varName == null) ? null : (($varName instanceof Integer) ? (Integer)$varName : (${hostDatatype.datatype})$varName)'; + } else if (!hostDatatype.isBuiltin && + classes.map((Class x) => x.name).contains(field.dataType)) { + return '${hostDatatype.datatype}.fromMap((Map)$varName)'; + } else { + return '(${hostDatatype.datatype})$varName'; + } +} + +/// Generates the ".java" file for the AST represented by [root] to [sink] with the +/// provided [options]. +void generateJava(JavaOptions options, Root root, StringSink sink) { + final Set rootClassNameSet = + root.classes.map((Class x) => x.name).toSet(); + final Set rootEnumNameSet = + root.enums.map((Enum x) => x.name).toSet(); + final Indent indent = Indent(sink); + if (options.copyrightHeader != null) { + addLines(indent, options.copyrightHeader!, linePrefix: '// '); + } + indent.writeln('// $generatedCodeWarning'); + indent.writeln('// $seeAlsoWarning'); + indent.addln(''); + if (options.package != null) { + indent.writeln('package ${options.package};'); + } + indent.addln(''); + indent.writeln('import io.flutter.plugin.common.BasicMessageChannel;'); + indent.writeln('import io.flutter.plugin.common.BinaryMessenger;'); + indent.writeln('import io.flutter.plugin.common.MessageCodec;'); + indent.writeln('import io.flutter.plugin.common.StandardMessageCodec;'); + indent.writeln('import java.io.ByteArrayOutputStream;'); + indent.writeln('import java.nio.ByteBuffer;'); + indent.writeln('import java.util.List;'); + indent.writeln('import java.util.Map;'); + indent.writeln('import java.util.HashMap;'); + + indent.addln(''); + indent.writeln('/** Generated class from Pigeon. */'); + indent.writeln( + '@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"})'); + indent.write('public class ${options.className!} '); + indent.scoped('{', '}', () { + for (final Enum anEnum in root.enums) { + indent.writeln(''); + indent.write('public enum ${anEnum.name} '); + indent.scoped('{', '}', () { + int index = 0; + for (final String member in anEnum.members) { + indent.writeln( + '$member($index)${index == anEnum.members.length - 1 ? ';' : ','}'); + index++; + } + indent.writeln(''); + // We use explicit indexing here as use of the ordinal() method is + // discouraged. The toMap and fromMap API matches class API to allow + // the same code to work with enums and classes, but this + // can also be done directly in the host and flutter APIs. + indent.writeln('private int index;'); + indent.write('private ${anEnum.name}(final int index) '); + indent.scoped('{', '}', () { + indent.writeln('this.index = index;'); + }); + }); + } + + for (final Class klass in root.classes) { + indent.addln(''); + indent.writeln( + '/** Generated class from Pigeon that represents data sent in messages. */'); + indent.write('public static class ${klass.name} '); + indent.scoped('{', '}', () { + for (final Field field in klass.fields) { + final HostDatatype hostDatatype = getHostDatatype( + field, root.classes, root.enums, _javaTypeForDartType); + indent.writeln('private ${hostDatatype.datatype} ${field.name};'); + indent.writeln( + 'public ${hostDatatype.datatype} ${_makeGetter(field)}() { return ${field.name}; }'); + indent.writeln( + 'public void ${_makeSetter(field)}(${hostDatatype.datatype} setterArg) { this.${field.name} = setterArg; }'); + indent.addln(''); + } + indent.write('Map toMap() '); + indent.scoped('{', '}', () { + indent.writeln('Map toMapResult = new HashMap<>();'); + for (final Field field in klass.fields) { + final HostDatatype hostDatatype = getHostDatatype( + field, root.classes, root.enums, _javaTypeForDartType); + String toWriteValue = ''; + if (!hostDatatype.isBuiltin && + rootClassNameSet.contains(field.dataType)) { + toWriteValue = '${field.name}.toMap()'; + } else if (!hostDatatype.isBuiltin && + rootEnumNameSet.contains(field.dataType)) { + toWriteValue = '${field.name}.index'; + } else { + toWriteValue = field.name; + } + indent.writeln('toMapResult.put("${field.name}", $toWriteValue);'); + } + indent.writeln('return toMapResult;'); + }); + indent.write('static ${klass.name} fromMap(Map map) '); + indent.scoped('{', '}', () { + indent.writeln('${klass.name} fromMapResult = new ${klass.name}();'); + for (final Field field in klass.fields) { + indent.writeln('Object ${field.name} = map.get("${field.name}");'); + if (rootEnumNameSet.contains(field.dataType)) { + indent.writeln( + 'fromMapResult.${field.name} = ${field.dataType}.values()[(int)${field.name}];'); + } else { + indent.writeln( + 'fromMapResult.${field.name} = ${_castObject(field, root.classes, root.enums, field.name)};'); + } + } + indent.writeln('return fromMapResult;'); + }); + }); + } + + if (root.apis.any((Api api) => + api.location == ApiLocation.host && + api.methods.any((Method it) => it.isAsynchronous))) { + indent.addln(''); + indent.write('public interface Result '); + indent.scoped('{', '}', () { + indent.writeln('void success(T result);'); + }); + } + + for (final Api api in root.apis) { + _writeCodec(indent, api); + indent.addln(''); + if (api.location == ApiLocation.host) { + _writeHostApi(indent, api); + } else if (api.location == ApiLocation.flutter) { + _writeFlutterApi(indent, api); + } + } + + indent.format(''' +private static Map wrapError(Throwable exception) { +\tMap errorMap = new HashMap<>(); +\terrorMap.put("${Keys.errorMessage}", exception.toString()); +\terrorMap.put("${Keys.errorCode}", exception.getClass().getSimpleName()); +\terrorMap.put("${Keys.errorDetails}", null); +\treturn errorMap; +}'''); + }); +} diff --git a/packages/pigeon/lib/objc_generator.dart b/packages/pigeon/lib/objc_generator.dart index 77612a49854f..d147be3b1fe9 100644 --- a/packages/pigeon/lib/objc_generator.dart +++ b/packages/pigeon/lib/objc_generator.dart @@ -79,37 +79,45 @@ const Map _objcTypeForDartTypeMap = { 'Map': 'NSDictionary', }; -const Map _propertyTypeForDartTypeMap = { - 'String': 'copy', - 'bool': 'strong', - 'int': 'strong', - 'double': 'strong', - 'Uint8List': 'strong', - 'Int32List': 'strong', - 'Int64List': 'strong', - 'Float64List': 'strong', - 'List': 'strong', - 'Map': 'strong', -}; +String _flattenTypeArguments(String? classPrefix, List args) { + final String result = args + .map((TypeArgument e) => '${_objcTypeForDartType(classPrefix, e)} *') + .reduce((String value, String element) => '$value, $element'); + return result; +} -String? _objcTypePtrForPrimitiveDartType(String type) { - return _objcTypeForDartTypeMap.containsKey(type) - ? '${_objcTypeForDartTypeMap[type]} *' +String? _objcTypePtrForPrimitiveDartType(String? classPrefix, Field field) { + return _objcTypeForDartTypeMap.containsKey(field.dataType) + ? '${_objcTypeForDartType(classPrefix, field)} *' : null; } -/// Returns the objc type for a dart [type], prepending the [classPrefix] for -/// generated classes. For example: -/// _objcTypeForDartType(null, 'int') => 'NSNumber'. -String _objcTypeForDartType(String? classPrefix, String type) { - final String? builtinObjcType = _objcTypeForDartTypeMap[type]; - return builtinObjcType ?? _className(classPrefix, type); +String _objcTypeForDartType( + String? classPrefix, T field) { + return _objcTypeForDartTypeMap.containsKey(field.dataType) + ? field.typeArguments == null + ? _objcTypeForDartTypeMap[field.dataType]! + : '${_objcTypeForDartTypeMap[field.dataType]}<${_flattenTypeArguments(classPrefix, field.typeArguments!)}>' + : _className(classPrefix, field.dataType); } -String _propertyTypeForDartType(String type) { - final String? result = _propertyTypeForDartTypeMap[type]; +String _propertyTypeForDartType(Field field) { + const Map propertyTypeForDartTypeMap = { + 'String': 'copy', + 'bool': 'strong', + 'int': 'strong', + 'double': 'strong', + 'Uint8List': 'strong', + 'Int32List': 'strong', + 'Int64List': 'strong', + 'Float64List': 'strong', + 'List': 'strong', + 'Map': 'strong', + }; + + final String? result = propertyTypeForDartTypeMap[field.dataType]; if (result == null) { - return 'assign'; + return 'strong'; } else { return result; } @@ -121,18 +129,16 @@ void _writeClassDeclarations( for (final Class klass in classes) { indent.writeln('@interface ${_className(prefix, klass.name)} : NSObject'); for (final Field field in klass.fields) { - final HostDatatype hostDatatype = getHostDatatype( - field, classes, enums, _objcTypePtrForPrimitiveDartType, + final HostDatatype hostDatatype = getHostDatatype(field, classes, enums, + (Field x) => _objcTypePtrForPrimitiveDartType(prefix, x), customResolver: enumNames.contains(field.dataType) ? (String x) => _className(prefix, x) : (String x) => '${_className(prefix, x)} *'); late final String propertyType; - if (hostDatatype.isBuiltin) { - propertyType = _propertyTypeForDartType(field.dataType); - } else if (enumNames.contains(field.dataType)) { + if (enumNames.contains(field.dataType)) { propertyType = 'assign'; } else { - propertyType = 'strong'; + propertyType = _propertyTypeForDartType(field); } final String nullability = hostDatatype.datatype.contains('*') ? ', nullable' : ''; @@ -230,36 +236,37 @@ void _writeHostApiDeclaration(Indent indent, Api api, ObjcOptions options) { final String returnTypeName = _objcTypeForDartType(options.prefix, func.returnType); if (func.isAsynchronous) { - if (func.returnType == 'void') { - if (func.argType == 'void') { + if (func.returnType.dataType == 'void') { + if (func.arguments.isEmpty) { indent.writeln( '-(void)${func.name}:(void(^)(FlutterError *_Nullable))completion;'); } else { final String argType = - _objcTypeForDartType(options.prefix, func.argType); + _objcTypeForDartType(options.prefix, func.arguments[0]); indent.writeln( '-(void)${func.name}:(nullable $argType *)input completion:(void(^)(FlutterError *_Nullable))completion;'); } } else { - if (func.argType == 'void') { + if (func.arguments.isEmpty) { indent.writeln( '-(void)${func.name}:(void(^)($returnTypeName *_Nullable, FlutterError *_Nullable))completion;'); } else { final String argType = - _objcTypeForDartType(options.prefix, func.argType); + _objcTypeForDartType(options.prefix, func.arguments[0]); indent.writeln( '-(void)${func.name}:(nullable $argType *)input completion:(void(^)($returnTypeName *_Nullable, FlutterError *_Nullable))completion;'); } } } else { - final String returnType = - func.returnType == 'void' ? 'void' : 'nullable $returnTypeName *'; - if (func.argType == 'void') { + final String returnType = func.returnType.dataType == 'void' + ? 'void' + : 'nullable $returnTypeName *'; + if (func.arguments.isEmpty) { indent.writeln( '-($returnType)${func.name}:(FlutterError *_Nullable *_Nonnull)error;'); } else { final String argType = - _objcTypeForDartType(options.prefix, func.argType); + _objcTypeForDartType(options.prefix, func.arguments[0]); indent.writeln( '-($returnType)${func.name}:($argType*)input error:(FlutterError *_Nullable *_Nonnull)error;'); } @@ -280,11 +287,13 @@ void _writeFlutterApiDeclaration(Indent indent, Api api, ObjcOptions options) { for (final Method func in api.methods) { final String returnType = _objcTypeForDartType(options.prefix, func.returnType); - final String callbackType = _callbackForType(func.returnType, returnType); - if (func.argType == 'void') { + final String callbackType = + _callbackForType(func.returnType.dataType, returnType); + if (func.arguments.isEmpty) { indent.writeln('- (void)${func.name}:($callbackType)completion;'); } else { - final String argType = _objcTypeForDartType(options.prefix, func.argType); + final String argType = + _objcTypeForDartType(options.prefix, func.arguments[0]); indent.writeln( '- (void)${func.name}:($argType*)input completion:($callbackType)completion;'); } @@ -403,18 +412,18 @@ void _writeHostApiSource(Indent indent, ObjcOptions options, Api api) { final String returnType = _objcTypeForDartType(options.prefix, func.returnType); String syncCall; - if (func.argType == 'void') { + if (func.arguments.isEmpty) { syncCall = '[api ${func.name}:&error]'; } else { final String argType = - _objcTypeForDartType(options.prefix, func.argType); + _objcTypeForDartType(options.prefix, func.arguments[0]); indent.writeln('$argType *input = message;'); syncCall = '[api ${func.name}:input error:&error]'; } if (func.isAsynchronous) { - if (func.returnType == 'void') { + if (func.returnType.dataType == 'void') { const String callback = 'callback(wrapResult(nil, error));'; - if (func.argType == 'void') { + if (func.arguments.isEmpty) { indent.writeScoped( '[api ${func.name}:^(FlutterError *_Nullable error) {', '}];', () { @@ -429,7 +438,7 @@ void _writeHostApiSource(Indent indent, ObjcOptions options, Api api) { } } else { const String callback = 'callback(wrapResult(output, error));'; - if (func.argType == 'void') { + if (func.arguments.isEmpty) { indent.writeScoped( '[api ${func.name}:^($returnType *_Nullable output, FlutterError *_Nullable error) {', '}];', () { @@ -445,7 +454,7 @@ void _writeHostApiSource(Indent indent, ObjcOptions options, Api api) { } } else { indent.writeln('FlutterError *error;'); - if (func.returnType == 'void') { + if (func.returnType.dataType == 'void') { indent.writeln('$syncCall;'); indent.writeln('callback(wrapResult(nil, error));'); } else { @@ -487,14 +496,16 @@ void _writeFlutterApiSource(Indent indent, ObjcOptions options, Api api) { for (final Method func in api.methods) { final String returnType = _objcTypeForDartType(options.prefix, func.returnType); - final String callbackType = _callbackForType(func.returnType, returnType); + final String callbackType = + _callbackForType(func.returnType.dataType, returnType); String sendArgument; - if (func.argType == 'void') { + if (func.arguments.isEmpty) { indent.write('- (void)${func.name}:($callbackType)completion '); sendArgument = 'nil'; } else { - final String argType = _objcTypeForDartType(options.prefix, func.argType); + final String argType = + _objcTypeForDartType(options.prefix, func.arguments[0]); indent.write( '- (void)${func.name}:($argType*)input completion:($callbackType)completion '); sendArgument = 'input'; @@ -512,7 +523,7 @@ void _writeFlutterApiSource(Indent indent, ObjcOptions options, Api api) { indent.dec(); indent.write('[channel sendMessage:$sendArgument reply:^(id reply) '); indent.scoped('{', '}];', () { - if (func.returnType == 'void') { + if (func.returnType.dataType == 'void') { indent.writeln('completion(nil);'); } else { indent.writeln('$returnType * output = reply;'); diff --git a/packages/pigeon/lib/pigeon_lib.dart b/packages/pigeon/lib/pigeon_lib.dart index a1252226f29b..05f86f2f07f9 100644 --- a/packages/pigeon/lib/pigeon_lib.dart +++ b/packages/pigeon/lib/pigeon_lib.dart @@ -13,7 +13,8 @@ import 'package:analyzer/dart/analysis/analysis_context_collection.dart' import 'package:analyzer/dart/analysis/results.dart' show ParsedUnitResult; import 'package:analyzer/dart/analysis/session.dart' show AnalysisSession; import 'package:analyzer/dart/ast/ast.dart' as dart_ast; -import 'package:analyzer/dart/ast/ast.dart' show CompilationUnit; +import 'package:analyzer/dart/ast/syntactic_entity.dart' + as dart_ast_syntactic_entity; import 'package:analyzer/dart/ast/visitor.dart' as dart_ast_visitor; import 'package:analyzer/error/error.dart' show AnalysisError; import 'package:args/args.dart'; @@ -379,12 +380,17 @@ List _validateAst(Root root, String source) { for (final Class klass in root.classes) { for (final Field field in klass.fields) { if (field.typeArguments != null) { - result.add(Error( - message: - 'Unsupported datatype:"${field.dataType}" in class "${klass.name}". Generic fields aren\'t yet supported (https://github.com/flutter/flutter/issues/63468).', - lineNumber: _calculateLineNumberNullable(source, field.offset), - )); - } else if (!(validTypes.contains(field.dataType) || + for (final TypeArgument typeArgument in field.typeArguments!) { + if (!typeArgument.isNullable) { + result.add(Error( + message: + 'Generic type arguments must be nullable in field "${field.name}" in class "${klass.name}".', + lineNumber: _calculateLineNumberNullable(source, field.offset), + )); + } + } + } + if (!(validTypes.contains(field.dataType) || customClasses.contains(field.dataType) || customEnums.contains(field.dataType))) { result.add(Error( @@ -397,17 +403,24 @@ List _validateAst(Root root, String source) { } for (final Api api in root.apis) { for (final Method method in api.methods) { - if (method.isReturnNullable) { + if (method.returnType.isNullable) { result.add(Error( message: - 'Nullable return types types aren\'t supported for Pigeon methods: "${method.argType}" in API: "${api.name}" method: "${method.name}"', + 'Nullable return types types aren\'t supported for Pigeon methods: "${method.arguments[0].dataType}" in API: "${api.name}" method: "${method.name}"', lineNumber: _calculateLineNumberNullable(source, method.offset), )); } - if (method.isArgNullable) { + if (method.arguments.length > 1) { result.add(Error( message: - 'Nullable argument types aren\'t supported for Pigeon methods: "${method.argType}" in API: "${api.name}" method: "${method.name}"', + 'Multiple arguments aren\'t yet supported, in API: "${api.name}" method: "${method.name} (https://github.com/flutter/flutter/issues/86971)"', + lineNumber: _calculateLineNumberNullable(source, method.offset), + )); + } + if (method.arguments.isNotEmpty && method.arguments[0].isNullable) { + result.add(Error( + message: + 'Nullable argument types aren\'t supported for Pigeon methods: "${method.arguments[0].dataType}" in API: "${api.name}" method: "${method.name}"', lineNumber: _calculateLineNumberNullable(source, method.offset), )); } @@ -462,8 +475,10 @@ class _RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { final Set referencedTypes = {}; for (final Api api in _apis) { for (final Method method in api.methods) { - referencedTypes.add(method.argType); - referencedTypes.add(method.returnType); + if (method.arguments.isNotEmpty) { + referencedTypes.add(method.arguments[0].dataType); + } + referencedTypes.add(method.returnType.dataType); } } @@ -613,30 +628,50 @@ class _RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { return null; } + Field formalParameterToField(dart_ast.FormalParameter parameter) { + final dart_ast.TypeName typeName = parameter.childEntities.firstWhere( + (dart_ast_syntactic_entity.SyntacticEntity e) => + e is dart_ast.TypeName) as dart_ast.TypeName; + final String argType = typeName.name.name; + final bool isNullable = typeName.question != null; + final List? argTypeArguments = + typeAnnotationsToTypeArguments(typeName.typeArguments); + return Field( + dataType: argType, + isNullable: isNullable, + name: '', + typeArguments: argTypeArguments, + ); + } + + static T? getFirstChildOfType(dart_ast.AstNode entity) { + for (final dart_ast_syntactic_entity.SyntacticEntity child + in entity.childEntities) { + if (child is T) { + return child as T; + } + } + return null; + } + @override Object? visitMethodDeclaration(dart_ast.MethodDeclaration node) { final dart_ast.FormalParameterList parameters = node.parameters!; - late String argType; - bool isNullable = false; - if (parameters.parameters.isEmpty) { - argType = 'void'; - } else { - final dart_ast.FormalParameter firstParameter = - parameters.parameters.first; - final dart_ast.TypeName typeName = firstParameter.childEntities - // ignore: always_specify_types - .firstWhere((e) => e is dart_ast.TypeName) as dart_ast.TypeName; - argType = typeName.name.name; - isNullable = typeName.question != null; - } + final List arguments = + parameters.parameters.map(formalParameterToField).toList(); final bool isAsynchronous = _hasMetadata(node.metadata, 'async'); if (_currentApi != null) { _currentApi!.methods.add(Method( name: node.name.name, - returnType: node.returnType.toString(), - argType: argType, - isReturnNullable: node.returnType!.question != null, - isArgNullable: isNullable, + returnType: Field( + name: '', + dataType: getFirstChildOfType( + node.returnType!)! + .name, + typeArguments: typeAnnotationsToTypeArguments( + (node.returnType as dart_ast.NamedType?)!.typeArguments), + isNullable: node.returnType!.question != null), + arguments: arguments, isAsynchronous: isAsynchronous, offset: node.offset)); } else if (_currentClass != null) { @@ -660,6 +695,23 @@ class _RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { return null; } + List? typeAnnotationsToTypeArguments( + dart_ast.TypeArgumentList? typeArguments) { + List? result; + if (typeArguments != null) { + for (final Object x in typeArguments.childEntities) { + if (x is dart_ast.TypeName) { + result ??= []; + result.add(TypeArgument( + dataType: x.name.name, + isNullable: x.question != null, + typeArguments: typeAnnotationsToTypeArguments(x.typeArguments))); + } + } + } + return result; + } + @override Object? visitFieldDeclaration(dart_ast.FieldDeclaration node) { if (_currentClass != null) { @@ -683,20 +735,7 @@ class _RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { name: node.fields.variables[0].name.name, dataType: type.name.name, isNullable: type.question != null, - // TODO(aaclarke): This probably has to be recursive at some point. - // ignore: prefer_null_aware_operators - typeArguments: typeArguments == null - ? null - : typeArguments.arguments - .map((dart_ast.TypeAnnotation e) => Field( - name: '', - dataType: (e.childEntities.first - as dart_ast.SimpleIdentifier) - .name, - isNullable: e.question != null, - offset: e.offset, - )) - .toList(), + typeArguments: typeAnnotationsToTypeArguments(typeArguments), offset: node.offset, )); } @@ -764,7 +803,7 @@ class Pigeon { final ParsedUnitResult result = session.getParsedUnit2(path) as ParsedUnitResult; if (result.errors.isEmpty) { - final CompilationUnit unit = result.unit; + final dart_ast.CompilationUnit unit = result.unit; unit.accept(rootBuilder); } else { for (final AnalysisError error in result.errors) { diff --git a/packages/pigeon/pigeons/all_datatypes.dart b/packages/pigeon/pigeons/all_datatypes.dart index 0a8c2c27626f..138b4caf62c3 100644 --- a/packages/pigeon/pigeons/all_datatypes.dart +++ b/packages/pigeon/pigeons/all_datatypes.dart @@ -17,6 +17,8 @@ class Everything { List? aList; // ignore: always_specify_types Map? aMap; + List?>? nestedList; + Map? mapWithAnnotations; } @HostApi() diff --git a/packages/pigeon/pigeons/primitive.dart b/packages/pigeon/pigeons/primitive.dart index 2b453532b084..cc8eea5cc49c 100644 --- a/packages/pigeon/pigeons/primitive.dart +++ b/packages/pigeon/pigeons/primitive.dart @@ -15,6 +15,8 @@ abstract class PrimitiveHostApi { // ignore: always_specify_types List aList(List value); Int32List anInt32List(Int32List value); + List aBoolList(List value); + Map aStringIntMap(Map value); } @FlutterApi() @@ -28,4 +30,6 @@ abstract class PrimitiveFlutterApi { // ignore: always_specify_types List aList(List value); Int32List anInt32List(Int32List value); + List aBoolList(List value); + Map aStringIntMap(Map value); } diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/test/java/com/example/android_unit_tests/PrimitiveTest.java b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/test/java/com/example/android_unit_tests/PrimitiveTest.java index 36ce51cd35a0..d557d2154049 100644 --- a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/test/java/com/example/android_unit_tests/PrimitiveTest.java +++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/test/java/com/example/android_unit_tests/PrimitiveTest.java @@ -37,10 +37,10 @@ public void primitiveInt() { PrimitiveFlutterApi api = new PrimitiveFlutterApi(binaryMessenger); boolean[] didCall = {false}; api.anInt( - 1, - (Integer result) -> { + 1L, + (Long result) -> { didCall[0] = true; - assertEquals(result, (Integer) 1); + assertEquals(result, (Long) 1L); }); assertTrue(didCall[0]); } @@ -94,7 +94,7 @@ public void primitiveMap() { boolean[] didCall = {false}; api.aMap( Collections.singletonMap("hello", 1), - (Map result) -> { + (Map result) -> { didCall[0] = true; assertEquals(result, Collections.singletonMap("hello", 1)); }); @@ -108,7 +108,7 @@ public void primitiveList() { boolean[] didCall = {false}; api.aList( Collections.singletonList("hello"), - (List result) -> { + (List result) -> { didCall[0] = true; assertEquals(result, Collections.singletonList("hello")); }); 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 09db4570911d..adf2c866aa95 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 @@ -23,6 +23,8 @@ class Everything { Float64List? aFloatArray; List? aList; Map? aMap; + List?>? nestedList; + Map? mapWithAnnotations; Object encode() { final Map pigeonMap = {}; @@ -36,6 +38,8 @@ class Everything { pigeonMap['aFloatArray'] = aFloatArray; pigeonMap['aList'] = aList; pigeonMap['aMap'] = aMap; + pigeonMap['nestedList'] = nestedList; + pigeonMap['mapWithAnnotations'] = mapWithAnnotations; return pigeonMap; } @@ -51,7 +55,12 @@ class Everything { ..a8ByteArray = pigeonMap['a8ByteArray'] as Int64List? ..aFloatArray = pigeonMap['aFloatArray'] as Float64List? ..aList = pigeonMap['aList'] as List? - ..aMap = pigeonMap['aMap'] as Map?; + ..aMap = pigeonMap['aMap'] as Map? + ..nestedList = + (pigeonMap['nestedList'] as List?)?.cast?>() + ..mapWithAnnotations = + (pigeonMap['mapWithAnnotations'] as Map?) + ?.cast(); } } 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 new file mode 100644 index 000000000000..ae36e10b22c4 --- /dev/null +++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/primitive.dart @@ -0,0 +1,411 @@ +// 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 (v0.3.0), 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 _PrimitiveHostApiCodec extends StandardMessageCodec { + const _PrimitiveHostApiCodec(); +} + +class PrimitiveHostApi { + /// Constructor for [PrimitiveHostApi]. 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. + PrimitiveHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _PrimitiveHostApiCodec(); + + Future anInt(int arg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PrimitiveHostApi.anInt', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(arg) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } 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?)!; + } + } + + Future aBool(bool arg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PrimitiveHostApi.aBool', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(arg) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } 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 bool?)!; + } + } + + Future aString(String arg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PrimitiveHostApi.aString', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(arg) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } 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 String?)!; + } + } + + Future aDouble(double arg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PrimitiveHostApi.aDouble', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(arg) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } 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 double?)!; + } + } + + Future> aMap(Map arg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PrimitiveHostApi.aMap', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(arg) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } 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 Map?)!; + } + } + + Future> aList(List arg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PrimitiveHostApi.aList', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(arg) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as List?)!; + } + } + + Future anInt32List(Int32List arg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PrimitiveHostApi.anInt32List', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(arg) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } 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 Int32List?)!; + } + } + + Future> aBoolList(List arg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PrimitiveHostApi.aBoolList', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(arg) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as List?)!.cast(); + } + } + + Future> aStringIntMap(Map arg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PrimitiveHostApi.aStringIntMap', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(arg) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } 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 Map?)! + .cast(); + } + } +} + +class _PrimitiveFlutterApiCodec extends StandardMessageCodec { + const _PrimitiveFlutterApiCodec(); +} + +abstract class PrimitiveFlutterApi { + static const MessageCodec codec = _PrimitiveFlutterApiCodec(); + + int anInt(int arg); + bool aBool(bool arg); + String aString(String arg); + double aDouble(double arg); + Map aMap(Map arg); + List aList(List arg); + Int32List anInt32List(Int32List arg); + List aBoolList(List arg); + Map aStringIntMap(Map arg); + static void setup(PrimitiveFlutterApi? api) { + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PrimitiveFlutterApi.anInt', codec); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.PrimitiveFlutterApi.anInt was null. Expected int.'); + final int input = (message as int?)!; + final int output = api.anInt(input); + return output; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PrimitiveFlutterApi.aBool', codec); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.PrimitiveFlutterApi.aBool was null. Expected bool.'); + final bool input = (message as bool?)!; + final bool output = api.aBool(input); + return output; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PrimitiveFlutterApi.aString', codec); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.PrimitiveFlutterApi.aString was null. Expected String.'); + final String input = (message as String?)!; + final String output = api.aString(input); + return output; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PrimitiveFlutterApi.aDouble', codec); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.PrimitiveFlutterApi.aDouble was null. Expected double.'); + final double input = (message as double?)!; + final double output = api.aDouble(input); + return output; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PrimitiveFlutterApi.aMap', codec); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.PrimitiveFlutterApi.aMap was null. Expected Map.'); + final Map input = + (message as Map?)!; + final Map output = api.aMap(input); + return output; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PrimitiveFlutterApi.aList', codec); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.PrimitiveFlutterApi.aList was null. Expected List.'); + final List input = (message as List?)!; + final List output = api.aList(input); + return output; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PrimitiveFlutterApi.anInt32List', codec); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.PrimitiveFlutterApi.anInt32List was null. Expected Int32List.'); + final Int32List input = (message as Int32List?)!; + final Int32List output = api.anInt32List(input); + return output; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PrimitiveFlutterApi.aBoolList', codec); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.PrimitiveFlutterApi.aBoolList was null. Expected List.'); + final List input = (message as List?)!; + final List output = api.aBoolList(input); + return output; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PrimitiveFlutterApi.aStringIntMap', codec); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.PrimitiveFlutterApi.aStringIntMap was null. Expected Map.'); + final Map input = (message as Map?)!; + final Map output = api.aStringIntMap(input); + return output; + }); + } + } + } +} diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/all_datatypes_test.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/all_datatypes_test.dart index 7061fb8fa47a..4cad76d88853 100644 --- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/all_datatypes_test.dart +++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/all_datatypes_test.dart @@ -35,6 +35,8 @@ void main() { expect(result.aFloatArray, isNull); expect(result.aList, isNull); expect(result.aMap, isNull); + expect(result.nestedList, isNull); + expect(result.mapWithAnnotations, isNull); }); test('with values', () async { @@ -50,6 +52,10 @@ void main() { Float64List.fromList([1.0, 2.5, 3.0, 4.25]); everything.aList = [1, 2, 3, 4]; everything.aMap = {'hello': 1234}; + everything.aList = >[ + [true] + ]; + everything.mapWithAnnotations = {'hello': 'world'}; final BinaryMessenger mockMessenger = MockBinaryMessenger(); when(mockMessenger.send('dev.flutter.pigeon.HostEverything.echo', any)) .thenAnswer((Invocation realInvocation) async { @@ -70,5 +76,7 @@ void main() { expect(result.aFloatArray, everything.aFloatArray); expect(result.aList, everything.aList); expect(result.aMap, everything.aMap); + expect(result.aList, everything.aList); + expect(result.mapWithAnnotations, everything.mapWithAnnotations); }); } diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/primitive_test.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/primitive_test.dart new file mode 100644 index 000000000000..3c20172496a9 --- /dev/null +++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/primitive_test.dart @@ -0,0 +1,61 @@ +// 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. + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_unit_tests/primitive.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'primitive_test.mocks.dart'; + +@GenerateMocks([BinaryMessenger]) +void main() { + test('test anInt', () async { + final BinaryMessenger mockMessenger = MockBinaryMessenger(); + when(mockMessenger.send('dev.flutter.pigeon.PrimitiveHostApi.anInt', any)) + .thenAnswer((Invocation realInvocation) async { + const MessageCodec codec = PrimitiveHostApi.codec; + final Object? input = + codec.decodeMessage(realInvocation.positionalArguments[1]); + return codec.encodeMessage({'result': input!}); + }); + final PrimitiveHostApi api = + PrimitiveHostApi(binaryMessenger: mockMessenger); + final int result = await api.anInt(1); + expect(result, 1); + }); + + test('test List', () async { + final BinaryMessenger mockMessenger = MockBinaryMessenger(); + when(mockMessenger.send( + 'dev.flutter.pigeon.PrimitiveHostApi.aBoolList', any)) + .thenAnswer((Invocation realInvocation) async { + const MessageCodec codec = PrimitiveHostApi.codec; + final Object? input = + codec.decodeMessage(realInvocation.positionalArguments[1]); + return codec.encodeMessage({'result': input!}); + }); + final PrimitiveHostApi api = + PrimitiveHostApi(binaryMessenger: mockMessenger); + final List result = await api.aBoolList([true]); + expect(result[0], true); + }); + + test('test Map', () async { + final BinaryMessenger mockMessenger = MockBinaryMessenger(); + when(mockMessenger.send( + 'dev.flutter.pigeon.PrimitiveHostApi.aStringIntMap', any)) + .thenAnswer((Invocation realInvocation) async { + const MessageCodec codec = PrimitiveHostApi.codec; + final Object? input = + codec.decodeMessage(realInvocation.positionalArguments[1]); + return codec.encodeMessage({'result': input!}); + }); + final PrimitiveHostApi api = + PrimitiveHostApi(binaryMessenger: mockMessenger); + final Map result = + await api.aStringIntMap({'hello': 1}); + expect(result['hello'], 1); + }); +} diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/primitive_test.mocks.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/primitive_test.mocks.dart new file mode 100644 index 000000000000..814ea419359e --- /dev/null +++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/primitive_test.mocks.dart @@ -0,0 +1,44 @@ +// Mocks generated by Mockito 5.0.7 from annotations +// in flutter_unit_tests/test/null_safe_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i3; +import 'dart:typed_data' as _i4; +import 'dart:ui' as _i5; + +import 'package:flutter/src/services/binary_messenger.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: comment_references +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: always_specify_types +// Added manually; several methods have moved to +// flutter_test/lib/src/deprecated.dart on master, but that hasn't reached +// stable yet. +// ignore_for_file: override_on_non_overriding_member + +/// A class which mocks [BinaryMessenger]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBinaryMessenger extends _i1.Mock implements _i2.BinaryMessenger { + MockBinaryMessenger() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future handlePlatformMessage(String? channel, _i4.ByteData? data, + _i5.PlatformMessageResponseCallback? callback) => + (super.noSuchMethod( + Invocation.method(#handlePlatformMessage, [channel, data, callback]), + returnValue: Future.value(null), + returnValueForMissingStub: + Future.value()) as _i3.Future); + @override + _i3.Future<_i4.ByteData?>? send(String? channel, _i4.ByteData? message) => + (super.noSuchMethod(Invocation.method(#send, [channel, message])) + as _i3.Future<_i4.ByteData?>?); + @override + void setMessageHandler(String? channel, _i2.MessageHandler? handler) => super + .noSuchMethod(Invocation.method(#setMessageHandler, [channel, handler]), + returnValueForMissingStub: null); +} diff --git a/packages/pigeon/run_tests.sh b/packages/pigeon/run_tests.sh index 341967574189..2ed2c295b38f 100755 --- a/packages/pigeon/run_tests.sh +++ b/packages/pigeon/run_tests.sh @@ -194,17 +194,22 @@ test_running_without_arguments() { } run_flutter_unittests() { + local flutter_tests="platform_tests/flutter_null_safe_unit_tests" pushd $PWD $run_pigeon \ --input pigeons/flutter_unittests.dart \ - --dart_out platform_tests/flutter_null_safe_unit_tests/lib/null_safe_pigeon.dart + --dart_out "$flutter_tests/lib/null_safe_pigeon.dart" $run_pigeon \ --input pigeons/all_datatypes.dart \ - --dart_out platform_tests/flutter_null_safe_unit_tests/lib/all_datatypes.dart - cd platform_tests/flutter_null_safe_unit_tests + --dart_out "$flutter_tests/lib/all_datatypes.dart" + $run_pigeon \ + --input pigeons/primitive.dart \ + --dart_out "$flutter_tests/lib/primitive.dart" + cd "$flutter_tests" flutter pub get flutter test test/null_safe_test.dart flutter test test/all_datatypes_test.dart + flutter test test/primitive_test.dart popd } diff --git a/packages/pigeon/test/dart_generator_test.dart b/packages/pigeon/test/dart_generator_test.dart index 04b3488b303d..225704e073f2 100644 --- a/packages/pigeon/test/dart_generator_test.dart +++ b/packages/pigeon/test/dart_generator_test.dart @@ -57,9 +57,10 @@ void main() { Api(name: 'Api', location: ApiLocation.host, methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: false, - returnType: 'Output', + arguments: [ + Field(name: '', isNullable: false, dataType: 'Input') + ], + returnType: Field(name: '', dataType: 'Output', isNullable: false), isAsynchronous: false, ) ]) @@ -131,9 +132,10 @@ void main() { Api(name: 'Api', location: ApiLocation.flutter, methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: false, - returnType: 'Output', + arguments: [ + Field(name: '', isNullable: false, dataType: 'Input') + ], + returnType: Field(name: '', dataType: 'Output', isNullable: false), isAsynchronous: false, ) ]) @@ -165,9 +167,10 @@ void main() { Api(name: 'Api', location: ApiLocation.host, methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: false, - returnType: 'void', + arguments: [ + Field(name: '', isNullable: false, dataType: 'Input') + ], + returnType: Field(name: '', dataType: 'void', isNullable: false), isAsynchronous: false, ) ]) @@ -192,9 +195,10 @@ void main() { Api(name: 'Api', location: ApiLocation.flutter, methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: false, - returnType: 'void', + arguments: [ + Field(name: '', isNullable: false, dataType: 'Input') + ], + returnType: Field(name: '', dataType: 'void', isNullable: false), isAsynchronous: false, ) ]) @@ -222,9 +226,8 @@ void main() { Api(name: 'Api', location: ApiLocation.flutter, methods: [ Method( name: 'doSomething', - argType: 'void', - isArgNullable: false, - returnType: 'Output', + arguments: [], + returnType: Field(name: '', dataType: 'Output', isNullable: false), isAsynchronous: false, ) ]) @@ -249,9 +252,10 @@ void main() { Api(name: 'Api', location: ApiLocation.flutter, methods: [ Method( name: 'doSomething', - argType: 'EnumClass', - isArgNullable: false, - returnType: 'EnumClass', + arguments: [ + Field(name: '', isNullable: false, dataType: 'EnumClass') + ], + returnType: Field(name: '', dataType: 'EnumClass', isNullable: false), isAsynchronous: false, ) ]) @@ -286,9 +290,10 @@ void main() { Api(name: 'Api', location: ApiLocation.flutter, methods: [ Method( name: 'doSomething', - argType: 'EnumClass', - isArgNullable: false, - returnType: 'EnumClass', + arguments: [ + Field(name: '', isNullable: false, dataType: 'EnumClass') + ], + returnType: Field(name: '', dataType: 'EnumClass', isNullable: false), isAsynchronous: false, ) ]) @@ -325,9 +330,8 @@ void main() { Api(name: 'Api', location: ApiLocation.host, methods: [ Method( name: 'doSomething', - argType: 'void', - isArgNullable: false, - returnType: 'Output', + arguments: [], + returnType: Field(name: '', dataType: 'Output', isNullable: false), isAsynchronous: false, ) ]) @@ -355,16 +359,19 @@ void main() { methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: false, - returnType: 'Output', + arguments: [ + Field(name: '', isNullable: false, dataType: 'Input') + ], + returnType: + Field(name: '', dataType: 'Output', isNullable: false), isAsynchronous: false, ), Method( name: 'voidReturner', - argType: 'Input', - isArgNullable: false, - returnType: 'void', + arguments: [ + Field(name: '', isNullable: false, dataType: 'Input') + ], + returnType: Field(name: '', dataType: 'void', isNullable: false), isAsynchronous: false, ) ]) @@ -432,9 +439,10 @@ void main() { Api(name: 'Api', location: ApiLocation.flutter, methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: false, - returnType: 'Output', + arguments: [ + Field(name: '', isNullable: false, dataType: 'Input') + ], + returnType: Field(name: '', dataType: 'Output', isNullable: false), isAsynchronous: true, ) ]) @@ -468,9 +476,10 @@ void main() { Api(name: 'Api', location: ApiLocation.flutter, methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: false, - returnType: 'void', + arguments: [ + Field(name: '', isNullable: false, dataType: 'Input') + ], + returnType: Field(name: '', dataType: 'void', isNullable: false), isAsynchronous: true, ) ]) @@ -503,9 +512,10 @@ void main() { Api(name: 'Api', location: ApiLocation.host, methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: false, - returnType: 'Output', + arguments: [ + Field(name: '', isNullable: false, dataType: 'Input') + ], + returnType: Field(name: '', dataType: 'Output', isNullable: false), isAsynchronous: true, ) ]) @@ -537,9 +547,8 @@ void main() { Api(name: 'Api', location: ApiLocation.host, methods: [ Method( name: 'doSomething', - argType: 'void', - isArgNullable: false, - returnType: 'Output', + arguments: [], + returnType: Field(name: '', dataType: 'Output', isNullable: false), isAsynchronous: true, ) ]) @@ -574,4 +583,176 @@ void main() { final String code = sink.toString(); expect(code, startsWith('// hello world')); }); + + test('generics', () { + final Class klass = Class( + name: 'Foobar', + fields: [ + Field( + name: 'field1', + dataType: 'List', + isNullable: true, + typeArguments: [ + TypeArgument(dataType: 'int', isNullable: true) + ], + ), + ], + ); + final Root root = Root( + apis: [], + classes: [klass], + enums: [], + ); + final StringBuffer sink = StringBuffer(); + generateDart(const DartOptions(isNullSafe: true), root, sink); + final String code = sink.toString(); + expect(code, contains('class Foobar')); + expect(code, contains(' List? field1;')); + }); + + test('map generics', () { + final Class klass = Class( + name: 'Foobar', + fields: [ + Field( + name: 'field1', + dataType: 'Map', + isNullable: true, + typeArguments: [ + TypeArgument(dataType: 'String', isNullable: true), + TypeArgument(dataType: 'int', isNullable: true), + ], + ), + ], + ); + final Root root = Root( + apis: [], + classes: [klass], + enums: [], + ); + final StringBuffer sink = StringBuffer(); + generateDart(const DartOptions(isNullSafe: true), root, sink); + final String code = sink.toString(); + expect(code, contains('class Foobar')); + expect(code, contains(' Map? field1;')); + }); + + test('host generics argument', () { + final Root root = Root( + apis: [ + Api(name: 'Api', location: ApiLocation.host, methods: [ + Method( + name: 'doit', + returnType: Field(dataType: 'void', isNullable: false, name: ''), + arguments: [ + Field( + name: 'arg', + dataType: 'List', + isNullable: false, + typeArguments: [ + TypeArgument(dataType: 'int', isNullable: true) + ]) + ]) + ]) + ], + classes: [], + enums: [], + ); + final StringBuffer sink = StringBuffer(); + generateDart(const DartOptions(isNullSafe: true), root, sink); + final String code = sink.toString(); + expect(code, contains('doit(List arg')); + }); + + test('flutter generics argument', () { + final Root root = Root( + apis: [ + Api(name: 'Api', location: ApiLocation.flutter, methods: [ + Method( + name: 'doit', + returnType: Field(dataType: 'void', isNullable: false, name: ''), + arguments: [ + Field( + name: 'arg', + dataType: 'List', + isNullable: false, + typeArguments: [ + TypeArgument(dataType: 'int', isNullable: true) + ]) + ]) + ]) + ], + classes: [], + enums: [], + ); + final StringBuffer sink = StringBuffer(); + generateDart(const DartOptions(isNullSafe: true), root, sink); + final String code = sink.toString(); + expect(code, contains('doit(List arg')); + }); + + test('host generics return', () { + final Root root = Root( + apis: [ + Api(name: 'Api', location: ApiLocation.host, methods: [ + Method( + name: 'doit', + returnType: Field( + name: 'arg', + dataType: 'List', + isNullable: false, + typeArguments: [ + TypeArgument(dataType: '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 List?)!.cast();')); + }); + + test('host generics return', () { + final Root root = Root( + apis: [ + Api(name: 'Api', location: ApiLocation.flutter, methods: [ + Method( + name: 'doit', + returnType: Field( + name: 'arg', + dataType: 'List', + isNullable: false, + typeArguments: [ + TypeArgument(dataType: 'int', isNullable: true) + ]), + arguments: [ + Field( + name: 'arg', + dataType: 'List', + isNullable: false, + typeArguments: [ + TypeArgument(dataType: 'int', isNullable: true) + ]) + ]) + ]) + ], + classes: [], + enums: [], + ); + final StringBuffer sink = StringBuffer(); + generateDart(const DartOptions(isNullSafe: true), root, sink); + final String code = sink.toString(); + expect(code, contains('List doit(')); + expect( + code, contains('final List input = (message as List?)!')); + expect(code, contains('final List output = api.doit(input)')); + }); } diff --git a/packages/pigeon/test/java_generator_test.dart b/packages/pigeon/test/java_generator_test.dart index 4ac1ab63071b..d2048cf4e23f 100644 --- a/packages/pigeon/test/java_generator_test.dart +++ b/packages/pigeon/test/java_generator_test.dart @@ -87,9 +87,10 @@ void main() { Api(name: 'Api', location: ApiLocation.host, methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: true, - returnType: 'Output', + arguments: [ + Field(name: '', dataType: 'Input', isNullable: false) + ], + returnType: Field(name: '', dataType: 'Output', isNullable: false), isAsynchronous: false, ) ]) @@ -143,9 +144,10 @@ void main() { Api(name: 'Api', location: ApiLocation.flutter, methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: false, - returnType: 'Output', + arguments: [ + Field(name: '', dataType: 'Input', isNullable: false) + ], + returnType: Field(name: '', dataType: 'Output', isNullable: false), isAsynchronous: false, ) ]) @@ -170,9 +172,10 @@ void main() { Api(name: 'Api', location: ApiLocation.host, methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: false, - returnType: 'void', + arguments: [ + Field(name: '', dataType: 'Input', isNullable: false) + ], + returnType: Field(name: '', dataType: 'void', isNullable: false), isAsynchronous: false, ) ]) @@ -194,9 +197,10 @@ void main() { Api(name: 'Api', location: ApiLocation.flutter, methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: false, - returnType: 'void', + arguments: [ + Field(name: '', dataType: 'Input', isNullable: false) + ], + returnType: Field(name: '', dataType: 'void', isNullable: false), isAsynchronous: false, ) ]) @@ -218,9 +222,8 @@ void main() { Api(name: 'Api', location: ApiLocation.host, methods: [ Method( name: 'doSomething', - argType: 'void', - isArgNullable: false, - returnType: 'Output', + arguments: [], + returnType: Field(name: '', dataType: 'Output', isNullable: false), isAsynchronous: false, ) ]) @@ -242,9 +245,8 @@ void main() { Api(name: 'Api', location: ApiLocation.flutter, methods: [ Method( name: 'doSomething', - argType: 'void', - isArgNullable: false, - returnType: 'Output', + arguments: [], + returnType: Field(name: '', dataType: 'Output', isNullable: false), isAsynchronous: false, ) ]) @@ -332,9 +334,10 @@ void main() { Api(name: 'Api', location: ApiLocation.host, methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: false, - returnType: 'Output', + arguments: [ + Field(name: '', dataType: 'Input', isNullable: false) + ], + returnType: Field(name: '', dataType: 'Output', isNullable: false), isAsynchronous: true, ) ]) @@ -366,9 +369,10 @@ void main() { Api(name: 'Api', location: ApiLocation.flutter, methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: false, - returnType: 'Output', + arguments: [ + Field(name: '', dataType: 'Input', isNullable: false) + ], + returnType: Field(name: '', dataType: 'Output', isNullable: false), isAsynchronous: true, ) ]) @@ -441,4 +445,141 @@ void main() { final String code = sink.toString(); expect(code, startsWith('// hello world')); }); + + test('generics', () { + final Class klass = Class( + name: 'Foobar', + fields: [ + Field( + name: 'field1', + dataType: 'List', + isNullable: true, + typeArguments: [ + TypeArgument(dataType: 'int', isNullable: true) + ], + ), + ], + ); + final Root root = Root( + apis: [], + classes: [klass], + enums: [], + ); + final StringBuffer sink = StringBuffer(); + const JavaOptions javaOptions = JavaOptions(className: 'Messages'); + generateJava(javaOptions, root, sink); + final String code = sink.toString(); + expect(code, contains('class Foobar')); + expect(code, contains('List field1;')); + }); + + test('host generics argument', () { + final Root root = Root( + apis: [ + Api(name: 'Api', location: ApiLocation.host, methods: [ + Method( + name: 'doit', + returnType: Field(dataType: 'void', isNullable: false, name: ''), + arguments: [ + Field( + name: 'arg', + dataType: 'List', + isNullable: false, + typeArguments: [ + TypeArgument(dataType: 'int', isNullable: true) + ]) + ]) + ]) + ], + classes: [], + enums: [], + ); + final StringBuffer sink = StringBuffer(); + const JavaOptions javaOptions = JavaOptions(className: 'Messages'); + generateJava(javaOptions, root, sink); + final String code = sink.toString(); + expect(code, contains('doit(List arg')); + }); + + test('flutter generics argument', () { + final Root root = Root( + apis: [ + Api(name: 'Api', location: ApiLocation.flutter, methods: [ + Method( + name: 'doit', + returnType: Field(dataType: 'void', isNullable: false, name: ''), + arguments: [ + Field( + name: 'arg', + dataType: 'List', + isNullable: false, + typeArguments: [ + TypeArgument(dataType: 'int', isNullable: true) + ]) + ]) + ]) + ], + classes: [], + enums: [], + ); + final StringBuffer sink = StringBuffer(); + const JavaOptions javaOptions = JavaOptions(className: 'Messages'); + generateJava(javaOptions, root, sink); + final String code = sink.toString(); + expect(code, contains('doit(List arg')); + }); + + test('host generics return', () { + final Root root = Root( + apis: [ + Api(name: 'Api', location: ApiLocation.host, methods: [ + Method( + name: 'doit', + returnType: Field( + name: 'arg', + dataType: 'List', + isNullable: false, + typeArguments: [ + TypeArgument(dataType: '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('List doit(')); + expect(code, contains('List output =')); + }); + + test('flutter generics return', () { + final Root root = Root( + apis: [ + Api(name: 'Api', location: ApiLocation.flutter, methods: [ + Method( + name: 'doit', + returnType: Field( + name: 'arg', + dataType: 'List', + isNullable: false, + typeArguments: [ + TypeArgument(dataType: '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('doit(Reply> callback)')); + expect(code, contains('List output =')); + }); } diff --git a/packages/pigeon/test/objc_generator_test.dart b/packages/pigeon/test/objc_generator_test.dart index 8acf43920cc9..9bca1ab9f988 100644 --- a/packages/pigeon/test/objc_generator_test.dart +++ b/packages/pigeon/test/objc_generator_test.dart @@ -115,14 +115,51 @@ void main() { expect(code, contains('result.enum1 = [dict[@"enum1"] integerValue];')); }); + test('gen one class header with enum', () { + final Root root = Root( + apis: [], + classes: [ + Class( + name: 'Foobar', + fields: [ + Field( + name: 'field1', + dataType: 'String', + isNullable: true, + ), + Field( + name: 'enum1', + dataType: 'Enum1', + isNullable: true, + ), + ], + ), + ], + enums: [ + Enum( + name: 'Enum1', + members: [ + 'one', + 'two', + ], + ) + ], + ); + final StringBuffer sink = StringBuffer(); + generateObjcHeader(const ObjcOptions(header: 'foo.h'), root, sink); + final String code = sink.toString(); + expect(code, contains('@property(nonatomic, assign) Enum1 enum1')); + }); + test('gen one api header', () { final Root root = Root(apis: [ Api(name: 'Api', location: ApiLocation.host, methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: false, - returnType: 'Output') + arguments: [ + Field(name: '', dataType: 'Input', isNullable: false) + ], + returnType: Field(name: '', dataType: 'Output', isNullable: false)) ]) ], classes: [ Class(name: 'Input', fields: [ @@ -155,9 +192,10 @@ void main() { Api(name: 'Api', location: ApiLocation.host, methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: false, - returnType: 'Output') + arguments: [ + Field(name: '', dataType: 'Input', isNullable: false) + ], + returnType: Field(name: '', dataType: 'Output', isNullable: false)) ]) ], classes: [ Class(name: 'Input', fields: [ @@ -352,9 +390,10 @@ void main() { Api(name: 'Api', location: ApiLocation.host, methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: false, - returnType: 'Nested') + arguments: [ + Field(name: '', dataType: 'Input', isNullable: false) + ], + returnType: Field(name: '', dataType: 'Nested', isNullable: false)) ]) ], classes: [ Class(name: 'Input', fields: [ @@ -385,9 +424,10 @@ void main() { Api(name: 'Api', location: ApiLocation.host, methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: false, - returnType: 'Nested') + arguments: [ + Field(name: '', dataType: 'Input', isNullable: false) + ], + returnType: Field(name: '', dataType: 'Nested', isNullable: false)) ]) ], classes: [ Class(name: 'Input', fields: [ @@ -418,9 +458,10 @@ void main() { Api(name: 'Api', location: ApiLocation.flutter, methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: false, - returnType: 'Output') + arguments: [ + Field(name: '', dataType: 'Input', isNullable: false) + ], + returnType: Field(name: '', dataType: 'Output', isNullable: false)) ]) ], classes: [ Class(name: 'Input', fields: [ @@ -454,9 +495,10 @@ void main() { Api(name: 'Api', location: ApiLocation.flutter, methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: false, - returnType: 'Output') + arguments: [ + Field(name: '', dataType: 'Input', isNullable: false) + ], + returnType: Field(name: '', dataType: 'Output', isNullable: false)) ]) ], classes: [ Class(name: 'Input', fields: [ @@ -486,9 +528,10 @@ void main() { Api(name: 'Api', location: ApiLocation.host, methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: false, - returnType: 'void') + arguments: [ + Field(name: '', dataType: 'Input', isNullable: false) + ], + returnType: Field(name: '', dataType: 'void', isNullable: false)) ]) ], classes: [ Class(name: 'Input', fields: [ @@ -511,9 +554,10 @@ void main() { Api(name: 'Api', location: ApiLocation.host, methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: false, - returnType: 'void') + arguments: [ + Field(name: '', dataType: 'Input', isNullable: false) + ], + returnType: Field(name: '', dataType: 'void', isNullable: false)) ]) ], classes: [ Class(name: 'Input', fields: [ @@ -538,9 +582,10 @@ void main() { Api(name: 'Api', location: ApiLocation.flutter, methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: false, - returnType: 'void') + arguments: [ + Field(name: '', dataType: 'Input', isNullable: false) + ], + returnType: Field(name: '', dataType: 'void', isNullable: false)) ]) ], classes: [ Class(name: 'Input', fields: [ @@ -563,9 +608,10 @@ void main() { Api(name: 'Api', location: ApiLocation.flutter, methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: false, - returnType: 'void') + arguments: [ + Field(name: '', dataType: 'Input', isNullable: false) + ], + returnType: Field(name: '', dataType: 'void', isNullable: false)) ]) ], classes: [ Class(name: 'Input', fields: [ @@ -589,9 +635,8 @@ void main() { Api(name: 'Api', location: ApiLocation.host, methods: [ Method( name: 'doSomething', - argType: 'void', - isArgNullable: false, - returnType: 'Output') + arguments: [], + returnType: Field(name: '', dataType: 'Output', isNullable: false)) ]) ], classes: [ Class(name: 'Output', fields: [ @@ -614,9 +659,8 @@ void main() { Api(name: 'Api', location: ApiLocation.host, methods: [ Method( name: 'doSomething', - argType: 'void', - isArgNullable: false, - returnType: 'Output') + arguments: [], + returnType: Field(name: '', dataType: 'Output', isNullable: false)) ]) ], classes: [ Class(name: 'Output', fields: [ @@ -639,9 +683,8 @@ void main() { Api(name: 'Api', location: ApiLocation.flutter, methods: [ Method( name: 'doSomething', - argType: 'void', - isArgNullable: false, - returnType: 'Output') + arguments: [], + returnType: Field(name: '', dataType: 'Output', isNullable: false)) ]) ], classes: [ Class(name: 'Output', fields: [ @@ -667,9 +710,8 @@ void main() { Api(name: 'Api', location: ApiLocation.flutter, methods: [ Method( name: 'doSomething', - argType: 'void', - isArgNullable: false, - returnType: 'Output') + arguments: [], + returnType: Field(name: '', dataType: 'Output', isNullable: false)) ]) ], classes: [ Class(name: 'Output', fields: [ @@ -730,9 +772,10 @@ void main() { Api(name: 'Api', location: ApiLocation.host, methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: false, - returnType: 'void', + arguments: [ + Field(name: '', dataType: 'Input', isNullable: false) + ], + returnType: Field(name: '', dataType: 'void', isNullable: false), isAsynchronous: true) ]) ], classes: [ @@ -766,9 +809,10 @@ void main() { Api(name: 'Api', location: ApiLocation.host, methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: false, - returnType: 'Output', + arguments: [ + Field(name: '', dataType: 'Input', isNullable: false) + ], + returnType: Field(name: '', dataType: 'Output', isNullable: false), isAsynchronous: true) ]) ], classes: [ @@ -802,9 +846,8 @@ void main() { Api(name: 'Api', location: ApiLocation.host, methods: [ Method( name: 'doSomething', - argType: 'void', - isArgNullable: false, - returnType: 'Output', + arguments: [], + returnType: Field(name: '', dataType: 'Output', isNullable: false), isAsynchronous: true) ]) ], classes: [ @@ -831,9 +874,8 @@ void main() { Api(name: 'Api', location: ApiLocation.host, methods: [ Method( name: 'doSomething', - argType: 'void', - isArgNullable: false, - returnType: 'void', + arguments: [], + returnType: Field(name: '', dataType: 'void', isNullable: false), isAsynchronous: true) ]) ], classes: [], enums: []); @@ -852,9 +894,10 @@ void main() { Api(name: 'Api', location: ApiLocation.host, methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: false, - returnType: 'Output', + arguments: [ + Field(name: '', dataType: 'Input', isNullable: false) + ], + returnType: Field(name: '', dataType: 'Output', isNullable: false), isAsynchronous: true) ]) ], classes: [ @@ -888,9 +931,10 @@ void main() { Api(name: 'Api', location: ApiLocation.host, methods: [ Method( name: 'doSomething', - argType: 'Input', - isArgNullable: false, - returnType: 'void', + arguments: [ + Field(name: '', dataType: 'Input', isNullable: false) + ], + returnType: Field(name: '', dataType: 'void', isNullable: false), isAsynchronous: true) ]) ], classes: [ @@ -924,9 +968,8 @@ void main() { Api(name: 'Api', location: ApiLocation.host, methods: [ Method( name: 'doSomething', - argType: 'void', - isArgNullable: false, - returnType: 'void', + arguments: [], + returnType: Field(name: '', dataType: 'void', isNullable: false), isAsynchronous: true) ]) ], classes: [], enums: []); @@ -943,9 +986,8 @@ void main() { Api(name: 'Api', location: ApiLocation.host, methods: [ Method( name: 'doSomething', - argType: 'void', - isArgNullable: false, - returnType: 'Output', + arguments: [], + returnType: Field(name: '', dataType: 'Output', isNullable: false), isAsynchronous: true) ]) ], classes: [ @@ -1000,4 +1042,209 @@ void main() { final String code = sink.toString(); expect(code, startsWith('// hello world')); }); + + test('field generics', () { + final Class klass = Class( + name: 'Foobar', + fields: [ + Field( + name: 'field1', + dataType: 'List', + isNullable: true, + typeArguments: [ + TypeArgument(dataType: 'int', isNullable: true) + ], + ), + ], + ); + final Root root = Root( + apis: [], + classes: [klass], + enums: [], + ); + final StringBuffer sink = StringBuffer(); + generateObjcHeader( + const ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink); + final String code = sink.toString(); + expect(code, contains('NSArray * field1')); + }); + + test('host generics argument', () { + final Root root = Root( + apis: [ + Api(name: 'Api', location: ApiLocation.host, methods: [ + Method( + name: 'doit', + returnType: Field(dataType: 'void', isNullable: false, name: ''), + arguments: [ + Field( + name: 'arg', + dataType: 'List', + isNullable: false, + typeArguments: [ + TypeArgument(dataType: 'int', isNullable: true) + ]) + ]) + ]) + ], + classes: [], + enums: [], + ); + { + final StringBuffer sink = StringBuffer(); + generateObjcHeader( + const ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink); + final String code = sink.toString(); + expect(code, contains('doit:(NSArray*)input')); + } + { + final StringBuffer sink = StringBuffer(); + generateObjcSource( + const ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink); + final String code = sink.toString(); + expect(code, contains('NSArray *input = message')); + } + }); + + test('flutter generics argument', () { + final Root root = Root( + apis: [ + Api(name: 'Api', location: ApiLocation.flutter, methods: [ + Method( + name: 'doit', + returnType: Field(dataType: 'void', isNullable: false, name: ''), + arguments: [ + Field( + name: 'arg', + dataType: 'List', + isNullable: false, + typeArguments: [ + TypeArgument(dataType: 'int', isNullable: true) + ]) + ]) + ]) + ], + classes: [], + enums: [], + ); + { + final StringBuffer sink = StringBuffer(); + generateObjcHeader( + const ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink); + final String code = sink.toString(); + expect(code, contains('doit:(NSArray*)input')); + } + { + final StringBuffer sink = StringBuffer(); + generateObjcSource( + const ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink); + final String code = sink.toString(); + expect(code, contains('doit:(NSArray*)input')); + } + }); + + test('host nested generic argument', () { + final Root root = Root( + apis: [ + Api(name: 'Api', location: ApiLocation.host, methods: [ + Method( + name: 'doit', + returnType: Field(dataType: 'void', isNullable: false, name: ''), + arguments: [ + Field( + name: 'arg', + dataType: 'List', + isNullable: false, + typeArguments: [ + TypeArgument( + dataType: 'List', + isNullable: true, + typeArguments: [ + TypeArgument(dataType: 'bool', isNullable: true) + ]), + ]) + ]) + ]) + ], + classes: [], + enums: [], + ); + { + final StringBuffer sink = StringBuffer(); + generateObjcHeader( + const ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink); + final String code = sink.toString(); + expect(code, contains('doit:(NSArray *>*)input')); + } + }); + + test('host generics return', () { + final Root root = Root( + apis: [ + Api(name: 'Api', location: ApiLocation.host, methods: [ + Method( + name: 'doit', + returnType: Field( + name: 'arg', + dataType: 'List', + isNullable: false, + typeArguments: [ + TypeArgument(dataType: 'int', isNullable: true) + ]), + arguments: []) + ]) + ], + classes: [], + enums: [], + ); + { + final StringBuffer sink = StringBuffer(); + generateObjcHeader( + const ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink); + final String code = sink.toString(); + expect(code, contains('-(nullable NSArray *)doit:')); + } + { + final StringBuffer sink = StringBuffer(); + generateObjcSource( + const ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink); + final String code = sink.toString(); + expect(code, contains('NSArray *output =')); + } + }); + + test('host generics return', () { + final Root root = Root( + apis: [ + Api(name: 'Api', location: ApiLocation.flutter, methods: [ + Method( + name: 'doit', + returnType: Field( + name: 'arg', + dataType: 'List', + isNullable: false, + typeArguments: [ + TypeArgument(dataType: 'int', isNullable: true) + ]), + arguments: []) + ]) + ], + classes: [], + enums: [], + ); + { + final StringBuffer sink = StringBuffer(); + generateObjcHeader( + const ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink); + final String code = sink.toString(); + expect(code, contains('doit:(void(^)(NSArray*')); + } + { + final StringBuffer sink = StringBuffer(); + generateObjcSource( + const ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink); + final String code = sink.toString(); + expect(code, contains('doit:(void(^)(NSArray*')); + } + }); } diff --git a/packages/pigeon/test/pigeon_lib_test.dart b/packages/pigeon/test/pigeon_lib_test.dart index 6aba0a002f30..780c2c97acab 100644 --- a/packages/pigeon/test/pigeon_lib_test.dart +++ b/packages/pigeon/test/pigeon_lib_test.dart @@ -93,8 +93,8 @@ abstract class Api1 { expect(root.apis[0].name, equals('Api1')); expect(root.apis[0].methods.length, equals(1)); expect(root.apis[0].methods[0].name, equals('doit')); - expect(root.apis[0].methods[0].argType, equals('Input1')); - expect(root.apis[0].methods[0].returnType, equals('Output1')); + expect(root.apis[0].methods[0].arguments[0].dataType, equals('Input1')); + expect(root.apis[0].methods[0].returnType.dataType, equals('Output1')); Class? input; Class? output; @@ -241,7 +241,7 @@ abstract class VoidApi { expect(results.root.apis.length, equals(1)); expect(results.root.apis[0].methods.length, equals(1)); expect(results.root.apis[0].name, equals('VoidApi')); - expect(results.root.apis[0].methods[0].returnType, equals('void')); + expect(results.root.apis[0].methods[0].returnType.dataType, equals('void')); }); test('void arg host api', () { @@ -260,8 +260,9 @@ abstract class VoidArgApi { expect(results.root.apis.length, equals(1)); expect(results.root.apis[0].methods.length, equals(1)); expect(results.root.apis[0].name, equals('VoidArgApi')); - expect(results.root.apis[0].methods[0].returnType, equals('Output1')); - expect(results.root.apis[0].methods[0].argType, equals('void')); + expect( + results.root.apis[0].methods[0].returnType.dataType, equals('Output1')); + expect(results.root.apis[0].methods[0].arguments.isEmpty, isTrue); }); test('mockDartClass', () { @@ -537,38 +538,155 @@ abstract class Api { expect(parseResults.errors.length, 0); }); - test('error with generics', () { + test('error with static field', () { const String code = ''' -class WithTemplate { - List? list; +class WithStaticField { + static int? x; + int? y; } @HostApi() -abstract class WithTemplateApi { - void doit(WithTemplate withTemplate); +abstract class WithStaticFieldApi { + void doit(WithStaticField withTemplate); } '''; final ParseResults parseResult = _parseSource(code); expect(parseResult.errors.length, equals(1)); - expect(parseResult.errors[0].message, contains('Generic fields')); + expect(parseResult.errors[0].message, contains('static field')); expect(parseResult.errors[0].lineNumber, isNotNull); }); - test('error with static field', () { + test('parse generics', () { const String code = ''' -class WithStaticField { - static int? x; - int? y; +class Foo { + List? list; } @HostApi() -abstract class WithStaticFieldApi { - void doit(WithStaticField withTemplate); +abstract class Api { + void doit(Foo foo); +} +'''; + final ParseResults parseResult = _parseSource(code); + expect(parseResult.errors.length, equals(0)); + final Field field = parseResult.root.classes[0].fields[0]; + expect(field.typeArguments!.length, 1); + expect(field.typeArguments![0].dataType, 'int'); + }); + + test('parse recursive generics', () { + const String code = ''' +class Foo { + List?>? list; +} + +@HostApi() +abstract class Api { + void doit(Foo foo); +} +'''; + final ParseResults parseResult = _parseSource(code); + expect(parseResult.errors.length, equals(0)); + final Field field = parseResult.root.classes[0].fields[0]; + expect(field.typeArguments!.length, 1); + expect(field.typeArguments![0].dataType, 'List'); + expect(field.typeArguments![0].typeArguments![0].dataType, 'int'); + }); + + test('error nonnull type argument', () { + const String code = ''' +class Foo { + List list; +} + +@HostApi() +abstract class Api { + void doit(Foo foo); } '''; final ParseResults parseResult = _parseSource(code); expect(parseResult.errors.length, equals(1)); - expect(parseResult.errors[0].message, contains('static field')); - expect(parseResult.errors[0].lineNumber, isNotNull); + expect(parseResult.errors[0].message, + contains('Generic type arguments must be nullable')); + expect(parseResult.errors[0].message, contains('"list"')); + expect(parseResult.errors[0].lineNumber, 2); + }); + + test('return type generics', () { + const String code = ''' +@HostApi() +abstract class Api { + List doit(); +} +'''; + final ParseResults parseResult = _parseSource(code); + expect(parseResult.root.apis[0].methods[0].returnType.dataType, 'List'); + expect( + parseResult + .root.apis[0].methods[0].returnType.typeArguments![0].dataType, + 'double'); + expect( + parseResult + .root.apis[0].methods[0].returnType.typeArguments![0].isNullable, + isTrue); + }); + + test('argument generics', () { + const String code = ''' +@HostApi() +abstract class Api { + void doit(List value); +} +'''; + final ParseResults parseResult = _parseSource(code); + expect(parseResult.root.apis[0].methods[0].arguments[0].dataType, 'List'); + expect( + parseResult + .root.apis[0].methods[0].arguments[0].typeArguments![0].dataType, + 'double'); + expect( + parseResult + .root.apis[0].methods[0].arguments[0].typeArguments![0].isNullable, + isTrue); + }); + + test('map generics', () { + const String code = ''' +class Foo { + Map map; +} + +@HostApi() +abstract class Api { + void doit(Foo foo); +} +'''; + final ParseResults parseResult = _parseSource(code); + final Field field = parseResult.root.classes[0].fields[0]; + expect(field.typeArguments!.length, 2); + expect(field.typeArguments![0].dataType, 'String'); + expect(field.typeArguments![1].dataType, 'int'); + }); + + test('two arguments', () { + const String code = ''' +class Input { + String? input; +} + +@HostApi() +abstract class Api { + void method(Input input1, Input input2); +} +'''; + final ParseResults results = _parseSource(code); + expect(results.errors.length, 1); + expect(results.errors[0].lineNumber, 7); + expect(results.errors[0].message, contains('Multiple arguments')); + // TODO(gaaclarke): Make this not an error, https://github.com/flutter/flutter/issues/86971. + // expect(results.root.apis.length, 1); + // expect(results.root.apis[0].methods.length, equals(1)); + // expect(results.root.apis[0].methods[0].name, equals('method')); + // expect(results.root.apis[0].methods[0].arguments.length, 2); }); }