diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md index f09c06d1c1..83c597e278 100644 --- a/packages/pigeon/CHANGELOG.md +++ b/packages/pigeon/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.1.11 + +* Added flag to generate null safety annotated Dart code `--dart-null-safety`. +* Made it so Dart API setup methods can take null. + ## 0.1.10+1 * Updated the examples page. diff --git a/packages/pigeon/lib/dart_generator.dart b/packages/pigeon/lib/dart_generator.dart index e7ce4efcde..121056803f 100644 --- a/packages/pigeon/lib/dart_generator.dart +++ b/packages/pigeon/lib/dart_generator.dart @@ -5,8 +5,15 @@ import 'ast.dart'; import 'generator_tools.dart'; -void _writeHostApi(Indent indent, Api api) { +/// Options that control how Dart code will be generated. +class DartOptions { + /// Determines if the generated code has null safety annotations (Dart >2.10 required). + bool isNullSafe = false; +} + +void _writeHostApi(DartOptions opt, Indent indent, Api api) { assert(api.location == ApiLocation.host); + final String nullTag = opt.isNullSafe ? '?' : ''; indent.write('class ${api.name} '); indent.scoped('{', '}', () { for (Method func in api.methods) { @@ -38,7 +45,7 @@ void _writeHostApi(Indent indent, Api api) { ? '// noop' : 'return ${func.returnType}._fromMap(replyMap[\'${Keys.result}\']);'; indent.format( - '''final Map replyMap = await channel.send($sendArgument); + '''final Map$nullTag replyMap = await channel.send($sendArgument); if (replyMap == null) { \tthrow PlatformException( \t\tcode: 'channel-error', @@ -60,9 +67,10 @@ if (replyMap == null) { indent.writeln(''); } -void _writeFlutterApi(Indent indent, Api api, +void _writeFlutterApi(DartOptions opt, Indent indent, Api api, {String Function(Method) channelNameFunc, bool isMockHandler = false}) { assert(api.location == ApiLocation.flutter); + final String nullTag = opt.isNullSafe ? '?' : ''; indent.write('abstract class ${api.name} '); indent.scoped('{', '}', () { for (Method func in api.methods) { @@ -73,7 +81,7 @@ void _writeFlutterApi(Indent indent, Api api, func.argType == 'void' ? '' : '${func.argType} arg'; indent.writeln('$returnType ${func.name}($argSignature);'); } - indent.write('static void setup(${api.name} api) '); + indent.write('static void setup(${api.name}$nullTag api) '); indent.scoped('{', '}', () { for (Method func in api.methods) { indent.write(''); @@ -90,39 +98,45 @@ void _writeFlutterApi(Indent indent, Api api, indent.dec(); final String messageHandlerSetter = isMockHandler ? 'setMockMessageHandler' : 'setMessageHandler'; - indent - .write('channel.$messageHandlerSetter((dynamic message) async '); - indent.scoped('{', '});', () { - final String argType = func.argType; - final String returnType = func.returnType; - final bool isAsync = func.isAsynchronous; - String call; - if (argType == 'void') { - call = 'api.${func.name}()'; - } else { - indent.writeln( - 'final Map mapMessage = message as Map;'); - indent.writeln( - 'final $argType input = $argType._fromMap(mapMessage);'); - call = 'api.${func.name}(input)'; - } - if (returnType == 'void') { - indent.writeln('$call;'); - if (isMockHandler) { - indent.writeln('return {};'); - } - } else { - if (isAsync) { - indent.writeln('final $returnType output = await $call;'); + indent.write('if (api == null) '); + indent.scoped('{', '} else {', () { + indent.writeln('channel.$messageHandlerSetter(null);'); + }); + indent.scoped('', '}', () { + indent.write( + 'channel.$messageHandlerSetter((dynamic message) async '); + indent.scoped('{', '});', () { + final String argType = func.argType; + final String returnType = func.returnType; + final bool isAsync = func.isAsynchronous; + String call; + if (argType == 'void') { + call = 'api.${func.name}()'; } else { - indent.writeln('final $returnType output = $call;'); + indent.writeln( + 'final Map mapMessage = message as Map;'); + indent.writeln( + 'final $argType input = $argType._fromMap(mapMessage);'); + call = 'api.${func.name}(input)'; } - const String returnExpresion = 'output._toMap()'; - final String returnStatement = isMockHandler - ? 'return {\'${Keys.result}\': $returnExpresion};' - : 'return $returnExpresion;'; - indent.writeln(returnStatement); - } + if (returnType == 'void') { + indent.writeln('$call;'); + if (isMockHandler) { + indent.writeln('return {};'); + } + } else { + if (isAsync) { + indent.writeln('final $returnType output = await $call;'); + } else { + indent.writeln('final $returnType output = $call;'); + } + const String returnExpresion = 'output._toMap()'; + final String returnStatement = isMockHandler + ? 'return {\'${Keys.result}\': $returnExpresion};' + : 'return $returnExpresion;'; + indent.writeln(returnStatement); + } + }); }); }); } @@ -133,7 +147,7 @@ void _writeFlutterApi(Indent indent, Api api, /// Generates Dart source code for the given AST represented by [root], /// outputting the code to [sink]. -void generateDart(Root root, StringSink sink) { +void generateDart(DartOptions opt, Root root, StringSink sink) { final List customClassNames = root.classes.map((Class x) => x.name).toList(); final Indent indent = Indent(sink); @@ -141,18 +155,21 @@ void generateDart(Root root, StringSink sink) { indent.writeln('// $seeAlsoWarning'); indent.writeln( '// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import'); - indent.writeln('// @dart = 2.8'); + indent.writeln('// @dart = ${opt.isNullSafe ? '2.10' : '2.8'}'); indent.writeln('import \'dart:async\';'); indent.writeln('import \'package:flutter/services.dart\';'); indent.writeln( 'import \'dart:typed_data\' show Uint8List, Int32List, Int64List, Float64List;'); indent.writeln(''); + final String nullBang = opt.isNullSafe ? '!' : ''; for (Class klass in root.classes) { sink.write('class ${klass.name} '); indent.scoped('{', '}', () { for (Field field in klass.fields) { - indent.writeln('${field.dataType} ${field.name};'); + final String datatype = + opt.isNullSafe ? '${field.dataType}?' : field.dataType; + indent.writeln('$datatype ${field.name};'); } indent.writeln('// ignore: unused_element'); indent.write('Map _toMap() '); @@ -163,7 +180,7 @@ void generateDart(Root root, StringSink sink) { indent.write('pigeonMap[\'${field.name}\'] = '); if (customClassNames.contains(field.dataType)) { indent.addln( - '${field.name} == null ? null : ${field.name}._toMap();'); + '${field.name} == null ? null : ${field.name}$nullBang._toMap();'); } else { indent.addln('${field.name};'); } @@ -174,16 +191,12 @@ void generateDart(Root root, StringSink sink) { indent.write( 'static ${klass.name} _fromMap(Map pigeonMap) '); indent.scoped('{', '}', () { - indent.write('if (pigeonMap == null)'); - indent.scoped('{', '}', () { - indent.writeln('return null;'); - }); indent.writeln('final ${klass.name} result = ${klass.name}();'); for (Field field in klass.fields) { indent.write('result.${field.name} = '); if (customClassNames.contains(field.dataType)) { indent.addln( - '${field.dataType}._fromMap(pigeonMap[\'${field.name}\']);'); + 'pigeonMap[\'${field.name}\'] != null ? ${field.dataType}._fromMap(pigeonMap[\'${field.name}\']) : null;'); } else { indent.addln('pigeonMap[\'${field.name}\'];'); } @@ -195,18 +208,18 @@ void generateDart(Root root, StringSink sink) { } for (Api api in root.apis) { if (api.location == ApiLocation.host) { - _writeHostApi(indent, api); + _writeHostApi(opt, indent, api); if (api.dartHostTestHandler != null) { final Api mockApi = Api( name: api.dartHostTestHandler, methods: api.methods, location: ApiLocation.flutter); - _writeFlutterApi(indent, mockApi, + _writeFlutterApi(opt, indent, mockApi, channelNameFunc: (Method func) => makeChannelName(api, func), isMockHandler: true); } } else if (api.location == ApiLocation.flutter) { - _writeFlutterApi(indent, api); + _writeFlutterApi(opt, indent, api); } } } diff --git a/packages/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart index 3a4bbdb45f..257651d8f4 100644 --- a/packages/pigeon/lib/generator_tools.dart +++ b/packages/pigeon/lib/generator_tools.dart @@ -8,7 +8,7 @@ import 'dart:mirrors'; import 'ast.dart'; /// The current version of pigeon. -const String pigeonVersion = '0.1.10+1'; +const String pigeonVersion = '0.1.11'; /// Read all the content from [stdin] to a String. String readStdin() { diff --git a/packages/pigeon/lib/pigeon_lib.dart b/packages/pigeon/lib/pigeon_lib.dart index 58b5a31e64..79de4994f3 100644 --- a/packages/pigeon/lib/pigeon_lib.dart +++ b/packages/pigeon/lib/pigeon_lib.dart @@ -133,6 +133,9 @@ class PigeonOptions { /// Options that control how Java will be generated. JavaOptions javaOptions = JavaOptions(); + + /// Options that control how Dart will be generated. + DartOptions dartOptions = DartOptions(); } /// A collection of an AST represented as a [Root] and [Error]'s. @@ -282,6 +285,8 @@ options: ..addOption('java_out', help: 'Path to generated Java file (.java).') ..addOption('java_package', help: 'The package that generated Java code will be in.') + ..addFlag('dart_null_safety', + help: 'Makes generated Dart code have null safety annotations') ..addOption('objc_header_out', help: 'Path to generated Objective-C header file (.h).') ..addOption('objc_prefix', @@ -299,6 +304,7 @@ options: opts.objcOptions.prefix = results['objc_prefix']; opts.javaOut = results['java_out']; opts.javaOptions.package = results['java_package']; + opts.dartOptions.isNullSafe = results['dart_null_safety']; return opts; } @@ -402,8 +408,10 @@ options: errors.add(Error(message: err.message, filename: options.input)); } if (options.dartOut != null) { - await _runGenerator(options.dartOut, - (StringSink sink) => generateDart(parseResults.root, sink)); + await _runGenerator( + options.dartOut, + (StringSink sink) => + generateDart(options.dartOptions, parseResults.root, sink)); } if (options.objcHeaderOut != null) { await _runGenerator( diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml index a5c5cc5d2f..aad6d70fd9 100644 --- a/packages/pigeon/pubspec.yaml +++ b/packages/pigeon/pubspec.yaml @@ -1,5 +1,5 @@ name: pigeon -version: 0.1.10+1 +version: 0.1.11 description: Code generator tool to make communication between Flutter and the host platform type-safe and easier. homepage: https://github.com/flutter/packages/tree/master/packages/pigeon dependencies: diff --git a/packages/pigeon/run_tests.sh b/packages/pigeon/run_tests.sh index ab7ecf6206..0ebfb4a3f5 100755 --- a/packages/pigeon/run_tests.sh +++ b/packages/pigeon/run_tests.sh @@ -67,6 +67,22 @@ test_pigeon_android() { rm -rf $temp_dir } +# test_null_safe_dart() +# +# Compiles the pigeon file to a temp directory and attempts to run the dart +# analyzer on it with null safety turned on. +test_null_safe_dart() { + temp_dir=$(mktemp -d -t pigeon) + + pub run pigeon \ + --input $1 \ + --dart_null_safety \ + --dart_out $temp_dir/pigeon.dart + + dartanalyzer $temp_dir/pigeon.dart --fatal-infos --fatal-warnings --packages ./e2e_tests/test_objc/.packages --enable-experiment=non-nullable + rm -rf $temp_dir +} + ############################################################################### # Dart unit tests ############################################################################### @@ -76,7 +92,7 @@ pub run test test/ ############################################################################### # Execute without arguments test ############################################################################### -pub run pigeon +pub run pigeon 1> /dev/null ############################################################################### # Compilation tests (Code is generated and compiled) @@ -88,6 +104,7 @@ pushd $PWD cd e2e_tests/test_objc/ flutter pub get popd +test_null_safe_dart ./pigeons/message.dart test_pigeon_android ./pigeons/voidflutter.dart test_pigeon_android ./pigeons/voidhost.dart test_pigeon_android ./pigeons/host2flutter.dart diff --git a/packages/pigeon/test/dart_generator_test.dart b/packages/pigeon/test/dart_generator_test.dart index 1200e216b5..b90e60f669 100644 --- a/packages/pigeon/test/dart_generator_test.dart +++ b/packages/pigeon/test/dart_generator_test.dart @@ -20,7 +20,7 @@ void main() { ..apis = [] ..classes = [klass]; final StringBuffer sink = StringBuffer(); - generateDart(root, sink); + generateDart(DartOptions(), root, sink); final String code = sink.toString(); expect(code, contains('class Foobar')); expect(code, contains(' dataType1 field1;')); @@ -45,7 +45,7 @@ void main() { fields: [Field(name: 'output', dataType: 'String')]) ]); final StringBuffer sink = StringBuffer(); - generateDart(root, sink); + generateDart(DartOptions(), root, sink); final String code = sink.toString(); expect(code, contains('class Api')); expect(code, matches('Output.*doSomething.*Input')); @@ -61,14 +61,16 @@ void main() { fields: [Field(name: 'nested', dataType: 'Input')]) ]); final StringBuffer sink = StringBuffer(); - generateDart(root, sink); + generateDart(DartOptions(), root, sink); final String code = sink.toString(); expect( code, contains( 'pigeonMap[\'nested\'] = nested == null ? null : nested._toMap()')); - expect(code, - contains('result.nested = Input._fromMap(pigeonMap[\'nested\']);')); + expect( + code, + contains( + 'result.nested = pigeonMap[\'nested\'] != null ? Input._fromMap(pigeonMap[\'nested\']) : null;')); }); test('flutterapi', () { @@ -90,7 +92,7 @@ void main() { fields: [Field(name: 'output', dataType: 'String')]) ]); final StringBuffer sink = StringBuffer(); - generateDart(root, sink); + generateDart(DartOptions(), root, sink); final String code = sink.toString(); expect(code, contains('abstract class Api')); expect(code, contains('static void setup(Api')); @@ -112,7 +114,7 @@ void main() { fields: [Field(name: 'input', dataType: 'String')]), ]); final StringBuffer sink = StringBuffer(); - generateDart(root, sink); + generateDart(DartOptions(), root, sink); final String code = sink.toString(); expect(code, contains('Future doSomething')); expect(code, contains('// noop')); @@ -134,7 +136,7 @@ void main() { fields: [Field(name: 'input', dataType: 'String')]), ]); final StringBuffer sink = StringBuffer(); - generateDart(root, sink); + generateDart(DartOptions(), root, sink); final String code = sink.toString(); expect(code, isNot(matches('=.*doSomething'))); expect(code, contains('doSomething(')); @@ -157,7 +159,7 @@ void main() { fields: [Field(name: 'output', dataType: 'String')]), ]); final StringBuffer sink = StringBuffer(); - generateDart(root, sink); + generateDart(DartOptions(), root, sink); final String code = sink.toString(); expect(code, matches('output.*=.*doSomething[(][)]')); expect(code, contains('Output doSomething();')); @@ -179,7 +181,7 @@ void main() { fields: [Field(name: 'output', dataType: 'String')]), ]); final StringBuffer sink = StringBuffer(); - generateDart(root, sink); + generateDart(DartOptions(), root, sink); final String code = sink.toString(); expect(code, matches('channel\.send[(]null[)]')); }); @@ -213,7 +215,7 @@ void main() { fields: [Field(name: 'output', dataType: 'String')]) ]); final StringBuffer sink = StringBuffer(); - generateDart(root, sink); + generateDart(DartOptions(), root, sink); final String code = sink.toString(); expect(code, matches('abstract class ApiMock')); expect(code, isNot(matches('\.ApiMock\.doSomething'))); @@ -233,7 +235,7 @@ void main() { ..apis = [] ..classes = [klass]; final StringBuffer sink = StringBuffer(); - generateDart(root, sink); + generateDart(DartOptions(), root, sink); final String code = sink.toString(); expect(code, contains('// @dart = 2.8')); }); @@ -257,7 +259,7 @@ void main() { fields: [Field(name: 'output', dataType: 'String')]) ]); final StringBuffer sink = StringBuffer(); - generateDart(root, sink); + generateDart(DartOptions(), root, sink); final String code = sink.toString(); expect(code, contains('abstract class Api')); expect(code, contains('Future doSomething(Input arg);')); @@ -284,7 +286,7 @@ void main() { fields: [Field(name: 'output', dataType: 'String')]) ]); final StringBuffer sink = StringBuffer(); - generateDart(root, sink); + generateDart(DartOptions(), root, sink); final String code = sink.toString(); expect(code, contains('class Api')); expect(code, matches('Output.*doSomething.*Input')); @@ -306,7 +308,7 @@ void main() { fields: [Field(name: 'output', dataType: 'String')]), ]); final StringBuffer sink = StringBuffer(); - generateDart(root, sink); + generateDart(DartOptions(), root, sink); final String code = sink.toString(); expect(code, matches('channel\.send[(]null[)]')); }); diff --git a/packages/pigeon/test/pigeon_lib_test.dart b/packages/pigeon/test/pigeon_lib_test.dart index 0059687d3c..a842beb04c 100644 --- a/packages/pigeon/test/pigeon_lib_test.dart +++ b/packages/pigeon/test/pigeon_lib_test.dart @@ -237,4 +237,10 @@ void main() { final ParseResults results = pigeon.parse([InvalidReturnTypeApi]); expect(results.errors.length, 1); }); + + test('null saftey flag', () { + final PigeonOptions results = + Pigeon.parseArgs(['--dart_null_safety']); + expect(results.dartOptions.isNullSafe, true); + }); }