40e18905f2
Closes https://github.com/dart-lang/sdk/pull/49478 TEST=Manual GitOrigin-RevId: f4c9c6869dfe73639295e86574a021523b3d374d Change-Id: I134a97caed4eec59d70e9cbca16b7e9a472cf2c1 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/251902 Reviewed-by: Michael Thomsen <mit@google.com> Commit-Queue: Alexander Thomas <athom@google.com> Reviewed-by: Aske Simon Christensen <askesc@google.com> Reviewed-by: Kevin Chisholm <kevinjchisholm@google.com> Reviewed-by: Alexander Thomas <athom@google.com>
2130 lines
66 KiB
Dart
2130 lines
66 KiB
Dart
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
library generate_vm_service_dart;
|
|
|
|
import 'package:markdown/markdown.dart';
|
|
|
|
import '../common/generate_common.dart';
|
|
import '../common/parser.dart';
|
|
import '../common/src_gen_common.dart';
|
|
import 'src_gen_dart.dart';
|
|
|
|
export 'src_gen_dart.dart' show DartGenerator;
|
|
|
|
late Api api;
|
|
|
|
String? _coerceRefType(String? typeName) {
|
|
if (typeName == 'Object') typeName = 'Obj';
|
|
if (typeName == '@Object') typeName = 'ObjRef';
|
|
if (typeName == 'Null') typeName = 'NullVal';
|
|
if (typeName == '@Null') typeName = 'NullValRef';
|
|
if (typeName == 'Function') typeName = 'Func';
|
|
if (typeName == '@Function') typeName = 'FuncRef';
|
|
|
|
if (typeName!.startsWith('@')) typeName = typeName.substring(1) + 'Ref';
|
|
|
|
if (typeName == 'string') typeName = 'String';
|
|
if (typeName == 'map') typeName = 'Map';
|
|
|
|
return typeName;
|
|
}
|
|
|
|
String _typeRefListToString(List<TypeRef> types) =>
|
|
'const [' + types.map((e) => "'" + e.name! + "'").join(',') + ']';
|
|
|
|
final String _headerCode = r'''
|
|
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
// This is a generated file.
|
|
|
|
/// A library to access the VM Service API.
|
|
///
|
|
/// The main entry-point for this library is the [VmService] class.
|
|
|
|
import 'dart:async';
|
|
import 'dart:convert' show base64, jsonDecode, jsonEncode, utf8;
|
|
import 'dart:typed_data';
|
|
|
|
import 'service_extension_registry.dart';
|
|
|
|
export 'service_extension_registry.dart' show ServiceExtensionRegistry;
|
|
export 'snapshot_graph.dart' show HeapSnapshotClass,
|
|
HeapSnapshotExternalProperty,
|
|
HeapSnapshotField,
|
|
HeapSnapshotGraph,
|
|
HeapSnapshotObject,
|
|
HeapSnapshotObjectLengthData,
|
|
HeapSnapshotObjectNoData,
|
|
HeapSnapshotObjectNullData;
|
|
''';
|
|
|
|
final String _implCode = r'''
|
|
|
|
/// Call an arbitrary service protocol method. This allows clients to call
|
|
/// methods not explicitly exposed by this library.
|
|
Future<Response> callMethod(String method, {
|
|
String? isolateId,
|
|
Map<String, dynamic>? args
|
|
}) {
|
|
return callServiceExtension(method, isolateId: isolateId, args: args);
|
|
}
|
|
|
|
/// Invoke a specific service protocol extension method.
|
|
///
|
|
/// See https://api.dart.dev/stable/dart-developer/dart-developer-library.html.
|
|
@override
|
|
Future<Response> callServiceExtension(String method, {
|
|
String? isolateId,
|
|
Map<String, dynamic>? args
|
|
}) {
|
|
if (args == null && isolateId == null) {
|
|
return _call(method);
|
|
} else if (args == null) {
|
|
return _call(method, {'isolateId': isolateId!});
|
|
} else {
|
|
args = Map.from(args);
|
|
if (isolateId != null) {
|
|
args['isolateId'] = isolateId;
|
|
}
|
|
return _call(method, args);
|
|
}
|
|
}
|
|
|
|
Stream<String> get onSend => _onSend.stream;
|
|
|
|
Stream<String> get onReceive => _onReceive.stream;
|
|
|
|
Future<void> dispose() async {
|
|
await _streamSub.cancel();
|
|
_outstandingRequests.forEach((id, request) {
|
|
request._completer.completeError(RPCError(
|
|
request.method, RPCError.kServerError, 'Service connection disposed',));
|
|
});
|
|
_outstandingRequests.clear();
|
|
if (_disposeHandler != null) {
|
|
await _disposeHandler!();
|
|
}
|
|
if (!_onDoneCompleter.isCompleted) {
|
|
_onDoneCompleter.complete();
|
|
}
|
|
}
|
|
|
|
Future get onDone => _onDoneCompleter.future;
|
|
|
|
Future<T> _call<T>(String method, [Map args = const {}]) async {
|
|
final request = _OutstandingRequest(method);
|
|
_outstandingRequests[request.id] = request;
|
|
Map m = {'jsonrpc': '2.0', 'id': request.id, 'method': method, 'params': args,};
|
|
String message = jsonEncode(m);
|
|
_onSend.add(message);
|
|
_writeMessage(message);
|
|
return await request.future as T;
|
|
}
|
|
|
|
/// Register a service for invocation.
|
|
void registerServiceCallback(String service, ServiceCallback cb) {
|
|
if (_services.containsKey(service)) {
|
|
throw Exception('Service \'${service}\' already registered');
|
|
}
|
|
_services[service] = cb;
|
|
}
|
|
|
|
void _processMessage(dynamic message) {
|
|
// Expect a String, an int[], or a ByteData.
|
|
|
|
if (message is String) {
|
|
_processMessageStr(message);
|
|
} else if (message is List<int>) {
|
|
Uint8List list = Uint8List.fromList(message);
|
|
_processMessageByteData(ByteData.view(list.buffer));
|
|
} else if (message is ByteData) {
|
|
_processMessageByteData(message);
|
|
} else {
|
|
_log.warning('unknown message type: ${message.runtimeType}');
|
|
}
|
|
}
|
|
|
|
void _processMessageByteData(ByteData bytes) {
|
|
final int metaOffset = 4;
|
|
final int dataOffset = bytes.getUint32(0, Endian.little);
|
|
final metaLength = dataOffset - metaOffset;
|
|
final dataLength = bytes.lengthInBytes - dataOffset;
|
|
final meta = utf8.decode(Uint8List.view(
|
|
bytes.buffer, bytes.offsetInBytes + metaOffset, metaLength));
|
|
final data = ByteData.view(
|
|
bytes.buffer, bytes.offsetInBytes + dataOffset, dataLength);
|
|
dynamic map = jsonDecode(meta)!;
|
|
if (map['method'] == 'streamNotify') {
|
|
String streamId = map['params']['streamId'];
|
|
Map event = map['params']['event'];
|
|
event['data'] = data;
|
|
_getEventController(streamId)
|
|
.add(createServiceObject(event, const ['Event'])! as Event);
|
|
}
|
|
}
|
|
|
|
void _processMessageStr(String message) {
|
|
late Map<String, dynamic> json;
|
|
try {
|
|
_onReceive.add(message);
|
|
json = jsonDecode(message)!;
|
|
} catch (e, s) {
|
|
_log.severe('unable to decode message: ${message}, ${e}\n${s}');
|
|
return;
|
|
}
|
|
|
|
if (json.containsKey('method')) {
|
|
if (json.containsKey('id')) {
|
|
_processRequest(json);
|
|
} else {
|
|
_processNotification(json);
|
|
}
|
|
} else if (json.containsKey('id') &&
|
|
(json.containsKey('result') || json.containsKey('error'))) {
|
|
_processResponse(json);
|
|
}
|
|
else {
|
|
_log.severe('unknown message type: ${message}');
|
|
}
|
|
}
|
|
|
|
void _processResponse(Map<String, dynamic> json) {
|
|
final request = _outstandingRequests.remove(json['id']);
|
|
if (request == null) {
|
|
_log.severe('unmatched request response: ${jsonEncode(json)}');
|
|
} else if (json['error'] != null) {
|
|
request.completeError(RPCError.parse(request.method, json['error']));
|
|
} else {
|
|
Map<String, dynamic> result = json['result'] as Map<String, dynamic>;
|
|
String? type = result['type'];
|
|
if (type == 'Sentinel') {
|
|
request.completeError(SentinelException.parse(request.method, result));
|
|
} else if (_typeFactories[type] == null) {
|
|
request.complete(Response.parse(result));
|
|
} else {
|
|
List<String> returnTypes = _methodReturnTypes[request.method] ?? [];
|
|
request.complete(createServiceObject(result, returnTypes));
|
|
}
|
|
}
|
|
}
|
|
|
|
Future _processRequest(Map<String, dynamic> json) async {
|
|
final Map m = await _routeRequest(json['method'], json['params'] ?? <String, dynamic>{});
|
|
m['id'] = json['id'];
|
|
m['jsonrpc'] = '2.0';
|
|
String message = jsonEncode(m);
|
|
_onSend.add(message);
|
|
_writeMessage(message);
|
|
}
|
|
|
|
Future _processNotification(Map<String, dynamic> json) async {
|
|
final String method = json['method'];
|
|
final Map<String, dynamic> params = json['params'] ?? <String, dynamic>{};
|
|
if (method == 'streamNotify') {
|
|
String streamId = params['streamId'];
|
|
_getEventController(streamId).add(createServiceObject(params['event'], const ['Event'])! as Event);
|
|
} else {
|
|
await _routeRequest(method, params);
|
|
}
|
|
}
|
|
|
|
Future<Map> _routeRequest(String method, Map<String, dynamic> params) async{
|
|
final service = _services[method];
|
|
if (service == null) {
|
|
RPCError error = RPCError(
|
|
method, RPCError.kMethodNotFound, 'method not found \'$method\'');
|
|
return {'error': error.toMap()};
|
|
}
|
|
|
|
try {
|
|
return await service(params);
|
|
} catch (e, st) {
|
|
RPCError error = RPCError.withDetails(
|
|
method, RPCError.kServerError, '$e', details: '$st',);
|
|
return {'error': error.toMap()};
|
|
}
|
|
}
|
|
''';
|
|
|
|
final String _rpcError = r'''
|
|
|
|
|
|
typedef DisposeHandler = Future Function();
|
|
|
|
class RPCError implements Exception {
|
|
/// Application specific error codes.
|
|
static const int kServerError = -32000;
|
|
|
|
/// The JSON sent is not a valid Request object.
|
|
static const int kInvalidRequest = -32600;
|
|
|
|
/// The method does not exist or is not available.
|
|
static const int kMethodNotFound = -32601;
|
|
|
|
/// Invalid method parameter(s), such as a mismatched type.
|
|
static const int kInvalidParams = -32602;
|
|
|
|
/// Internal JSON-RPC error.
|
|
static const int kInternalError = -32603;
|
|
|
|
static RPCError parse(String callingMethod, dynamic json) {
|
|
return RPCError(callingMethod, json['code'], json['message'], json['data']);
|
|
}
|
|
|
|
final String? callingMethod;
|
|
final int code;
|
|
final String message;
|
|
final Map? data;
|
|
|
|
RPCError(this.callingMethod, this.code, this.message, [this.data]);
|
|
|
|
RPCError.withDetails(this.callingMethod, this.code, this.message,
|
|
{Object? details})
|
|
: data = details == null ? null : <String, dynamic>{} {
|
|
if (details != null) {
|
|
data!['details'] = details;
|
|
}
|
|
}
|
|
|
|
String? get details => data == null ? null : data!['details'];
|
|
|
|
/// Return a map representation of this error suitable for conversion to
|
|
/// json.
|
|
Map<String, dynamic> toMap() {
|
|
Map<String, dynamic> map = {
|
|
'code': code,
|
|
'message': message,
|
|
};
|
|
if (data != null) {
|
|
map['data'] = data;
|
|
}
|
|
return map;
|
|
}
|
|
|
|
String toString() {
|
|
if (details == null) {
|
|
return '$callingMethod: ($code) $message';
|
|
} else {
|
|
return '$callingMethod: ($code) $message\n$details';
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Thrown when an RPC response is a [Sentinel].
|
|
class SentinelException implements Exception {
|
|
final String callingMethod;
|
|
final Sentinel sentinel;
|
|
|
|
SentinelException.parse(this.callingMethod, Map<String, dynamic> data) :
|
|
sentinel = Sentinel.parse(data)!;
|
|
|
|
String toString() => '$sentinel from ${callingMethod}()';
|
|
}
|
|
|
|
/// An `ExtensionData` is an arbitrary map that can have any contents.
|
|
class ExtensionData {
|
|
static ExtensionData? parse(Map<String, dynamic>? json) =>
|
|
json == null ? null : ExtensionData._fromJson(json);
|
|
|
|
final Map<String, dynamic> data;
|
|
|
|
ExtensionData() : data = {};
|
|
|
|
ExtensionData._fromJson(this.data);
|
|
|
|
String toString() => '[ExtensionData ${data}]';
|
|
}
|
|
|
|
/// A logging handler you can pass to a [VmService] instance in order to get
|
|
/// notifications of non-fatal service protocol warnings and errors.
|
|
abstract class Log {
|
|
/// Log a warning level message.
|
|
void warning(String message);
|
|
|
|
/// Log an error level message.
|
|
void severe(String message);
|
|
}
|
|
|
|
class _NullLog implements Log {
|
|
void warning(String message) {}
|
|
void severe(String message) {}
|
|
}
|
|
''';
|
|
|
|
final _registerServiceImpl = '''
|
|
_serviceExtensionRegistry.registerExtension(params!['service'], this);
|
|
response = Success();''';
|
|
|
|
final _streamListenCaseImpl = '''
|
|
var id = params!['streamId'];
|
|
if (_streamSubscriptions.containsKey(id)) {
|
|
throw RPCError.withDetails(
|
|
'streamListen', 103, 'Stream already subscribed',
|
|
details: "The stream '\$id' is already subscribed",
|
|
);
|
|
}
|
|
|
|
var stream = id == 'Service'
|
|
? _serviceExtensionRegistry.onExtensionEvent
|
|
: _serviceImplementation.onEvent(id);
|
|
_streamSubscriptions[id] = stream.listen((e) {
|
|
_responseSink.add({
|
|
'jsonrpc': '2.0',
|
|
'method': 'streamNotify',
|
|
'params': {
|
|
'streamId': id,
|
|
'event': e.toJson(),
|
|
},
|
|
});
|
|
});
|
|
response = Success();''';
|
|
|
|
final _streamCancelCaseImpl = '''
|
|
var id = params!['streamId'];
|
|
var existing = _streamSubscriptions.remove(id);
|
|
if (existing == null) {
|
|
throw RPCError.withDetails(
|
|
'streamCancel', 104, 'Stream not subscribed',
|
|
details: "The stream '\$id' is not subscribed",
|
|
);
|
|
}
|
|
await existing.cancel();
|
|
response = Success();''';
|
|
|
|
abstract class Member {
|
|
String? get name;
|
|
|
|
String? get docs => null;
|
|
|
|
void generate(DartGenerator gen);
|
|
|
|
bool get hasDocs => docs != null;
|
|
|
|
String toString() => name!;
|
|
}
|
|
|
|
class Api extends Member with ApiParseUtil {
|
|
String? serviceVersion;
|
|
List<Method> methods = [];
|
|
List<Enum> enums = [];
|
|
List<Type?> types = [];
|
|
List<StreamCategory> streamCategories = [];
|
|
|
|
void parse(List<Node> nodes) {
|
|
serviceVersion = ApiParseUtil.parseVersionString(nodes);
|
|
|
|
// Look for h3 nodes
|
|
// the pre following it is the definition
|
|
// the optional p following that is the documentation
|
|
|
|
String? h3Name;
|
|
|
|
for (int i = 0; i < nodes.length; i++) {
|
|
Node node = nodes[i];
|
|
|
|
if (isPre(node) && h3Name != null) {
|
|
String definition = textForCode(node);
|
|
String? docs = '';
|
|
|
|
while (i + 1 < nodes.length &&
|
|
(isPara(nodes[i + 1]) || isBlockquote(nodes[i + 1])) ||
|
|
isList(nodes[i + 1])) {
|
|
Element p = nodes[++i] as Element;
|
|
String str = TextOutputVisitor.printText(p);
|
|
if (!str.contains('|') &&
|
|
!str.contains('``') &&
|
|
!str.startsWith('- ')) {
|
|
str = collapseWhitespace(str);
|
|
}
|
|
docs = '${docs}\n\n${str}';
|
|
}
|
|
|
|
docs = docs!.trim();
|
|
if (docs.isEmpty) docs = null;
|
|
|
|
_parse(h3Name, definition, docs);
|
|
} else if (isH3(node)) {
|
|
h3Name = textForElement(node);
|
|
} else if (isHeader(node)) {
|
|
h3Name = null;
|
|
}
|
|
}
|
|
|
|
for (Type? type in types) {
|
|
type!.calculateFieldOverrides();
|
|
}
|
|
|
|
Method streamListenMethod =
|
|
methods.singleWhere((method) => method.name == 'streamListen');
|
|
_parseStreamListenDocs(streamListenMethod.docs!);
|
|
}
|
|
|
|
String get name => 'api';
|
|
|
|
String? get docs => null;
|
|
|
|
void _parse(String name, String definition, [String? docs]) {
|
|
name = name.trim();
|
|
definition = definition.trim();
|
|
// clean markdown introduced changes
|
|
definition = definition.replaceAll('<', '<').replaceAll('>', '>');
|
|
if (docs != null) docs = docs.trim();
|
|
|
|
if (definition.startsWith('class ')) {
|
|
types.add(Type(this, name, definition, docs));
|
|
} else if (name.substring(0, 1).toLowerCase() == name.substring(0, 1)) {
|
|
methods.add(Method(name, definition, docs));
|
|
} else if (definition.startsWith('enum ')) {
|
|
enums.add(Enum(name, definition, docs));
|
|
} else {
|
|
throw 'unexpected entity: ${name}, ${definition}';
|
|
}
|
|
}
|
|
|
|
static String printNode(Node n) {
|
|
if (n is Text) {
|
|
return n.text;
|
|
} else if (n is Element) {
|
|
if (n.tag != 'h3') return n.tag;
|
|
return '${n.tag}:[${n.children!.map((c) => printNode(c)).join(', ')}]';
|
|
} else {
|
|
return '${n}';
|
|
}
|
|
}
|
|
|
|
void generate(DartGenerator gen) {
|
|
gen.out(_headerCode);
|
|
gen.writeln("const String vmServiceVersion = '${serviceVersion}';");
|
|
gen.writeln();
|
|
gen.writeln('''
|
|
/// @optional
|
|
const String optional = 'optional';
|
|
|
|
/// Decode a string in Base64 encoding into the equivalent non-encoded string.
|
|
/// This is useful for handling the results of the Stdout or Stderr events.
|
|
String decodeBase64(String str) => utf8.decode(base64.decode(str));
|
|
|
|
// Returns true if a response is the Dart `null` instance.
|
|
bool _isNullInstance(Map json) => ((json['type'] == '@Instance') &&
|
|
(json['kind'] == 'Null'));
|
|
|
|
Object? createServiceObject(dynamic json, List<String> expectedTypes) {
|
|
if (json == null) return null;
|
|
|
|
if (json is List) {
|
|
return json.map((e) => createServiceObject(e, expectedTypes)).toList();
|
|
} else if (json is Map<String, dynamic>) {
|
|
String? type = json['type'];
|
|
|
|
// Not a Response type.
|
|
if (type == null) {
|
|
// If there's only one expected type, we'll just use that type.
|
|
if (expectedTypes.length == 1) {
|
|
type = expectedTypes.first;
|
|
} else {
|
|
return Response.parse(json);
|
|
}
|
|
} else if (_isNullInstance(json) && (!expectedTypes.contains('InstanceRef'))) {
|
|
// Replace null instances with null when we don't expect an instance to
|
|
// be returned.
|
|
return null;
|
|
}
|
|
final typeFactory = _typeFactories[type];
|
|
if (typeFactory == null) {
|
|
return null;
|
|
} else {
|
|
return typeFactory(json);
|
|
}
|
|
} else {
|
|
// Handle simple types.
|
|
return json;
|
|
}
|
|
}
|
|
|
|
dynamic _createSpecificObject(dynamic json, dynamic creator(Map<String, dynamic> map)) {
|
|
if (json == null) return null;
|
|
|
|
if (json is List) {
|
|
return json.map((e) => creator(e)).toList();
|
|
} else if (json is Map) {
|
|
return creator({
|
|
for (String key in json.keys)
|
|
key: json[key],
|
|
});
|
|
} else {
|
|
// Handle simple types.
|
|
return json;
|
|
}
|
|
}
|
|
|
|
void _setIfNotNull(Map<String, dynamic> json, String key, Object? value) {
|
|
if (value == null) return;
|
|
json[key] = value;
|
|
}
|
|
|
|
Future<T> extensionCallHelper<T>(VmService service, String method, Map args) {
|
|
return service._call(method, args);
|
|
}
|
|
|
|
typedef ServiceCallback = Future<Map<String, dynamic>> Function(
|
|
Map<String, dynamic> params);
|
|
|
|
void addTypeFactory(String name, Function factory) {
|
|
if (_typeFactories.containsKey(name)) {
|
|
throw StateError('Factory already registered for \$name');
|
|
}
|
|
_typeFactories[name] = factory;
|
|
}
|
|
|
|
''');
|
|
gen.writeln();
|
|
gen.writeln('Map<String, Function> _typeFactories = {');
|
|
types.forEach((Type? type) {
|
|
gen.writeln("'${type!.rawName}': ${type.name}.parse,");
|
|
});
|
|
gen.writeln('};');
|
|
gen.writeln();
|
|
|
|
gen.writeln('Map<String, List<String>> _methodReturnTypes = {');
|
|
methods.forEach((Method method) {
|
|
String returnTypes = _typeRefListToString(method.returnType.types);
|
|
gen.writeln("'${method.name}' : $returnTypes,");
|
|
});
|
|
gen.writeln('};');
|
|
gen.writeln();
|
|
|
|
// The service interface, both servers and clients implement this.
|
|
gen.writeStatement('''
|
|
/// A class representation of the Dart VM Service Protocol.
|
|
///
|
|
/// Both clients and servers should implement this interface.
|
|
abstract class VmServiceInterface {
|
|
/// Returns the stream for a given stream id.
|
|
///
|
|
/// This is not a part of the spec, but is needed for both the client and
|
|
/// server to get access to the real event streams.
|
|
Stream<Event> onEvent(String streamId);
|
|
|
|
/// Handler for calling extra service extensions.
|
|
Future<Response> callServiceExtension(String method, {String? isolateId, Map<String, dynamic>? args});
|
|
''');
|
|
methods.forEach((m) {
|
|
m.generateDefinition(gen);
|
|
gen.write(';');
|
|
});
|
|
gen.write('}');
|
|
gen.writeln();
|
|
|
|
// The server class, takes a VmServiceInterface and delegates to it
|
|
// automatically.
|
|
gen.write('''
|
|
class _PendingServiceRequest {
|
|
Future<Map<String, Object?>> get future => _completer.future;
|
|
final _completer = Completer<Map<String, Object?>>();
|
|
|
|
final dynamic originalId;
|
|
|
|
_PendingServiceRequest(this.originalId);
|
|
|
|
void complete(Map<String, Object?> response) {
|
|
response['id'] = originalId;
|
|
_completer.complete(response);
|
|
}
|
|
}
|
|
|
|
/// A Dart VM Service Protocol connection that delegates requests to a
|
|
/// [VmServiceInterface] implementation.
|
|
///
|
|
/// One of these should be created for each client, but they should generally
|
|
/// share the same [VmServiceInterface] and [ServiceExtensionRegistry]
|
|
/// instances.
|
|
class VmServerConnection {
|
|
final Stream<Map<String, Object>> _requestStream;
|
|
final StreamSink<Map<String, Object?>> _responseSink;
|
|
final ServiceExtensionRegistry _serviceExtensionRegistry;
|
|
final VmServiceInterface _serviceImplementation;
|
|
/// Used to create unique ids when acting as a proxy between clients.
|
|
int _nextServiceRequestId = 0;
|
|
|
|
/// Manages streams for `streamListen` and `streamCancel` requests.
|
|
final _streamSubscriptions = <String, StreamSubscription>{};
|
|
|
|
/// Completes when [_requestStream] is done.
|
|
Future<void> get done => _doneCompleter.future;
|
|
final _doneCompleter = Completer<void>();
|
|
|
|
/// Pending service extension requests to this client by id.
|
|
final _pendingServiceExtensionRequests = <dynamic, _PendingServiceRequest>{};
|
|
|
|
VmServerConnection(
|
|
this._requestStream, this._responseSink, this._serviceExtensionRegistry,
|
|
this._serviceImplementation) {
|
|
_requestStream.listen(_delegateRequest, onDone: _doneCompleter.complete);
|
|
done.then(
|
|
(_) => _streamSubscriptions.values.forEach((sub) => sub.cancel()));
|
|
}
|
|
|
|
/// Invoked when the current client has registered some extension, and
|
|
/// another client sends an RPC request for that extension.
|
|
///
|
|
/// We don't attempt to do any serialization or deserialization of the
|
|
/// request or response in this case
|
|
Future<Map<String, Object?>> _forwardServiceExtensionRequest(
|
|
Map<String, Object?> request) {
|
|
final originalId = request['id'];
|
|
request = Map<String, Object?>.of(request);
|
|
// Modify the request ID to ensure we don't have conflicts between
|
|
// multiple clients ids.
|
|
final newId = '\${_nextServiceRequestId++}:\$originalId';
|
|
request['id'] = newId;
|
|
var pendingRequest = _PendingServiceRequest(originalId);
|
|
_pendingServiceExtensionRequests[newId] = pendingRequest;
|
|
_responseSink.add(request);
|
|
return pendingRequest.future;
|
|
}
|
|
|
|
void _delegateRequest(Map<String, Object?> request) async {
|
|
try {
|
|
var id = request['id'];
|
|
// Check if this is actually a response to a pending request.
|
|
if (_pendingServiceExtensionRequests.containsKey(id)) {
|
|
final pending = _pendingServiceExtensionRequests[id]!;
|
|
pending.complete(Map<String, Object?>.of(request));
|
|
return;
|
|
}
|
|
final method = request['method'] as String?;
|
|
if (method == null) {
|
|
throw RPCError(
|
|
null, RPCError.kInvalidRequest, 'Invalid Request', request);
|
|
}
|
|
final params = request['params'] as Map<String, dynamic>?;
|
|
late Response response;
|
|
|
|
switch(method) {
|
|
case 'registerService':
|
|
$_registerServiceImpl
|
|
break;
|
|
''');
|
|
methods.forEach((m) {
|
|
if (m.name != 'registerService') {
|
|
gen.writeln("case '${m.name}':");
|
|
if (m.name == 'streamListen') {
|
|
gen.writeln(_streamListenCaseImpl);
|
|
} else if (m.name == 'streamCancel') {
|
|
gen.writeln(_streamCancelCaseImpl);
|
|
} else {
|
|
bool firstParam = true;
|
|
final nullCheck = () {
|
|
final result = firstParam ? '!' : '';
|
|
if (firstParam) {
|
|
firstParam = false;
|
|
}
|
|
return result;
|
|
};
|
|
if (m.deprecated) {
|
|
gen.writeln("// ignore: deprecated_member_use_from_same_package");
|
|
}
|
|
gen.write("response = await _serviceImplementation.${m.name}(");
|
|
// Positional args
|
|
m.args.where((arg) => !arg.optional).forEach((MethodArg arg) {
|
|
if (arg.type.isArray) {
|
|
gen.write(
|
|
"${arg.type.listCreationRef}.from(params${nullCheck()}['${arg.name}'] ?? []), ");
|
|
} else {
|
|
gen.write("params${nullCheck()}['${arg.name}'], ");
|
|
}
|
|
});
|
|
// Optional named args
|
|
var namedArgs = m.args.where((arg) => arg.optional);
|
|
if (namedArgs.isNotEmpty) {
|
|
namedArgs.forEach((arg) {
|
|
if (arg.name == 'scope') {
|
|
gen.writeln(
|
|
"${arg.name}: params${nullCheck()}['${arg.name}']?.cast<String, String>(), ");
|
|
} else {
|
|
gen.writeln(
|
|
"${arg.name}: params${nullCheck()}['${arg.name}'], ");
|
|
}
|
|
});
|
|
}
|
|
gen.writeln(");");
|
|
}
|
|
gen.writeln('break;');
|
|
}
|
|
});
|
|
// Handle service extensions
|
|
gen.writeln('default:');
|
|
gen.writeln('''
|
|
final registeredClient = _serviceExtensionRegistry.clientFor(method);
|
|
if (registeredClient != null) {
|
|
// Check for any client which has registered this extension, if we
|
|
// have one then delegate the request to that client.
|
|
_responseSink.add(
|
|
await registeredClient._forwardServiceExtensionRequest(request));
|
|
// Bail out early in this case, we are just acting as a proxy and
|
|
// never get a `Response` instance.
|
|
return;
|
|
} else if (method.startsWith('ext.')) {
|
|
// Remaining methods with `ext.` are assumed to be registered via
|
|
// dart:developer, which the service implementation handles.
|
|
final args = params == null ? null : Map<String, dynamic>.of(params);
|
|
final isolateId = args?.remove('isolateId');
|
|
response = await _serviceImplementation.callServiceExtension(method,
|
|
isolateId: isolateId, args: args);
|
|
} else {
|
|
throw RPCError(method, RPCError.kMethodNotFound, 'Method not found', request);
|
|
}
|
|
''');
|
|
// Terminate the switch
|
|
gen.writeln('}');
|
|
|
|
// Generate the json success response
|
|
gen.write("""_responseSink.add({
|
|
'jsonrpc': '2.0',
|
|
'id': id,
|
|
'result': response.toJson(),
|
|
});
|
|
""");
|
|
|
|
// Close the try block, handle errors
|
|
gen.write(r'''
|
|
} catch (e, st) {
|
|
final error = e is RPCError
|
|
? e.toMap()
|
|
: {
|
|
'code': RPCError.kInternalError,
|
|
'message': '${request['method']}: $e',
|
|
'data': {'details': '$st'},
|
|
};
|
|
_responseSink.add({
|
|
'jsonrpc': '2.0',
|
|
'id': request['id'],
|
|
'error': error,
|
|
});
|
|
}
|
|
''');
|
|
|
|
// terminate the _delegateRequest method
|
|
gen.write('}');
|
|
gen.writeln();
|
|
|
|
gen.write('}');
|
|
gen.writeln();
|
|
|
|
gen.write('''
|
|
class _OutstandingRequest<T> {
|
|
_OutstandingRequest(this.method);
|
|
static int _idCounter = 0;
|
|
final String id = '\${_idCounter++}';
|
|
final String method;
|
|
final StackTrace _stackTrace = StackTrace.current;
|
|
final Completer<T> _completer = Completer<T>();
|
|
|
|
Future<T> get future => _completer.future;
|
|
|
|
void complete(T value) => _completer.complete(value);
|
|
void completeError(Object error) =>
|
|
_completer.completeError(error, _stackTrace);
|
|
}
|
|
''');
|
|
|
|
// The client side service implementation.
|
|
gen.writeStatement('class VmService implements VmServiceInterface {');
|
|
gen.writeStatement('late final StreamSubscription _streamSub;');
|
|
gen.writeStatement('late final Function _writeMessage;');
|
|
gen.writeStatement(
|
|
'final Map<String, _OutstandingRequest> _outstandingRequests = {};');
|
|
gen.writeStatement('Map<String, ServiceCallback> _services = {};');
|
|
gen.writeStatement('late final Log _log;');
|
|
gen.write('''
|
|
|
|
StreamController<String> _onSend = StreamController.broadcast(sync: true);
|
|
StreamController<String> _onReceive = StreamController.broadcast(sync: true);
|
|
|
|
final Completer _onDoneCompleter = Completer();
|
|
|
|
Map<String, StreamController<Event>> _eventControllers = {};
|
|
|
|
StreamController<Event> _getEventController(String eventName) {
|
|
StreamController<Event>? controller = _eventControllers[eventName];
|
|
if (controller == null) {
|
|
controller = StreamController.broadcast();
|
|
_eventControllers[eventName] = controller;
|
|
}
|
|
return controller;
|
|
}
|
|
|
|
late final DisposeHandler? _disposeHandler;
|
|
|
|
VmService(Stream<dynamic> /*String|List<int>*/ inStream, void writeMessage(String message), {
|
|
Log? log,
|
|
DisposeHandler? disposeHandler,
|
|
Future? streamClosed,
|
|
}) {
|
|
_streamSub = inStream.listen(_processMessage, onDone: ()=> _onDoneCompleter.complete());
|
|
_writeMessage = writeMessage;
|
|
_log = log == null ? _NullLog() : log;
|
|
_disposeHandler = disposeHandler;
|
|
streamClosed?.then((_) {
|
|
if (!_onDoneCompleter.isCompleted) {
|
|
_onDoneCompleter.complete();
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
Stream<Event> onEvent(String streamId) => _getEventController(streamId).stream;
|
|
''');
|
|
|
|
// streamCategories
|
|
streamCategories.forEach((s) => s.generate(gen));
|
|
|
|
gen.writeln();
|
|
methods.forEach((m) => m.generate(gen));
|
|
gen.out(_implCode);
|
|
gen.writeStatement('}');
|
|
gen.writeln();
|
|
gen.out(_rpcError);
|
|
gen.writeln('// enums');
|
|
enums.forEach((e) {
|
|
if (e.name == 'EventKind') {
|
|
_generateEventStream(gen);
|
|
}
|
|
e.generate(gen);
|
|
});
|
|
gen.writeln();
|
|
gen.writeln('// types');
|
|
types.where((t) => !t!.skip).forEach((t) => t!.generate(gen));
|
|
}
|
|
|
|
void setDefaultValue(String typeName, String fieldName, String defaultValue) {
|
|
types
|
|
.firstWhere((t) => t!.name == typeName)!
|
|
.fields
|
|
.firstWhere((f) => f.name == fieldName)
|
|
.defaultValue = defaultValue;
|
|
}
|
|
|
|
bool isEnumName(String? typeName) =>
|
|
enums.any((Enum e) => e.name == typeName);
|
|
|
|
Type? getType(String? name) =>
|
|
types.firstWhere((t) => t!.name == name, orElse: () => null);
|
|
|
|
void _parseStreamListenDocs(String docs) {
|
|
Iterator<String> lines = docs.split('\n').map((l) => l.trim()).iterator;
|
|
bool inStreamDef = false;
|
|
|
|
while (lines.moveNext()) {
|
|
final String line = lines.current;
|
|
|
|
if (line.startsWith('streamId |')) {
|
|
inStreamDef = true;
|
|
lines.moveNext();
|
|
} else if (inStreamDef) {
|
|
if (line.isEmpty) {
|
|
inStreamDef = false;
|
|
} else {
|
|
streamCategories.add(StreamCategory(line));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void _generateEventStream(DartGenerator gen) {
|
|
gen.writeln();
|
|
gen.writeDocs('An enum of available event streams.');
|
|
gen.writeln('class EventStreams {');
|
|
gen.writeln('EventStreams._();');
|
|
gen.writeln();
|
|
|
|
streamCategories.forEach((c) {
|
|
gen.writeln("static const String k${c.name} = '${c.name}';");
|
|
});
|
|
|
|
gen.writeln('}');
|
|
}
|
|
}
|
|
|
|
class StreamCategory {
|
|
String? _name;
|
|
List<String>? _events;
|
|
|
|
StreamCategory(String line) {
|
|
// Debug | PauseStart, PauseExit, ...
|
|
_name = line.split('|')[0].trim();
|
|
|
|
line = line.split('|')[1];
|
|
_events = line.split(',').map((w) => w.trim()).toList();
|
|
}
|
|
|
|
String? get name => _name;
|
|
|
|
List<String>? get events => _events;
|
|
|
|
void generate(DartGenerator gen) {
|
|
gen.writeln();
|
|
gen.writeln('// ${events!.join(', ')}');
|
|
gen.writeln(
|
|
"Stream<Event> get on${name}Event => _getEventController('$name').stream;");
|
|
}
|
|
|
|
String toString() => '$name: $events';
|
|
}
|
|
|
|
class Method extends Member {
|
|
final String name;
|
|
final String? docs;
|
|
|
|
MemberType returnType = MemberType();
|
|
bool get deprecated => deprecationMessage != null;
|
|
String? deprecationMessage;
|
|
List<MethodArg> args = [];
|
|
|
|
Method(this.name, String definition, [this.docs]) {
|
|
_parse(Tokenizer(definition).tokenize());
|
|
}
|
|
|
|
bool get hasArgs => args.isNotEmpty;
|
|
|
|
bool get hasOptionalArgs => args.any((MethodArg arg) => arg.optional);
|
|
|
|
void generate(DartGenerator gen) {
|
|
generateDefinition(gen, withDocs: false, withOverrides: true);
|
|
if (!hasArgs) {
|
|
gen.writeStatement("=> _call('${name}');");
|
|
} else if (hasOptionalArgs) {
|
|
gen.writeStatement("=> _call('$name', {");
|
|
gen.write(args
|
|
.where((MethodArg a) => !a.optional)
|
|
.map((arg) => "'${arg.name}': ${arg.name},")
|
|
.join());
|
|
|
|
args.where((MethodArg a) => a.optional).forEach((MethodArg arg) {
|
|
String? valueRef = arg.name;
|
|
// Special case for `getAllocationProfile`. We do not want to add these
|
|
// params if they are false.
|
|
if (name == 'getAllocationProfile') {
|
|
gen.writeln("if (${arg.name} != null && ${arg.name})");
|
|
} else {
|
|
gen.writeln("if (${arg.name} != null)");
|
|
}
|
|
gen.writeln("'${arg.name}': $valueRef,");
|
|
});
|
|
|
|
gen.writeln('});');
|
|
} else {
|
|
gen.write("=> _call('${name}', {");
|
|
gen.write(args.map((MethodArg arg) {
|
|
return "'${arg.name}': ${arg.name}";
|
|
}).join(', '));
|
|
gen.writeStatement('});');
|
|
}
|
|
}
|
|
|
|
/// Writes the method definition without the body.
|
|
///
|
|
/// Does not write an opening or closing bracket, or a trailing semicolon.
|
|
///
|
|
/// If [withOverrides] is `true` then it will add an `@override` annotation
|
|
/// before each method.
|
|
void generateDefinition(DartGenerator gen,
|
|
{bool withDocs = true, bool withOverrides = false}) {
|
|
gen.writeln();
|
|
if (withDocs && docs != null) {
|
|
String _docs = docs == null ? '' : docs!;
|
|
if (returnType.isMultipleReturns) {
|
|
_docs += '\n\nThe return value can be one of '
|
|
'${joinLast(returnType.types.map((t) => '[${t}]'), ', ', ' or ')}.';
|
|
_docs = _docs.trim();
|
|
}
|
|
if (returnType.canReturnSentinel) {
|
|
_docs +=
|
|
'\n\nThis method will throw a [SentinelException] in the case a [Sentinel] is returned.';
|
|
_docs = _docs.trim();
|
|
}
|
|
if (_docs.isNotEmpty) gen.writeDocs(_docs);
|
|
}
|
|
if (deprecated) {
|
|
gen.writeln("@Deprecated('$deprecationMessage')");
|
|
}
|
|
if (withOverrides) gen.writeln('@override');
|
|
gen.write('Future<${returnType.name}> ${name}(');
|
|
bool startedOptional = false;
|
|
gen.write(args.map((MethodArg arg) {
|
|
String typeName;
|
|
if (api.isEnumName(arg.type.name)) {
|
|
if (arg.type.isArray) {
|
|
typeName = typeName = '/*${arg.type}*/ List<String>';
|
|
} else {
|
|
typeName = '/*${arg.type}*/ String';
|
|
}
|
|
} else {
|
|
typeName = arg.type.ref;
|
|
}
|
|
final nullable = arg.optional ? '?' : '';
|
|
if (arg.optional && !startedOptional) {
|
|
startedOptional = true;
|
|
return '{${typeName}$nullable ${arg.name}';
|
|
} else {
|
|
return '${typeName}$nullable ${arg.name}';
|
|
}
|
|
}).join(', '));
|
|
if (args.length >= 4) gen.write(',');
|
|
if (startedOptional) gen.write('}');
|
|
gen.write(') ');
|
|
}
|
|
|
|
void _parse(Token? token) => MethodParser(token).parseInto(this);
|
|
}
|
|
|
|
class MemberType extends Member {
|
|
List<TypeRef> types = [];
|
|
|
|
MemberType();
|
|
|
|
void parse(Parser parser, {bool isReturnType = false}) {
|
|
// foo|bar[]|baz
|
|
// (@Instance|Sentinel)[]
|
|
bool loop = true;
|
|
bool nullable = false;
|
|
this.isReturnType = isReturnType;
|
|
|
|
final unionTypes = <String>[];
|
|
while (loop) {
|
|
if (parser.consume('(')) {
|
|
while (parser.peek()!.text != ')') {
|
|
if (parser.consume('Null')) {
|
|
nullable = true;
|
|
} else {
|
|
// @Instance | Sentinel
|
|
final token = parser.advance()!;
|
|
if (token.isName) {
|
|
unionTypes.add(_coerceRefType(token.text)!);
|
|
}
|
|
}
|
|
}
|
|
parser.consume(')');
|
|
TypeRef ref;
|
|
if (unionTypes.length == 1) {
|
|
ref = TypeRef(unionTypes.first)..nullable = nullable;
|
|
} else {
|
|
ref = TypeRef('dynamic');
|
|
}
|
|
while (parser.consume('[')) {
|
|
parser.expect(']');
|
|
ref.arrayDepth++;
|
|
}
|
|
types.add(ref);
|
|
} else {
|
|
Token t = parser.expectName();
|
|
TypeRef ref = TypeRef(_coerceRefType(t.text));
|
|
while (parser.consume('[')) {
|
|
parser.expect(']');
|
|
ref.arrayDepth++;
|
|
}
|
|
if (isReturnType && ref.name == 'Sentinel') {
|
|
canReturnSentinel = true;
|
|
} else {
|
|
types.add(ref);
|
|
}
|
|
}
|
|
|
|
loop = parser.consume('|');
|
|
}
|
|
}
|
|
|
|
String get name {
|
|
if (types.isEmpty) return '';
|
|
if (types.length == 1) return types.first.ref;
|
|
if (isReturnType) return 'Response';
|
|
return 'dynamic';
|
|
}
|
|
|
|
bool isReturnType = false;
|
|
bool canReturnSentinel = false;
|
|
|
|
bool get isMultipleReturns => types.length > 1;
|
|
|
|
bool get isSimple => types.length == 1 && types.first.isSimple;
|
|
|
|
bool get isEnum => types.length == 1 && api.isEnumName(types.first.name);
|
|
|
|
bool get isArray => types.length == 1 && types.first.isArray;
|
|
|
|
void generate(DartGenerator gen) => gen.write(name);
|
|
}
|
|
|
|
class TypeRef {
|
|
String? name;
|
|
int arrayDepth = 0;
|
|
bool nullable = false;
|
|
List<TypeRef>? genericTypes;
|
|
|
|
TypeRef(this.name);
|
|
|
|
String get ref {
|
|
if (arrayDepth == 2) {
|
|
return 'List<List<${name}${nullable ? "?" : ""}>>';
|
|
} else if (arrayDepth == 1) {
|
|
return 'List<${name}${nullable ? "?" : ""}>';
|
|
} else if (genericTypes != null) {
|
|
return '$name<${genericTypes!.join(', ')}>';
|
|
} else {
|
|
return name!.startsWith('_') ? name!.substring(1) : name!;
|
|
}
|
|
}
|
|
|
|
String get listCreationRef {
|
|
assert(arrayDepth == 1);
|
|
|
|
if (isListTypeSimple) {
|
|
return 'List<$name${nullable ? "?" : ""}>';
|
|
} else {
|
|
return 'List<String>';
|
|
}
|
|
}
|
|
|
|
String? get listTypeArg => arrayDepth == 2
|
|
? 'List<$name${nullable ? "?" : ""}>'
|
|
: '$name${nullable ? "?" : ""}';
|
|
|
|
bool get isArray => arrayDepth > 0;
|
|
|
|
bool get isSimple =>
|
|
arrayDepth == 0 &&
|
|
(name == 'int' ||
|
|
name == 'num' ||
|
|
name == 'String' ||
|
|
name == 'bool' ||
|
|
name == 'double' ||
|
|
name == 'ByteData');
|
|
|
|
bool get isListTypeSimple =>
|
|
arrayDepth == 1 &&
|
|
(name == 'int' ||
|
|
name == 'num' ||
|
|
name == 'String' ||
|
|
name == 'bool' ||
|
|
name == 'double' ||
|
|
name == 'ByteData');
|
|
|
|
String toString() => ref;
|
|
}
|
|
|
|
class MethodArg extends Member {
|
|
final Method parent;
|
|
TypeRef type;
|
|
String? name;
|
|
bool optional = false;
|
|
|
|
MethodArg(this.parent, this.type, this.name);
|
|
|
|
void generate(DartGenerator gen) {
|
|
gen.write('${type.ref} ${name}');
|
|
}
|
|
|
|
String toString() => '$type $name';
|
|
}
|
|
|
|
class Type extends Member {
|
|
final Api parent;
|
|
String? rawName;
|
|
String? name;
|
|
String? superName;
|
|
final String? docs;
|
|
List<TypeField> fields = [];
|
|
|
|
Type(this.parent, String categoryName, String definition, [this.docs]) {
|
|
_parse(Tokenizer(definition).tokenize());
|
|
}
|
|
|
|
Type._(this.parent, this.rawName, this.name, this.superName, this.docs);
|
|
|
|
factory Type.merge(Type t1, Type t2) {
|
|
final Api parent = t1.parent;
|
|
final String? rawName = t1.rawName;
|
|
final String? name = t1.name;
|
|
final String? superName = t1.superName;
|
|
final String docs = [t1.docs, t2.docs].where((e) => e != null).join('\n');
|
|
final Map<String?, TypeField> map = <String?, TypeField>{};
|
|
for (TypeField f in t2.fields.reversed) {
|
|
map[f.name] = f;
|
|
}
|
|
// The official service.md is the default
|
|
for (TypeField f in t1.fields.reversed) {
|
|
map[f.name] = f;
|
|
}
|
|
|
|
final fields = map.values.toList().reversed.toList();
|
|
|
|
return Type._(parent, rawName, name, superName, docs)..fields = fields;
|
|
}
|
|
|
|
bool get isResponse {
|
|
if (superName == null) return false;
|
|
if (name == 'Response' || superName == 'Response') return true;
|
|
return parent.getType(superName)!.isResponse;
|
|
}
|
|
|
|
bool get isRef => name!.endsWith('Ref');
|
|
|
|
bool get supportsIdentity {
|
|
if (fields.any((f) => f.name == 'id')) return true;
|
|
return superName == null ? false : getSuper()!.supportsIdentity;
|
|
}
|
|
|
|
Type? getSuper() => superName == null ? null : api.getType(superName);
|
|
|
|
List<TypeField> getAllFields() {
|
|
if (superName == null) return fields;
|
|
|
|
List<TypeField> all = [];
|
|
all.insertAll(0, fields);
|
|
|
|
Type? s = getSuper();
|
|
while (s != null) {
|
|
all.insertAll(0, s.fields);
|
|
s = s.getSuper();
|
|
}
|
|
|
|
return all;
|
|
}
|
|
|
|
bool get skip => name == 'ExtensionData';
|
|
|
|
void generate(DartGenerator gen) {
|
|
gen.writeln();
|
|
if (docs != null) gen.writeDocs(docs);
|
|
gen.write('class ${name} ');
|
|
Type? superType;
|
|
if (superName != null) {
|
|
superType = parent.getType(superName);
|
|
gen.write('extends ${superName} ');
|
|
}
|
|
if (parent.getType('${name}Ref') != null) {
|
|
gen.write('implements ${name}Ref ');
|
|
}
|
|
gen.writeln('{');
|
|
gen.writeln('static ${name}? parse(Map<String, dynamic>? json) => '
|
|
'json == null ? null : ${name}._fromJson(json);');
|
|
gen.writeln();
|
|
|
|
if (name == 'Response' || name == 'TimelineEvent') {
|
|
gen.writeln('Map<String, dynamic>? json;');
|
|
}
|
|
if (name == 'Script') {
|
|
gen.writeln('final _tokenToLine = <int, int>{};');
|
|
gen.writeln('final _tokenToColumn = <int, int>{};');
|
|
}
|
|
|
|
// fields
|
|
fields.forEach((TypeField field) => field.generate(gen));
|
|
gen.writeln();
|
|
|
|
// ctors
|
|
|
|
bool hasRequiredParentFields = superType != null &&
|
|
(superType.name == 'ObjRef' || superType.name == 'Obj');
|
|
// Default
|
|
gen.write('${name}(');
|
|
if (fields.isNotEmpty) {
|
|
gen.write('{');
|
|
fields.where((field) => !field.optional).forEach((field) {
|
|
final fromParent = (name == 'Instance' && field.name == 'classRef');
|
|
field.generateNamedParameter(gen, fromParent: fromParent);
|
|
});
|
|
if (hasRequiredParentFields) {
|
|
superType.fields.where((field) => !field.optional).forEach(
|
|
(field) => field.generateNamedParameter(gen, fromParent: true));
|
|
}
|
|
fields
|
|
.where((field) => field.optional)
|
|
.forEach((field) => field.generateNamedParameter(gen));
|
|
gen.write('}');
|
|
}
|
|
gen.write(')');
|
|
if (hasRequiredParentFields) {
|
|
gen.write(' : super(');
|
|
superType.fields.where((field) => !field.optional).forEach((field) {
|
|
String? name = field.generatableName;
|
|
gen.writeln('$name: $name,');
|
|
});
|
|
if (name == 'Instance') {
|
|
gen.writeln('classRef: classRef,');
|
|
}
|
|
gen.write(')');
|
|
} else if (name!.contains('NullVal')) {
|
|
gen.writeln(' : super(');
|
|
gen.writeln("id: 'instance/null',");
|
|
gen.writeln('identityHashCode: 0,');
|
|
gen.writeln('kind: InstanceKind.kNull,');
|
|
gen.writeln("classRef: ClassRef(id: 'class/null',");
|
|
gen.writeln("library: LibraryRef(id: '', name: 'dart:core',");
|
|
gen.writeln("uri: 'dart:core',),");
|
|
gen.writeln("name: 'Null',),");
|
|
gen.writeln(')');
|
|
}
|
|
|
|
gen.writeln(';');
|
|
|
|
// Build from JSON.
|
|
gen.writeln();
|
|
String superCall = superName == null ? '' : ": super._fromJson(json) ";
|
|
if (name == 'Response' || name == 'TimelineEvent') {
|
|
gen.write('${name}._fromJson(this.json)');
|
|
} else {
|
|
gen.write('${name}._fromJson(Map<String, dynamic> json) ${superCall}');
|
|
}
|
|
|
|
if (fields.isEmpty) {
|
|
gen.writeln(';');
|
|
} else {
|
|
gen.writeln('{');
|
|
}
|
|
|
|
fields.forEach((TypeField field) {
|
|
if (field.type.isSimple || field.type.isEnum) {
|
|
// Special case `AllocationProfile`.
|
|
if (name == 'AllocationProfile' && field.type.name == 'int') {
|
|
gen.write(
|
|
"${field.generatableName} = json['${field.name}'] is String ? "
|
|
"int.parse(json['${field.name}']) : json['${field.name}']");
|
|
} else {
|
|
gen.write("${field.generatableName} = json['${field.name}']");
|
|
}
|
|
if (field.defaultValue != null) {
|
|
gen.write(' ?? ${field.defaultValue}');
|
|
} else if (!field.optional) {
|
|
// If a default isn't provided and the field is required, generate a
|
|
// sane default initializer to avoid TypeErrors at runtime when
|
|
// running in a null-safe context.
|
|
dynamic defaultValue;
|
|
switch (field.type.name) {
|
|
case 'int':
|
|
case 'num':
|
|
case 'double':
|
|
defaultValue = -1;
|
|
break;
|
|
case 'bool':
|
|
defaultValue = false;
|
|
break;
|
|
case 'String':
|
|
defaultValue = "''";
|
|
break;
|
|
case 'ByteData':
|
|
defaultValue = "ByteData(0)";
|
|
break;
|
|
default:
|
|
{
|
|
if (field.type.isEnum) {
|
|
// TODO(bkonyi): Figure out if there's a more correct way to
|
|
// determine a default value for enums.
|
|
defaultValue = "''";
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
gen.write(' ?? $defaultValue');
|
|
}
|
|
gen.writeln(';');
|
|
// } else if (field.type.isEnum) {
|
|
// // Parse the enum.
|
|
// String enumTypeName = field.type.types.first.name;
|
|
// gen.writeln(
|
|
// "${field.generatableName} = _parse${enumTypeName}[json['${field.name}']];");
|
|
} else if (name == 'Event' && field.name == 'extensionData') {
|
|
// Special case `Event.extensionData`.
|
|
gen.writeln(
|
|
"extensionData = ExtensionData.parse(json['extensionData']);");
|
|
} else if (name == 'Instance' && field.name == 'associations') {
|
|
// Special case `Instance.associations`.
|
|
gen.writeln("associations = json['associations'] == null "
|
|
"? null : List<MapAssociation>.from("
|
|
"_createSpecificObject(json['associations'], MapAssociation.parse));");
|
|
} else if (name == 'Instance' && field.name == 'classRef') {
|
|
// This is populated by `Obj`
|
|
} else if (name == '_CpuProfile' && field.name == 'codes') {
|
|
// Special case `_CpuProfile.codes`.
|
|
gen.writeln("codes = List<CodeRegion>.from("
|
|
"_createSpecificObject(json['codes']!, CodeRegion.parse));");
|
|
} else if (name == '_CpuProfile' && field.name == 'functions') {
|
|
// Special case `_CpuProfile.functions`.
|
|
gen.writeln("functions = List<ProfileFunction>.from("
|
|
"_createSpecificObject(json['functions']!, ProfileFunction.parse));");
|
|
} else if (name == 'SourceReport' && field.name == 'ranges') {
|
|
// Special case `SourceReport.ranges`.
|
|
gen.writeln("ranges = List<SourceReportRange>.from("
|
|
"_createSpecificObject(json['ranges']!, SourceReportRange.parse));");
|
|
} else if (name == 'SourceReportRange' && field.name == 'coverage') {
|
|
// Special case `SourceReportRange.coverage`.
|
|
gen.writeln("coverage = _createSpecificObject("
|
|
"json['coverage'], SourceReportCoverage.parse);");
|
|
} else if (name == 'Library' && field.name == 'dependencies') {
|
|
// Special case `Library.dependencies`.
|
|
gen.writeln("dependencies = List<LibraryDependency>.from("
|
|
"_createSpecificObject(json['dependencies']!, "
|
|
"LibraryDependency.parse));");
|
|
} else if (name == 'Script' && field.name == 'tokenPosTable') {
|
|
// Special case `Script.tokenPosTable`.
|
|
gen.write("tokenPosTable = ");
|
|
if (field.optional) {
|
|
gen.write("json['tokenPosTable'] == null ? null : ");
|
|
}
|
|
gen.writeln("List<List<int>>.from(json['tokenPosTable']!.map"
|
|
"((dynamic list) => List<int>.from(list)));");
|
|
gen.writeln('_parseTokenPosTable();');
|
|
} else if (field.type.isArray) {
|
|
TypeRef fieldType = field.type.types.first;
|
|
String typesList = _typeRefListToString(field.type.types);
|
|
String ref = "json['${field.name}']";
|
|
if (field.optional) {
|
|
if (fieldType.isListTypeSimple) {
|
|
gen.writeln("${field.generatableName} = $ref == null ? null : "
|
|
"List<${fieldType.listTypeArg}>.from($ref);");
|
|
} else {
|
|
gen.writeln("${field.generatableName} = $ref == null ? null : "
|
|
"List<${fieldType.listTypeArg}>.from(createServiceObject($ref, $typesList)! as List);");
|
|
}
|
|
} else {
|
|
if (fieldType.isListTypeSimple) {
|
|
// Special case `ClassHeapStats`. Pre 3.18, responses included keys
|
|
// `new` and `old`. Post 3.18, these will be null.
|
|
if (name == 'ClassHeapStats') {
|
|
gen.writeln("${field.generatableName} = $ref == null ? null : "
|
|
"List<${fieldType.listTypeArg}>.from($ref);");
|
|
} else {
|
|
gen.writeln("${field.generatableName} = "
|
|
"List<${fieldType.listTypeArg}>.from($ref);");
|
|
}
|
|
} else {
|
|
// Special case `InstanceSet`. Pre 3.20, instances were sent in a
|
|
// field named 'samples' instead of 'instances'.
|
|
if (name == 'InstanceSet') {
|
|
gen.writeln("${field.generatableName} = "
|
|
"List<${fieldType.listTypeArg}>.from(createServiceObject(($ref ?? json['samples']!) as List, $typesList)! as List);");
|
|
} else {
|
|
gen.writeln("${field.generatableName} = "
|
|
"List<${fieldType.listTypeArg}>.from(createServiceObject($ref, $typesList) as List? ?? []);");
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
String typesList = _typeRefListToString(field.type.types);
|
|
String nullable = field.type.name != 'dynamic' ? '?' : '';
|
|
gen.writeln("${field.generatableName} = "
|
|
"createServiceObject(json['${field.name}'], "
|
|
"$typesList) as ${field.type.name}$nullable;");
|
|
}
|
|
});
|
|
if (fields.isNotEmpty) {
|
|
gen.writeln('}');
|
|
}
|
|
gen.writeln();
|
|
|
|
if (name == 'Script') {
|
|
generateScriptTypeMethods(gen);
|
|
}
|
|
|
|
// toJson support, the base Response type is not supported
|
|
if (name == 'Response') {
|
|
gen.writeln("String get type => 'Response';");
|
|
gen.writeln();
|
|
gen.writeln('''
|
|
Map<String, dynamic> toJson() {
|
|
final localJson = json;
|
|
final result = localJson == null ? <String, dynamic>{} : Map<String, dynamic>.of(localJson);
|
|
result['type'] = type;
|
|
return result;
|
|
}''');
|
|
} else if (name == 'TimelineEvent') {
|
|
// TimelineEvent doesn't have any declared properties as the response is
|
|
// fairly dynamic. Return the json directly.
|
|
gen.writeln('''
|
|
Map<String, dynamic> toJson() {
|
|
final localJson = json;
|
|
final result = localJson == null ? <String, dynamic>{} : Map<String, dynamic>.of(localJson);
|
|
result['type'] = 'TimelineEvent';
|
|
return result;
|
|
}
|
|
''');
|
|
} else {
|
|
if (isResponse) {
|
|
gen.writeln('@override');
|
|
gen.writeln("String get type => '$rawName';");
|
|
gen.writeln();
|
|
}
|
|
|
|
if (isResponse) {
|
|
gen.writeln('@override');
|
|
}
|
|
gen.writeln('Map<String, dynamic> toJson() {');
|
|
if (superName == null || superName == 'Response') {
|
|
// The base Response type doesn't have a toJson
|
|
gen.writeln('final json = <String, dynamic>{};');
|
|
} else {
|
|
gen.writeln('final json = super.toJson();');
|
|
}
|
|
|
|
// Only Response objects have a `type` field, as defined by protocol.
|
|
if (isResponse) {
|
|
// Overwrites "type" from the super class if we had one.
|
|
gen.writeln("json['type'] = type;");
|
|
}
|
|
|
|
var requiredFields = fields.where((f) => !f.optional);
|
|
if (requiredFields.isNotEmpty) {
|
|
gen.writeln('json.addAll({');
|
|
requiredFields.forEach((TypeField field) {
|
|
gen.write("'${field.name}': ");
|
|
generateSerializedFieldAccess(field, gen);
|
|
gen.writeln(',');
|
|
});
|
|
gen.writeln('});');
|
|
}
|
|
|
|
var optionalFields = fields.where((f) => f.optional);
|
|
optionalFields.forEach((TypeField field) {
|
|
gen.write("_setIfNotNull(json, '${field.name}', ");
|
|
generateSerializedFieldAccess(field, gen);
|
|
gen.writeln(');');
|
|
});
|
|
gen.writeln('return json;');
|
|
gen.writeln('}');
|
|
gen.writeln();
|
|
}
|
|
|
|
// equals and hashCode
|
|
if (supportsIdentity) {
|
|
gen.writeStatement('int get hashCode => id.hashCode;');
|
|
gen.writeln();
|
|
|
|
gen.writeStatement(
|
|
'bool operator ==(Object other) => other is ${name} && id == other.id;');
|
|
gen.writeln();
|
|
}
|
|
|
|
// toString()
|
|
Iterable<TypeField> toStringFields =
|
|
getAllFields().where((f) => !f.optional);
|
|
const maxFieldsShownInToString = 8;
|
|
if (toStringFields.length <= maxFieldsShownInToString) {
|
|
String properties = toStringFields
|
|
.map(
|
|
(TypeField f) => "${f.generatableName}: \${${f.generatableName}}")
|
|
.join(', ');
|
|
if (properties.length > 60) {
|
|
int index = properties.indexOf(', ', 55);
|
|
if (index != -1) {
|
|
properties = properties.substring(0, index + 2) +
|
|
"' //\n'" +
|
|
properties.substring(index + 2);
|
|
}
|
|
gen.writeln("String toString() => '[${name} ' //\n'${properties}]';");
|
|
} else {
|
|
final formattedProperties = (properties.isEmpty) ? '' : ' $properties';
|
|
gen.writeln("String toString() => '[$name$formattedProperties]';");
|
|
}
|
|
} else {
|
|
gen.writeln("String toString() => '[${name}]';");
|
|
}
|
|
|
|
gen.writeln('}');
|
|
}
|
|
|
|
// Special methods for Script objects.
|
|
void generateScriptTypeMethods(DartGenerator gen) {
|
|
gen.writeDocs('''This function maps a token position to a line number.
|
|
The VM considers the first line to be line 1.''');
|
|
gen.writeln(
|
|
'int? getLineNumberFromTokenPos(int tokenPos) => _tokenToLine[tokenPos];');
|
|
gen.writeln();
|
|
gen.writeDocs('''This function maps a token position to a column number.
|
|
The VM considers the first column to be column 1.''');
|
|
gen.writeln(
|
|
'int? getColumnNumberFromTokenPos(int tokenPos) => _tokenToColumn[tokenPos];');
|
|
gen.writeln();
|
|
gen.writeln('''
|
|
void _parseTokenPosTable() {
|
|
final tokenPositionTable = tokenPosTable;
|
|
if (tokenPositionTable == null) {
|
|
return;
|
|
}
|
|
final lineSet = <int>{};
|
|
for (List line in tokenPositionTable) {
|
|
// Each entry begins with a line number...
|
|
int lineNumber = line[0];
|
|
lineSet.add(lineNumber);
|
|
for (var pos = 1; pos < line.length; pos += 2) {
|
|
// ...and is followed by (token offset, col number) pairs.
|
|
final int tokenOffset = line[pos];
|
|
final int colNumber = line[pos + 1];
|
|
_tokenToLine[tokenOffset] = lineNumber;
|
|
_tokenToColumn[tokenOffset] = colNumber;
|
|
}
|
|
}
|
|
}''');
|
|
}
|
|
|
|
// Writes the code to retrieve the serialized value of a field.
|
|
void generateSerializedFieldAccess(TypeField field, DartGenerator gen) {
|
|
if (field.type.isSimple || field.type.isEnum) {
|
|
gen.write('${field.generatableName}');
|
|
if (field.defaultValue != null) {
|
|
gen.write(' ?? ${field.defaultValue}');
|
|
}
|
|
} else if (name == 'Event' && field.name == 'extensionData') {
|
|
// Special case `Event.extensionData`.
|
|
gen.writeln('extensionData?.data');
|
|
} else if (field.type.isArray) {
|
|
gen.write('${field.generatableName}?.map((f) => f');
|
|
// Special case `tokenPosTable` which is a List<List<int>>.
|
|
if (field.name == 'tokenPosTable') {
|
|
gen.write('.toList()');
|
|
} else if (!field.type.types.first.isListTypeSimple) {
|
|
gen.write('.toJson()');
|
|
}
|
|
gen.write(').toList()');
|
|
} else {
|
|
gen.write('${field.generatableName}?.toJson()');
|
|
}
|
|
}
|
|
|
|
void generateAssert(DartGenerator gen) {
|
|
gen.writeln('vms.${name} assert${name}(vms.${name} obj) {');
|
|
gen.writeln('assertNotNull(obj);');
|
|
for (TypeField field in getAllFields()) {
|
|
if (!field.optional) {
|
|
MemberType type = field.type;
|
|
if (type.isArray) {
|
|
TypeRef arrayType = type.types.first;
|
|
if (arrayType.arrayDepth == 1) {
|
|
String assertMethodName = 'assertListOf' +
|
|
arrayType.name!.substring(0, 1).toUpperCase() +
|
|
arrayType.name!.substring(1);
|
|
gen.writeln('$assertMethodName(obj.${field.generatableName}!);');
|
|
} else {
|
|
gen.writeln(
|
|
'// assert obj.${field.generatableName} is ${type.name}');
|
|
}
|
|
} else if (type.isMultipleReturns) {
|
|
bool first = true;
|
|
for (TypeRef typeRef in type.types) {
|
|
if (!first) gen.write('} else ');
|
|
first = false;
|
|
gen.writeln(
|
|
'if (obj.${field.generatableName} is vms.${typeRef.name}) {');
|
|
String assertMethodName = 'assert' +
|
|
typeRef.name!.substring(0, 1).toUpperCase() +
|
|
typeRef.name!.substring(1);
|
|
gen.writeln('$assertMethodName(obj.${field.generatableName}!);');
|
|
}
|
|
gen.writeln('} else {');
|
|
gen.writeln(
|
|
'throw "Unexpected value: \${obj.${field.generatableName}}";');
|
|
gen.writeln('}');
|
|
} else {
|
|
String assertMethodName = 'assert' +
|
|
type.name.substring(0, 1).toUpperCase() +
|
|
type.name.substring(1);
|
|
gen.writeln('$assertMethodName(obj.${field.generatableName}!);');
|
|
}
|
|
}
|
|
}
|
|
gen.writeln('return obj;');
|
|
gen.writeln('}');
|
|
gen.writeln('');
|
|
}
|
|
|
|
void generateListAssert(DartGenerator gen) {
|
|
gen.writeln('List<vms.${name}> '
|
|
'assertListOf${name}(List<vms.${name}> list) {');
|
|
gen.writeln('for (vms.${name} elem in list) {');
|
|
gen.writeln('assert${name}(elem);');
|
|
gen.writeln('}');
|
|
gen.writeln('return list;');
|
|
gen.writeln('}');
|
|
gen.writeln('');
|
|
}
|
|
|
|
void _parse(Token? token) => TypeParser(token).parseInto(this);
|
|
|
|
void calculateFieldOverrides() {
|
|
for (TypeField field in fields.toList()) {
|
|
if (superName == null) continue;
|
|
|
|
if (getSuper()!.hasField(field.name)) {
|
|
field.setOverrides();
|
|
}
|
|
}
|
|
}
|
|
|
|
bool hasField(String? name) {
|
|
if (fields.any((field) => field.name == name)) return true;
|
|
return getSuper()?.hasField(name) ?? false;
|
|
}
|
|
}
|
|
|
|
class TypeField extends Member {
|
|
static final Map<String, String> _nameRemap = {
|
|
'const': 'isConst',
|
|
'final': 'isFinal',
|
|
'static': 'isStatic',
|
|
'abstract': 'isAbstract',
|
|
'super': 'superClass',
|
|
'class': 'classRef',
|
|
'new': 'new_',
|
|
};
|
|
|
|
final Type parent;
|
|
final String? _docs;
|
|
MemberType type = MemberType();
|
|
String? name;
|
|
bool optional = false;
|
|
String? defaultValue;
|
|
bool overrides = false;
|
|
|
|
TypeField(this.parent, this._docs);
|
|
|
|
void setOverrides() => overrides = true;
|
|
|
|
String? get docs {
|
|
String str = _docs == null ? '' : _docs!;
|
|
if (type.isMultipleReturns) {
|
|
str += '\n\n[${generatableName}] can be one of '
|
|
'${joinLast(type.types.map((t) => '[${t}]'), ', ', ' or ')}.';
|
|
str = str.trim();
|
|
}
|
|
return str;
|
|
}
|
|
|
|
String? get generatableName {
|
|
return _nameRemap[name] != null ? _nameRemap[name] : name;
|
|
}
|
|
|
|
void generate(DartGenerator gen) {
|
|
if (docs!.isNotEmpty) gen.writeDocs(docs);
|
|
if (optional) gen.write('@optional ');
|
|
if (overrides) gen.write('@override ');
|
|
// Special case where Instance extends Obj, but 'classRef' is not optional
|
|
// for Instance although it is for Obj.
|
|
/*if (parent.name == 'Instance' && generatableName == 'classRef') {
|
|
gen.writeStatement('covariant late final ClassRef classRef;');
|
|
} else if (parent.name!.contains('NullVal') &&
|
|
generatableName == 'valueAsString') {
|
|
gen.writeStatement('covariant late final String valueAsString;');
|
|
} else */
|
|
{
|
|
String? typeName =
|
|
api.isEnumName(type.name) ? '/*${type.name}*/ String' : type.name;
|
|
if (typeName != 'dynamic') {
|
|
typeName = '$typeName?';
|
|
}
|
|
gen.writeStatement('${typeName} ${generatableName};');
|
|
if (parent.fields.any((field) => field.hasDocs)) gen.writeln();
|
|
}
|
|
}
|
|
|
|
void generateNamedParameter(DartGenerator gen, {bool fromParent = false}) {
|
|
if (fromParent) {
|
|
String? typeName =
|
|
api.isEnumName(type.name) ? '/*${type.name}*/ String' : type.name;
|
|
gen.writeStatement('required $typeName ${generatableName},');
|
|
} else {
|
|
gen.writeStatement('this.${generatableName},');
|
|
}
|
|
}
|
|
}
|
|
|
|
class Enum extends Member {
|
|
final String name;
|
|
final String? docs;
|
|
|
|
List<EnumValue> enums = [];
|
|
|
|
Enum(this.name, String definition, [this.docs]) {
|
|
_parse(Tokenizer(definition).tokenize());
|
|
}
|
|
|
|
Enum._(this.name, this.docs);
|
|
|
|
factory Enum.merge(Enum e1, Enum e2) {
|
|
final String name = e1.name;
|
|
final String docs = [e1.docs, e2.docs].where((e) => e != null).join('\n');
|
|
final Map<String?, EnumValue> map = <String?, EnumValue>{};
|
|
for (EnumValue e in e2.enums.reversed) {
|
|
map[e.name] = e;
|
|
}
|
|
// The official service.md is the default
|
|
for (EnumValue e in e1.enums.reversed) {
|
|
map[e.name] = e;
|
|
}
|
|
|
|
final enums = map.values.toList().reversed.toList();
|
|
|
|
return Enum._(name, docs)..enums = enums;
|
|
}
|
|
|
|
String get prefix =>
|
|
name.endsWith('Kind') ? name.substring(0, name.length - 4) : name;
|
|
|
|
void generate(DartGenerator gen) {
|
|
gen.writeln();
|
|
if (docs != null) gen.writeDocs(docs);
|
|
gen.writeStatement('class ${name} {');
|
|
gen.writeStatement('${name}._();');
|
|
gen.writeln();
|
|
enums.forEach((e) => e.generate(gen));
|
|
gen.writeStatement('}');
|
|
}
|
|
|
|
void generateAssert(DartGenerator gen) {
|
|
gen.writeln('String assert${name}(String obj) {');
|
|
List<EnumValue> sorted = enums.toList()
|
|
..sort((EnumValue e1, EnumValue e2) => e1.name!.compareTo(e2.name!));
|
|
for (EnumValue value in sorted) {
|
|
gen.writeln(' if (obj == "${value.name}") return obj;');
|
|
}
|
|
gen.writeln(' throw "invalid ${name}: \$obj";');
|
|
gen.writeln('}');
|
|
gen.writeln('');
|
|
}
|
|
|
|
void _parse(Token? token) => EnumParser(token).parseInto(this);
|
|
}
|
|
|
|
class EnumValue extends Member {
|
|
final Enum parent;
|
|
final String? name;
|
|
final String? docs;
|
|
|
|
EnumValue(this.parent, this.name, [this.docs]);
|
|
|
|
bool get isLast => parent.enums.last == this;
|
|
|
|
void generate(DartGenerator gen) {
|
|
if (docs != null) gen.writeDocs(docs);
|
|
gen.writeStatement("static const String k${name} = '${name}';");
|
|
}
|
|
}
|
|
|
|
class TextOutputVisitor implements NodeVisitor {
|
|
static String printText(Node node) {
|
|
TextOutputVisitor visitor = TextOutputVisitor();
|
|
node.accept(visitor);
|
|
return visitor.toString();
|
|
}
|
|
|
|
StringBuffer buf = StringBuffer();
|
|
bool _em = false;
|
|
bool _href = false;
|
|
bool _blockquote = false;
|
|
|
|
TextOutputVisitor();
|
|
|
|
bool visitElementBefore(Element element) {
|
|
if (element.tag == 'em' || element.tag == 'code') {
|
|
buf.write('`');
|
|
_em = true;
|
|
} else if (element.tag == 'p') {
|
|
// Nothing to do.
|
|
} else if (element.tag == 'blockquote') {
|
|
buf.write('```\n');
|
|
_blockquote = true;
|
|
} else if (element.tag == 'a') {
|
|
_href = true;
|
|
} else if (element.tag == 'strong') {
|
|
buf.write('**');
|
|
} else if (element.tag == 'ul') {
|
|
// Nothing to do.
|
|
} else if (element.tag == 'li') {
|
|
buf.write('- ');
|
|
} else {
|
|
throw 'unknown node type: ${element.tag}';
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void visitText(Text text) {
|
|
String? t = text.text;
|
|
if (_em) {
|
|
t = _coerceRefType(t);
|
|
} else if (_href) {
|
|
t = '[${_coerceRefType(t)}]';
|
|
}
|
|
|
|
if (_blockquote) {
|
|
buf.write('${t}\n```');
|
|
} else {
|
|
buf.write(t);
|
|
}
|
|
}
|
|
|
|
void visitElementAfter(Element element) {
|
|
if (element.tag == 'em' || element.tag == 'code') {
|
|
buf.write('`');
|
|
_em = false;
|
|
} else if (element.tag == 'p') {
|
|
buf.write('\n\n');
|
|
} else if (element.tag == 'blockquote') {
|
|
_blockquote = false;
|
|
} else if (element.tag == 'a') {
|
|
_href = false;
|
|
} else if (element.tag == 'strong') {
|
|
buf.write('**');
|
|
} else if (element.tag == 'ul') {
|
|
// Nothing to do.
|
|
} else if (element.tag == 'li') {
|
|
buf.write('\n');
|
|
} else {
|
|
throw 'unknown node type: ${element.tag}';
|
|
}
|
|
}
|
|
|
|
String toString() => buf.toString().trim();
|
|
}
|
|
|
|
// @Instance|@Error|Sentinel evaluate(
|
|
// string isolateId,
|
|
// string targetId [optional],
|
|
// string expression)
|
|
class MethodParser extends Parser {
|
|
MethodParser(Token? startToken) : super(startToken);
|
|
|
|
void parseInto(Method method) {
|
|
// method is return type, name, (, args )
|
|
// args is type name, [optional], comma
|
|
if (peek()?.text?.startsWith('@deprecated') ?? false) {
|
|
advance();
|
|
expect('(');
|
|
method.deprecationMessage = consumeString()!;
|
|
expect(')');
|
|
}
|
|
method.returnType.parse(this, isReturnType: true);
|
|
|
|
Token t = expectName();
|
|
validate(
|
|
t.text == method.name, 'method name ${method.name} equals ${t.text}');
|
|
|
|
expect('(');
|
|
|
|
while (peek()!.text != ')') {
|
|
Token type = expectName();
|
|
TypeRef ref = TypeRef(_coerceRefType(type.text));
|
|
if (peek()!.text == '[') {
|
|
while (consume('[')) {
|
|
expect(']');
|
|
ref.arrayDepth++;
|
|
}
|
|
} else if (peek()!.text == '<') {
|
|
// handle generics
|
|
expect('<');
|
|
ref.genericTypes = [];
|
|
while (peek()!.text != '>') {
|
|
Token genericTypeName = expectName();
|
|
ref.genericTypes!.add(TypeRef(_coerceRefType(genericTypeName.text)));
|
|
consume(',');
|
|
}
|
|
expect('>');
|
|
}
|
|
|
|
Token name = expectName();
|
|
MethodArg arg = MethodArg(method, ref, name.text);
|
|
if (consume('[')) {
|
|
expect('optional');
|
|
expect(']');
|
|
arg.optional = true;
|
|
}
|
|
method.args.add(arg);
|
|
consume(',');
|
|
}
|
|
|
|
expect(')');
|
|
|
|
method.args.sort((MethodArg a, MethodArg b) {
|
|
if (!a.optional && b.optional) return -1;
|
|
if (a.optional && !b.optional) return 1;
|
|
return 0;
|
|
});
|
|
}
|
|
}
|
|
|
|
class TypeParser extends Parser {
|
|
TypeParser(Token? startToken) : super(startToken);
|
|
|
|
void parseInto(Type type) {
|
|
// class ClassList extends Response {
|
|
// // Docs here.
|
|
// @Class[] classes [optional];
|
|
// }
|
|
expect('class');
|
|
|
|
Token t = expectName();
|
|
type.rawName = t.text;
|
|
type.name = _coerceRefType(type.rawName);
|
|
if (consume('extends')) {
|
|
t = expectName();
|
|
type.superName = _coerceRefType(t.text);
|
|
}
|
|
|
|
expect('{');
|
|
|
|
while (peek()!.text != '}') {
|
|
TypeField field = TypeField(type, collectComments());
|
|
field.type.parse(this);
|
|
field.name = expectName().text;
|
|
if (consume('[')) {
|
|
expect('optional');
|
|
expect(']');
|
|
field.optional = true;
|
|
}
|
|
type.fields.add(field);
|
|
expect(';');
|
|
}
|
|
|
|
// Special case for Event in order to expose binary response for
|
|
// HeapSnapshot events.
|
|
if (type.rawName == 'Event') {
|
|
final comment = 'Binary data associated with the event.\n\n'
|
|
'This is provided for the event kinds:\n - HeapSnapshot';
|
|
TypeField dataField = TypeField(type, comment);
|
|
dataField.type.types.add(TypeRef('ByteData'));
|
|
dataField.name = 'data';
|
|
dataField.optional = true;
|
|
type.fields.add(dataField);
|
|
} else if (type.rawName == 'Response') {
|
|
type.fields.removeWhere((field) => field.name == 'type');
|
|
}
|
|
|
|
expect('}');
|
|
}
|
|
}
|
|
|
|
class EnumParser extends Parser {
|
|
EnumParser(Token? startToken) : super(startToken);
|
|
|
|
void parseInto(Enum e) {
|
|
// enum ErrorKind { UnhandledException, Foo, Bar }
|
|
// enum name { (comment* name ,)+ }
|
|
expect('enum');
|
|
|
|
Token t = expectName();
|
|
validate(t.text == e.name, 'enum name ${e.name} equals ${t.text}');
|
|
expect('{');
|
|
|
|
while (!t.eof) {
|
|
if (consume('}')) break;
|
|
String? docs = collectComments();
|
|
t = expectName();
|
|
consume(',');
|
|
|
|
e.enums.add(EnumValue(e, t.text, docs));
|
|
}
|
|
}
|
|
}
|