blob: da6faf40470969d97f9f60005d6000de4a075338 [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 'ast.dart';
import 'functional.dart';
import 'generator.dart';
import 'generator_tools.dart';
import 'pigeon_lib.dart' show TaskQueueType;
/// Documentation open symbol.
const String _docCommentPrefix = '/**';
/// Documentation continuation symbol.
const String _docCommentContinuation = ' *';
/// Documentation close symbol.
const String _docCommentSuffix = ' */';
/// Documentation comment spec.
const DocumentCommentSpecification _docCommentSpec =
DocumentCommentSpecification(
_docCommentPrefix,
closeCommentToken: _docCommentSuffix,
blockContinuationToken: _docCommentContinuation,
);
/// Options that control how Kotlin code will be generated.
class KotlinOptions {
/// Creates a [KotlinOptions] object
const KotlinOptions({
this.package,
this.copyrightHeader,
this.errorClassName,
});
/// The package where the generated class will live.
final String? package;
/// A copyright header that will get prepended to generated code.
final Iterable<String>? copyrightHeader;
/// The name of the error class used for passing custom error parameters.
final String? errorClassName;
/// Creates a [KotlinOptions] from a Map representation where:
/// `x = KotlinOptions.fromMap(x.toMap())`.
static KotlinOptions fromMap(Map<String, Object> map) {
return KotlinOptions(
package: map['package'] as String?,
copyrightHeader: map['copyrightHeader'] as Iterable<String>?,
errorClassName: map['errorClassName'] as String?,
);
}
/// Converts a [KotlinOptions] to a Map representation where:
/// `x = KotlinOptions.fromMap(x.toMap())`.
Map<String, Object> toMap() {
final Map<String, Object> result = <String, Object>{
if (package != null) 'package': package!,
if (copyrightHeader != null) 'copyrightHeader': copyrightHeader!,
if (errorClassName != null) 'errorClassName': errorClassName!,
};
return result;
}
/// Overrides any non-null parameters from [options] into this to make a new
/// [KotlinOptions].
KotlinOptions merge(KotlinOptions options) {
return KotlinOptions.fromMap(mergeMaps(toMap(), options.toMap()));
}
}
/// Class that manages all Kotlin code generation.
class KotlinGenerator extends StructuredGenerator<KotlinOptions> {
/// Instantiates a Kotlin Generator.
const KotlinGenerator();
@override
void writeFilePrologue(
KotlinOptions generatorOptions,
Root root,
Indent indent, {
required String dartPackageName,
}) {
if (generatorOptions.copyrightHeader != null) {
addLines(indent, generatorOptions.copyrightHeader!, linePrefix: '// ');
}
indent.writeln('// ${getGeneratedCodeWarning()}');
indent.writeln('// $seeAlsoWarning');
}
@override
void writeFileImports(
KotlinOptions generatorOptions,
Root root,
Indent indent, {
required String dartPackageName,
}) {
indent.newln();
if (generatorOptions.package != null) {
indent.writeln('package ${generatorOptions.package}');
}
indent.newln();
indent.writeln('import android.util.Log');
indent.writeln('import io.flutter.plugin.common.BasicMessageChannel');
indent.writeln('import io.flutter.plugin.common.BinaryMessenger');
indent.writeln('import io.flutter.plugin.common.MessageCodec');
indent.writeln('import io.flutter.plugin.common.StandardMessageCodec');
indent.writeln('import java.io.ByteArrayOutputStream');
indent.writeln('import java.nio.ByteBuffer');
}
@override
void writeEnum(
KotlinOptions generatorOptions,
Root root,
Indent indent,
Enum anEnum, {
required String dartPackageName,
}) {
indent.newln();
addDocumentationComments(
indent, anEnum.documentationComments, _docCommentSpec);
indent.write('enum class ${anEnum.name}(val raw: Int) ');
indent.addScoped('{', '}', () {
enumerate(anEnum.members, (int index, final EnumMember member) {
addDocumentationComments(
indent, member.documentationComments, _docCommentSpec);
indent.write('${member.name.toUpperCase()}($index)');
if (index != anEnum.members.length - 1) {
indent.addln(',');
} else {
indent.addln(';');
}
});
indent.newln();
indent.write('companion object ');
indent.addScoped('{', '}', () {
indent.write('fun ofRaw(raw: Int): ${anEnum.name}? ');
indent.addScoped('{', '}', () {
indent.writeln('return values().firstOrNull { it.raw == raw }');
});
});
});
}
@override
void writeDataClass(
KotlinOptions generatorOptions,
Root root,
Indent indent,
Class klass, {
required String dartPackageName,
}) {
final Set<String> customClassNames =
root.classes.map((Class x) => x.name).toSet();
final Set<String> customEnumNames =
root.enums.map((Enum x) => x.name).toSet();
const List<String> generatedMessages = <String>[
' Generated class from Pigeon that represents data sent in messages.'
];
indent.newln();
addDocumentationComments(
indent, klass.documentationComments, _docCommentSpec,
generatorComments: generatedMessages);
indent.write('data class ${klass.name} ');
indent.addScoped('(', '', () {
for (final NamedType element in getFieldsInSerializationOrder(klass)) {
_writeClassField(indent, element);
if (getFieldsInSerializationOrder(klass).last != element) {
indent.addln(',');
} else {
indent.newln();
}
}
});
indent.addScoped(') {', '}', () {
writeClassDecode(
generatorOptions,
root,
indent,
klass,
customClassNames,
customEnumNames,
dartPackageName: dartPackageName,
);
writeClassEncode(
generatorOptions,
root,
indent,
klass,
customClassNames,
customEnumNames,
dartPackageName: dartPackageName,
);
});
}
@override
void writeClassEncode(
KotlinOptions generatorOptions,
Root root,
Indent indent,
Class klass,
Set<String> customClassNames,
Set<String> customEnumNames, {
required String dartPackageName,
}) {
indent.write('fun toList(): List<Any?> ');
indent.addScoped('{', '}', () {
indent.write('return listOf<Any?>');
indent.addScoped('(', ')', () {
for (final NamedType field in getFieldsInSerializationOrder(klass)) {
final HostDatatype hostDatatype = _getHostDatatype(root, field);
String toWriteValue = '';
final String fieldName = field.name;
final String safeCall = field.type.isNullable ? '?' : '';
if (!hostDatatype.isBuiltin &&
customClassNames.contains(field.type.baseName)) {
toWriteValue = '$fieldName$safeCall.toList()';
} else if (!hostDatatype.isBuiltin &&
customEnumNames.contains(field.type.baseName)) {
toWriteValue = '$fieldName$safeCall.raw';
} else {
toWriteValue = fieldName;
}
indent.writeln('$toWriteValue,');
}
});
});
}
@override
void writeClassDecode(
KotlinOptions generatorOptions,
Root root,
Indent indent,
Class klass,
Set<String> customClassNames,
Set<String> customEnumNames, {
required String dartPackageName,
}) {
final String className = klass.name;
indent.write('companion object ');
indent.addScoped('{', '}', () {
indent.writeln('@Suppress("UNCHECKED_CAST")');
indent.write('fun fromList(list: List<Any?>): $className ');
indent.addScoped('{', '}', () {
enumerate(getFieldsInSerializationOrder(klass),
(int index, final NamedType field) {
final HostDatatype hostDatatype = _getHostDatatype(root, field);
// The StandardMessageCodec can give us [Integer, Long] for
// a Dart 'int'. To keep things simple we just use 64bit
// longs in Pigeon with Kotlin.
final bool isInt = field.type.baseName == 'int';
final String listValue = 'list[$index]';
final String fieldType = _kotlinTypeForDartType(field.type);
if (field.type.isNullable) {
if (!hostDatatype.isBuiltin &&
customClassNames.contains(field.type.baseName)) {
indent.write('val ${field.name}: $fieldType? = ');
indent.add('($listValue as List<Any?>?)?.let ');
indent.addScoped('{', '}', () {
indent.writeln('$fieldType.fromList(it)');
});
} else if (!hostDatatype.isBuiltin &&
customEnumNames.contains(field.type.baseName)) {
indent.write('val ${field.name}: $fieldType? = ');
indent.add('($listValue as Int?)?.let ');
indent.addScoped('{', '}', () {
indent.writeln('$fieldType.ofRaw(it)');
});
} else if (isInt) {
indent.write('val ${field.name} = $listValue');
indent.addln(
'.let { ${_cast(root, indent, listValue, type: field.type)} }');
} else {
indent.writeln(
'val ${field.name} = ${_cast(root, indent, listValue, type: field.type)}');
}
} else {
if (!hostDatatype.isBuiltin &&
customClassNames.contains(field.type.baseName)) {
indent.writeln(
'val ${field.name} = $fieldType.fromList($listValue as List<Any?>)');
} else if (!hostDatatype.isBuiltin &&
customEnumNames.contains(field.type.baseName)) {
indent.writeln(
'val ${field.name} = $fieldType.ofRaw($listValue as Int)!!');
} else if (isInt) {
indent.write('val ${field.name} = $listValue');
indent.addln(
'.let { ${_cast(root, indent, listValue, type: field.type)} }');
} else {
indent.writeln(
'val ${field.name} = ${_cast(root, indent, listValue, type: field.type)}');
}
}
});
indent.write('return $className(');
for (final NamedType field in getFieldsInSerializationOrder(klass)) {
final String comma =
getFieldsInSerializationOrder(klass).last == field ? '' : ', ';
indent.add('${field.name}$comma');
}
indent.addln(')');
});
});
}
void _writeClassField(Indent indent, NamedType field) {
addDocumentationComments(
indent, field.documentationComments, _docCommentSpec);
indent.write(
'val ${field.name}: ${_nullsafeKotlinTypeForDartType(field.type)}');
final String defaultNil = field.type.isNullable ? ' = null' : '';
indent.add(defaultNil);
}
@override
void writeApis(
KotlinOptions generatorOptions,
Root root,
Indent indent, {
required String dartPackageName,
}) {
if (root.apis.any((Api api) =>
api.location == ApiLocation.host &&
api.methods.any((Method it) => it.isAsynchronous))) {
indent.newln();
}
super.writeApis(generatorOptions, root, indent,
dartPackageName: dartPackageName);
}
/// Writes the code for a flutter [Api], [api].
/// Example:
/// class Foo(private val binaryMessenger: BinaryMessenger) {
/// fun add(x: Int, y: Int, callback: (Int?) -> Unit) {...}
/// }
@override
void writeFlutterApi(
KotlinOptions generatorOptions,
Root root,
Indent indent,
Api api, {
required String dartPackageName,
}) {
assert(api.location == ApiLocation.flutter);
final bool isCustomCodec = getCodecClasses(api, root).isNotEmpty;
if (isCustomCodec) {
_writeCodec(indent, api, root);
}
const List<String> generatedMessages = <String>[
' Generated class from Pigeon that represents Flutter messages that can be called from Kotlin.'
];
addDocumentationComments(indent, api.documentationComments, _docCommentSpec,
generatorComments: generatedMessages);
final String apiName = api.name;
indent.writeln('@Suppress("UNCHECKED_CAST")');
indent
.write('class $apiName(private val binaryMessenger: BinaryMessenger) ');
indent.addScoped('{', '}', () {
indent.write('companion object ');
indent.addScoped('{', '}', () {
indent.writeln('/** The codec used by $apiName. */');
indent.write('val codec: MessageCodec<Any?> by lazy ');
indent.addScoped('{', '}', () {
if (isCustomCodec) {
indent.writeln(_getCodecName(api));
} else {
indent.writeln('StandardMessageCodec()');
}
});
});
for (final Method func in api.methods) {
final String channelName = makeChannelName(api, func, dartPackageName);
final String returnType = func.returnType.isVoid
? ''
: _nullsafeKotlinTypeForDartType(func.returnType);
String sendArgument;
addDocumentationComments(
indent, func.documentationComments, _docCommentSpec);
if (func.arguments.isEmpty) {
indent.write('fun ${func.name}(callback: ($returnType) -> Unit) ');
sendArgument = 'null';
} else {
final Iterable<String> argTypes = func.arguments
.map((NamedType e) => _nullsafeKotlinTypeForDartType(e.type));
final Iterable<String> argNames =
indexMap(func.arguments, _getSafeArgumentName);
final Iterable<String> enumSafeArgNames = indexMap(
func.arguments,
(int count, NamedType type) =>
_getEnumSafeArgumentExpression(root, count, type));
sendArgument = 'listOf(${enumSafeArgNames.join(', ')})';
final String argsSignature = map2(argTypes, argNames,
(String type, String name) => '$name: $type').join(', ');
if (func.returnType.isVoid) {
indent.write(
'fun ${func.name}($argsSignature, callback: () -> Unit) ');
} else {
indent.write(
'fun ${func.name}($argsSignature, callback: ($returnType) -> Unit) ');
}
}
indent.addScoped('{', '}', () {
const String channel = 'channel';
indent.writeln(
'val $channel = BasicMessageChannel<Any?>(binaryMessenger, "$channelName", codec)');
indent.write('$channel.send($sendArgument) ');
if (func.returnType.isVoid) {
indent.addScoped('{', '}', () {
indent.writeln('callback()');
});
} else {
indent.addScoped('{', '}', () {
// Nullable enums require special handling.
if (isEnum(root, func.returnType) && func.returnType.isNullable) {
indent.writeScoped('val result = (it as Int?)?.let {', '}', () {
indent.writeln('${func.returnType.baseName}.ofRaw(it)');
});
} else {
indent.writeln(
'val result = ${_cast(root, indent, 'it', type: func.returnType)}');
}
indent.writeln('callback(result)');
});
}
});
}
});
}
/// Write the kotlin code that represents a host [Api], [api].
/// Example:
/// interface Foo {
/// Int add(x: Int, y: Int);
/// companion object {
/// fun setUp(binaryMessenger: BinaryMessenger, api: Api) {...}
/// }
/// }
///
@override
void writeHostApi(
KotlinOptions generatorOptions,
Root root,
Indent indent,
Api api, {
required String dartPackageName,
}) {
assert(api.location == ApiLocation.host);
final String apiName = api.name;
final bool isCustomCodec = getCodecClasses(api, root).isNotEmpty;
if (isCustomCodec) {
_writeCodec(indent, api, root);
}
const List<String> generatedMessages = <String>[
' Generated interface from Pigeon that represents a handler of messages from Flutter.'
];
addDocumentationComments(indent, api.documentationComments, _docCommentSpec,
generatorComments: generatedMessages);
indent.write('interface $apiName ');
indent.addScoped('{', '}', () {
for (final Method method in api.methods) {
final List<String> argSignature = <String>[];
if (method.arguments.isNotEmpty) {
final Iterable<String> argTypes = method.arguments
.map((NamedType e) => _nullsafeKotlinTypeForDartType(e.type));
final Iterable<String> argNames =
method.arguments.map((NamedType e) => e.name);
argSignature.addAll(
map2(argTypes, argNames, (String argType, String argName) {
return '$argName: $argType';
}));
}
final String returnType = method.returnType.isVoid
? ''
: _nullsafeKotlinTypeForDartType(method.returnType);
final String resultType =
method.returnType.isVoid ? 'Unit' : returnType;
addDocumentationComments(
indent, method.documentationComments, _docCommentSpec);
if (method.isAsynchronous) {
argSignature.add('callback: (Result<$resultType>) -> Unit');
indent.writeln('fun ${method.name}(${argSignature.join(', ')})');
} else if (method.returnType.isVoid) {
indent.writeln('fun ${method.name}(${argSignature.join(', ')})');
} else {
indent.writeln(
'fun ${method.name}(${argSignature.join(', ')}): $returnType');
}
}
indent.newln();
indent.write('companion object ');
indent.addScoped('{', '}', () {
indent.writeln('/** The codec used by $apiName. */');
indent.write('val codec: MessageCodec<Any?> by lazy ');
indent.addScoped('{', '}', () {
if (isCustomCodec) {
indent.writeln(_getCodecName(api));
} else {
indent.writeln('StandardMessageCodec()');
}
});
indent.writeln(
'/** Sets up an instance of `$apiName` to handle messages through the `binaryMessenger`. */');
indent.writeln('@Suppress("UNCHECKED_CAST")');
indent.write(
'fun setUp(binaryMessenger: BinaryMessenger, api: $apiName?) ');
indent.addScoped('{', '}', () {
for (final Method method in api.methods) {
indent.write('run ');
indent.addScoped('{', '}', () {
String? taskQueue;
if (method.taskQueueType != TaskQueueType.serial) {
taskQueue = 'taskQueue';
indent.writeln(
'val $taskQueue = binaryMessenger.makeBackgroundTaskQueue()');
}
final String channelName =
makeChannelName(api, method, dartPackageName);
indent.write(
'val channel = BasicMessageChannel<Any?>(binaryMessenger, "$channelName", codec');
if (taskQueue != null) {
indent.addln(', $taskQueue)');
} else {
indent.addln(')');
}
indent.write('if (api != null) ');
indent.addScoped('{', '}', () {
final String messageVarName =
method.arguments.isNotEmpty ? 'message' : '_';
indent.write('channel.setMessageHandler ');
indent.addScoped('{ $messageVarName, reply ->', '}', () {
final List<String> methodArguments = <String>[];
if (method.arguments.isNotEmpty) {
indent.writeln('val args = message as List<Any?>');
enumerate(method.arguments, (int index, NamedType arg) {
final String argName = _getSafeArgumentName(index, arg);
final String argIndex = 'args[$index]';
indent.writeln(
'val $argName = ${_castForceUnwrap(argIndex, arg.type, root, indent)}');
methodArguments.add(argName);
});
}
final String call =
'api.${method.name}(${methodArguments.join(', ')})';
if (method.isAsynchronous) {
indent.write('$call ');
final String resultType = method.returnType.isVoid
? 'Unit'
: _nullsafeKotlinTypeForDartType(method.returnType);
indent.addScoped('{ result: Result<$resultType> ->', '}',
() {
indent.writeln('val error = result.exceptionOrNull()');
indent.writeScoped('if (error != null) {', '}', () {
indent.writeln('reply.reply(wrapError(error))');
}, addTrailingNewline: false);
indent.addScoped(' else {', '}', () {
final String enumTagNullablePrefix =
method.returnType.isNullable ? '?' : '!!';
final String enumTag = isEnum(root, method.returnType)
? '$enumTagNullablePrefix.raw'
: '';
if (method.returnType.isVoid) {
indent.writeln('reply.reply(wrapResult(null))');
} else {
indent.writeln('val data = result.getOrNull()');
indent
.writeln('reply.reply(wrapResult(data$enumTag))');
}
});
});
} else {
indent.writeln('var wrapped: List<Any?>');
indent.write('try ');
indent.addScoped('{', '}', () {
if (method.returnType.isVoid) {
indent.writeln(call);
indent.writeln('wrapped = listOf<Any?>(null)');
} else {
String enumTag = '';
if (isEnum(root, method.returnType)) {
final String safeUnwrap =
method.returnType.isNullable ? '?' : '';
enumTag = '$safeUnwrap.raw';
}
indent.writeln('wrapped = listOf<Any?>($call$enumTag)');
}
}, addTrailingNewline: false);
indent.add(' catch (exception: Throwable) ');
indent.addScoped('{', '}', () {
indent.writeln('wrapped = wrapError(exception)');
});
indent.writeln('reply.reply(wrapped)');
}
});
}, addTrailingNewline: false);
indent.addScoped(' else {', '}', () {
indent.writeln('channel.setMessageHandler(null)');
});
});
}
});
});
});
}
/// Writes the codec class that will be used by [api].
/// Example:
/// private static class FooCodec extends StandardMessageCodec {...}
void _writeCodec(Indent indent, Api api, Root root) {
assert(getCodecClasses(api, root).isNotEmpty);
final Iterable<EnumeratedClass> codecClasses = getCodecClasses(api, root);
final String codecName = _getCodecName(api);
indent.writeln('@Suppress("UNCHECKED_CAST")');
indent.write('private object $codecName : StandardMessageCodec() ');
indent.addScoped('{', '}', () {
indent.write(
'override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? ');
indent.addScoped('{', '}', () {
indent.write('return when (type) ');
indent.addScoped('{', '}', () {
for (final EnumeratedClass customClass in codecClasses) {
indent.write('${customClass.enumeration}.toByte() -> ');
indent.addScoped('{', '}', () {
indent.write('return (readValue(buffer) as? List<Any?>)?.let ');
indent.addScoped('{', '}', () {
indent.writeln('${customClass.name}.fromList(it)');
});
});
}
indent.writeln('else -> super.readValueOfType(type, buffer)');
});
});
indent.write(
'override fun writeValue(stream: ByteArrayOutputStream, value: Any?) ');
indent.writeScoped('{', '}', () {
indent.write('when (value) ');
indent.addScoped('{', '}', () {
for (final EnumeratedClass customClass in codecClasses) {
indent.write('is ${customClass.name} -> ');
indent.addScoped('{', '}', () {
indent.writeln('stream.write(${customClass.enumeration})');
indent.writeln('writeValue(stream, value.toList())');
});
}
indent.writeln('else -> super.writeValue(stream, value)');
});
});
});
indent.newln();
}
void _writeWrapResult(Indent indent) {
indent.newln();
indent.write('private fun wrapResult(result: Any?): List<Any?> ');
indent.addScoped('{', '}', () {
indent.writeln('return listOf(result)');
});
}
void _writeWrapError(KotlinOptions generatorOptions, Indent indent) {
indent.newln();
indent.write('private fun wrapError(exception: Throwable): List<Any?> ');
indent.addScoped('{', '}', () {
indent.write(
'if (exception is ${generatorOptions.errorClassName ?? "FlutterError"}) ');
indent.addScoped('{', '}', () {
indent.write('return ');
indent.addScoped('listOf(', ')', () {
indent.writeln('exception.code,');
indent.writeln('exception.message,');
indent.writeln('exception.details');
});
}, addTrailingNewline: false);
indent.addScoped(' else {', '}', () {
indent.write('return ');
indent.addScoped('listOf(', ')', () {
indent.writeln('exception.javaClass.simpleName,');
indent.writeln('exception.toString(),');
indent.writeln(
'"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)');
});
});
});
}
void _writeErrorClass(KotlinOptions generatorOptions, Indent indent) {
indent.newln();
indent.writeln('/**');
indent.writeln(
' * Error class for passing custom error details to Flutter via a thrown PlatformException.');
indent.writeln(' * @property code The error code.');
indent.writeln(' * @property message The error message.');
indent.writeln(
' * @property details The error details. Must be a datatype supported by the api codec.');
indent.writeln(' */');
indent.write('class ${generatorOptions.errorClassName ?? "FlutterError"} ');
indent.addScoped('(', ')', () {
indent.writeln('val code: String,');
indent.writeln('override val message: String? = null,');
indent.writeln('val details: Any? = null');
}, addTrailingNewline: false);
indent.addln(' : Throwable()');
}
@override
void writeGeneralUtilities(
KotlinOptions generatorOptions,
Root root,
Indent indent, {
required String dartPackageName,
}) {
_writeWrapResult(indent);
_writeWrapError(generatorOptions, indent);
_writeErrorClass(generatorOptions, indent);
}
}
HostDatatype _getHostDatatype(Root root, NamedType field) {
return getFieldHostDatatype(field, root.classes, root.enums,
(TypeDeclaration x) => _kotlinTypeForBuiltinDartType(x));
}
/// Calculates the name of the codec that will be generated for [api].
String _getCodecName(Api api) => '${api.name}Codec';
String _getArgumentName(int count, NamedType argument) =>
argument.name.isEmpty ? 'arg$count' : argument.name;
/// Returns an argument name that can be used in a context where it is possible to collide
/// and append `.index` to enums.
String _getEnumSafeArgumentExpression(
Root root, int count, NamedType argument) {
if (isEnum(root, argument.type)) {
return argument.type.isNullable
? '${_getArgumentName(count, argument)}Arg?.raw'
: '${_getArgumentName(count, argument)}Arg.raw';
}
return '${_getArgumentName(count, argument)}Arg';
}
/// Returns an argument name that can be used in a context where it is possible to collide.
String _getSafeArgumentName(int count, NamedType argument) =>
'${_getArgumentName(count, argument)}Arg';
String _castForceUnwrap(
String value, TypeDeclaration type, Root root, Indent indent) {
if (isEnum(root, type)) {
final String forceUnwrap = type.isNullable ? '' : '!!';
final String nullableConditionPrefix =
type.isNullable ? 'if ($value == null) null else ' : '';
return '$nullableConditionPrefix${_kotlinTypeForDartType(type)}.ofRaw($value as Int)$forceUnwrap';
} else {
// The StandardMessageCodec can give us [Integer, Long] for
// a Dart 'int'. To keep things simple we just use 64bit
// longs in Pigeon with Kotlin.
if (type.baseName == 'int') {
return '$value.let { ${_cast(root, indent, value, type: type)} }';
} else {
return _cast(root, indent, value, type: type);
}
}
}
/// Converts a [List] of [TypeDeclaration]s to a comma separated [String] to be
/// used in Kotlin code.
String _flattenTypeArguments(List<TypeDeclaration> args) {
return args.map(_kotlinTypeForDartType).join(', ');
}
String _kotlinTypeForBuiltinGenericDartType(TypeDeclaration type) {
if (type.typeArguments.isEmpty) {
switch (type.baseName) {
case 'List':
return 'List<Any?>';
case 'Map':
return 'Map<Any, Any?>';
default:
return 'Any';
}
} else {
switch (type.baseName) {
case 'List':
return 'List<${_nullsafeKotlinTypeForDartType(type.typeArguments.first)}>';
case 'Map':
return 'Map<${_nullsafeKotlinTypeForDartType(type.typeArguments.first)}, ${_nullsafeKotlinTypeForDartType(type.typeArguments.last)}>';
default:
return '${type.baseName}<${_flattenTypeArguments(type.typeArguments)}>';
}
}
}
String? _kotlinTypeForBuiltinDartType(TypeDeclaration type) {
const Map<String, String> kotlinTypeForDartTypeMap = <String, String>{
'void': 'Void',
'bool': 'Boolean',
'String': 'String',
'int': 'Long',
'double': 'Double',
'Uint8List': 'ByteArray',
'Int32List': 'IntArray',
'Int64List': 'LongArray',
'Float32List': 'FloatArray',
'Float64List': 'DoubleArray',
'Object': 'Any',
};
if (kotlinTypeForDartTypeMap.containsKey(type.baseName)) {
return kotlinTypeForDartTypeMap[type.baseName];
} else if (type.baseName == 'List' || type.baseName == 'Map') {
return _kotlinTypeForBuiltinGenericDartType(type);
} else {
return null;
}
}
String _kotlinTypeForDartType(TypeDeclaration type) {
return _kotlinTypeForBuiltinDartType(type) ?? type.baseName;
}
String _nullsafeKotlinTypeForDartType(TypeDeclaration type) {
final String nullSafe = type.isNullable ? '?' : '';
return '${_kotlinTypeForDartType(type)}$nullSafe';
}
/// Returns an expression to cast [variable] to [kotlinType].
String _cast(Root root, Indent indent, String variable,
{required TypeDeclaration type}) {
// Special-case Any, since no-op casts cause warnings.
final String typeString = _kotlinTypeForDartType(type);
if (type.isNullable && typeString == 'Any') {
return variable;
}
if (typeString == 'Int' || typeString == 'Long') {
return _castInt(type.isNullable);
}
if (isEnum(root, type)) {
if (type.isNullable) {
return '($variable as Int?)?.let {\n'
'${indent.str} $typeString.ofRaw(it)\n'
'${indent.str}}';
}
return '${type.baseName}.ofRaw($variable as Int)!!';
}
return '$variable as ${_nullsafeKotlinTypeForDartType(type)}';
}
String _castInt(bool isNullable) {
final String nullability = isNullable ? '?' : '';
return 'if (it is Int) it.toLong() else it as Long$nullability';
}