| // Copyright (c) 2023, 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:collection/collection.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; |
| |
| 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 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; |
| |
| @override |
| String toString() => name!; |
| } |
| |
| abstract class Api 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!); |
| } |
| |
| void _parse(String name, String definition, [String? docs]) { |
| name = replaceHTMLEntities(name.trim()); |
| definition = replaceHTMLEntities(definition.trim()); |
| if (docs != null) docs = replaceHTMLEntities(docs.trim()); |
| |
| if (definition.startsWith('class ')) { |
| types.add(Type(this, this, name, definition, docs)); |
| } else if (name.substring(0, 1).toLowerCase() == name.substring(0, 1)) { |
| methods.add(Method(this, name, definition, docs)); |
| } else if (definition.startsWith('enum ')) { |
| enums.add(Enum(name, definition, docs)); |
| } else { |
| throw 'unexpected entity: $name, $definition'; |
| } |
| } |
| |
| 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)); |
| } |
| } |
| } |
| } |
| |
| 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); |
| |
| bool isEnumName(String typeName) => enums.any((Enum e) => e.name == typeName); |
| |
| Type? getType(String name) => types.firstWhereOrNull((t) => t.name == name); |
| } |
| |
| class StreamCategory { |
| final String name; |
| final List<String> events; |
| |
| factory StreamCategory(String line) { |
| // Debug | PauseStart, PauseExit, ... |
| String name = line.split('|')[0].trim(); |
| |
| line = line.split('|')[1]; |
| List<String> events = line.split(',').map((w) => w.trim()).toList(); |
| |
| return StreamCategory._(name: name, events: events); |
| } |
| |
| StreamCategory._({required this.name, required this.events}); |
| |
| void generate(DartGenerator gen) { |
| gen.writeln(); |
| gen.writeln('// ${events.join(', ')}'); |
| gen.writeln( |
| "Stream<Event> get on${name}Event => _getEventController('$name').stream;"); |
| } |
| |
| @override |
| String toString() => '$name: $events'; |
| } |
| |
| class Method extends Member { |
| final Api api; |
| |
| @override |
| final String name; |
| @override |
| final String? docs; |
| |
| late MemberType returnType = MemberType(api); |
| bool get deprecated => deprecationMessage != null; |
| String? deprecationMessage; |
| List<MethodArg> args = []; |
| |
| Method(this.api, this.name, String definition, [this.docs]) { |
| _parse(Tokenizer(definition).tokenize()); |
| } |
| |
| bool get hasArgs => args.isNotEmpty; |
| |
| bool get hasOptionalArgs => args.any((MethodArg arg) => arg.optional); |
| |
| @override |
| void generate(DartGenerator gen) { |
| generateDefinition(gen); |
| 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. |
| void generateDefinition(DartGenerator gen) { |
| gen.writeln(); |
| if (docs != null) { |
| String methodDocs = docs!; |
| if (returnType.isMultipleReturns) { |
| methodDocs += '\n\nThe return value can be one of ' |
| '${joinLast(returnType.types.map((t) => '[$t]'), ', ', ' or ')}.'; |
| methodDocs = methodDocs.trim(); |
| } |
| if (returnType.canReturnSentinel) { |
| methodDocs += |
| '\n\nThis method will throw a [SentinelException] in the case a [Sentinel] is returned.'; |
| methodDocs = methodDocs.trim(); |
| } |
| if (methodDocs.isNotEmpty) gen.writeDocs(methodDocs); |
| } |
| if (deprecated) { |
| gen.writeln("@Deprecated('$deprecationMessage')"); |
| } |
| 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 { |
| final Api api; |
| final List<TypeRef> types = []; |
| |
| MemberType(this.api); |
| |
| void parse(Parser parser, {bool isReturnType = false}) { |
| // foo|bar[]|baz |
| // (@Instance|Sentinel)[] |
| bool loop = true; |
| bool nullable = false; |
| this.isReturnType = isReturnType; |
| |
| final unionTypes = <String>[]; |
| while (loop) { |
| if (parser.consume('(')) { |
| while (parser.peek()!.text != ')') { |
| if (parser.consume('Null')) { |
| nullable = true; |
| } else { |
| // @Instance | Sentinel |
| final token = parser.advance()!; |
| if (token.isName) { |
| unionTypes.add(_coerceRefType(token.text!)); |
| } |
| } |
| } |
| parser.consume(')'); |
| TypeRef ref; |
| if (unionTypes.length == 1) { |
| ref = TypeRef(unionTypes.first, nullable: nullable); |
| } else { |
| ref = TypeRef('dynamic'); |
| } |
| while (parser.consume('[')) { |
| parser.expect(']'); |
| ref.arrayDepth++; |
| } |
| types.add(ref); |
| } else { |
| Token t = parser.expectName(); |
| TypeRef ref = TypeRef(_coerceRefType(t.text!)); |
| while (parser.consume('[')) { |
| parser.expect(']'); |
| ref.arrayDepth++; |
| } |
| if (isReturnType && ref.name == 'Sentinel') { |
| canReturnSentinel = true; |
| } else { |
| types.add(ref); |
| } |
| } |
| |
| loop = parser.consume('|'); |
| } |
| } |
| |
| @override |
| 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 areAllTypesSimple => types.every((type) => type.isSimple); |
| |
| List<String> get subsetOfTypesThatAreSimple => |
| types.fold(<String>[], (List<String> accumulator, TypeRef typeRef) { |
| if (typeRef.isSimple) { |
| accumulator.add(typeRef.ref); |
| } |
| return accumulator; |
| }); |
| |
| 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; |
| |
| @override |
| void generate(DartGenerator gen) => gen.write(name); |
| } |
| |
| class TypeRef { |
| final String name; |
| int arrayDepth = 0; |
| final bool nullable; |
| List<TypeRef>? genericTypes; |
| |
| TypeRef(this.name, {this.nullable = false}); |
| |
| String get ref { |
| if (arrayDepth == 2) { |
| return 'List<List<$name${nullable ? "?" : ""}>>'; |
| } else if (arrayDepth == 1) { |
| return 'List<$name${nullable ? "?" : ""}>'; |
| } else if (genericTypes != null) { |
| return '$name<${genericTypes!.join(', ')}>'; |
| } else { |
| return name.startsWith('_') ? name.substring(1) : name; |
| } |
| } |
| |
| String get listCreationRef { |
| assert(arrayDepth == 1); |
| |
| if (isListTypeSimple) { |
| return 'List<$name${nullable ? "?" : ""}>'; |
| } else { |
| return 'List<String>'; |
| } |
| } |
| |
| String get listTypeArg => arrayDepth == 2 |
| ? 'List<$name${nullable ? "?" : ""}>' |
| : '$name${nullable ? "?" : ""}'; |
| |
| bool get isArray => arrayDepth > 0; |
| |
| bool get isSimple => |
| arrayDepth == 0 && |
| (name == 'int' || |
| name == 'num' || |
| name == 'String' || |
| name == 'bool' || |
| name == 'double' || |
| name == 'ByteData'); |
| |
| bool get isListTypeSimple => |
| arrayDepth == 1 && |
| (name == 'int' || |
| name == 'num' || |
| name == 'String' || |
| name == 'bool' || |
| name == 'double' || |
| name == 'ByteData'); |
| |
| @override |
| String toString() => ref; |
| } |
| |
| class MethodArg extends Member { |
| final Method parent; |
| final TypeRef type; |
| @override |
| final String name; |
| bool optional = false; |
| |
| MethodArg(this.parent, this.type, this.name); |
| |
| @override |
| void generate(DartGenerator gen) { |
| gen.write('${type.ref} $name'); |
| } |
| |
| @override |
| String toString() => '$type $name'; |
| } |
| |
| class Type extends Member { |
| final Api api; |
| final Api parent; |
| String? rawName; |
| @override |
| String? name; |
| String? superName; |
| @override |
| final String? docs; |
| List<TypeField> fields = []; |
| |
| Type(this.api, this.parent, String categoryName, String definition, |
| [this.docs]) { |
| _parse(Tokenizer(definition).tokenize()); |
| } |
| |
| Type._(this.api, 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._(t1.api, 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'; |
| |
| @override |
| 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 |
| for (var field in fields) { |
| field.generate(gen); |
| } |
| gen.writeln(); |
| |
| // ctors |
| |
| bool hasRequiredParentFields = superType != null && |
| (superType.name == 'ObjRef' || superType.name == 'Obj'); |
| // Default |
| gen.write('$name('); |
| if (fields.isNotEmpty || hasRequiredParentFields) { |
| 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(); |
| if (name == 'Response' || name == 'TimelineEvent') { |
| gen.write('$name._fromJson(Map<String, dynamic> this.json)'); |
| } else if (superName != null && fields.isEmpty) { |
| gen.write('$name._fromJson(super.json): super._fromJson()'); |
| } else { |
| final superCall = superName == null ? '' : ': super._fromJson(json) '; |
| gen.write('$name._fromJson(Map<String, dynamic> json) $superCall'); |
| } |
| |
| if (fields.isEmpty) { |
| gen.writeln(';'); |
| } else { |
| gen.writeln('{'); |
| } |
| |
| for (var field in fields) { |
| 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}']"); |
| } |
| final defaultValue = field.defaultValue; |
| if (defaultValue != null) { |
| 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() => <String, Object?>{ |
| ...?json, |
| 'type': type, |
| }; |
| '''); |
| } 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() => <String, Object?>{ |
| ...?json, |
| 'type': 'TimelineEvent', |
| }; |
| '''); |
| } else { |
| if (isResponse) { |
| gen.writeln('@override'); |
| gen.writeln("String get type => '$rawName';"); |
| gen.writeln(); |
| } |
| |
| if (isResponse) { |
| gen.writeln('@override'); |
| } |
| gen.write('Map<String, dynamic> toJson() =>'); |
| gen.writeln('<String, Object?>{'); |
| if (superName != null && superName != 'Response') { |
| // The base Response type doesn't have a toJson. |
| gen.writeln('...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("'type': type,"); |
| } |
| |
| var requiredFields = fields.where((f) => !f.optional); |
| if (requiredFields.isNotEmpty) { |
| for (var field in requiredFields) { |
| gen.write("'${field.name}': "); |
| generateSerializedFieldAccess(field, gen); |
| gen.writeln(','); |
| } |
| } |
| |
| var optionalFields = fields.where((f) => f.optional); |
| for (var field in optionalFields) { |
| var fieldName = field.name; |
| var patternVariableName = '${fieldName}Value'; |
| gen.write('if ('); |
| generateSerializedFieldAccess(field, gen); |
| gen.writeln(' case final $patternVariableName?)'); |
| gen.writeln(" '$fieldName': $patternVariableName,"); |
| } |
| gen.writeln('};'); |
| gen.writeln(); |
| } |
| |
| // equals and hashCode |
| if (supportsIdentity) { |
| gen.writeln('@override'); |
| gen.writeStatement('int get hashCode => id.hashCode;'); |
| gen.writeln(); |
| |
| gen.writeln('@override'); |
| gen.writeStatement( |
| 'bool operator ==(Object other) => other is $name && id == other.id;'); |
| gen.writeln(); |
| } |
| |
| // toString() |
| Iterable<TypeField> toStringFields = |
| getAllFields().where((f) => !f.optional); |
| const maxFieldsShownInToString = 8; |
| gen.writeln('@override'); |
| if (toStringFields.length <= maxFieldsShownInToString) { |
| String properties = toStringFields |
| .map((TypeField f) => '${f.generatableName}: \$${f.generatableName}') |
| .join(', '); |
| if (properties.length > 60) { |
| int index = properties.indexOf(', ', 55); |
| if (index != -1) { |
| properties = |
| "${properties.substring(0, index + 2)}' //\n'${properties.substring(index + 2)}"; |
| } |
| gen.writeln("String toString() => '[$name ' //\n'$properties]';"); |
| } else { |
| final formattedProperties = (properties.isEmpty) ? '' : ' $properties'; |
| gen.writeln("String toString() => '[$name$formattedProperties]';"); |
| } |
| } else { |
| gen.writeln("String toString() => '[$name]';"); |
| } |
| |
| gen.writeln('}'); |
| } |
| |
| // Special methods for Script objects. |
| void generateScriptTypeMethods(DartGenerator gen) { |
| gen.writeDocs('''This function maps a token position to a line number. |
| The VM considers the first line to be line 1.'''); |
| gen.writeln( |
| 'int? getLineNumberFromTokenPos(int tokenPos) => _tokenToLine[tokenPos];'); |
| gen.writeln(); |
| gen.writeDocs('''This function maps a token position to a column number. |
| The VM considers the first column to be column 1.'''); |
| gen.writeln( |
| 'int? getColumnNumberFromTokenPos(int tokenPos) => _tokenToColumn[tokenPos];'); |
| gen.writeln(); |
| gen.writeln(''' |
| void _parseTokenPosTable() { |
| final tokenPositionTable = tokenPosTable; |
| if (tokenPositionTable == null) { |
| return; |
| } |
| final lineSet = <int>{}; |
| for (List line in tokenPositionTable) { |
| // Each entry begins with a line number... |
| int lineNumber = line[0]; |
| lineSet.add(lineNumber); |
| for (var pos = 1; pos < line.length; pos += 2) { |
| // ...and is followed by (token offset, col number) pairs. |
| final int tokenOffset = line[pos]; |
| final int colNumber = line[pos + 1]; |
| _tokenToLine[tokenOffset] = lineNumber; |
| _tokenToColumn[tokenOffset] = colNumber; |
| } |
| } |
| }'''); |
| } |
| |
| // Writes the code to retrieve the serialized value of a field. |
| void generateSerializedFieldAccess(TypeField field, DartGenerator gen) { |
| if (field.type.isSimple || |
| field.type.isEnum || |
| field.type.areAllTypesSimple) { |
| gen.write('${field.generatableName}'); |
| final defaultValue = field.defaultValue; |
| if (defaultValue != null) { |
| gen.write(' ?? $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 { |
| final subsetOfTypesThatAreSimple = field.type.subsetOfTypesThatAreSimple; |
| if (subsetOfTypesThatAreSimple.isNotEmpty) { |
| for (int i = 0; i < subsetOfTypesThatAreSimple.length; i++) { |
| if (i >= 1) { |
| gen.write(' || '); |
| } |
| gen.write( |
| '${field.generatableName} is ${subsetOfTypesThatAreSimple[i]}'); |
| } |
| gen.write(' ? ${field.generatableName} : '); |
| } |
| 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 Api api; |
| final Type parent; |
| final String? _docs; |
| late MemberType type = MemberType(api); |
| @override |
| String? name; |
| bool optional = false; |
| String? _defaultValue; |
| bool overrides = false; |
| |
| TypeField(this.api, this.parent, this._docs); |
| |
| void setOverrides() => overrides = true; |
| |
| @override |
| String get docs { |
| String str = _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] ?? name; |
| } |
| |
| set defaultValue(String? value) { |
| _defaultValue = value; |
| } |
| |
| String? get defaultValue { |
| if (_defaultValue != null) { |
| return _defaultValue; |
| } |
| if (optional) { |
| return null; |
| } |
| // 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. |
| switch (type.name) { |
| case 'int': |
| case 'num': |
| case 'double': |
| return '-1'; |
| case 'bool': |
| return 'false'; |
| case 'String': |
| return "''"; |
| case 'ByteData': |
| return 'ByteData(0)'; |
| } |
| if (type.isEnum) { |
| // TODO(bkonyi): Figure out if there's a more correct way to determine a |
| // default value for enums. |
| return "''"; |
| } |
| return null; |
| } |
| |
| @override |
| void generate(DartGenerator gen) { |
| Type? refType = parent.parent.getType('${parent.name}Ref'); |
| bool interfaceOverride = refType?.hasField(name) ?? false; |
| |
| if (docs.isNotEmpty) gen.writeDocs(docs); |
| if (optional) gen.write('@optional '); |
| if (overrides || interfaceOverride) 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') { |
| // Since this package is used to interact with DWDS as well as the |
| // native VM service, we need to be extremely careful when making types |
| // non-nullable. DWDS isn't completely compliant with the service |
| // protocol specification, meaning that some fields aren't provided in |
| // responses even though they're not marked as optional. This has been a |
| // headache for years, but until we're able to make DWDS consistent with |
| // the native VM service responses and test package:vm_service against |
| // it, we need to be extremely defensive in our parsing logic. |
| typeName = '$typeName?'; |
| } |
| gen.writeStatement('$typeName $generatableName;'); |
| if (parent.fields.any((field) => field.hasDocs)) gen.writeln(); |
| } |
| } |
| |
| void generateNamedParameter(DartGenerator gen, {bool fromParent = false}) { |
| if (fromParent) { |
| String typeName = |
| api.isEnumName(type.name) ? '/*${type.name}*/ String' : type.name; |
| gen.writeStatement('required $typeName $generatableName,'); |
| } else { |
| gen.writeStatement('this.$generatableName,'); |
| } |
| } |
| } |
| |
| class Enum extends Member { |
| @override |
| final String name; |
| @override |
| 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].nonNulls.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; |
| |
| @override |
| void generate(DartGenerator gen) { |
| gen.writeln(); |
| if (docs != null) gen.writeDocs(docs!); |
| gen.writeStatement('abstract class $name {'); |
| gen.writeln(); |
| for (var e in enums) { |
| 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; |
| @override |
| final String name; |
| @override |
| final String? docs; |
| |
| EnumValue(this.parent, this.name, [this.docs]); |
| |
| bool get isLast => parent.enums.last == this; |
| |
| @override |
| 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(); |
| |
| @override |
| 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; |
| } |
| |
| @override |
| 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); |
| } |
| } |
| |
| @override |
| 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}'; |
| } |
| } |
| |
| @override |
| String toString() => buf.toString().trim(); |
| } |
| |
| // @Instance|@Error|Sentinel evaluate( |
| // string isolateId, |
| // string targetId [optional], |
| // string expression) |
| class MethodParser extends Parser { |
| MethodParser(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(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.api, 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.api, 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(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)); |
| } |
| } |
| } |