blob: be79d4c665610a6d2bf7492db32e4f351b78b59d [file] [log] [blame]
// 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));
}
}
}