blob: 265052ebb5084b1955a97be127e25012c72d27c5 [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.
// ignore_for_file: avoid_print
import 'dart:convert';
import 'dart:io';
import 'dart:mirrors';
import 'package:analyzer/dart/analysis/analysis_context.dart'
show AnalysisContext;
import 'package:analyzer/dart/analysis/analysis_context_collection.dart'
show AnalysisContextCollection;
import 'package:analyzer/dart/analysis/results.dart' show ParsedUnitResult;
import 'package:analyzer/dart/analysis/session.dart' show AnalysisSession;
import 'package:analyzer/dart/ast/ast.dart' as dart_ast;
import 'package:analyzer/dart/ast/syntactic_entity.dart'
as dart_ast_syntactic_entity;
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/ast/visitor.dart' as dart_ast_visitor;
import 'package:analyzer/error/error.dart' show AnalysisError;
import 'package:args/args.dart';
import 'package:path/path.dart' as path;
import 'ast.dart';
import 'ast_generator.dart';
import 'cpp_generator.dart';
import 'dart_generator.dart';
import 'generator_tools.dart';
import 'generator_tools.dart' as generator_tools;
import 'java_generator.dart';
import 'kotlin_generator.dart';
import 'objc_generator.dart';
import 'swift_generator.dart';
class _Asynchronous {
const _Asynchronous();
}
/// Metadata to annotate a Api method as asynchronous
const Object async = _Asynchronous();
/// Metadata annotation used to configure how Pigeon will generate code.
class ConfigurePigeon {
/// Constructor for ConfigurePigeon.
const ConfigurePigeon(this.options);
/// The [PigeonOptions] that will be merged into the command line options.
final PigeonOptions options;
}
/// 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();
}
/// Metadata to annotation methods to control the selector used for objc output.
/// The number of components in the provided selector must match the number of
/// arguments in the annotated method.
/// For example:
/// @ObjcSelector('divideValue:by:') double divide(int x, int y);
class ObjCSelector {
/// Constructor.
const ObjCSelector(this.value);
/// The string representation of the selector.
final String value;
}
/// Metadata to annotate methods to control the signature used for Swift output.
///
/// The number of components in the provided signature must match the number of
/// arguments in the annotated method.
/// For example:
/// @SwiftFunction('divide(_:by:)') double divide(int x, String y);
class SwiftFunction {
/// Constructor.
const SwiftFunction(this.value);
/// The string representation of the function signature.
final String value;
}
/// Type of TaskQueue which determines how handlers are dispatched for
/// HostApi's.
enum TaskQueueType {
/// Handlers are invoked serially on the default thread. This is the value if
/// unspecified.
serial,
/// Handlers are invoked serially on a background thread.
serialBackgroundThread,
// TODO(gaaclarke): Add support for concurrent task queues.
// /// Handlers are invoked concurrently on a background thread.
// concurrentBackgroundThread,
}
/// Metadata annotation to control how handlers are dispatched for HostApi's.
/// Note that the TaskQueue API might not be available on the target version of
/// Flutter, see also:
/// https://docs.flutter.dev/development/platform-integration/platform-channels.
class TaskQueue {
/// The constructor for a TaskQueue.
const TaskQueue({required this.type});
/// The type of the TaskQueue.
final TaskQueueType type;
}
/// 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)';
}
}
/// Options used when running the code generator.
class PigeonOptions {
/// Creates a instance of PigeonOptions
const PigeonOptions(
{this.input,
this.dartOut,
this.dartTestOut,
this.objcHeaderOut,
this.objcSourceOut,
this.objcOptions,
this.javaOut,
this.javaOptions,
this.swiftOut,
this.swiftOptions,
this.kotlinOut,
this.kotlinOptions,
this.cppHeaderOut,
this.cppSourceOut,
this.cppOptions,
this.dartOptions,
this.copyrightHeader,
this.oneLanguage,
this.astOut,
this.debugGenerators});
/// Path to the file which will be processed.
final String? input;
/// Path to the dart file that will be generated.
final String? dartOut;
/// Path to the dart file that will be generated for test support classes.
final String? dartTestOut;
/// Path to the ".h" Objective-C file will be generated.
final String? objcHeaderOut;
/// Path to the ".m" Objective-C file will be generated.
final String? objcSourceOut;
/// Options that control how Objective-C will be generated.
final ObjcOptions? objcOptions;
/// Path to the java file that will be generated.
final String? javaOut;
/// Options that control how Java will be generated.
final JavaOptions? javaOptions;
/// Path to the swift file that will be generated.
final String? swiftOut;
/// Options that control how Swift will be generated.
final SwiftOptions? swiftOptions;
/// Path to the kotlin file that will be generated.
final String? kotlinOut;
/// Options that control how Kotlin will be generated.
final KotlinOptions? kotlinOptions;
/// Path to the ".h" C++ file that will be generated.
final String? cppHeaderOut;
/// Path to the ".cpp" C++ file that will be generated.
final String? cppSourceOut;
/// Options that control how C++ will be generated.
final CppOptions? cppOptions;
/// Options that control how Dart will be generated.
final DartOptions? dartOptions;
/// Path to a copyright header that will get prepended to generated code.
final String? copyrightHeader;
/// If Pigeon allows generating code for one language.
final bool? oneLanguage;
/// Path to AST debugging output.
final String? astOut;
/// True means print out line number of generators in comments at newlines.
final bool? debugGenerators;
/// Creates a [PigeonOptions] from a Map representation where:
/// `x = PigeonOptions.fromMap(x.toMap())`.
static PigeonOptions fromMap(Map<String, Object> map) {
return PigeonOptions(
input: map['input'] as String?,
dartOut: map['dartOut'] as String?,
dartTestOut: map['dartTestOut'] as String?,
objcHeaderOut: map['objcHeaderOut'] as String?,
objcSourceOut: map['objcSourceOut'] as String?,
objcOptions: map.containsKey('objcOptions')
? ObjcOptions.fromMap(map['objcOptions']! as Map<String, Object>)
: null,
javaOut: map['javaOut'] as String?,
javaOptions: map.containsKey('javaOptions')
? JavaOptions.fromMap(map['javaOptions']! as Map<String, Object>)
: null,
swiftOut: map['swiftOut'] as String?,
swiftOptions: map.containsKey('swiftOptions')
? SwiftOptions.fromList(map['swiftOptions']! as Map<String, Object>)
: null,
kotlinOut: map['kotlinOut'] as String?,
kotlinOptions: map.containsKey('kotlinOptions')
? KotlinOptions.fromMap(map['kotlinOptions']! as Map<String, Object>)
: null,
cppHeaderOut: map['cppHeaderOut'] as String?,
cppSourceOut: map['cppSourceOut'] as String?,
cppOptions: map.containsKey('cppOptions')
? CppOptions.fromMap(map['cppOptions']! as Map<String, Object>)
: null,
dartOptions: map.containsKey('dartOptions')
? DartOptions.fromMap(map['dartOptions']! as Map<String, Object>)
: null,
copyrightHeader: map['copyrightHeader'] as String?,
oneLanguage: map['oneLanguage'] as bool?,
astOut: map['astOut'] as String?,
debugGenerators: map['debugGenerators'] as bool?,
);
}
/// Converts a [PigeonOptions] to a Map representation where:
/// `x = PigeonOptions.fromMap(x.toMap())`.
Map<String, Object> toMap() {
final Map<String, Object> result = <String, Object>{
if (input != null) 'input': input!,
if (dartOut != null) 'dartOut': dartOut!,
if (dartTestOut != null) 'dartTestOut': dartTestOut!,
if (objcHeaderOut != null) 'objcHeaderOut': objcHeaderOut!,
if (objcSourceOut != null) 'objcSourceOut': objcSourceOut!,
if (objcOptions != null) 'objcOptions': objcOptions!.toMap(),
if (javaOut != null) 'javaOut': javaOut!,
if (javaOptions != null) 'javaOptions': javaOptions!.toMap(),
if (swiftOut != null) 'swiftOut': swiftOut!,
if (swiftOptions != null) 'swiftOptions': swiftOptions!.toMap(),
if (kotlinOut != null) 'kotlinOut': kotlinOut!,
if (kotlinOptions != null) 'kotlinOptions': kotlinOptions!.toMap(),
if (cppHeaderOut != null) 'cppHeaderOut': cppHeaderOut!,
if (cppSourceOut != null) 'cppSourceOut': cppSourceOut!,
if (cppOptions != null) 'cppOptions': cppOptions!.toMap(),
if (dartOptions != null) 'dartOptions': dartOptions!.toMap(),
if (copyrightHeader != null) 'copyrightHeader': copyrightHeader!,
if (astOut != null) 'astOut': astOut!,
if (oneLanguage != null) 'oneLanguage': oneLanguage!,
if (debugGenerators != null) 'debugGenerators': debugGenerators!,
};
return result;
}
/// Overrides any non-null parameters from [options] into this to make a new
/// [PigeonOptions].
PigeonOptions merge(PigeonOptions options) {
return PigeonOptions.fromMap(mergeMaps(toMap(), options.toMap()));
}
}
/// 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,
required this.pigeonOptions,
});
/// The resulting AST.
final Root root;
/// Errors generated while parsing input.
final List<Error> errors;
/// The Map representation of any [PigeonOptions] specified with
/// [ConfigurePigeon] during parsing.
final Map<String, Object>? pigeonOptions;
}
Iterable<String> _lineReader(String path) sync* {
final String contents = File(path).readAsStringSync();
const LineSplitter lineSplitter = LineSplitter();
final List<String> lines = lineSplitter.convert(contents);
for (final String line in lines) {
yield line;
}
}
IOSink? _openSink(String? output) {
if (output == null) {
return null;
}
IOSink sink;
File file;
if (output == 'stdout') {
sink = stdout;
} else {
file = File(output);
sink = file.openWrite();
}
return sink;
}
/// An adapter that will call a generator to write code to a sink
/// based on the contents of [PigeonOptions].
abstract class GeneratorAdapter {
/// Constructor for [GeneratorAdapter]
GeneratorAdapter(this.fileTypeList);
/// A list of file types the generator should create.
List<FileType> fileTypeList;
/// Returns an [IOSink] instance to be written to
/// if the [GeneratorAdapter] should generate.
///
/// If it returns `null`, the [GeneratorAdapter] will be skipped.
IOSink? shouldGenerate(PigeonOptions options, FileType fileType);
/// Write the generated code described in [root] to [sink] using the [options].
void generate(
StringSink sink, PigeonOptions options, Root root, FileType fileType);
/// Generates errors that would only be appropriate for this [GeneratorAdapter].
///
/// For example, if a certain feature isn't implemented in a [GeneratorAdapter] yet.
List<Error> validate(PigeonOptions options, Root root);
}
DartOptions _dartOptionsWithCopyrightHeader(
DartOptions? dartOptions, String? copyrightHeader,
{String? dartOutPath, String? testOutPath}) {
dartOptions = dartOptions ?? const DartOptions();
return dartOptions.merge(DartOptions(
sourceOutPath: dartOutPath,
testOutPath: testOutPath,
copyrightHeader:
copyrightHeader != null ? _lineReader(copyrightHeader) : null));
}
/// A [GeneratorAdapter] that generates the AST.
class AstGeneratorAdapter implements GeneratorAdapter {
/// Constructor for [AstGeneratorAdapter].
AstGeneratorAdapter();
@override
List<FileType> fileTypeList = const <FileType>[FileType.na];
@override
void generate(
StringSink sink, PigeonOptions options, Root root, FileType fileType) {
generateAst(root, sink);
}
@override
IOSink? shouldGenerate(PigeonOptions options, FileType _) =>
_openSink(options.astOut);
@override
List<Error> validate(PigeonOptions options, Root root) => <Error>[];
}
/// A [GeneratorAdapter] that generates Dart source code.
class DartGeneratorAdapter implements GeneratorAdapter {
/// Constructor for [DartGeneratorAdapter].
DartGeneratorAdapter();
@override
List<FileType> fileTypeList = const <FileType>[FileType.na];
@override
void generate(
StringSink sink, PigeonOptions options, Root root, FileType fileType) {
final DartOptions dartOptionsWithHeader = _dartOptionsWithCopyrightHeader(
options.dartOptions, options.copyrightHeader);
const DartGenerator generator = DartGenerator();
generator.generate(dartOptionsWithHeader, root, sink);
}
@override
IOSink? shouldGenerate(PigeonOptions options, FileType _) =>
_openSink(options.dartOut);
@override
List<Error> validate(PigeonOptions options, Root root) => <Error>[];
}
/// A [GeneratorAdapter] that generates Dart test source code.
class DartTestGeneratorAdapter implements GeneratorAdapter {
/// Constructor for [DartTestGeneratorAdapter].
DartTestGeneratorAdapter();
@override
List<FileType> fileTypeList = const <FileType>[FileType.na];
@override
void generate(
StringSink sink, PigeonOptions options, Root root, FileType fileType) {
final DartOptions dartOptionsWithHeader = _dartOptionsWithCopyrightHeader(
options.dartOptions,
options.copyrightHeader,
dartOutPath: options.dartOut,
testOutPath: options.dartTestOut,
);
const DartGenerator testGenerator = DartGenerator();
testGenerator.generateTest(dartOptionsWithHeader, root, sink);
}
@override
IOSink? shouldGenerate(PigeonOptions options, FileType _) {
if (options.dartTestOut != null) {
return _openSink(options.dartTestOut);
} else {
return null;
}
}
@override
List<Error> validate(PigeonOptions options, Root root) => <Error>[];
}
/// A [GeneratorAdapter] that generates Objective-C code.
class ObjcGeneratorAdapter implements GeneratorAdapter {
/// Constructor for [ObjcGeneratorAdapter].
ObjcGeneratorAdapter(
{this.fileTypeList = const <FileType>[FileType.header, FileType.source]});
@override
List<FileType> fileTypeList;
@override
void generate(
StringSink sink, PigeonOptions options, Root root, FileType fileType) {
final ObjcOptions objcOptions = options.objcOptions ?? const ObjcOptions();
final ObjcOptions objcOptionsWithHeader = objcOptions.merge(ObjcOptions(
copyrightHeader: options.copyrightHeader != null
? _lineReader(options.copyrightHeader!)
: null,
));
final OutputFileOptions<ObjcOptions> outputFileOptions =
OutputFileOptions<ObjcOptions>(
fileType: fileType, languageOptions: objcOptionsWithHeader);
const ObjcGenerator generator = ObjcGenerator();
generator.generate(outputFileOptions, root, sink);
}
@override
IOSink? shouldGenerate(PigeonOptions options, FileType fileType) {
if (fileType == FileType.source) {
return _openSink(options.objcSourceOut);
} else {
return _openSink(options.objcHeaderOut);
}
}
@override
List<Error> validate(PigeonOptions options, Root root) => <Error>[];
}
/// A [GeneratorAdapter] that generates Java source code.
class JavaGeneratorAdapter implements GeneratorAdapter {
/// Constructor for [JavaGeneratorAdapter].
JavaGeneratorAdapter();
@override
List<FileType> fileTypeList = const <FileType>[FileType.na];
@override
void generate(
StringSink sink, PigeonOptions options, Root root, FileType fileType) {
JavaOptions javaOptions = options.javaOptions ?? const JavaOptions();
javaOptions = javaOptions.merge(JavaOptions(
className: javaOptions.className ??
path.basenameWithoutExtension(options.javaOut!),
copyrightHeader: options.copyrightHeader != null
? _lineReader(options.copyrightHeader!)
: null));
const JavaGenerator generator = JavaGenerator();
generator.generate(javaOptions, root, sink);
}
@override
IOSink? shouldGenerate(PigeonOptions options, FileType _) =>
_openSink(options.javaOut);
@override
List<Error> validate(PigeonOptions options, Root root) => <Error>[];
}
/// A [GeneratorAdapter] that generates Swift source code.
class SwiftGeneratorAdapter implements GeneratorAdapter {
/// Constructor for [SwiftGeneratorAdapter].
SwiftGeneratorAdapter();
@override
List<FileType> fileTypeList = const <FileType>[FileType.na];
@override
void generate(
StringSink sink, PigeonOptions options, Root root, FileType fileType) {
SwiftOptions swiftOptions = options.swiftOptions ?? const SwiftOptions();
swiftOptions = swiftOptions.merge(SwiftOptions(
copyrightHeader: options.copyrightHeader != null
? _lineReader(options.copyrightHeader!)
: null));
const SwiftGenerator generator = SwiftGenerator();
generator.generate(swiftOptions, root, sink);
}
@override
IOSink? shouldGenerate(PigeonOptions options, FileType _) =>
_openSink(options.swiftOut);
@override
List<Error> validate(PigeonOptions options, Root root) => <Error>[];
}
/// A [GeneratorAdapter] that generates C++ source code.
class CppGeneratorAdapter implements GeneratorAdapter {
/// Constructor for [CppGeneratorAdapter].
CppGeneratorAdapter(
{this.fileTypeList = const <FileType>[FileType.header, FileType.source]});
@override
List<FileType> fileTypeList;
@override
void generate(
StringSink sink, PigeonOptions options, Root root, FileType fileType) {
final CppOptions cppOptions = options.cppOptions ?? const CppOptions();
final CppOptions cppOptionsWithHeader = cppOptions.merge(CppOptions(
copyrightHeader: options.copyrightHeader != null
? _lineReader(options.copyrightHeader!)
: null,
));
final OutputFileOptions<CppOptions> outputFileOptions =
OutputFileOptions<CppOptions>(
fileType: fileType, languageOptions: cppOptionsWithHeader);
const CppGenerator generator = CppGenerator();
generator.generate(outputFileOptions, root, sink);
}
@override
IOSink? shouldGenerate(PigeonOptions options, FileType fileType) {
if (fileType == FileType.source) {
return _openSink(options.cppSourceOut);
} else {
return _openSink(options.cppHeaderOut);
}
}
@override
List<Error> validate(PigeonOptions options, Root root) => <Error>[];
}
/// A [GeneratorAdapter] that generates Kotlin source code.
class KotlinGeneratorAdapter implements GeneratorAdapter {
/// Constructor for [KotlinGeneratorAdapter].
KotlinGeneratorAdapter({this.fileTypeList = const <FileType>[FileType.na]});
@override
List<FileType> fileTypeList;
@override
void generate(
StringSink sink, PigeonOptions options, Root root, FileType fileType) {
KotlinOptions kotlinOptions =
options.kotlinOptions ?? const KotlinOptions();
kotlinOptions = kotlinOptions.merge(KotlinOptions(
errorClassName: kotlinOptions.errorClassName ?? 'FlutterError',
copyrightHeader: options.copyrightHeader != null
? _lineReader(options.copyrightHeader!)
: null));
const KotlinGenerator generator = KotlinGenerator();
generator.generate(kotlinOptions, root, sink);
}
@override
IOSink? shouldGenerate(PigeonOptions options, FileType _) =>
_openSink(options.kotlinOut);
@override
List<Error> validate(PigeonOptions options, Root root) => <Error>[];
}
dart_ast.Annotation? _findMetadata(
dart_ast.NodeList<dart_ast.Annotation> metadata, String query) {
final Iterable<dart_ast.Annotation> annotations = metadata
.where((dart_ast.Annotation element) => element.name.name == query);
return annotations.isEmpty ? null : annotations.first;
}
bool _hasMetadata(
dart_ast.NodeList<dart_ast.Annotation> metadata, String query) {
return _findMetadata(metadata, query) != null;
}
extension _ObjectAs on Object {
/// A convenience for chaining calls with casts.
T? asNullable<T>() => this as T?;
}
List<Error> _validateAst(Root root, String source) {
final List<Error> result = <Error>[];
final List<String> customClasses =
root.classes.map((Class x) => x.name).toList();
final Iterable<String> customEnums = root.enums.map((Enum x) => x.name);
for (final Class klass in root.classes) {
for (final NamedType field in getFieldsInSerializationOrder(klass)) {
for (final TypeDeclaration typeArgument in field.type.typeArguments) {
if (!typeArgument.isNullable) {
result.add(Error(
message:
'Generic type arguments must be nullable in field "${field.name}" in class "${klass.name}".',
lineNumber: _calculateLineNumberNullable(source, field.offset),
));
}
if (customEnums.contains(typeArgument.baseName)) {
result.add(Error(
message:
'Enum types aren\'t supported in type arguments in "${field.name}" in class "${klass.name}".',
lineNumber: _calculateLineNumberNullable(source, field.offset),
));
}
}
if (!(validTypes.contains(field.type.baseName) ||
customClasses.contains(field.type.baseName) ||
customEnums.contains(field.type.baseName))) {
result.add(Error(
message:
'Unsupported datatype:"${field.type.baseName}" in class "${klass.name}".',
lineNumber: _calculateLineNumberNullable(source, field.offset),
));
}
}
}
for (final Api api in root.apis) {
for (final Method method in api.methods) {
if (api.location == ApiLocation.flutter &&
method.arguments.isNotEmpty &&
method.arguments.any((NamedType element) =>
customEnums.contains(element.type.baseName))) {
result.add(Error(
message:
'Enums aren\'t yet supported for primitive arguments in FlutterApis: "${method.arguments[0]}" in API: "${api.name}" method: "${method.name}" (https://github.com/flutter/flutter/issues/87307)',
lineNumber: _calculateLineNumberNullable(source, method.offset),
));
}
if (customEnums.contains(method.returnType.baseName)) {
result.add(Error(
message:
'Enums aren\'t yet supported for primitive return types: "${method.returnType}" in API: "${api.name}" method: "${method.name}" (https://github.com/flutter/flutter/issues/87307)',
));
}
if (method.arguments.any((NamedType arg) =>
(arg.type.baseName == 'List' || arg.type.baseName == 'Map') &&
arg.type.typeArguments.any(
(TypeDeclaration genericType) => isEnum(root, genericType)))) {
result.add(Error(
message:
'Enums aren\'t yet supported for collection types: "${method.arguments[0]}" in API: "${api.name}" method: "${method.name}" (https://github.com/flutter/flutter/issues/87307)',
lineNumber: _calculateLineNumberNullable(source, method.offset),
));
}
for (final NamedType unnamedType in method.arguments
.where((NamedType element) => element.type.baseName.isEmpty)) {
result.add(Error(
message:
'Arguments must specify their type in method "${method.name}" in API: "${api.name}"',
lineNumber: _calculateLineNumberNullable(source, unnamedType.offset),
));
}
if (method.objcSelector.isNotEmpty) {
if (':'.allMatches(method.objcSelector).length !=
method.arguments.length) {
result.add(Error(
message:
'Invalid selector, expected ${method.arguments.length} arguments.',
lineNumber: _calculateLineNumberNullable(source, method.offset),
));
}
}
if (method.swiftFunction.isNotEmpty) {
final RegExp signatureRegex =
RegExp('\\w+ *\\((\\w+:){${method.arguments.length}}\\)');
if (!signatureRegex.hasMatch(method.swiftFunction)) {
result.add(Error(
message:
'Invalid function signature, expected ${method.arguments.length} arguments.',
lineNumber: _calculateLineNumberNullable(source, method.offset),
));
}
}
if (method.taskQueueType != TaskQueueType.serial &&
api.location != ApiLocation.host) {
result.add(Error(
message: 'Unsupported TaskQueue specification on ${method.name}',
lineNumber: _calculateLineNumberNullable(source, method.offset),
));
}
}
}
return result;
}
class _FindInitializer extends dart_ast_visitor.RecursiveAstVisitor<Object?> {
dart_ast.Expression? initializer;
@override
Object? visitVariableDeclaration(dart_ast.VariableDeclaration node) {
if (node.initializer != null) {
initializer = node.initializer;
}
return null;
}
}
class _RootBuilder extends dart_ast_visitor.RecursiveAstVisitor<Object?> {
_RootBuilder(this.source);
final List<Api> _apis = <Api>[];
final List<Enum> _enums = <Enum>[];
final List<Class> _classes = <Class>[];
final List<Error> _errors = <Error>[];
final String source;
Class? _currentClass;
Api? _currentApi;
Map<String, Object>? _pigeonOptions;
void _storeCurrentApi() {
if (_currentApi != null) {
_apis.add(_currentApi!);
_currentApi = null;
}
}
void _storeCurrentClass() {
if (_currentClass != null) {
_classes.add(_currentClass!);
_currentClass = null;
}
}
ParseResults results() {
_storeCurrentApi();
_storeCurrentClass();
final Map<TypeDeclaration, List<int>> referencedTypes =
getReferencedTypes(_apis, _classes);
final Set<String> referencedTypeNames =
referencedTypes.keys.map((TypeDeclaration e) => e.baseName).toSet();
final List<Class> referencedClasses = List<Class>.from(_classes);
referencedClasses
.removeWhere((Class x) => !referencedTypeNames.contains(x.name));
final List<Enum> referencedEnums = List<Enum>.from(_enums);
final Root completeRoot =
Root(apis: _apis, classes: referencedClasses, enums: referencedEnums);
final List<Error> validateErrors = _validateAst(completeRoot, source);
final List<Error> totalErrors = List<Error>.from(_errors);
totalErrors.addAll(validateErrors);
for (final MapEntry<TypeDeclaration, List<int>> element
in referencedTypes.entries) {
if (!referencedClasses
.map((Class e) => e.name)
.contains(element.key.baseName) &&
!referencedEnums
.map((Enum e) => e.name)
.contains(element.key.baseName) &&
!validTypes.contains(element.key.baseName) &&
!element.key.isVoid &&
element.key.baseName != 'dynamic' &&
element.key.baseName != 'Object' &&
element.key.baseName.isNotEmpty) {
final int? lineNumber = element.value.isEmpty
? null
: _calculateLineNumber(source, element.value.first);
totalErrors.add(Error(
message: 'Unknown type: ${element.key.baseName}',
lineNumber: lineNumber));
}
}
return ParseResults(
root: totalErrors.isEmpty
? completeRoot
: Root(apis: <Api>[], classes: <Class>[], enums: <Enum>[]),
errors: totalErrors,
pigeonOptions: _pigeonOptions,
);
}
Object _expressionToMap(dart_ast.Expression expression) {
if (expression is dart_ast.MethodInvocation) {
final Map<String, Object> result = <String, Object>{};
for (final dart_ast.Expression argument
in expression.argumentList.arguments) {
if (argument is dart_ast.NamedExpression) {
result[argument.name.label.name] =
_expressionToMap(argument.expression);
} else {
_errors.add(Error(
message: 'expected NamedExpression but found $expression',
lineNumber: _calculateLineNumber(source, argument.offset),
));
}
}
return result;
} else if (expression is dart_ast.SimpleStringLiteral) {
return expression.value;
} else if (expression is dart_ast.IntegerLiteral) {
return expression.value!;
} else if (expression is dart_ast.BooleanLiteral) {
return expression.value;
} else if (expression is dart_ast.ListLiteral) {
final List<dynamic> list = <dynamic>[];
for (final dart_ast.CollectionElement element in expression.elements) {
if (element is dart_ast.Expression) {
list.add(_expressionToMap(element));
} else {
_errors.add(Error(
message: 'expected Expression but found $element',
lineNumber: _calculateLineNumber(source, element.offset),
));
}
}
return list;
} else {
_errors.add(Error(
message:
'unrecongized expression type ${expression.runtimeType} $expression',
lineNumber: _calculateLineNumber(source, expression.offset),
));
return 0;
}
}
@override
Object? visitImportDirective(dart_ast.ImportDirective node) {
if (node.uri.stringValue != 'package:pigeon/pigeon.dart') {
_errors.add(Error(
message:
"Unsupported import ${node.uri}, only imports of 'package:pigeon/pigeon.dart' are supported.",
lineNumber: _calculateLineNumber(source, node.offset),
));
}
return null;
}
@override
Object? visitAnnotation(dart_ast.Annotation node) {
if (node.name.name == 'ConfigurePigeon') {
if (node.arguments == null) {
_errors.add(Error(
message: 'ConfigurePigeon expects a PigeonOptions() call.',
lineNumber: _calculateLineNumber(source, node.offset),
));
}
final Map<String, Object> pigeonOptionsMap =
_expressionToMap(node.arguments!.arguments.first)
as Map<String, Object>;
_pigeonOptions = pigeonOptionsMap;
}
node.visitChildren(this);
return null;
}
@override
Object? visitClassDeclaration(dart_ast.ClassDeclaration node) {
_storeCurrentApi();
_storeCurrentClass();
if (node.abstractKeyword != null) {
if (_hasMetadata(node.metadata, 'HostApi')) {
final dart_ast.Annotation hostApi = node.metadata.firstWhere(
(dart_ast.Annotation element) => element.name.name == 'HostApi');
String? dartHostTestHandler;
if (hostApi.arguments != null) {
for (final dart_ast.Expression expression
in hostApi.arguments!.arguments) {
if (expression is dart_ast.NamedExpression) {
if (expression.name.label.name == 'dartHostTestHandler') {
final dart_ast.Expression dartHostTestHandlerExpression =
expression.expression;
if (dartHostTestHandlerExpression
is dart_ast.SimpleStringLiteral) {
dartHostTestHandler = dartHostTestHandlerExpression.value;
}
}
}
}
}
_currentApi = Api(
name: node.name.lexeme,
location: ApiLocation.host,
methods: <Method>[],
dartHostTestHandler: dartHostTestHandler,
documentationComments:
_documentationCommentsParser(node.documentationComment?.tokens),
);
} else if (_hasMetadata(node.metadata, 'FlutterApi')) {
_currentApi = Api(
name: node.name.lexeme,
location: ApiLocation.flutter,
methods: <Method>[],
documentationComments:
_documentationCommentsParser(node.documentationComment?.tokens),
);
}
} else {
_currentClass = Class(
name: node.name.lexeme,
fields: <NamedType>[],
documentationComments:
_documentationCommentsParser(node.documentationComment?.tokens),
);
}
node.visitChildren(this);
return null;
}
/// Converts Token's to Strings and removes documentation comment symbol.
List<String> _documentationCommentsParser(List<Token>? comments) {
const String docCommentPrefix = '///';
return comments
?.map((Token line) => line.length > docCommentPrefix.length
? line.toString().substring(docCommentPrefix.length)
: '')
.toList() ??
<String>[];
}
NamedType formalParameterToField(dart_ast.FormalParameter parameter) {
final dart_ast.NamedType? namedType =
getFirstChildOfType<dart_ast.NamedType>(parameter);
if (namedType != null) {
final String argTypeBaseName = namedType.name.name;
final bool isNullable = namedType.question != null;
final List<TypeDeclaration> argTypeArguments =
typeAnnotationsToTypeArguments(namedType.typeArguments);
return NamedType(
type: TypeDeclaration(
baseName: argTypeBaseName,
isNullable: isNullable,
typeArguments: argTypeArguments),
name: parameter.name?.lexeme ?? '',
offset: parameter.offset);
} else {
return NamedType(
name: '',
type: const TypeDeclaration(baseName: '', isNullable: false),
offset: parameter.offset,
);
}
}
static T? getFirstChildOfType<T>(dart_ast.AstNode entity) {
for (final dart_ast_syntactic_entity.SyntacticEntity child
in entity.childEntities) {
if (child is T) {
return child as T;
}
}
return null;
}
T? _stringToEnum<T>(List<T> values, String? str) {
if (str == null) {
return null;
}
for (final T value in values) {
if (value.toString() == str) {
return value;
}
}
return null;
}
@override
Object? visitMethodDeclaration(dart_ast.MethodDeclaration node) {
final dart_ast.FormalParameterList parameters = node.parameters!;
final List<NamedType> arguments =
parameters.parameters.map(formalParameterToField).toList();
final bool isAsynchronous = _hasMetadata(node.metadata, 'async');
final String objcSelector = _findMetadata(node.metadata, 'ObjCSelector')
?.arguments
?.arguments
.first
.asNullable<dart_ast.SimpleStringLiteral>()
?.value ??
'';
final String swiftFunction = _findMetadata(node.metadata, 'SwiftFunction')
?.arguments
?.arguments
.first
.asNullable<dart_ast.SimpleStringLiteral>()
?.value ??
'';
final dart_ast.ArgumentList? taskQueueArguments =
_findMetadata(node.metadata, 'TaskQueue')?.arguments;
final String? taskQueueTypeName = taskQueueArguments == null
? null
: getFirstChildOfType<dart_ast.NamedExpression>(taskQueueArguments)
?.expression
.asNullable<dart_ast.PrefixedIdentifier>()
?.name;
final TaskQueueType taskQueueType =
_stringToEnum(TaskQueueType.values, taskQueueTypeName) ??
TaskQueueType.serial;
if (_currentApi != null) {
// Methods without named return types aren't supported.
final dart_ast.TypeAnnotation returnType = node.returnType!;
returnType as dart_ast.NamedType;
_currentApi!.methods.add(
Method(
name: node.name.lexeme,
returnType: TypeDeclaration(
baseName: returnType.name.name,
typeArguments:
typeAnnotationsToTypeArguments(returnType.typeArguments),
isNullable: returnType.question != null),
arguments: arguments,
isAsynchronous: isAsynchronous,
objcSelector: objcSelector,
swiftFunction: swiftFunction,
offset: node.offset,
taskQueueType: taskQueueType,
documentationComments:
_documentationCommentsParser(node.documentationComment?.tokens),
),
);
} else if (_currentClass != null) {
_errors.add(Error(
message:
'Methods aren\'t supported in Pigeon data classes ("${node.name.lexeme}").',
lineNumber: _calculateLineNumber(source, node.offset)));
}
node.visitChildren(this);
return null;
}
@override
Object? visitEnumDeclaration(dart_ast.EnumDeclaration node) {
_enums.add(Enum(
name: node.name.lexeme,
members: node.constants
.map((dart_ast.EnumConstantDeclaration e) => EnumMember(
name: e.name.lexeme,
documentationComments: _documentationCommentsParser(
e.documentationComment?.tokens),
))
.toList(),
documentationComments:
_documentationCommentsParser(node.documentationComment?.tokens),
));
node.visitChildren(this);
return null;
}
List<TypeDeclaration> typeAnnotationsToTypeArguments(
dart_ast.TypeArgumentList? typeArguments) {
final List<TypeDeclaration> result = <TypeDeclaration>[];
if (typeArguments != null) {
for (final Object x in typeArguments.childEntities) {
if (x is dart_ast.NamedType) {
result.add(TypeDeclaration(
baseName: x.name.name,
isNullable: x.question != null,
typeArguments: typeAnnotationsToTypeArguments(x.typeArguments)));
}
}
}
return result;
}
@override
Object? visitFieldDeclaration(dart_ast.FieldDeclaration node) {
if (_currentClass != null) {
final dart_ast.TypeAnnotation? type = node.fields.type;
if (node.isStatic) {
_errors.add(Error(
message:
'Pigeon doesn\'t support static fields ("$node"), consider using enums.',
lineNumber: _calculateLineNumber(source, node.offset)));
} else if (type is dart_ast.NamedType) {
final _FindInitializer findInitializerVisitor = _FindInitializer();
node.visitChildren(findInitializerVisitor);
if (findInitializerVisitor.initializer != null) {
_errors.add(Error(
message:
'Initialization isn\'t supported for fields in Pigeon data classes ("$node"), just use nullable types with no initializer (example "int? x;").',
lineNumber: _calculateLineNumber(source, node.offset)));
} else {
final dart_ast.TypeArgumentList? typeArguments = type.typeArguments;
_currentClass!.fields.add(NamedType(
type: TypeDeclaration(
baseName: type.name.name,
isNullable: type.question != null,
typeArguments: typeAnnotationsToTypeArguments(typeArguments),
),
name: node.fields.variables[0].name.lexeme,
offset: node.offset,
documentationComments:
_documentationCommentsParser(node.documentationComment?.tokens),
));
}
} else {
_errors.add(Error(
message: 'Expected a named type but found "$node".',
lineNumber: _calculateLineNumber(source, node.offset)));
}
} else if (_currentApi != null) {
_errors.add(Error(
message: 'Fields aren\'t supported in Pigeon API classes ("$node").',
lineNumber: _calculateLineNumber(source, node.offset)));
}
node.visitChildren(this);
return null;
}
@override
Object? visitConstructorDeclaration(dart_ast.ConstructorDeclaration node) {
if (_currentApi != null) {
_errors.add(Error(
message: 'Constructors aren\'t supported in API classes ("$node").',
lineNumber: _calculateLineNumber(source, node.offset)));
} else {
if (node.body.beginToken.lexeme != ';') {
_errors.add(Error(
message:
'Constructor bodies aren\'t supported in data classes ("$node").',
lineNumber: _calculateLineNumber(source, node.offset)));
} else if (node.initializers.isNotEmpty) {
_errors.add(Error(
message:
'Constructor initializers aren\'t supported in data classes (use "this.fieldName") ("$node").',
lineNumber: _calculateLineNumber(source, node.offset)));
}
}
node.visitChildren(this);
return null;
}
}
int? _calculateLineNumberNullable(String contents, int? offset) {
return (offset == null) ? null : _calculateLineNumber(contents, offset);
}
int _calculateLineNumber(String contents, int offset) {
int result = 1;
for (int i = 0; i < offset; ++i) {
if (contents[i] == '\n') {
result += 1;
}
}
return result;
}
/// Tool for generating code to facilitate platform channels usage.
class Pigeon {
/// Create and setup a [Pigeon] instance.
static Pigeon setup() {
return Pigeon();
}
/// Reads the file located at [path] and generates [ParseResults] by parsing
/// it. [types] optionally filters out what datatypes are actually parsed.
/// [sdkPath] for specifying the Dart SDK path for
/// [AnalysisContextCollection].
ParseResults parseFile(String inputPath, {String? sdkPath}) {
final List<String> includedPaths = <String>[
path.absolute(path.normalize(inputPath))
];
final AnalysisContextCollection collection = AnalysisContextCollection(
includedPaths: includedPaths,
sdkPath: sdkPath,
);
final List<Error> compilationErrors = <Error>[];
final _RootBuilder rootBuilder =
_RootBuilder(File(inputPath).readAsStringSync());
for (final AnalysisContext context in collection.contexts) {
for (final String path in context.contextRoot.analyzedFiles()) {
final AnalysisSession session = context.currentSession;
final ParsedUnitResult result =
session.getParsedUnit(path) as ParsedUnitResult;
if (result.errors.isEmpty) {
final dart_ast.CompilationUnit unit = result.unit;
unit.accept(rootBuilder);
} else {
for (final AnalysisError error in result.errors) {
compilationErrors.add(Error(
message: error.message,
filename: error.source.fullName,
lineNumber: _calculateLineNumber(
error.source.contents.data, error.offset)));
}
}
}
}
if (compilationErrors.isEmpty) {
return rootBuilder.results();
} else {
return ParseResults(
root: Root.makeEmpty(),
errors: compilationErrors,
pigeonOptions: null,
);
}
}
/// 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: 'Path to generated Dart source file (.dart). '
'Required if one_language is not specified.')
..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('java_use_generated_annotation',
help: 'Adds the java.annotation.Generated annotation to the output.')
..addOption(
'swift_out',
help: 'Path to generated Swift file (.swift).',
aliases: const <String>['experimental_swift_out'],
)
..addOption(
'kotlin_out',
help: 'Path to generated Kotlin file (.kt).',
aliases: const <String>['experimental_kotlin_out'],
)
..addOption(
'kotlin_package',
help: 'The package that generated Kotlin code will be in.',
aliases: const <String>['experimental_kotlin_package'],
)
..addOption(
'cpp_header_out',
help: 'Path to generated C++ header file (.h).',
aliases: const <String>['experimental_cpp_header_out'],
)
..addOption(
'cpp_source_out',
help: 'Path to generated C++ classes file (.cpp).',
aliases: const <String>['experimental_cpp_source_out'],
)
..addOption('cpp_namespace',
help: 'The namespace that generated C++ code will be in.')
..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.')
..addOption('copyright_header',
help:
'Path to file with copyright header to be prepended to generated code.')
..addFlag('one_language',
help: 'Allow Pigeon to only generate code for one language.')
..addOption('ast_out',
help:
'Path to generated AST debugging info. (Warning: format subject to change)')
..addFlag('debug_generators',
help:
'Print the line number of the generator in comments at newlines.');
/// 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(
input: results['input'] as String?,
dartOut: results['dart_out'] as String?,
dartTestOut: results['dart_test_out'] as String?,
objcHeaderOut: results['objc_header_out'] as String?,
objcSourceOut: results['objc_source_out'] as String?,
objcOptions: ObjcOptions(
prefix: results['objc_prefix'] as String?,
),
javaOut: results['java_out'] as String?,
javaOptions: JavaOptions(
package: results['java_package'] as String?,
useGeneratedAnnotation:
results['java_use_generated_annotation'] as bool?,
),
swiftOut: results['swift_out'] as String?,
kotlinOut: results['kotlin_out'] as String?,
kotlinOptions: KotlinOptions(
package: results['kotlin_package'] as String?,
),
cppHeaderOut: results['cpp_header_out'] as String?,
cppSourceOut: results['cpp_source_out'] as String?,
cppOptions: CppOptions(
namespace: results['cpp_namespace'] as String?,
),
copyrightHeader: results['copyright_header'] as String?,
oneLanguage: results['one_language'] as bool?,
astOut: results['ast_out'] as String?,
debugGenerators: results['debug_generators'] as bool?,
);
return opts;
}
/// 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.");
}
}
}
}
}
/// The 'main' entrypoint used by the command-line tool. [args] are the
/// command-line arguments. The optional parameter [adapters] allows you to
/// customize the generators that pigeon will use. The optional parameter
/// [sdkPath] allows you to specify the Dart SDK path.
static Future<int> run(List<String> args,
{List<GeneratorAdapter>? adapters, String? sdkPath}) {
final PigeonOptions options = Pigeon.parseArgs(args);
return runWithOptions(options, adapters: adapters, sdkPath: sdkPath);
}
/// The 'main' entrypoint used by external packages. [options] is
/// used when running the code generator. The optional parameter [adapters] allows you to
/// customize the generators that pigeon will use. The optional parameter
/// [sdkPath] allows you to specify the Dart SDK path.
static Future<int> runWithOptions(PigeonOptions options,
{List<GeneratorAdapter>? adapters, String? sdkPath}) async {
final Pigeon pigeon = Pigeon.setup();
if (options.debugGenerators ?? false) {
generator_tools.debugGenerators = true;
}
final List<GeneratorAdapter> safeGeneratorAdapters = adapters ??
<GeneratorAdapter>[
DartGeneratorAdapter(),
JavaGeneratorAdapter(),
SwiftGeneratorAdapter(),
KotlinGeneratorAdapter(),
CppGeneratorAdapter(),
DartTestGeneratorAdapter(),
ObjcGeneratorAdapter(),
AstGeneratorAdapter(),
];
_executeConfigurePigeon(options);
if (options.input == null) {
print(usage);
return 0;
}
final ParseResults parseResults =
pigeon.parseFile(options.input!, sdkPath: sdkPath);
final List<Error> errors = <Error>[];
errors.addAll(parseResults.errors);
// Helper to clean up non-Stdout sinks.
Future<void> releaseSink(IOSink sink) async {
if (sink is! Stdout) {
await sink.close();
}
}
for (final GeneratorAdapter adapter in safeGeneratorAdapters) {
final IOSink? sink = adapter.shouldGenerate(options, FileType.source);
if (sink != null) {
final List<Error> adapterErrors =
adapter.validate(options, parseResults.root);
errors.addAll(adapterErrors);
await releaseSink(sink);
}
}
if (errors.isNotEmpty) {
printErrors(errors
.map((Error err) => Error(
message: err.message,
filename: options.input,
lineNumber: err.lineNumber))
.toList());
return 1;
}
if (parseResults.pigeonOptions != null) {
options = PigeonOptions.fromMap(
mergeMaps(options.toMap(), parseResults.pigeonOptions!));
}
if (options.oneLanguage == false && options.dartOut == null) {
print(usage);
return 1;
}
if (options.objcHeaderOut != null) {
options = options.merge(PigeonOptions(
objcOptions: options.objcOptions!.merge(ObjcOptions(
headerIncludePath: path.basename(options.objcHeaderOut!)))));
}
if (options.cppHeaderOut != null) {
options = options.merge(PigeonOptions(
cppOptions: options.cppOptions!.merge(CppOptions(
headerIncludePath: path.basename(options.cppHeaderOut!)))));
}
for (final GeneratorAdapter adapter in safeGeneratorAdapters) {
for (final FileType fileType in adapter.fileTypeList) {
final IOSink? sink = adapter.shouldGenerate(options, fileType);
if (sink != null) {
adapter.generate(sink, options, parseResults.root, fileType);
await sink.flush();
await releaseSink(sink);
}
}
}
return 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}');
}
}
}
}