blob: 72456e681e8fb9b51f851d0bbd1d887a81245a1e [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 'dart:io';
import 'ast.dart';
import 'functional.dart';
import 'generator.dart';
import 'generator_tools.dart';
import 'pigeon_lib.dart'
show Error, PigeonOptions, TaskQueueType, lineReader, openSink;
/// 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,
});
/// 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;
/// 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>?,
);
}
/// 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!,
};
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()));
}
}
/// A [Generator] that generates Kotlin source code.
class KotlinGenerator implements Generator {
/// Constructor for [KotlinGenerator].
const KotlinGenerator();
@override
void generate(StringSink sink, PigeonOptions options, Root root) {
KotlinOptions kotlinOptions =
options.kotlinOptions ?? const KotlinOptions();
kotlinOptions = kotlinOptions.merge(KotlinOptions(
copyrightHeader: options.copyrightHeader != null
? lineReader(options.copyrightHeader!)
: null));
generateKotlin(kotlinOptions, root, sink);
}
@override
IOSink? shouldGenerate(PigeonOptions options) => openSink(options.kotlinOut);
@override
List<Error> validate(PigeonOptions options, Root root) => <Error>[];
}
/// Calculates the name of the codec that will be generated for [api].
String _getCodecName(Api api) => '${api.name}Codec';
/// 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.scoped('{', '}', () {
indent.write(
'override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? ');
indent.scoped('{', '}', () {
indent.write('return when (type) ');
indent.scoped('{', '}', () {
for (final EnumeratedClass customClass in codecClasses) {
indent.write('${customClass.enumeration}.toByte() -> ');
indent.scoped('{', '}', () {
indent.write('return (readValue(buffer) as? List<Any?>)?.let ');
indent.scoped('{', '}', () {
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.scoped('{', '}', () {
for (final EnumeratedClass customClass in codecClasses) {
indent.write('is ${customClass.name} -> ');
indent.scoped('{', '}', () {
indent.writeln('stream.write(${customClass.enumeration})');
indent.writeln('writeValue(stream, value.toList())');
});
}
indent.writeln('else -> super.writeValue(stream, value)');
});
});
});
}
/// 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) {...}
/// }
/// }
void _writeHostApi(Indent indent, Api api, Root root) {
assert(api.location == ApiLocation.host);
final String apiName = api.name;
final bool isCustomCodec = getCodecClasses(api, root).isNotEmpty;
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.scoped('{', '}', () {
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);
addDocumentationComments(
indent, method.documentationComments, _docCommentSpec);
if (method.isAsynchronous) {
argSignature.add('callback: ($returnType) -> 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.addln('');
indent.write('companion object ');
indent.scoped('{', '}', () {
indent.writeln('/** The codec used by $apiName. */');
indent.write('val codec: MessageCodec<Any?> by lazy ');
indent.scoped('{', '}', () {
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.scoped('{', '}', () {
for (final Method method in api.methods) {
indent.write('run ');
indent.scoped('{', '}', () {
String? taskQueue;
if (method.taskQueueType != TaskQueueType.serial) {
taskQueue = 'taskQueue';
indent.writeln(
'val $taskQueue = binaryMessenger.makeBackgroundTaskQueue()');
}
final String channelName = makeChannelName(api, method);
indent.write(
'val channel = BasicMessageChannel<Any?>(binaryMessenger, "$channelName", codec');
if (taskQueue != null) {
indent.addln(', $taskQueue)');
} else {
indent.addln(')');
}
indent.write('if (api != null) ');
indent.scoped('{', '}', () {
final String messageVarName =
method.arguments.isNotEmpty ? 'message' : '_';
indent.write('channel.setMessageHandler ');
indent.scoped('{ $messageVarName, reply ->', '}', () {
indent.writeln('var wrapped = listOf<Any?>()');
indent.write('try ');
indent.scoped('{', '}', () {
final List<String> methodArgument = <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)}');
methodArgument.add(argName);
});
}
final String call =
'api.${method.name}(${methodArgument.join(', ')})';
if (method.isAsynchronous) {
indent.write('$call ');
final String resultValue =
method.returnType.isVoid ? 'null' : 'it';
indent.scoped('{', '}', () {
indent.writeln('reply.reply(wrapResult($resultValue))');
});
} else if (method.returnType.isVoid) {
indent.writeln(call);
indent.writeln('wrapped = listOf<Any?>(null)');
} else {
indent.writeln('wrapped = listOf<Any?>($call)');
}
}, addTrailingNewline: false);
indent.add(' catch (exception: Error) ');
indent.scoped('{', '}', () {
indent.writeln('wrapped = wrapError(exception)');
if (method.isAsynchronous) {
indent.writeln('reply.reply(wrapped)');
}
});
if (!method.isAsynchronous) {
indent.writeln('reply.reply(wrapped)');
}
});
}, addTrailingNewline: false);
indent.scoped(' else {', '}', () {
indent.writeln('channel.setMessageHandler(null)');
});
});
}
});
});
});
}
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.
String _getSafeArgumentName(int count, NamedType argument) =>
'${_getArgumentName(count, argument)}Arg';
/// Writes the code for a flutter [Api], [api].
/// Example:
/// class Foo(private val binaryMessenger: BinaryMessenger) {
/// fun add(x: Int, y: Int, callback: (Int?) -> Unit) {...}
/// }
void _writeFlutterApi(Indent indent, Api api, Root root) {
assert(api.location == ApiLocation.flutter);
final bool isCustomCodec = getCodecClasses(api, root).isNotEmpty;
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.scoped('{', '}', () {
indent.write('companion object ');
indent.scoped('{', '}', () {
indent.writeln('/** The codec used by $apiName. */');
indent.write('val codec: MessageCodec<Any?> by lazy ');
indent.scoped('{', '}', () {
if (isCustomCodec) {
indent.writeln(_getCodecName(api));
} else {
indent.writeln('StandardMessageCodec()');
}
});
});
for (final Method func in api.methods) {
final String channelName = makeChannelName(api, func);
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);
sendArgument = 'listOf(${argNames.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.scoped('{', '}', () {
const String channel = 'channel';
indent.writeln(
'val $channel = BasicMessageChannel<Any?>(binaryMessenger, "$channelName", codec)');
indent.write('$channel.send($sendArgument) ');
if (func.returnType.isVoid) {
indent.scoped('{', '}', () {
indent.writeln('callback()');
});
} else {
final String forceUnwrap = func.returnType.isNullable ? '?' : '';
indent.scoped('{', '}', () {
indent.writeln('val result = it as$forceUnwrap $returnType');
indent.writeln('callback(result)');
});
}
});
}
});
}
String _castForceUnwrap(String value, TypeDeclaration type, Root root) {
if (isEnum(root, type)) {
final String forceUnwrap = type.isNullable ? '' : '!!';
final String nullableConditionPrefix =
type.isNullable ? '$value == null ? null : ' : '';
return '$nullableConditionPrefix${_kotlinTypeForDartType(type)}.ofRaw($value as Int)$forceUnwrap';
} else {
final String castUnwrap = type.isNullable ? '?' : '';
// 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 { if (it is Int) it.toLong() else it as$castUnwrap Long }';
} else {
return '$value as$castUnwrap ${_kotlinTypeForDartType(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';
}
/// Generates the ".kotlin" file for the AST represented by [root] to [sink] with the
/// provided [options].
void generateKotlin(KotlinOptions options, Root root, StringSink sink) {
final Set<String> rootClassNameSet =
root.classes.map((Class x) => x.name).toSet();
final Set<String> rootEnumNameSet =
root.enums.map((Enum x) => x.name).toSet();
final Indent indent = Indent(sink);
HostDatatype getHostDatatype(NamedType field) {
return getFieldHostDatatype(field, root.classes, root.enums,
(TypeDeclaration x) => _kotlinTypeForBuiltinDartType(x));
}
void writeHeader() {
if (options.copyrightHeader != null) {
addLines(indent, options.copyrightHeader!, linePrefix: '// ');
}
indent.writeln('// $generatedCodeWarning');
indent.writeln('// $seeAlsoWarning');
}
void writeImports() {
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');
}
void writeEnum(Enum anEnum) {
addDocumentationComments(
indent, anEnum.documentationComments, _docCommentSpec);
indent.write('enum class ${anEnum.name}(val raw: Int) ');
indent.scoped('{', '}', () {
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.writeln('');
indent.write('companion object ');
indent.scoped('{', '}', () {
indent.write('fun ofRaw(raw: Int): ${anEnum.name}? ');
indent.scoped('{', '}', () {
indent.writeln('return values().firstOrNull { it.raw == raw }');
});
});
});
}
void writeDataClass(Class klass) {
void writeField(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);
}
void writeToList() {
indent.write('fun toList(): List<Any?> ');
indent.scoped('{', '}', () {
indent.write('return listOf<Any?>');
indent.scoped('(', ')', () {
for (final NamedType field in getFieldsInSerializationOrder(klass)) {
final HostDatatype hostDatatype = getHostDatatype(field);
String toWriteValue = '';
final String fieldName = field.name;
if (!hostDatatype.isBuiltin &&
rootClassNameSet.contains(field.type.baseName)) {
toWriteValue = '$fieldName?.toList()';
} else if (!hostDatatype.isBuiltin &&
rootEnumNameSet.contains(field.type.baseName)) {
toWriteValue = '$fieldName?.raw';
} else {
toWriteValue = fieldName;
}
indent.writeln('$toWriteValue,');
}
});
});
}
void writeFromList() {
final String className = klass.name;
indent.write('companion object ');
indent.scoped('{', '}', () {
indent.writeln('@Suppress("UNCHECKED_CAST")');
indent.write('fun fromList(list: List<Any?>): $className ');
indent.scoped('{', '}', () {
enumerate(getFieldsInSerializationOrder(klass),
(int index, final NamedType field) {
final HostDatatype hostDatatype = getHostDatatype(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 &&
rootClassNameSet.contains(field.type.baseName)) {
indent.write('val ${field.name}: $fieldType? = ');
indent.add('($listValue as? List<Any?>)?.let ');
indent.scoped('{', '}', () {
indent.writeln('$fieldType.fromList(it)');
});
} else if (!hostDatatype.isBuiltin &&
rootEnumNameSet.contains(field.type.baseName)) {
indent.write('val ${field.name}: $fieldType? = ');
indent.add('($listValue as? Int)?.let ');
indent.scoped('{', '}', () {
indent.writeln('$fieldType.ofRaw(it)');
});
} else if (isInt) {
indent.write('val ${field.name} = $listValue');
indent.addln(
'.let { if (it is Int) it.toLong() else it as? Long }');
} else {
indent.writeln('val ${field.name} = $listValue as? $fieldType');
}
} else {
if (!hostDatatype.isBuiltin &&
rootClassNameSet.contains(field.type.baseName)) {
indent.writeln(
'val ${field.name} = $fieldType.fromList($listValue as List<Any?>)');
} else if (!hostDatatype.isBuiltin &&
rootEnumNameSet.contains(field.type.baseName)) {
indent.write(
'val ${field.name} = $fieldType.ofRaw($listValue as Int)!!');
} else {
indent.writeln('val ${field.name} = $listValue as $fieldType');
}
}
});
indent.writeln('');
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(')');
});
});
}
const List<String> generatedMessages = <String>[
' Generated class from Pigeon that represents data sent in messages.'
];
addDocumentationComments(
indent, klass.documentationComments, _docCommentSpec,
generatorComments: generatedMessages);
indent.write('data class ${klass.name} ');
indent.scoped('(', '', () {
for (final NamedType element in getFieldsInSerializationOrder(klass)) {
writeField(element);
if (getFieldsInSerializationOrder(klass).last != element) {
indent.addln(',');
} else {
indent.addln('');
}
}
});
indent.scoped(') {', '}', () {
writeFromList();
writeToList();
});
}
void writeApi(Api api) {
if (api.location == ApiLocation.host) {
_writeHostApi(indent, api, root);
} else if (api.location == ApiLocation.flutter) {
_writeFlutterApi(indent, api, root);
}
}
void writeWrapResult() {
indent.write('private fun wrapResult(result: Any?): List<Any?> ');
indent.scoped('{', '}', () {
indent.writeln('return listOf(result)');
});
}
void writeWrapError() {
indent.write('private fun wrapError(exception: Throwable): List<Any> ');
indent.scoped('{', '}', () {
indent.write('return ');
indent.scoped('listOf<Any>(', ')', () {
indent.writeln('exception.javaClass.simpleName,');
indent.writeln('exception.toString(),');
indent.writeln(
'"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)');
});
});
}
writeHeader();
indent.addln('');
if (options.package != null) {
indent.writeln('package ${options.package}');
}
indent.addln('');
writeImports();
indent.addln('');
indent.writeln('/** Generated class from Pigeon. */');
for (final Enum anEnum in root.enums) {
indent.writeln('');
writeEnum(anEnum);
}
for (final Class klass in root.classes) {
indent.addln('');
writeDataClass(klass);
}
if (root.apis.any((Api api) =>
api.location == ApiLocation.host &&
api.methods.any((Method it) => it.isAsynchronous))) {
indent.addln('');
}
for (final Api api in root.apis) {
if (getCodecClasses(api, root).isNotEmpty) {
_writeCodec(indent, api, root);
indent.addln('');
}
writeApi(api);
}
indent.addln('');
writeWrapResult();
indent.addln('');
writeWrapError();
}