| // 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 converstion 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 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) { |
| return obj; |
| } |
| |
| int assertInt(int obj) { |
| return obj; |
| } |
| |
| double assertDouble(double obj) { |
| return obj; |
| } |
| |
| dynamic assertDynamic(dynamic obj) { |
| assertNotNull(obj); |
| return obj; |
| } |
| |
| List<dynamic> assertListOfDynamic(List<dynamic> list) { |
| return list; |
| } |
| |
| 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; |
| } |
| |
| List<vms.IsolateFlag> assertListOfIsolateFlag(List<vms.IsolateFlag> list) { |
| for (vms.IsolateFlag elem in list) { |
| assertIsolateFlag(elem); |
| } |
| return list; |
| } |
| |
| String assertString(String obj) { |
| if (obj.isEmpty) throw 'expected non-zero length string'; |
| return obj; |
| } |
| |
| vms.Success assertSuccess(vms.Success 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', |
| 'ProcessMemoryItem', |
| 'ProfileFunction', |
| 'ProcessMemoryItem', |
| 'Protocol', |
| '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(); |
| 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; |
| this.isReturnType = isReturnType; |
| |
| 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++; |
| } |
| 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; |
| 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} '); |
| 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); |
| 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 { |
| 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 (!optional) { |
| gen.write('required '); |
| } |
| if (fromParent) { |
| String? typeName = |
| api.isEnumName(type.name) ? '/*${type.name}*/ String' : type.name; |
| gen.writeStatement('$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)); |
| } |
| } |
| } |