[pigeon] added null safety flag for dart (#222)

This commit is contained in:
gaaclarke
2020-10-12 10:31:28 -07:00
committed by GitHub
parent 6ea4cd0124
commit 5026d61840
8 changed files with 118 additions and 67 deletions

View File

@ -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.

View File

@ -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<dynamic, dynamic> replyMap = await channel.send($sendArgument);
'''final Map<dynamic, dynamic>$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,8 +98,13 @@ void _writeFlutterApi(Indent indent, Api api,
indent.dec();
final String messageHandlerSetter =
isMockHandler ? 'setMockMessageHandler' : 'setMessageHandler';
indent
.write('channel.$messageHandlerSetter((dynamic message) async ');
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;
@ -125,6 +138,7 @@ void _writeFlutterApi(Indent indent, Api api,
}
});
});
});
}
});
});
@ -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<String> 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<dynamic, dynamic> _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<dynamic, dynamic> 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);
}
}
}

View File

@ -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() {

View File

@ -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(

View File

@ -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:

View File

@ -67,6 +67,22 @@ test_pigeon_android() {
rm -rf $temp_dir
}
# test_null_safe_dart(<path to pigeon file>)
#
# 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

View File

@ -20,7 +20,7 @@ void main() {
..apis = <Api>[]
..classes = <Class>[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>[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>[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>[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>[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<void> doSomething'));
expect(code, contains('// noop'));
@ -134,7 +136,7 @@ void main() {
fields: <Field>[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>[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>[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>[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 = <Api>[]
..classes = <Class>[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>[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<Output> doSomething(Input arg);'));
@ -284,7 +286,7 @@ void main() {
fields: <Field>[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>[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[)]'));
});

View File

@ -237,4 +237,10 @@ void main() {
final ParseResults results = pigeon.parse(<Type>[InvalidReturnTypeApi]);
expect(results.errors.length, 1);
});
test('null saftey flag', () {
final PigeonOptions results =
Pigeon.parseArgs(<String>['--dart_null_safety']);
expect(results.dartOptions.isNullSafe, true);
});
}