-
Notifications
You must be signed in to change notification settings - Fork 3.8k
[pigeon] added null safety flag for dart #222
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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>{};'); | ||
| 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 Map<dynamic, dynamic> mapMessage = message as Map<dynamic, dynamic>;'); | ||
| indent.writeln( | ||
| 'final $argType input = $argType._fromMap(mapMessage);'); | ||
| call = 'api.${func.name}(input)'; | ||
| } | ||
| } else { | ||
| if (isAsync) { | ||
| indent.writeln('final $returnType output = await $call;'); | ||
| if (returnType == 'void') { | ||
| indent.writeln('$call;'); | ||
| if (isMockHandler) { | ||
| indent.writeln('return <dynamic, dynamic>{};'); | ||
| } | ||
| } else { | ||
| indent.writeln('final $returnType output = $call;'); | ||
| 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); | ||
| } | ||
| const String returnExpresion = 'output._toMap()'; | ||
| final String returnStatement = isMockHandler | ||
| ? 'return <dynamic, dynamic>{\'${Keys.result}\': $returnExpresion};' | ||
| : 'return $returnExpresion;'; | ||
| indent.writeln(returnStatement); | ||
| } | ||
| }); | ||
| }); | ||
| }); | ||
| } | ||
|
|
@@ -133,26 +147,29 @@ 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); | ||
| indent.writeln('// $generatedCodeWarning'); | ||
| 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'}'); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: This is control via the SDK in pubspec.yaml, so there is no need to add
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I need it for the dartanalyzer, maybe there is a command line flag that can replace it? |
||
| 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); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
blasten marked this conversation as resolved.
|
||
| 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does it make sense to test the other scenarios?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did it when I was developing but then I realized I don't think any of the other cases are exercising the code differently. The changes were in pieces of code that would have shown up in message.dart. |
||
| test_pigeon_android ./pigeons/voidflutter.dart | ||
| test_pigeon_android ./pigeons/voidhost.dart | ||
| test_pigeon_android ./pigeons/host2flutter.dart | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would call this
null safety awarecode rather thannull safeno?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The feature of the annotations is called "null safety". Even if you are using a ?-type you are getting extra safety by enforcing explicit casting to non-?-types. If we support non-null types in the generated code it would still be generated via this flag.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The flag sounds good. I'd leave that as is. I just meant the
generate null safepart. I'd argue a non-nullable type being passed through the function is "null safe". Unsafe casting a nullable type is "null safety aware".i.e. I wouldn't make it sound like the user can just run pigeon once in v0.1.11, have "null safe" code and never have to think about it or regenerate from a future version again since it sounds like it's "done".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, i see what you are saying. I switched it to be called "null safety annotated".