blob: 3b80ca6e06e96183bed9cf6e3a1e91daa9c75efe [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 'dart:mirrors';
import 'package:args/args.dart';
import 'package:path/path.dart' as path;
import 'package:path/path.dart';
import 'package:pigeon/java_generator.dart';
import 'ast.dart';
import 'dart_generator.dart';
import 'generator_tools.dart';
import 'objc_generator.dart';
const List<String> _validTypes = <String>[
'String',
'bool',
'int',
'double',
'Uint8List',
'Int32List',
'Int64List',
'Float64List',
'List',
'Map',
];
class _Asynchronous {
const _Asynchronous();
}
/// Metadata to annotate a Api method as asynchronous
const _Asynchronous async = _Asynchronous();
/// Metadata to annotate a Pigeon API implemented by the host-platform.
///
/// The abstract class with this annotation groups a collection of Dart↔host
/// interop methods. These methods are invoked by Dart and are received by a
/// host-platform (such as in Android or iOS) by a class implementing the
/// generated host-platform interface.
class HostApi {
/// Parametric constructor for [HostApi].
const HostApi({this.dartHostTestHandler});
/// The name of an interface generated for tests. Implement this
/// interface and invoke `[name of this handler].setup` to receive
/// calls from your real [HostApi] class in Dart instead of the host
/// platform code, as is typical.
///
/// When using this, you must specify the `--out_test_dart` argument
/// to specify where to generate the test file.
///
/// Prefer to use a mock of the real [HostApi] with a mocking library for unit
/// tests. Generating this Dart handler is sometimes useful in integration
/// testing.
///
/// Defaults to `null` in which case no handler will be generated.
final String? dartHostTestHandler;
}
/// Metadata to annotate a Pigeon API implemented by Flutter.
///
/// The abstract class with this annotation groups a collection of Dart↔host
/// interop methods. These methods are invoked by the host-platform (such as in
/// Android or iOS) and are received by Flutter by a class implementing the
/// generated Dart interface.
class FlutterApi {
/// Parametric constructor for [FlutterApi].
const FlutterApi();
}
/// Represents an error as a result of parsing and generating code.
class Error {
/// Parametric constructor for Error.
Error({
required this.message,
this.filename,
this.lineNumber,
});
/// A description of the error.
String message;
/// What file caused the [Error].
String? filename;
/// What line the error happened on.
int? lineNumber;
@override
String toString() {
return '(Error message:"$message" filename:"$filename" lineNumber:$lineNumber)';
}
}
bool _isApi(ClassMirror classMirror) {
return classMirror.isAbstract &&
(_getHostApi(classMirror) != null || _isFlutterApi(classMirror));
}
HostApi? _getHostApi(ClassMirror apiMirror) {
for (final InstanceMirror instance in apiMirror.metadata) {
if (instance.reflectee is HostApi) {
return instance.reflectee;
}
}
return null;
}
bool _isFlutterApi(ClassMirror apiMirror) {
for (final InstanceMirror instance in apiMirror.metadata) {
if (instance.reflectee is FlutterApi) {
return true;
}
}
return false;
}
/// Options used when running the code generator.
class PigeonOptions {
/// Creates a instance of PigeonOptions
PigeonOptions();
/// Path to the file which will be processed.
String? input;
/// Path to the dart file that will be generated.
String? dartOut;
/// Path to the dart file that will be generated for test support classes.
String? dartTestOut;
/// Path to the ".h" Objective-C file will be generated.
String? objcHeaderOut;
/// Path to the ".m" Objective-C file will be generated.
String? objcSourceOut;
/// Options that control how Objective-C will be generated.
ObjcOptions? objcOptions;
/// Path to the java file that will be generated.
String? javaOut;
/// Options that control how Java will be generated.
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.
class ParseResults {
/// Parametric constructor for [ParseResults].
ParseResults({
required this.root,
required this.errors,
});
/// The resulting AST.
final Root root;
/// Errors generated while parsing input.
final List<Error> errors;
}
/// Tool for generating code to facilitate platform channels usage.
class Pigeon {
/// Create and setup a [Pigeon] instance.
static Pigeon setup() {
return Pigeon();
}
Class _parseClassMirror(ClassMirror klassMirror) {
final List<Field> fields = <Field>[];
for (final DeclarationMirror declaration
in klassMirror.declarations.values) {
if (declaration is VariableMirror) {
fields.add(Field(
name: MirrorSystem.getName(declaration.simpleName),
dataType: MirrorSystem.getName(
declaration.type.simpleName,
),
));
}
}
final Class klass = Class(
name: MirrorSystem.getName(klassMirror.simpleName),
fields: fields,
);
return klass;
}
Iterable<Class> _parseClassMirrors(Iterable<ClassMirror> mirrors) sync* {
for (final ClassMirror mirror in mirrors) {
yield _parseClassMirror(mirror);
final Iterable<ClassMirror> nestedTypes = mirror.declarations.values
.whereType<VariableMirror>()
.map((VariableMirror variable) => variable.type)
.whereType<ClassMirror>()
///note: This will need to be changed if we support generic types.
.where((ClassMirror mirror) =>
!_validTypes.contains(MirrorSystem.getName(mirror.simpleName)) &&
!mirror.isEnum);
for (final Class klass in _parseClassMirrors(nestedTypes)) {
yield klass;
}
}
}
Iterable<T> _unique<T, U>(Iterable<T> iter, U Function(T val) getKey) sync* {
final Set<U> seen = <U>{};
for (final T val in iter) {
if (seen.add(getKey(val))) {
yield val;
}
}
}
/// Use reflection to parse the [types] provided.
ParseResults parse(List<Type> types) {
final Set<ClassMirror> classes = <ClassMirror>{};
final Set<ClassMirror> enums = <ClassMirror>{};
final List<ClassMirror> apis = <ClassMirror>[];
for (final Type type in types) {
final ClassMirror classMirror = reflectClass(type);
if (_isApi(classMirror)) {
apis.add(classMirror);
} else {
classes.add(classMirror);
}
}
for (final ClassMirror apiMirror in apis) {
for (final DeclarationMirror declaration
in apiMirror.declarations.values) {
if (declaration is MethodMirror && !declaration.isConstructor) {
if (!isVoid(declaration.returnType)) {
classes.add(declaration.returnType as ClassMirror);
}
if (declaration.parameters.isNotEmpty) {
classes.add(declaration.parameters[0].type as ClassMirror);
}
}
}
}
// Parse referenced enum types out of classes.
for (final ClassMirror klass in classes) {
for (final DeclarationMirror declaration in klass.declarations.values) {
if (declaration is VariableMirror) {
if (declaration.type is ClassMirror &&
(declaration.type as ClassMirror).isEnum) {
enums.add(declaration.type as ClassMirror);
}
}
}
}
final Root root = Root(
classes:
_unique(_parseClassMirrors(classes), (Class x) => x.name).toList(),
apis: <Api>[],
enums: <Enum>[],
);
for (final ClassMirror apiMirror in apis) {
final List<Method> functions = <Method>[];
for (final DeclarationMirror declaration
in apiMirror.declarations.values) {
if (declaration is MethodMirror && !declaration.isConstructor) {
final bool isAsynchronous =
declaration.metadata.any((InstanceMirror it) {
return MirrorSystem.getName(it.type.simpleName) ==
'${async.runtimeType}';
});
functions.add(Method(
name: MirrorSystem.getName(declaration.simpleName),
argType: declaration.parameters.isEmpty
? 'void'
: MirrorSystem.getName(
declaration.parameters[0].type.simpleName),
returnType: MirrorSystem.getName(declaration.returnType.simpleName),
isAsynchronous: isAsynchronous,
));
}
}
final HostApi? hostApi = _getHostApi(apiMirror);
root.apis.add(Api(
name: MirrorSystem.getName(apiMirror.simpleName),
location: hostApi != null ? ApiLocation.host : ApiLocation.flutter,
methods: functions,
dartHostTestHandler: hostApi?.dartHostTestHandler,
));
}
for (final ClassMirror enumMirror in enums) {
// These declarations are innate to enums and are skipped as they are
// not user defined values.
final Set<String> skippedEnumDeclarations = <String>{
'index',
'_name',
'values',
'toString',
'TestEnum',
MirrorSystem.getName(enumMirror.simpleName),
};
final List<String> members = <String>[];
final List<Symbol> keys = enumMirror.declarations.keys.toList();
for (int i = 0; i < enumMirror.declarations.keys.length; i++) {
final String name = MirrorSystem.getName(keys[i]);
if (skippedEnumDeclarations.contains(name)) {
continue;
}
members.add(name);
}
root.enums.add(Enum(
name: MirrorSystem.getName(enumMirror.simpleName), members: members));
}
final List<Error> validateErrors = _validateAst(root);
return ParseResults(root: root, errors: validateErrors);
}
/// String that describes how the tool is used.
static String get usage {
return '''
Pigeon is a tool for generating type-safe communication code between Flutter
and the host platform.
usage: pigeon --input <pigeon path> --dart_out <dart path> [option]*
options:
''' +
_argParser.usage;
}
static final ArgParser _argParser = ArgParser()
..addOption('input', help: 'REQUIRED: Path to pigeon file.')
..addOption('dart_out',
help: 'REQUIRED: Path to generated Dart source file (.dart).')
..addOption('dart_test_out',
help: 'Path to generated library for Dart tests, when using '
'@HostApi(dartHostTestHandler:).')
..addOption('objc_source_out',
help: 'Path to generated Objective-C source file (.m).')
..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',
defaultsTo: true)
..addOption('objc_header_out',
help: 'Path to generated Objective-C header file (.h).')
..addOption('objc_prefix',
help: 'Prefix for generated Objective-C classes and protocols.');
/// Convert command-line arguments to [PigeonOptions].
static PigeonOptions parseArgs(List<String> args) {
// Note: This function shouldn't perform any logic, just translate the args
// to PigeonOptions. Synthesized values inside of the PigeonOption should
// get set in the `run` function to accomodate users that are using the
// `configurePigeon` function.
final ArgResults results = _argParser.parse(args);
final PigeonOptions opts = PigeonOptions();
opts.input = results['input'];
opts.dartOut = results['dart_out'];
opts.dartTestOut = results['dart_test_out'];
opts.objcHeaderOut = results['objc_header_out'];
opts.objcSourceOut = results['objc_source_out'];
opts.objcOptions = ObjcOptions(
prefix: results['objc_prefix'],
);
opts.javaOut = results['java_out'];
opts.javaOptions = JavaOptions(
package: results['java_package'],
);
opts.dartOptions = DartOptions()..isNullSafe = results['dart_null_safety'];
return opts;
}
static Future<void> _runGenerator(
String output, void Function(IOSink sink) func) async {
IOSink sink;
File file;
if (output == 'stdout') {
sink = stdout;
} else {
file = File(output);
sink = file.openWrite();
}
func(sink);
await sink.flush();
}
List<Error> _validateAst(Root root) {
final List<Error> result = <Error>[];
final List<String> customClasses =
root.classes.map((Class x) => x.name).toList();
final List<String> customEnums =
root.enums.map((Enum x) => x.name).toList();
for (final Class klass in root.classes) {
for (final Field field in klass.fields) {
if (!(_validTypes.contains(field.dataType) ||
customClasses.contains(field.dataType) ||
customEnums.contains(field.dataType))) {
result.add(Error(
message:
'Unsupported datatype:"${field.dataType}" in class "${klass.name}".'));
}
}
}
for (final Api api in root.apis) {
for (final Method method in api.methods) {
if (_validTypes.contains(method.argType)) {
result.add(Error(
message:
'Unsupported argument type: "${method.argType}" in API: "${api.name}" method: "${method.name}'));
}
if (_validTypes.contains(method.returnType)) {
result.add(Error(
message:
'Unsupported return type: "${method.returnType}" in API: "${api.name}" method: "${method.name}'));
}
}
}
return result;
}
/// Crawls through the reflection system looking for a configurePigeon method and
/// executing it.
static void _executeConfigurePigeon(PigeonOptions options) {
for (final LibraryMirror library
in currentMirrorSystem().libraries.values) {
for (final DeclarationMirror declaration in library.declarations.values) {
if (declaration is MethodMirror &&
MirrorSystem.getName(declaration.simpleName) == 'configurePigeon') {
if (declaration.parameters.length == 1 &&
declaration.parameters[0].type == reflectClass(PigeonOptions)) {
library.invoke(declaration.simpleName, <dynamic>[options]);
} else {
print('warning: invalid \'configurePigeon\' method defined.');
}
}
}
}
}
static String _posixify(String input) {
final path.Context context = path.Context(style: path.Style.posix);
return context.fromUri(path.toUri(path.absolute(input)));
}
/// The 'main' entrypoint used by the command-line tool. [args] are the
/// command-line arguments.
static Future<int> run(List<String> args) async {
final Pigeon pigeon = Pigeon.setup();
final PigeonOptions options = Pigeon.parseArgs(args);
_executeConfigurePigeon(options);
if (options.input == null || options.dartOut == null) {
print(usage);
return 0;
}
final List<Error> errors = <Error>[];
final List<Type> apis = <Type>[];
if (options.objcHeaderOut != null) {
options.objcOptions?.header = basename(options.objcHeaderOut!);
}
for (final LibraryMirror library
in currentMirrorSystem().libraries.values) {
for (final DeclarationMirror declaration in library.declarations.values) {
if (declaration is ClassMirror && _isApi(declaration)) {
apis.add(declaration.reflectedType);
}
}
}
if (apis.isNotEmpty) {
final ParseResults parseResults = pigeon.parse(apis);
for (final Error err in parseResults.errors) {
errors.add(Error(message: err.message, filename: options.input));
}
if (options.dartOut != null) {
await _runGenerator(
options.dartOut!,
(StringSink sink) => generateDart(
options.dartOptions ?? DartOptions(), parseResults.root, sink));
}
if (options.dartTestOut != null && options.dartOut != null) {
final String mainPath = context.relative(
_posixify(options.dartOut!),
from: _posixify(path.dirname(options.dartTestOut!)),
);
await _runGenerator(
options.dartTestOut!,
(StringSink sink) => generateTestDart(
options.dartOptions ?? DartOptions(),
parseResults.root,
sink,
mainPath,
),
);
}
if (options.objcHeaderOut != null) {
await _runGenerator(
options.objcHeaderOut!,
(StringSink sink) => generateObjcHeader(
options.objcOptions ?? ObjcOptions(), parseResults.root, sink));
}
if (options.objcSourceOut != null) {
await _runGenerator(
options.objcSourceOut!,
(StringSink sink) => generateObjcSource(
options.objcOptions ?? ObjcOptions(), parseResults.root, sink));
}
if (options.javaOut != null) {
if (options.javaOptions!.className == null) {
options.javaOptions!.className =
path.basenameWithoutExtension(options.javaOut!);
}
await _runGenerator(
options.javaOut!,
(StringSink sink) => generateJava(
options.javaOptions ?? JavaOptions(), parseResults.root, sink));
}
} else {
errors.add(Error(message: 'No pigeon classes found, nothing generated.'));
}
printErrors(errors);
return errors.isNotEmpty ? 1 : 0;
}
/// Print a list of errors to stderr.
static void printErrors(List<Error> errors) {
for (final Error err in errors) {
if (err.filename != null) {
if (err.lineNumber != null) {
stderr.writeln(
'Error: ${err.filename}:${err.lineNumber}: ${err.message}');
} else {
stderr.writeln('Error: ${err.filename}: ${err.message}');
}
} else {
stderr.writeln('Error: ${err.message}');
}
}
}
}