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..dd74c00c456d 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 '(Api name:$name returnType:$returnType argType:$arguments isAsynchronous:$isAsynchronous)'; } } @@ -82,6 +74,30 @@ class Api extends Node { } } +/// A parameter to a generic entity. For example, "String" to "List". +class TypeArgument { + /// Constructor for [TypeArgument]. + TypeArgument({ + required this.dataType, + required this.isNullable, + this.typeArguments, + }); + + /// A string representation of the base datatype. + final String dataType; + + /// The type arguments to this [TypeArgument]. + final List? typeArguments; + + /// True if the type is nullable. + final bool isNullable; + + @override + String toString() { + return '(TypeArgument dataType:$dataType isNullable:$isNullable typeArguments:$typeArguments)'; + } +} + /// Represents a field on a [Class]. class Field extends Node { /// Parametric constructor for [Field]. @@ -106,11 +122,11 @@ class Field extends Node { bool isNullable; /// Type parameters used for generics. - List? typeArguments; + 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..f20f7c6b59f5 100644 --- a/packages/pigeon/lib/dart_generator.dart +++ b/packages/pigeon/lib/dart_generator.dart @@ -121,12 +121,12 @@ final BinaryMessenger$nullTag _binaryMessenger; } String argSignature = ''; String sendArgument = 'null'; - if (func.argType != 'void') { - argSignature = '${func.argType} arg'; + if (func.arguments.isNotEmpty) { + argSignature = '${func.arguments[0].dataType} arg'; sendArgument = 'arg'; } indent.write( - 'Future<${func.returnType}> ${func.name}($argSignature) async ', + 'Future<${func.returnType.dataType}> ${func.name}($argSignature) async ', ); indent.scoped('{', '}', () { final String channelName = makeChannelName(api, func); @@ -137,9 +137,9 @@ final BinaryMessenger$nullTag _binaryMessenger; '\'$channelName\', codec, binaryMessenger: _binaryMessenger);', ); }); - final String returnStatement = func.returnType == 'void' + final String returnStatement = func.returnType.dataType == 'void' ? '// noop' - : 'return (replyMap[\'${Keys.result}\'] as ${func.returnType}$nullTag)$unwrapOperator;'; + : 'return (replyMap[\'${Keys.result}\'] as ${func.returnType.dataType}$nullTag)$unwrapOperator;'; indent.format(''' final Map$nullTag replyMap =\n\t\tawait channel.send($sendArgument) as Map$nullTag; if (replyMap == null) { @@ -181,10 +181,11 @@ 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 returnType = isAsync + ? 'Future<${func.returnType.dataType}>' + : func.returnType.dataType; final String argSignature = - func.argType == 'void' ? '' : '${func.argType} arg'; + func.arguments.isEmpty ? '' : '${func.arguments[0].dataType} arg'; indent.writeln('$returnType ${func.name}($argSignature);'); } indent.write('static void setup(${api.name}$nullTag api) '); @@ -215,19 +216,19 @@ void _writeFlutterApi( 'channel.$messageHandlerSetter((Object$nullTag message) async ', ); indent.scoped('{', '});', () { - final String argType = func.argType; - final String returnType = func.returnType; + final String returnType = func.returnType.dataType; 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 = func.arguments[0].dataType; indent.writeln( 'assert(message != null, \'Argument for $channelName was null. Expected $argType.\');', ); @@ -263,14 +264,30 @@ 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(Field field, String nullTag) { + switch (field.dataType) { case 'List': - return 'List$nullTag'; + return (field.typeArguments == null) + ? 'List$nullTag' + : 'List<${_flattenTypeArguments(field.typeArguments!, nullTag)}>$nullTag'; case 'Map': - return 'Map$nullTag'; + return (field.typeArguments == null) + ? 'Map$nullTag' + : 'Map<${_flattenTypeArguments(field.typeArguments!, nullTag)}>$nullTag'; default: - return '$dataType$nullTag'; + return '${field.dataType}$nullTag'; } } @@ -315,7 +332,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 = _addGenericTypes(field, nullTag); indent.writeln('$datatype ${field.name};'); } if (klass.fields.isNotEmpty) { @@ -365,9 +382,13 @@ 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.dataType == 'Map' && field.typeArguments != null) { + indent.add( + '(pigeonMap[\'${field.name}\'] as Map$nullTag)$nullTag.cast<${_flattenTypeArguments(field.typeArguments!, nullTag)}>()', + ); } else { indent.add( - 'pigeonMap[\'${field.name}\'] as ${_addGenericTypes(field.dataType, nullTag)}', + 'pigeonMap[\'${field.name}\'] as ${_addGenericTypes(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..3f9ff0eb3896 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 @@ -137,17 +124,19 @@ 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); + : _javaTypeForDartTypePassthrough(method.returnType.dataType); final List argSignature = []; - if (method.argType != 'void') { + if (method.arguments.isNotEmpty) { + final String argType = + _javaTypeForDartTypePassthrough(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' + : method.returnType.dataType; argSignature.add('Result<$returnType> result'); } indent.writeln('$returnType ${method.name}(${argSignature.join(', ')});'); @@ -180,15 +169,15 @@ 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); + _javaTypeForDartTypePassthrough(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 = _javaTypeForDartTypePassthrough( + method.arguments[0].dataType); indent.writeln('@SuppressWarnings("ConstantConditions")'); indent.writeln('$argType input = ($argType)message;'); indent.write('if (input == null) '); @@ -200,7 +189,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 +201,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 +254,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); + : _javaTypeForDartTypePassthrough(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 = + _javaTypeForDartTypePassthrough(func.arguments[0].dataType); indent.write( 'public void ${func.name}($argType argInput, Reply<$returnType> callback) '); sendArgument = 'argInput'; @@ -288,7 +278,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 +303,39 @@ 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) => e.typeArguments == null + ? _javaTypeForDartTypePassthrough(e.dataType) + : '${_javaTypeForDartTypePassthrough(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( diff --git a/packages/pigeon/lib/objc_generator.dart b/packages/pigeon/lib/objc_generator.dart index 77612a49854f..87e0994e3b66 100644 --- a/packages/pigeon/lib/objc_generator.dart +++ b/packages/pigeon/lib/objc_generator.dart @@ -92,9 +92,21 @@ const Map _propertyTypeForDartTypeMap = { 'Map': 'strong', }; -String? _objcTypePtrForPrimitiveDartType(String type) { - return _objcTypeForDartTypeMap.containsKey(type) - ? '${_objcTypeForDartTypeMap[type]} *' +/// Converts a [List] of [TypeArgument]s to a comma separated [String] to be +/// used in objc code. +String _flattenTypeArguments(String? classPrefix, List args) { + return args + .map((TypeArgument e) => e.typeArguments == null + ? '${_objcTypeForDartType(classPrefix, e.dataType)} *' + : '${_objcTypeForDartType(classPrefix, e.dataType)}<${_flattenTypeArguments(classPrefix, e.typeArguments!)}> *') + .reduce((String value, String element) => '$value, $element'); +} + +String? _objcTypePtrForPrimitiveDartType(String? classPrefix, Field field) { + return _objcTypeForDartTypeMap.containsKey(field.dataType) + ? field.typeArguments == null + ? '${_objcTypeForDartTypeMap[field.dataType]} *' + : '${_objcTypeForDartTypeMap[field.dataType]}<${_flattenTypeArguments(classPrefix, field.typeArguments!)}> *' : null; } @@ -121,8 +133,8 @@ 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)} *'); @@ -228,38 +240,39 @@ void _writeHostApiDeclaration(Indent indent, Api api, ObjcOptions options) { indent.writeln('@protocol $apiName'); for (final Method func in api.methods) { final String returnTypeName = - _objcTypeForDartType(options.prefix, func.returnType); + _objcTypeForDartType(options.prefix, func.returnType.dataType); 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].dataType); 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].dataType); 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].dataType); indent.writeln( '-($returnType)${func.name}:($argType*)input error:(FlutterError *_Nullable *_Nonnull)error;'); } @@ -279,12 +292,14 @@ void _writeFlutterApiDeclaration(Indent indent, Api api, ObjcOptions options) { '- (instancetype)initWithBinaryMessenger:(id)binaryMessenger;'); 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') { + _objcTypeForDartType(options.prefix, func.returnType.dataType); + 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].dataType); indent.writeln( '- (void)${func.name}:($argType*)input completion:($callbackType)completion;'); } @@ -401,20 +416,20 @@ void _writeHostApiSource(Indent indent, ObjcOptions options, Api api) { '[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) '); indent.scoped('{', '}];', () { final String returnType = - _objcTypeForDartType(options.prefix, func.returnType); + _objcTypeForDartType(options.prefix, func.returnType.dataType); String syncCall; - if (func.argType == 'void') { + if (func.arguments.isEmpty) { syncCall = '[api ${func.name}:&error]'; } else { - final String argType = - _objcTypeForDartType(options.prefix, func.argType); + final String argType = _objcTypeForDartType( + options.prefix, func.arguments[0].dataType); 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 +444,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 +460,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 { @@ -486,15 +501,17 @@ void _writeFlutterApiSource(Indent indent, ObjcOptions options, Api api) { indent.addln(''); for (final Method func in api.methods) { final String returnType = - _objcTypeForDartType(options.prefix, func.returnType); - final String callbackType = _callbackForType(func.returnType, returnType); + _objcTypeForDartType(options.prefix, func.returnType.dataType); + 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].dataType); indent.write( '- (void)${func.name}:($argType*)input completion:($callbackType)completion '); sendArgument = 'input'; @@ -512,7 +529,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..25413423a983 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,39 @@ 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.arguments[0].dataType}" in API: "${api.name}" method: "${method.name}"', + lineNumber: _calculateLineNumberNullable(source, method.offset), + )); + } + if (method.returnType.typeArguments != null) { + result.add(Error( + message: + 'Generic type arguments for primitive return values aren\'t yet supported: "${method.returnType.dataType}" in API: "${api.name}" method: "${method.name} (https://github.com/flutter/flutter/issues/86963)"', + lineNumber: _calculateLineNumberNullable(source, method.offset), + )); + } + if (method.arguments.length > 1) { result.add(Error( message: - 'Nullable return types 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.isArgNullable) { + if (method.arguments.isNotEmpty && method.arguments[0].isNullable) { result.add(Error( message: - 'Nullable argument types aren\'t supported for Pigeon methods: "${method.argType}" in API: "${api.name}" method: "${method.name}"', + '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), + )); + } + if (method.arguments.isNotEmpty && + method.arguments[0].typeArguments != null) { + result.add(Error( + message: + 'Generic type arguments for primitive arguments aren\'t yet supported: "${method.arguments[0].dataType}" in API: "${api.name}" method: "${method.name} (https://github.com/flutter/flutter/issues/86963)"', lineNumber: _calculateLineNumberNullable(source, method.offset), )); } @@ -462,8 +490,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 +643,38 @@ 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, + ); + } + @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: node.returnType.toString(), + 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 +698,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 +738,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 +806,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/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..dd60777ef72f 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,11 @@ 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?>? + ..mapWithAnnotations = + (pigeonMap['mapWithAnnotations'] as Map?) + ?.cast(); } } 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/test/dart_generator_test.dart b/packages/pigeon/test/dart_generator_test.dart index 04b3488b303d..805d6c719e16 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,57 @@ 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;')); + }); } diff --git a/packages/pigeon/test/java_generator_test.dart b/packages/pigeon/test/java_generator_test.dart index 4ac1ab63071b..9b9803285636 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,31 @@ 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;')); + }); } diff --git a/packages/pigeon/test/objc_generator_test.dart b/packages/pigeon/test/objc_generator_test.dart index 8acf43920cc9..9ef5f9e9b4e5 100644 --- a/packages/pigeon/test/objc_generator_test.dart +++ b/packages/pigeon/test/objc_generator_test.dart @@ -120,9 +120,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: [ @@ -155,9 +156,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 +354,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 +388,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 +422,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 +459,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 +492,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 +518,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 +546,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 +572,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 +599,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 +623,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 +647,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 +674,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 +736,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 +773,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 +810,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 +838,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 +858,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 +895,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 +932,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 +950,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 +1006,30 @@ 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(); + generateObjcHeader( + const ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink); + final String code = sink.toString(); + expect(code, contains('NSArray * field1')); + }); } diff --git a/packages/pigeon/test/pigeon_lib_test.dart b/packages/pigeon/test/pigeon_lib_test.dart index 6aba0a002f30..f783f79fd692 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,156 @@ 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.errors.length, equals(1)); + // TODO(gaaclarke): Make this not an error, https://github.com/flutter/flutter/issues/86963. + // 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.errors.length, equals(1)); + // TODO(gaaclarke): Make this not an error, https://github.com/flutter/flutter/issues/86963. + // expect( + // parseResult.root.apis[0].methods[0].argType.typeArguments![0].dataType, + // 'double'); + // expect( + // parseResult + // .root.apis[0].methods[0].argType.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); }); }