blob: 7a5fbbf828239bd4f86add3c9aabdc92c462b7a7 [file] [log] [blame]
// 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;
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.
library vm_service;
import 'dart:async';
import 'dart:convert' show base64, jsonDecode, jsonEncode, utf8;
import 'dart:typed_data';
import 'package:meta/meta.dart';
import 'src/service_extension_registry.dart';
export 'src/service_extension_registry.dart' show ServiceExtensionRegistry;
export 'src/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 args
}) {
return callServiceExtension(method, isolateId: isolateId, args: args);
}
/// Invoke a specific service protocol extension method.
///
/// See https://api.dartlang.org/stable/dart-developer/dart-developer-library.html.
@override
Future<Response> callServiceExtension(String method, {
String isolateId,
Map 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;
void dispose() {
_streamSub.cancel();
_completers.values.forEach((c) => c.completeError('disposed'));
if (_disposeHandler != null) {
_disposeHandler();
}
if (!_onDoneCompleter.isCompleted) {
_onDoneCompleter.complete();
}
}
Future get onDone => _onDoneCompleter.future;
Future<T> _call<T>(String method, [Map args]) {
String id = '${++_id}';
Completer<T> completer = Completer<T>();
_completers[id] = completer;
_methodCalls[id] = method;
Map m = {'id': id, 'method': method};
if (args != null) m['params'] = args;
String message = jsonEncode(m);
_onSend.add(message);
_writeMessage(message);
return completer.future;
}
/// 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 != null && map['method'] == 'streamNotify') {
String streamId = map['params']['streamId'];
Map event = map['params']['event'];
event['data'] = data;
_getEventController(streamId)
.add(createServiceObject(event, const ['Event']));
}
}
void _processMessageStr(String message) {
var 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) {
Completer completer = _completers.remove(json['id']);
String methodName = _methodCalls.remove(json['id']);
List<String> returnTypes = _methodReturnTypes[methodName];
if (completer == null) {
_log.severe('unmatched request response: ${jsonEncode(json)}');
} else if (json['error'] != null) {
completer.completeError(RPCError.parse(methodName, json['error']));
} else {
Map<String, dynamic> result = json['result'] as Map<String, dynamic>;
String type = result['type'];
if (_typeFactories[type] == null) {
completer.complete(Response.parse(result));
} else {
completer.complete(createServiceObject(result, returnTypes));
}
}
}
Future _processRequest(Map<String, dynamic> json) async {
final Map m = await _routeRequest(json['method'], json['params']);
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 params = json['params'];
if (method == 'streamNotify') {
String streamId = params['streamId'];
_getEventController(streamId).add(createServiceObject(params['event'], const ['Event']));
} else {
await _routeRequest(method, params);
}
}
Future<Map> _routeRequest(String method, Map params) async{
try {
if (_services.containsKey(method)) {
return await _services[method](params);
}
return {
'error': {
'code': -32601, // Method not found
'message': 'Method not found \'$method\''
}
};
} catch (e, st) {
return {
'error': {
'code': -32000, // SERVER ERROR
'message': 'Unexpected Server Error $e\n$st'
}
};
}
}
''';
final String _rpcError = r'''
typedef DisposeHandler = Future Function();
class RPCError {
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]);
String get details => data == null ? null : data['details'];
String toString() {
if (details == null) {
return '${message} (${code}) from ${callingMethod}()';
} else {
return '${message} (${code}) from ${callingMethod}():\n${details}';
}
}
}
/// An `ExtensionData` is an arbitrary map that can have any contents.
class ExtensionData {
static ExtensionData parse(Map json) =>
json == null ? null : ExtensionData._fromJson(json);
final Map 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('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('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]))) {
Element p = nodes[++i];
String str = TextOutputVisitor.printText(p);
if (!str.contains('|') && !str.contains('``')) {
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('&lt;', '<').replaceAll('&gt;', '>');
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) {
// Set default value for unspecified property
setDefaultValue('Instance', 'valueAsStringIsTruncated', 'false');
setDefaultValue('InstanceRef', 'valueAsStringIsTruncated', 'false');
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 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 null;
}
} else if (_isNullInstance(json) && (!expectedTypes.contains(type))) {
// Replace null instances with null when we don't expect an instance to
// be returned.
return null;
}
if (_typeFactories[type] == null) {
return null;
} else {
return _typeFactories[type](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) {
Map<String, dynamic> map = {};
for (dynamic key in json.keys) {
map[key as String] = json[key];
}
return creator(map);
} else {
// Handle simple types.
return json;
}
}
void _setIfNotNull(Map<String, Object> json, String key, Object value) {
if (value == null) return;
json[key] = value;
}
typedef ServiceCallback = Future<Map<String, dynamic>> Function(
Map<String, dynamic> params);
''');
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 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('''
/// 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 get done => _doneCompleter.future;
final _doneCompleter = Completer<Null>();
/// Pending service extension requests to this client by id.
final _pendingServiceExtensionRequests =
<String, Completer<Map<String, Object>>>{};
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) {
var originalId = request['id'];
request = Map.of(request);
// Modify the request ID to ensure we don't have conflicts between
// multiple clients ids.
var newId = '\${_nextServiceRequestId++}:\$originalId';
request['id'] = newId;
var responseCompleter = Completer<Map<String, Object>>();
_pendingServiceExtensionRequests[newId] = responseCompleter;
_responseSink.add(request);
return responseCompleter.future;
}
void _delegateRequest(Map<String, Object> request) async {
try {
var id = request['id'] as String;
// Check if this is actually a response to a pending request.
if (_pendingServiceExtensionRequests.containsKey(id)) {
// Restore the original request ID.
var originalId = id.substring(id.indexOf(':') + 1);
_pendingServiceExtensionRequests[id].complete(
Map.of(request)..['id'] = originalId);
return;
}
var method = request['method'] as String;
if (method == null) {
throw RPCError(null, -32600, 'Invalid Request', request);
}
var params = request['params'] as Map;
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 {
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['${arg.name}'] ?? []), ");
} else {
gen.write("params['${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['${arg.name}']?.cast<String, String>(), ");
} else {
gen.writeln("${arg.name}: params['${arg.name}'], ");
}
});
}
gen.writeln(");");
}
gen.writeln('break;');
}
});
// Handle service extensions
gen.writeln('default:');
gen.writeln('''
var 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.
var args = params == null ? null : Map.of(params);
var isolateId = args?.remove('isolateId');
response = await _serviceImplementation.callServiceExtension(method,
isolateId: isolateId, args: args);
} else {
throw RPCError(method, -32601, 'Method not found', request);
}
''');
// Terminate the switch
gen.writeln('}');
// Handle null responses
gen.write('''
if (response == null) {
throw StateError('Invalid null response from service');
}
''');
// Generate the json success response
gen.write("""_responseSink.add({
'jsonrpc': '2.0',
'result': response.toJson(),
'id': id,
});
""");
// Close the try block, handle errors
gen.write(r'''
} catch (e, st) {
var error = e is RPCError
? {'code': e.code, 'data': e.data, 'message': e.message}
: {'code': -32603, 'message': '$e\n$st'};
_responseSink.add({
'jsonrpc': '2.0',
'error': error,
'id': request['id'],
});
}
''');
// terminate the _delegateRequest method
gen.write('}');
gen.writeln();
gen.write('}');
gen.writeln();
// The client side service implementation.
gen.writeStatement('class VmService implements VmServiceInterface {');
gen.writeStatement('StreamSubscription _streamSub;');
gen.writeStatement('Function _writeMessage;');
gen.writeStatement('int _id = 0;');
gen.writeStatement('Map<String, Completer> _completers = {};');
gen.writeStatement('Map<String, String> _methodCalls = {};');
gen.writeStatement('Map<String, ServiceCallback> _services = {};');
gen.writeStatement('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;
}
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 generateAsserts(DartGenerator gen) {
gen.out(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 for asserting correct responses from the VM Service.
import 'package:vm_service/vm_service.dart' as vms;
dynamic assertNotNull(dynamic obj) {
if (obj == null) throw 'assert failed';
return obj;
}
bool assertBool(bool obj) {
assertNotNull(obj);
return obj;
}
int assertInt(int obj) {
assertNotNull(obj);
return obj;
}
double assertDouble(double obj) {
assertNotNull(obj);
return obj;
}
dynamic assertDynamic(dynamic obj) {
assertNotNull(obj);
return obj;
}
List<int> assertListOfInt(List<int> list) {
for (int elem in list) {
assertInt(elem);
}
return list;
}
List<String> assertListOfString(List<String> list) {
for (String elem in list) {
assertString(elem);
}
return list;
}
String assertString(String obj) {
assertNotNull(obj);
if (obj.isEmpty) throw 'expected non-zero length string';
return obj;
}
vms.Success assertSuccess(vms.Success obj) {
assertNotNull(obj);
if (obj.type != 'Success') throw 'expected Success';
return obj;
}
/// Assert PauseStart, PauseExit, PauseBreakpoint, PauseInterrupted,
/// PauseException, Resume, BreakpointAdded, BreakpointResolved,
/// BreakpointRemoved, and Inspect events.
vms.Event assertDebugEvent(vms.Event event) {
assertEvent(event);
if (event.kind == vms.EventKind.kPauseBreakpoint ||
event.kind == vms.EventKind.kBreakpointAdded ||
event.kind == vms.EventKind.kBreakpointRemoved ||
event.kind == vms.EventKind.kBreakpointResolved) {
assertBreakpoint(event.breakpoint);
}
if (event.kind == vms.EventKind.kPauseBreakpoint) {
for (vms.Breakpoint elem in event.pauseBreakpoints) {
assertBreakpoint(elem);
}
}
if (event.kind == vms.EventKind.kPauseBreakpoint ||
event.kind == vms.EventKind.kPauseInterrupted ||
event.kind == vms.EventKind.kPauseException ||
event.kind == vms.EventKind.kResume) {
// For PauseInterrupted events, there will be no top frame if the isolate is
// idle (waiting in the message loop).
// For the Resume event, the top frame is provided at all times except for
// the initial resume event that is delivered when an isolate begins
// execution.
if (event.topFrame != null ||
(event.kind != vms.EventKind.kPauseInterrupted &&
event.kind != vms.EventKind.kResume)) {
assertFrame(event.topFrame);
}
}
if (event.kind == vms.EventKind.kPauseException) {
assertInstanceRef(event.exception);
}
if (event.kind == vms.EventKind.kPauseBreakpoint ||
event.kind == vms.EventKind.kPauseInterrupted) {
assertBool(event.atAsyncSuspension);
}
if (event.kind == vms.EventKind.kInspect) {
assertInstanceRef(event.inspectee);
}
return event;
}
/// Assert IsolateStart, IsolateRunnable, IsolateExit, IsolateUpdate,
/// and ServiceExtensionAdded events.
vms.Event assertIsolateEvent(vms.Event event) {
assertEvent(event);
if (event.kind == vms.EventKind.kServiceExtensionAdded) {
assertString(event.extensionRPC);
}
return event;
}
''');
for (Enum e in enums) {
e.generateAssert(gen);
}
for (Type type in types) {
if (type.name == 'Success') continue;
type.generateAssert(gen);
if (type.name.endsWith('Ref') ||
[
'BoundVariable',
'Breakpoint',
'ClassHeapStats',
'CodeRegion',
'ContextElement',
'CpuSample',
'Flag',
'Frame',
'InboundReference',
'LibraryDependency',
'Message',
'ProfileFunction',
'RetainingObject',
'SourceReportRange',
'TimelineEvent',
].contains(type.name)) {
type.generateListAssert(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();
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('{');
gen.write('Map m = {');
gen.write(args
.where((MethodArg a) => !a.optional)
.map((arg) => "'${arg.name}': ${arg.name}")
.join(', '));
gen.writeln('};');
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("m['${arg.name}'] = ${valueRef};");
gen.writeln("}");
});
gen.writeStatement("return _call('${name}', m);");
gen.writeStatement('}');
} else {
gen.writeStatement('{');
gen.write("return _call('${name}', {");
gen.write(args.map((MethodArg arg) {
return "'${arg.name}': ${arg.name}";
}).join(', '));
gen.writeStatement('});');
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 (_docs.isNotEmpty) gen.writeDocs(_docs);
}
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;
}
if (arg.optional && !startedOptional) {
startedOptional = true;
return '{${typeName} ${arg.name}';
} else {
return '${typeName} ${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) {
// foo|bar[]|baz
// (@Instance|Sentinel)[]
bool loop = true;
while (loop) {
if (parser.consume('(')) {
while (parser.peek().text != ')') {
// @Instance | Sentinel
parser.advance();
}
parser.consume(')');
TypeRef 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++;
}
types.add(ref);
}
loop = parser.consume('|');
}
}
String get name {
if (types.isEmpty) return '';
if (types.length == 1) return types.first.ref;
return 'dynamic';
}
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;
List<TypeRef> genericTypes;
TypeRef(this.name);
String get ref {
if (arrayDepth == 2) {
return 'List<List<${name}>>';
} else if (arrayDepth == 1) {
return 'List<${name}>';
} 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>';
} else {
return 'List<String>';
}
}
String get listTypeArg => arrayDepth == 2 ? 'List<$name>' : name;
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} ');
if (superName != null) gen.write('extends ${superName} ');
gen.writeln('{');
gen.writeln('static ${name} parse(Map<String, dynamic> json) => '
'json == null ? null : ${name}._fromJson(json);');
gen.writeln();
if (name == 'Response') {
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
// Default
gen.write('${name}(');
if (fields.isNotEmpty) {
gen.write('{');
fields
.where((field) => !field.optional)
.forEach((field) => field.generateNamedParameter(gen));
fields
.where((field) => field.optional)
.forEach((field) => field.generateNamedParameter(gen));
gen.write('}');
}
gen.writeln(');');
// Build from JSON.
String superCall = superName == null ? '' : ": super._fromJson(json) ";
if (name == 'Response') {
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}');
}
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 == '_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));");
}
} 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'], $typesList));");
} else {
gen.writeln("${field.generatableName} = "
"List<${fieldType.listTypeArg}>.from(createServiceObject($ref, $typesList));");
}
}
}
} else {
String typesList = _typeRefListToString(field.type.types);
gen.writeln("${field.generatableName} = "
"createServiceObject(json['${field.name}'], $typesList);");
}
});
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('''
Map<String, dynamic> toJson() {
var result = json == null ? <String, dynamic>{} : Map.of(json);
result['type'] = type ?? 'Response';
return result;
}''');
} else {
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('var json = <String, dynamic>{};');
} else {
gen.writeln('var 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'] = '$rawName';");
}
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(
'operator==(other) => other is ${name} && id == other.id;');
gen.writeln();
}
// toString()
Iterable<TypeField> toStringFields =
getAllFields().where((f) => !f.optional);
if (toStringFields.length <= 7) {
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 {
gen.writeln("String toString() => '[${name} ${properties}]';");
}
} 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() {
if (tokenPosTable == null) {
return;
}
final lineSet = Set<int>();
for (List line in tokenPosTable) {
// 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) {
var nullAware = field.optional ? '?' : '';
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$nullAware.data');
} else if (field.type.isArray) {
gen.write('${field.generatableName}$nullAware.map((f) => f');
// Special case `tokenPosTable` which is a List<List<int>>.
if (field.name == 'tokenPosTable') {
gen.write('$nullAware.toList()');
} else if (!field.type.types.first.isListTypeSimple) {
gen.write('$nullAware.toJson()');
}
gen.write(')$nullAware.toList()');
} else {
gen.write('${field.generatableName}$nullAware.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 ');
String typeName =
api.isEnumName(type.name) ? '/*${type.name}*/ String' : type.name;
gen.writeStatement('${typeName} ${generatableName};');
if (parent.fields.any((field) => field.hasDocs)) gen.writeln();
}
void generateNamedParameter(DartGenerator gen) {
if (!optional) {
gen.write('@required ');
}
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;
}
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') {
//buf.write('```\n');
_blockquote = false;
} else if (element.tag == 'a') {
_href = false;
} else {
print(' </${element.tag}>');
buf.write(renderToHtml([element]));
}
}
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
method.returnType.parse(this);
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);
}
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));
}
}
}