mirror of
https://github.com/flutter/packages.git
synced 2025-07-01 23:51:55 +08:00
[pigeon] added null safety flag for dart (#222)
This commit is contained in:
@ -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.
|
||||
|
@ -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,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<dynamic, dynamic> mapMessage = message as Map<dynamic, dynamic>;');
|
||||
indent.writeln(
|
||||
'final $argType input = $argType._fromMap(mapMessage);');
|
||||
call = 'api.${func.name}(input)';
|
||||
}
|
||||
if (returnType == 'void') {
|
||||
indent.writeln('$call;');
|
||||
if (isMockHandler) {
|
||||
indent.writeln('return <dynamic, dynamic>{};');
|
||||
}
|
||||
} 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<dynamic, dynamic> mapMessage = message as Map<dynamic, dynamic>;');
|
||||
indent.writeln(
|
||||
'final $argType input = $argType._fromMap(mapMessage);');
|
||||
call = 'api.${func.name}(input)';
|
||||
}
|
||||
const String returnExpresion = 'output._toMap()';
|
||||
final String returnStatement = isMockHandler
|
||||
? 'return <dynamic, dynamic>{\'${Keys.result}\': $returnExpresion};'
|
||||
: 'return $returnExpresion;';
|
||||
indent.writeln(returnStatement);
|
||||
}
|
||||
if (returnType == 'void') {
|
||||
indent.writeln('$call;');
|
||||
if (isMockHandler) {
|
||||
indent.writeln('return <dynamic, dynamic>{};');
|
||||
}
|
||||
} 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 <dynamic, dynamic>{\'${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<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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(
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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[)]'));
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
Reference in New Issue
Block a user