blob: 746d01d9966cbad5b8c5833c20703ef9d10fc53a [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:path/path.dart' as path;
import 'ast.dart';
import 'functional.dart';
import 'generator.dart';
import 'generator_tools.dart';
/// Documentation comment open symbol.
const String _docCommentPrefix = '///';
/// Prefix for all local variables in host API methods.
///
/// This lowers the chances of variable name collisions with
/// user defined parameters.
const String _varNamePrefix = '__pigeon_';
/// Name of field used for host API codec.
const String _pigeonChannelCodec = 'pigeonChannelCodec';
/// Documentation comment spec.
const DocumentCommentSpecification _docCommentSpec =
DocumentCommentSpecification(_docCommentPrefix);
/// The standard codec for Flutter, used for any non custom codecs and extended for custom codecs.
const String _standardMessageCodec = 'StandardMessageCodec';
/// Options that control how Dart code will be generated.
class DartOptions {
/// Constructor for DartOptions.
const DartOptions({
this.copyrightHeader,
this.sourceOutPath,
this.testOutPath,
});
/// A copyright header that will get prepended to generated code.
final Iterable<String>? copyrightHeader;
/// Path to output generated Dart file.
final String? sourceOutPath;
/// Path to output generated Test file for tests.
final String? testOutPath;
/// Creates a [DartOptions] from a Map representation where:
/// `x = DartOptions.fromMap(x.toMap())`.
static DartOptions fromMap(Map<String, Object> map) {
final Iterable<dynamic>? copyrightHeader =
map['copyrightHeader'] as Iterable<dynamic>?;
return DartOptions(
copyrightHeader: copyrightHeader?.cast<String>(),
sourceOutPath: map['sourceOutPath'] as String?,
testOutPath: map['testOutPath'] as String?,
);
}
/// Converts a [DartOptions] to a Map representation where:
/// `x = DartOptions.fromMap(x.toMap())`.
Map<String, Object> toMap() {
final Map<String, Object> result = <String, Object>{
if (copyrightHeader != null) 'copyrightHeader': copyrightHeader!,
if (sourceOutPath != null) 'sourceOutPath': sourceOutPath!,
if (testOutPath != null) 'testOutPath': testOutPath!,
};
return result;
}
/// Overrides any non-null parameters from [options] into this to make a new
/// [DartOptions].
DartOptions merge(DartOptions options) {
return DartOptions.fromMap(mergeMaps(toMap(), options.toMap()));
}
}
/// Class that manages all Dart code generation.
class DartGenerator extends StructuredGenerator<DartOptions> {
/// Instantiates a Dart Generator.
const DartGenerator();
@override
void writeFilePrologue(
DartOptions generatorOptions,
Root root,
Indent indent, {
required String dartPackageName,
}) {
if (generatorOptions.copyrightHeader != null) {
addLines(indent, generatorOptions.copyrightHeader!, linePrefix: '// ');
}
indent.writeln('// ${getGeneratedCodeWarning()}');
indent.writeln('// $seeAlsoWarning');
indent.writeln(
'// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers',
);
indent.newln();
}
@override
void writeFileImports(
DartOptions generatorOptions,
Root root,
Indent indent, {
required String dartPackageName,
}) {
indent.writeln("import 'dart:async';");
indent.writeln(
"import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;",
);
indent.newln();
indent.writeln(
"import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;");
indent.writeln("import 'package:flutter/services.dart';");
}
@override
void writeEnum(
DartOptions generatorOptions,
Root root,
Indent indent,
Enum anEnum, {
required String dartPackageName,
}) {
indent.newln();
addDocumentationComments(
indent, anEnum.documentationComments, _docCommentSpec);
indent.write('enum ${anEnum.name} ');
indent.addScoped('{', '}', () {
for (final EnumMember member in anEnum.members) {
addDocumentationComments(
indent, member.documentationComments, _docCommentSpec);
indent.writeln('${member.name},');
}
});
}
@override
void writeDataClass(
DartOptions generatorOptions,
Root root,
Indent indent,
Class classDefinition, {
required String dartPackageName,
}) {
indent.newln();
addDocumentationComments(
indent, classDefinition.documentationComments, _docCommentSpec);
indent.write('class ${classDefinition.name} ');
indent.addScoped('{', '}', () {
_writeConstructor(indent, classDefinition);
indent.newln();
for (final NamedType field
in getFieldsInSerializationOrder(classDefinition)) {
addDocumentationComments(
indent, field.documentationComments, _docCommentSpec);
final String datatype = _addGenericTypesNullable(field.type);
indent.writeln('$datatype ${field.name};');
indent.newln();
}
writeClassEncode(
generatorOptions,
root,
indent,
classDefinition,
dartPackageName: dartPackageName,
);
indent.newln();
writeClassDecode(
generatorOptions,
root,
indent,
classDefinition,
dartPackageName: dartPackageName,
);
});
}
void _writeConstructor(Indent indent, Class classDefinition) {
indent.write(classDefinition.name);
indent.addScoped('({', '});', () {
for (final NamedType field
in getFieldsInSerializationOrder(classDefinition)) {
final String required =
!field.type.isNullable && field.defaultValue == null
? 'required '
: '';
final String defaultValueString =
field.defaultValue == null ? '' : ' = ${field.defaultValue}';
indent.writeln('${required}this.${field.name}$defaultValueString,');
}
});
}
@override
void writeClassEncode(
DartOptions generatorOptions,
Root root,
Indent indent,
Class classDefinition, {
required String dartPackageName,
}) {
indent.write('Object encode() ');
indent.addScoped('{', '}', () {
indent.write(
'return <Object?>',
);
indent.addScoped('[', '];', () {
for (final NamedType field
in getFieldsInSerializationOrder(classDefinition)) {
final String conditional = field.type.isNullable ? '?' : '';
if (field.type.isClass) {
indent.writeln(
'${field.name}$conditional.encode(),',
);
} else if (field.type.isEnum) {
indent.writeln(
'${field.name}$conditional.index,',
);
} else {
indent.writeln('${field.name},');
}
}
});
});
}
@override
void writeClassDecode(
DartOptions generatorOptions,
Root root,
Indent indent,
Class classDefinition, {
required String dartPackageName,
}) {
void writeValueDecode(NamedType field, int index) {
final String resultAt = 'result[$index]';
final String castCallPrefix = field.type.isNullable ? '?' : '!';
final String genericType = _makeGenericTypeArguments(field.type);
final String castCall = _makeGenericCastCall(field.type);
final String nullableTag = field.type.isNullable ? '?' : '';
if (field.type.isClass) {
final String nonNullValue =
'${field.type.baseName}.decode($resultAt! as List<Object?>)';
if (field.type.isNullable) {
indent.format('''
$resultAt != null
\t\t? $nonNullValue
\t\t: null''', leadingSpace: false, trailingNewline: false);
} else {
indent.add(nonNullValue);
}
} else if (field.type.isEnum) {
final String nonNullValue =
'${field.type.baseName}.values[$resultAt! as int]';
if (field.type.isNullable) {
indent.format('''
$resultAt != null
\t\t? $nonNullValue
\t\t: null''', leadingSpace: false, trailingNewline: false);
} else {
indent.add(nonNullValue);
}
} else if (field.type.typeArguments.isNotEmpty) {
indent.add(
'($resultAt as $genericType?)$castCallPrefix$castCall',
);
} else {
final String castCallForcePrefix = field.type.isNullable ? '' : '!';
final String castString = field.type.baseName == 'Object'
? ''
: ' as $genericType$nullableTag';
indent.add(
'$resultAt$castCallForcePrefix$castString',
);
}
}
indent.write(
'static ${classDefinition.name} decode(Object result) ',
);
indent.addScoped('{', '}', () {
indent.writeln('result as List<Object?>;');
indent.write('return ${classDefinition.name}');
indent.addScoped('(', ');', () {
enumerate(getFieldsInSerializationOrder(classDefinition),
(int index, final NamedType field) {
indent.write('${field.name}: ');
writeValueDecode(field, index);
indent.addln(',');
});
});
});
}
/// Writes the code for host [Api], [api].
/// Example:
/// class FooCodec extends StandardMessageCodec {...}
///
/// abstract class Foo {
/// static const MessageCodec<Object?> codec = FooCodec();
/// int add(int x, int y);
/// static void setup(Foo api, {BinaryMessenger? binaryMessenger}) {...}
/// }
@override
void writeFlutterApi(
DartOptions generatorOptions,
Root root,
Indent indent,
Api api, {
String Function(Method)? channelNameFunc,
bool isMockHandler = false,
required String dartPackageName,
}) {
assert(api.location == ApiLocation.flutter);
String codecName = _standardMessageCodec;
if (getCodecClasses(api, root).isNotEmpty) {
codecName = _getCodecName(api);
_writeCodec(indent, codecName, api, root);
}
indent.newln();
addDocumentationComments(
indent, api.documentationComments, _docCommentSpec);
indent.write('abstract class ${api.name} ');
indent.addScoped('{', '}', () {
if (isMockHandler) {
indent.writeln(
'static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => TestDefaultBinaryMessengerBinding.instance;');
}
indent.writeln(
'static const MessageCodec<Object?> $_pigeonChannelCodec = $codecName();');
indent.newln();
for (final Method func in api.methods) {
addDocumentationComments(
indent, func.documentationComments, _docCommentSpec);
final bool isAsync = func.isAsynchronous;
final String returnType = isAsync
? 'Future<${_addGenericTypesNullable(func.returnType)}>'
: _addGenericTypesNullable(func.returnType);
final String argSignature = _getMethodParameterSignature(func);
indent.writeln('$returnType ${func.name}($argSignature);');
indent.newln();
}
indent.write(
'static void setup(${api.name}? api, {BinaryMessenger? binaryMessenger}) ');
indent.addScoped('{', '}', () {
for (final Method func in api.methods) {
indent.write('');
indent.addScoped('{', '}', () {
indent.writeln(
'final BasicMessageChannel<Object?> ${_varNamePrefix}channel = BasicMessageChannel<Object?>(',
);
final String channelName = channelNameFunc == null
? makeChannelName(api, func, dartPackageName)
: channelNameFunc(func);
indent.nest(2, () {
indent.writeln("'$channelName', $_pigeonChannelCodec,");
indent.writeln(
'binaryMessenger: binaryMessenger);',
);
});
final String messageHandlerSetterWithOpeningParentheses = isMockHandler
? '_testBinaryMessengerBinding!.defaultBinaryMessenger.setMockDecodedMessageHandler<Object?>(${_varNamePrefix}channel, '
: '${_varNamePrefix}channel.setMessageHandler(';
indent.write('if (api == null) ');
indent.addScoped('{', '}', () {
indent.writeln(
'${messageHandlerSetterWithOpeningParentheses}null);');
}, addTrailingNewline: false);
indent.add(' else ');
indent.addScoped('{', '}', () {
indent.write(
'$messageHandlerSetterWithOpeningParentheses(Object? message) async ',
);
indent.addScoped('{', '});', () {
final String returnType =
_addGenericTypesNullable(func.returnType);
final bool isAsync = func.isAsynchronous;
const String emptyReturnStatement =
'return wrapResponse(empty: true);';
String call;
if (func.parameters.isEmpty) {
call = 'api.${func.name}()';
} else {
indent.writeln('assert(message != null,');
indent.writeln("'Argument for $channelName was null.');");
const String argsArray = 'args';
indent.writeln(
'final List<Object?> $argsArray = (message as List<Object?>?)!;');
String argNameFunc(int index, NamedType type) =>
_getSafeArgumentName(index, type);
enumerate(func.parameters, (int count, NamedType arg) {
final String argType = _addGenericTypes(arg.type);
final String argName = argNameFunc(count, arg);
final String genericArgType =
_makeGenericTypeArguments(arg.type);
final String castCall = _makeGenericCastCall(arg.type);
final String leftHandSide = 'final $argType? $argName';
if (arg.type.isEnum) {
indent.writeln(
'$leftHandSide = $argsArray[$count] == null ? null : $argType.values[$argsArray[$count]! as int];');
} else {
indent.writeln(
'$leftHandSide = ($argsArray[$count] as $genericArgType?)${castCall.isEmpty ? '' : '?$castCall'};');
}
if (!arg.type.isNullable) {
indent.writeln('assert($argName != null,');
indent.writeln(
" 'Argument for $channelName was null, expected non-null $argType.');");
}
});
final Iterable<String> argNames =
indexMap(func.parameters, (int index, Parameter field) {
final String name = _getSafeArgumentName(index, field);
return '${field.isNamed ? '${field.name}: ' : ''}$name${field.type.isNullable ? '' : '!'}';
});
call = 'api.${func.name}(${argNames.join(', ')})';
}
indent.writeScoped('try {', '} ', () {
if (func.returnType.isVoid) {
if (isAsync) {
indent.writeln('await $call;');
} else {
indent.writeln('$call;');
}
indent.writeln(emptyReturnStatement);
} else {
if (isAsync) {
indent.writeln('final $returnType output = await $call;');
} else {
indent.writeln('final $returnType output = $call;');
}
const String returnExpression = 'output';
final String nullability =
func.returnType.isNullable ? '?' : '';
final String valueExtraction =
func.returnType.isEnum ? '$nullability.index' : '';
final String returnStatement = isMockHandler
? 'return <Object?>[$returnExpression$valueExtraction];'
: 'return wrapResponse(result: $returnExpression$valueExtraction);';
indent.writeln(returnStatement);
}
}, addTrailingNewline: false);
indent.addScoped('on PlatformException catch (e) {', '}', () {
indent.writeln('return wrapResponse(error: e);');
}, addTrailingNewline: false);
indent.writeScoped('catch (e) {', '}', () {
indent.writeln(
"return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));");
});
});
});
});
}
});
});
}
/// Writes the code for host [Api], [api].
/// Example:
/// class FooCodec extends StandardMessageCodec {...}
///
/// class Foo {
/// Foo(BinaryMessenger? binaryMessenger) {}
/// static const MessageCodec<Object?> codec = FooCodec();
/// Future<int> add(int x, int y) async {...}
/// }
///
/// Messages will be sent and received in a list.
///
/// If the message received was successful,
/// the result will be contained at the 0'th index.
///
/// If the message was a failure, the list will contain 3 items:
/// a code, a message, and details in that order.
@override
void writeHostApi(
DartOptions generatorOptions,
Root root,
Indent indent,
Api api, {
required String dartPackageName,
}) {
assert(api.location == ApiLocation.host);
String codecName = _standardMessageCodec;
if (getCodecClasses(api, root).isNotEmpty) {
codecName = _getCodecName(api);
_writeCodec(indent, codecName, api, root);
}
indent.newln();
bool first = true;
addDocumentationComments(
indent, api.documentationComments, _docCommentSpec);
indent.write('class ${api.name} ');
indent.addScoped('{', '}', () {
indent.format('''
/// Constructor for [${api.name}]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
${api.name}({BinaryMessenger? binaryMessenger})
\t\t: ${_varNamePrefix}binaryMessenger = binaryMessenger;
final BinaryMessenger? ${_varNamePrefix}binaryMessenger;
''');
indent.writeln(
'static const MessageCodec<Object?> $_pigeonChannelCodec = $codecName();');
indent.newln();
for (final Method func in api.methods) {
if (!first) {
indent.newln();
} else {
first = false;
}
addDocumentationComments(
indent, func.documentationComments, _docCommentSpec);
String argSignature = '';
String sendArgument = 'null';
if (func.parameters.isNotEmpty) {
final Iterable<String> argExpressions =
indexMap(func.parameters, (int index, NamedType type) {
final String name = _getParameterName(index, type);
if (type.type.isEnum) {
return '$name${type.type.isNullable ? '?' : ''}.index';
} else {
return name;
}
});
sendArgument = '<Object?>[${argExpressions.join(', ')}]';
argSignature = _getMethodParameterSignature(func);
}
indent.write(
'Future<${_addGenericTypesNullable(func.returnType)}> ${func.name}($argSignature) async ',
);
indent.addScoped('{', '}', () {
indent.writeln(
"const String ${_varNamePrefix}channelName = '${makeChannelName(api, func, dartPackageName)}';");
indent.writeScoped(
'final BasicMessageChannel<Object?> ${_varNamePrefix}channel = BasicMessageChannel<Object?>(',
');', () {
indent.writeln('${_varNamePrefix}channelName,');
indent.writeln('$_pigeonChannelCodec,');
indent
.writeln('binaryMessenger: ${_varNamePrefix}binaryMessenger,');
});
final String returnType = _makeGenericTypeArguments(func.returnType);
final String genericCastCall = _makeGenericCastCall(func.returnType);
const String accessor = '${_varNamePrefix}replyList[0]';
// Avoid warnings from pointlessly casting to `Object?`.
final String nullablyTypedAccessor =
returnType == 'Object' ? accessor : '($accessor as $returnType?)';
final String nullHandler = func.returnType.isNullable
? (genericCastCall.isEmpty ? '' : '?')
: '!';
String returnStatement = 'return';
if (func.returnType.isEnum) {
if (func.returnType.isNullable) {
returnStatement =
'$returnStatement ($accessor as int?) == null ? null : $returnType.values[$accessor! as int]';
} else {
returnStatement =
'$returnStatement $returnType.values[$accessor! as int]';
}
} else if (!func.returnType.isVoid) {
returnStatement =
'$returnStatement $nullablyTypedAccessor$nullHandler$genericCastCall';
}
returnStatement = '$returnStatement;';
indent.format('''
final List<Object?>? ${_varNamePrefix}replyList =
\t\tawait ${_varNamePrefix}channel.send($sendArgument) as List<Object?>?;
if (${_varNamePrefix}replyList == null) {
\tthrow _createConnectionError(${_varNamePrefix}channelName);
} else if (${_varNamePrefix}replyList.length > 1) {
\tthrow PlatformException(
\t\tcode: ${_varNamePrefix}replyList[0]! as String,
\t\tmessage: ${_varNamePrefix}replyList[1] as String?,
\t\tdetails: ${_varNamePrefix}replyList[2],
\t);''');
// On iOS we can return nil from functions to accommodate error
// handling. Returning a nil value and not returning an error is an
// exception.
if (!func.returnType.isNullable && !func.returnType.isVoid) {
indent.format('''
} else if (${_varNamePrefix}replyList[0] == null) {
\tthrow PlatformException(
\t\tcode: 'null-error',
\t\tmessage: 'Host platform returned null value for non-null return value.',
\t);''');
}
indent.format('''
} else {
\t$returnStatement
}''');
});
}
});
}
/// Generates Dart source code for test support libraries based on the given AST
/// represented by [root], outputting the code to [sink]. [sourceOutPath] is the
/// path of the generated dart code to be tested. [testOutPath] is where the
/// test code will be generated.
void generateTest(
DartOptions generatorOptions,
Root root,
StringSink sink, {
required String dartPackageName,
required String dartOutputPackageName,
}) {
final Indent indent = Indent(sink);
final String sourceOutPath = generatorOptions.sourceOutPath ?? '';
final String testOutPath = generatorOptions.testOutPath ?? '';
_writeTestPrologue(generatorOptions, root, indent);
_writeTestImports(generatorOptions, root, indent);
final String relativeDartPath =
path.Context(style: path.Style.posix).relative(
_posixify(sourceOutPath),
from: _posixify(path.dirname(testOutPath)),
);
if (!relativeDartPath.contains('/lib/')) {
// If we can't figure out the package name or the relative path doesn't
// include a 'lib' directory, try relative path import which only works in
// certain (older) versions of Dart.
// TODO(gaaclarke): We should add a command-line parameter to override this import.
indent.writeln(
"import '${_escapeForDartSingleQuotedString(relativeDartPath)}';");
} else {
final String path =
relativeDartPath.replaceFirst(RegExp(r'^.*/lib/'), '');
indent.writeln("import 'package:$dartOutputPackageName/$path';");
}
for (final Api api in root.apis) {
if (api.location == ApiLocation.host && api.dartHostTestHandler != null) {
final Api mockApi = Api(
name: api.dartHostTestHandler!,
methods: api.methods,
location: ApiLocation.flutter,
dartHostTestHandler: api.dartHostTestHandler,
documentationComments: api.documentationComments,
);
writeFlutterApi(
generatorOptions,
root,
indent,
mockApi,
channelNameFunc: (Method func) =>
makeChannelName(api, func, dartPackageName),
isMockHandler: true,
dartPackageName: dartPackageName,
);
}
}
}
/// Writes file header to sink.
void _writeTestPrologue(DartOptions opt, Root root, Indent indent) {
if (opt.copyrightHeader != null) {
addLines(indent, opt.copyrightHeader!, linePrefix: '// ');
}
indent.writeln('// ${getGeneratedCodeWarning()}');
indent.writeln('// $seeAlsoWarning');
indent.writeln(
'// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import, no_leading_underscores_for_local_identifiers',
);
indent.writeln('// ignore_for_file: avoid_relative_lib_imports');
}
/// Writes file imports to sink.
void _writeTestImports(DartOptions opt, Root root, Indent indent) {
indent.writeln("import 'dart:async';");
indent.writeln(
"import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;",
);
indent.writeln(
"import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;");
indent.writeln("import 'package:flutter/services.dart';");
indent.writeln("import 'package:flutter_test/flutter_test.dart';");
indent.newln();
}
@override
void writeGeneralUtilities(
DartOptions generatorOptions,
Root root,
Indent indent, {
required String dartPackageName,
}) {
final bool hasHostApi = root.apis.any((Api api) =>
api.methods.isNotEmpty && api.location == ApiLocation.host);
final bool hasFlutterApi = root.apis.any((Api api) =>
api.methods.isNotEmpty && api.location == ApiLocation.flutter);
if (hasHostApi) {
_writeCreateConnectionError(indent);
}
if (hasFlutterApi || generatorOptions.testOutPath != null) {
_writeWrapResponse(generatorOptions, root, indent);
}
}
/// Writes [wrapResponse] method.
void _writeWrapResponse(DartOptions opt, Root root, Indent indent) {
indent.newln();
indent.writeScoped(
'List<Object?> wrapResponse({Object? result, PlatformException? error, bool empty = false}) {',
'}', () {
indent.writeScoped('if (empty) {', '}', () {
indent.writeln('return <Object?>[];');
});
indent.writeScoped('if (error == null) {', '}', () {
indent.writeln('return <Object?>[result];');
});
indent.writeln(
'return <Object?>[error.code, error.message, error.details];');
});
}
void _writeCreateConnectionError(Indent indent) {
indent.newln();
indent.format('''
PlatformException _createConnectionError(String channelName) {
\treturn PlatformException(
\t\tcode: 'channel-error',
\t\tmessage: 'Unable to establish connection on channel: "\$channelName".',
\t);
}''');
}
}
String _escapeForDartSingleQuotedString(String raw) {
return raw
.replaceAll(r'\', r'\\')
.replaceAll(r'$', r'\$')
.replaceAll(r"'", r"\'");
}
/// Calculates the name of the codec class that will be generated for [api].
String _getCodecName(Api api) => '_${api.name}Codec';
/// Writes the codec that will be used by [api].
/// Example:
///
/// class FooCodec extends StandardMessageCodec {...}
void _writeCodec(Indent indent, String codecName, Api api, Root root) {
assert(getCodecClasses(api, root).isNotEmpty);
final Iterable<EnumeratedClass> codecClasses = getCodecClasses(api, root);
indent.newln();
indent.write('class $codecName extends $_standardMessageCodec');
indent.addScoped(' {', '}', () {
indent.writeln('const $codecName();');
indent.writeln('@override');
indent.write('void writeValue(WriteBuffer buffer, Object? value) ');
indent.addScoped('{', '}', () {
enumerate(codecClasses, (int index, final EnumeratedClass customClass) {
final String ifValue = 'if (value is ${customClass.name}) ';
if (index == 0) {
indent.write('');
}
indent.add(ifValue);
indent.addScoped('{', '} else ', () {
indent.writeln('buffer.putUint8(${customClass.enumeration});');
indent.writeln('writeValue(buffer, value.encode());');
}, addTrailingNewline: false);
});
indent.addScoped('{', '}', () {
indent.writeln('super.writeValue(buffer, value);');
});
});
indent.newln();
indent.writeln('@override');
indent.write('Object? readValueOfType(int type, ReadBuffer buffer) ');
indent.addScoped('{', '}', () {
indent.write('switch (type) ');
indent.addScoped('{', '}', () {
for (final EnumeratedClass customClass in codecClasses) {
indent.writeln('case ${customClass.enumeration}: ');
indent.nest(1, () {
indent.writeln(
'return ${customClass.name}.decode(readValue(buffer)!);');
});
}
indent.writeln('default:');
indent.nest(1, () {
indent.writeln('return super.readValueOfType(type, buffer);');
});
});
});
});
}
/// Creates a Dart type where all type arguments are [Objects].
String _makeGenericTypeArguments(TypeDeclaration type) {
return type.typeArguments.isNotEmpty
? '${type.baseName}<${type.typeArguments.map<String>((TypeDeclaration e) => 'Object?').join(', ')}>'
: _addGenericTypes(type);
}
/// Creates a `.cast<>` call for an type. Returns an empty string if the
/// type has no type arguments.
String _makeGenericCastCall(TypeDeclaration type) {
return type.typeArguments.isNotEmpty
? '.cast<${_flattenTypeArguments(type.typeArguments)}>()'
: '';
}
/// Returns an argument name that can be used in a context where it is possible to collide.
String _getSafeArgumentName(int count, NamedType field) =>
field.name.isEmpty ? 'arg$count' : 'arg_${field.name}';
/// Generates a parameter name if one isn't defined.
String _getParameterName(int count, NamedType field) =>
field.name.isEmpty ? 'arg$count' : field.name;
/// Generates the parameters code for [func]
/// Example: (func, _getParameterName) -> 'String? foo, int bar'
String _getMethodParameterSignature(Method func) {
String signature = '';
if (func.parameters.isEmpty) {
return signature;
}
final List<Parameter> requiredPositionalParams = func.parameters
.where((Parameter p) => p.isPositional && !p.isOptional)
.toList();
final List<Parameter> optionalPositionalParams = func.parameters
.where((Parameter p) => p.isPositional && p.isOptional)
.toList();
final List<Parameter> namedParams =
func.parameters.where((Parameter p) => !p.isPositional).toList();
String getParameterString(Parameter p) {
final String required = p.isRequired && !p.isPositional ? 'required ' : '';
final String type = _addGenericTypesNullable(p.type);
final String defaultValue =
p.defaultValue == null ? '' : ' = ${p.defaultValue}';
return '$required$type ${p.name}$defaultValue';
}
final String baseParameterString = requiredPositionalParams
.map((Parameter p) => getParameterString(p))
.join(', ');
final String optionalParameterString = optionalPositionalParams
.map((Parameter p) => getParameterString(p))
.join(', ');
final String namedParameterString =
namedParams.map((Parameter p) => getParameterString(p)).join(', ');
// Parameter lists can end with either named or optional positional parameters, but not both.
if (requiredPositionalParams.isNotEmpty) {
signature = baseParameterString;
}
final String trailingComma =
optionalPositionalParams.isNotEmpty || namedParams.isNotEmpty ? ',' : '';
final String baseParams =
signature.isNotEmpty ? '$signature$trailingComma ' : '';
if (optionalPositionalParams.isNotEmpty) {
final String trailingComma =
requiredPositionalParams.length + optionalPositionalParams.length > 2
? ','
: '';
return '$baseParams[$optionalParameterString$trailingComma]';
}
if (namedParams.isNotEmpty) {
final String trailingComma =
requiredPositionalParams.length + namedParams.length > 2 ? ',' : '';
return '$baseParams{$namedParameterString$trailingComma}';
}
return signature;
}
/// Converts a [List] of [TypeDeclaration]s to a comma separated [String] to be
/// used in Dart code.
String _flattenTypeArguments(List<TypeDeclaration> args) {
return args
.map<String>((TypeDeclaration arg) => arg.typeArguments.isEmpty
? '${arg.baseName}?'
: '${arg.baseName}<${_flattenTypeArguments(arg.typeArguments)}>?')
.join(', ');
}
/// Creates the type declaration for use in Dart code from a [NamedType] making sure
/// that type arguments are used for primitive generic types.
String _addGenericTypes(TypeDeclaration type) {
final List<TypeDeclaration> typeArguments = type.typeArguments;
switch (type.baseName) {
case 'List':
return (typeArguments.isEmpty)
? 'List<Object?>'
: 'List<${_flattenTypeArguments(typeArguments)}>';
case 'Map':
return (typeArguments.isEmpty)
? 'Map<Object?, Object?>'
: 'Map<${_flattenTypeArguments(typeArguments)}>';
default:
return type.baseName;
}
}
String _addGenericTypesNullable(TypeDeclaration type) {
final String genericType = _addGenericTypes(type);
return type.isNullable ? '$genericType?' : genericType;
}
/// Converts [inputPath] to a posix absolute path.
String _posixify(String inputPath) {
final path.Context context = path.Context(style: path.Style.posix);
return context.fromUri(path.toUri(path.absolute(inputPath)));
}