88fdbea483
paired with this so the sdk roll can succeed. TEST=Fixes static errors Change-Id: Ia4e325936ea81d0d51fbf6dd939b732860d22100 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/183760 Commit-Queue: Jake Macdonald <jakemac@google.com> Reviewed-by: Ben Konyi <bkonyi@google.com> Reviewed-by: Nate Bosch <nbosch@google.com>
1150 lines
34 KiB
Dart
1150 lines
34 KiB
Dart
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
library generate_vm_service_java;
|
|
|
|
import 'package:markdown/markdown.dart';
|
|
import 'package:pub_semver/pub_semver.dart';
|
|
|
|
import '../common/generate_common.dart';
|
|
import '../common/parser.dart';
|
|
import '../common/src_gen_common.dart';
|
|
import 'src_gen_java.dart';
|
|
|
|
export 'src_gen_java.dart' show JavaGenerator;
|
|
|
|
const String servicePackage = 'org.dartlang.vm.service';
|
|
|
|
const List<String> simpleTypes = [
|
|
'BigDecimal',
|
|
'boolean',
|
|
'int',
|
|
'String',
|
|
'double'
|
|
];
|
|
|
|
const vmServiceJavadoc = '''
|
|
{@link VmService} allows control of and access to information in a running
|
|
Dart VM instance.
|
|
<br/>
|
|
Launch the Dart VM with the arguments:
|
|
<pre>
|
|
--pause_isolates_on_start
|
|
--observe
|
|
--enable-vm-service=some-port
|
|
</pre>
|
|
where <strong>some-port</strong> is a port number of your choice
|
|
which this client will use to communicate with the Dart VM.
|
|
See https://dart.dev/tools/dart-vm for more details.
|
|
Once the VM is running, instantiate a new {@link VmService}
|
|
to connect to that VM via {@link VmService#connect(String)}
|
|
or {@link VmService#localConnect(int)}.
|
|
<br/>
|
|
{@link VmService} is not thread safe and should only be accessed from
|
|
a single thread. In addition, a given VM should only be accessed from
|
|
a single instance of {@link VmService}.
|
|
<br/>
|
|
Calls to {@link VmService} should not be nested.
|
|
More specifically, you should not make any calls to {@link VmService}
|
|
from within any {@link Consumer} method.
|
|
''';
|
|
|
|
late Api api;
|
|
|
|
/// Convert documentation references
|
|
/// from spec style of [className] to javadoc style {@link className}
|
|
String? convertDocLinks(String? doc) {
|
|
if (doc == null) return null;
|
|
var sb = StringBuffer();
|
|
int start = 0;
|
|
int end = doc.indexOf('[');
|
|
while (end != -1) {
|
|
sb.write(doc.substring(start, end));
|
|
start = end;
|
|
end = doc.indexOf(']', start);
|
|
if (end == -1) break;
|
|
if (end == start + 1) {
|
|
sb.write('[]');
|
|
} else {
|
|
sb.write('{@link ');
|
|
sb.write(doc.substring(start + 1, end));
|
|
sb.write('}');
|
|
}
|
|
start = end + 1;
|
|
end = doc.indexOf('[', start);
|
|
}
|
|
sb.write(doc.substring(start));
|
|
return sb.toString();
|
|
}
|
|
|
|
String? _coerceRefType(String? typeName) {
|
|
if (typeName == 'Class') typeName = 'ClassObj';
|
|
if (typeName == 'Error') typeName = 'ErrorObj';
|
|
if (typeName == 'Object') typeName = 'Obj';
|
|
if (typeName == '@Object') typeName = 'ObjRef';
|
|
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 == 'bool') typeName = 'boolean';
|
|
if (typeName == 'num') typeName = 'BigDecimal';
|
|
if (typeName == 'map') typeName = 'Map';
|
|
return typeName;
|
|
}
|
|
|
|
class Api extends Member with ApiParseUtil {
|
|
int? serviceMajor;
|
|
int? serviceMinor;
|
|
String? serviceVersion;
|
|
List<Method> methods = [];
|
|
List<Enum?> enums = [];
|
|
List<Type?> types = [];
|
|
Map<String, List<String>> streamIdMap = {};
|
|
|
|
String? get docs => null;
|
|
|
|
String get name => 'api';
|
|
|
|
void addProperty(String typeName, String propertyName, {String? javadoc}) {
|
|
var t = types.firstWhere((t) => t!.name == typeName)!;
|
|
for (var f in t.fields) {
|
|
if (f.name == propertyName) {
|
|
print('$typeName already has $propertyName field');
|
|
return;
|
|
}
|
|
}
|
|
var f = TypeField(t, javadoc);
|
|
f.name = propertyName;
|
|
f.type = MemberType();
|
|
f.type.types = [TypeRef('String')];
|
|
t.fields.add(f);
|
|
print('added $propertyName field to $typeName');
|
|
}
|
|
|
|
void generate(JavaGenerator gen) {
|
|
_setFileHeader();
|
|
|
|
// Set default value for unspecified property
|
|
setDefaultValue('Instance', 'valueAsStringIsTruncated');
|
|
setDefaultValue('InstanceRef', 'valueAsStringIsTruncated');
|
|
|
|
// Hack to populate method argument docs
|
|
for (var m in methods) {
|
|
for (var a in m.args) {
|
|
if (a.hasDocs) continue;
|
|
var t = types.firstWhere((Type? t) => t!.name == a.type.name,
|
|
orElse: () => null);
|
|
if (t != null) {
|
|
a.docs = t.docs;
|
|
continue;
|
|
}
|
|
var e = enums.firstWhere((Enum? e) => e!.name == a.type.name,
|
|
orElse: () => null);
|
|
if (e != null) {
|
|
a.docs = e.docs;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
gen.writeType('$servicePackage.VmService', (TypeWriter writer) {
|
|
writer.addImport('com.google.gson.JsonArray');
|
|
writer.addImport('com.google.gson.JsonObject');
|
|
writer.addImport('com.google.gson.JsonPrimitive');
|
|
writer.addImport('java.util.List');
|
|
|
|
writer.addImport('$servicePackage.consumer.*');
|
|
writer.addImport('$servicePackage.element.*');
|
|
writer.javadoc = vmServiceJavadoc;
|
|
writer.superclassName = '$servicePackage.VmServiceBase';
|
|
|
|
for (String streamId in streamIdMap.keys.toList()..sort()) {
|
|
String alias = streamId.toUpperCase();
|
|
while (alias.startsWith('_')) {
|
|
alias = alias.substring(1);
|
|
}
|
|
writer.addField('${alias}_STREAM_ID', 'String',
|
|
modifiers: 'public static final', value: '"$streamId"');
|
|
}
|
|
|
|
writer.addField('versionMajor', 'int',
|
|
modifiers: 'public static final',
|
|
value: '$serviceMajor',
|
|
javadoc:
|
|
'The major version number of the protocol supported by this client.');
|
|
writer.addField('versionMinor', 'int',
|
|
modifiers: 'public static final',
|
|
value: '$serviceMinor',
|
|
javadoc:
|
|
'The minor version number of the protocol supported by this client.');
|
|
for (var m in methods) {
|
|
m.generateVmServiceMethod(writer);
|
|
if (m.hasOptionalArgs) {
|
|
m.generateVmServiceMethod(writer, includeOptional: true);
|
|
}
|
|
}
|
|
|
|
writer.addMethod('forwardResponse', [
|
|
JavaMethodArg('consumer', 'Consumer'),
|
|
JavaMethodArg('responseType', 'String'),
|
|
JavaMethodArg('json', 'JsonObject')
|
|
], (StatementWriter writer) {
|
|
var generatedForwards = Set<String>();
|
|
|
|
var sorted = methods.toList()
|
|
..sort((m1, m2) {
|
|
return m1.consumerTypeName.compareTo(m2.consumerTypeName);
|
|
});
|
|
for (var m in sorted) {
|
|
if (generatedForwards.add(m.consumerTypeName)) {
|
|
m.generateVmServiceForward(writer);
|
|
}
|
|
}
|
|
writer.addLine('if (consumer instanceof ServiceExtensionConsumer) {');
|
|
writer
|
|
.addLine(' ((ServiceExtensionConsumer) consumer).received(json);');
|
|
writer.addLine(' return;');
|
|
writer.addLine('}');
|
|
writer.addLine('logUnknownResponse(consumer, json);');
|
|
}, modifiers: null, isOverride: true);
|
|
|
|
writer.addMethod("convertMapToJsonObject", [
|
|
JavaMethodArg('map', 'Map<String, String>')
|
|
], (StatementWriter writer) {
|
|
writer.addLine('JsonObject obj = new JsonObject();');
|
|
writer.addLine('for (String key : map.keySet()) {');
|
|
writer.addLine(' obj.addProperty(key, map.get(key));');
|
|
writer.addLine('}');
|
|
writer.addLine('return obj;');
|
|
}, modifiers: "private", returnType: "JsonObject");
|
|
|
|
writer.addMethod(
|
|
"convertIterableToJsonArray", [JavaMethodArg('list', 'Iterable')],
|
|
(StatementWriter writer) {
|
|
writer.addLine('JsonArray arr = new JsonArray();');
|
|
writer.addLine('for (Object element : list) {');
|
|
writer.addLine(' arr.add(new JsonPrimitive(element.toString()));');
|
|
writer.addLine('}');
|
|
writer.addLine('return arr;');
|
|
}, modifiers: "private", returnType: "JsonArray");
|
|
});
|
|
|
|
for (var m in methods) {
|
|
m.generateConsumerInterface(gen);
|
|
}
|
|
for (var t in types) {
|
|
t!.generateElement(gen);
|
|
}
|
|
for (var e in enums) {
|
|
e!.generateEnum(gen);
|
|
}
|
|
}
|
|
|
|
Type? getType(String? name) =>
|
|
types.firstWhere((t) => t!.name == name, orElse: () => null);
|
|
|
|
bool isEnumName(String? typeName) =>
|
|
enums.any((Enum? e) => e!.name == typeName);
|
|
|
|
void parse(List<Node> nodes) {
|
|
Version version = ApiParseUtil.parseVersionSemVer(nodes);
|
|
serviceMajor = version.major;
|
|
serviceMinor = version.minor;
|
|
serviceVersion = '$serviceMajor.$serviceMinor';
|
|
|
|
// 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;
|
|
|
|
if (i + 1 < nodes.length && isPara(nodes[i + 1])) {
|
|
Element p = nodes[++i] as Element;
|
|
docs = collapseWhitespace(TextOutputVisitor.printText(p));
|
|
}
|
|
|
|
_parse(h3Name, definition, docs);
|
|
} else if (isH3(node)) {
|
|
h3Name = textForElement(node);
|
|
} else if (isHeader(node)) {
|
|
h3Name = null;
|
|
} else if (isPara(node)) {
|
|
var children = (node as Element).children!;
|
|
if (children.isNotEmpty && children.first is Text) {
|
|
var text = children.expand<String>((child) {
|
|
if (child is Text) return [child.text];
|
|
return [];
|
|
}).join();
|
|
if (text.startsWith('streamId |')) {
|
|
_parseStreamIds(text);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for (Type? type in types) {
|
|
type!.calculateFieldOverrides();
|
|
}
|
|
}
|
|
|
|
void setDefaultValue(String typeName, String propertyName) {
|
|
var type = types.firstWhere((t) => t!.name == typeName)!;
|
|
var field = type.fields.firstWhere((f) => f.name == propertyName);
|
|
field.defaultValue = 'false';
|
|
}
|
|
|
|
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}';
|
|
}
|
|
}
|
|
|
|
void _parseStreamIds(String text) {
|
|
for (String line in text.split('\n')) {
|
|
if (line.startsWith('streamId |')) continue;
|
|
if (line.startsWith('---')) continue;
|
|
var index = line.indexOf('|');
|
|
var streamId = line.substring(0, index).trim();
|
|
List<String> eventTypes =
|
|
List.from(line.substring(index + 1).split(',').map((t) => t.trim()));
|
|
eventTypes.sort();
|
|
streamIdMap[streamId] = eventTypes;
|
|
}
|
|
}
|
|
|
|
void _setFileHeader() {
|
|
fileHeader = r'''/*
|
|
* Copyright (c) 2015, the Dart project authors.
|
|
*
|
|
* Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
|
|
* in compliance with the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.eclipse.org/legal/epl-v10.html
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
* or implied. See the License for the specific language governing permissions and limitations under
|
|
* the License.
|
|
*/
|
|
''';
|
|
}
|
|
|
|
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}';
|
|
}
|
|
}
|
|
}
|
|
|
|
class Enum extends Member {
|
|
final String name;
|
|
final String? docs;
|
|
|
|
List<EnumValue> enums = [];
|
|
|
|
Enum(this.name, String definition, [this.docs]) {
|
|
_parse(Tokenizer(definition).tokenize());
|
|
}
|
|
|
|
String get elementTypeName => '$servicePackage.element.$name';
|
|
|
|
void generateEnum(JavaGenerator gen) {
|
|
gen.writeType(elementTypeName, (TypeWriter writer) {
|
|
writer.javadoc = convertDocLinks(docs);
|
|
writer.isEnum = true;
|
|
enums.sort((v1, v2) => v1.name!.compareTo(v2.name!));
|
|
for (var value in enums) {
|
|
writer.addEnumValue(value.name, javadoc: value.docs);
|
|
}
|
|
writer.addEnumValue('Unknown',
|
|
javadoc: 'Represents a value returned by the VM'
|
|
' but unknown to this client.',
|
|
isLast: true);
|
|
});
|
|
}
|
|
|
|
void _parse(Token? token) {
|
|
EnumParser(token).parseInto(this);
|
|
}
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
abstract class Member {
|
|
String? get docs => null;
|
|
|
|
bool get hasDocs => docs != null;
|
|
|
|
String? get name;
|
|
|
|
String toString() => name!;
|
|
}
|
|
|
|
class MemberType extends Member {
|
|
List<TypeRef> types = [];
|
|
|
|
MemberType();
|
|
|
|
bool get hasSentinel => types.any((t) => t.name == 'Sentinel');
|
|
|
|
bool get isEnum => types.length == 1 && api.isEnumName(types.first.name);
|
|
|
|
bool get isMultipleReturns => types.length > 1;
|
|
|
|
bool get isSimple => types.length == 1 && types.first.isSimple;
|
|
|
|
bool get isValueAndSentinel => types.length == 2 && hasSentinel;
|
|
|
|
String? get name {
|
|
if (types.isEmpty) return '';
|
|
if (types.length == 1) return types.first.ref;
|
|
return 'dynamic';
|
|
}
|
|
|
|
TypeRef? get valueType {
|
|
if (types.length == 1) return types.first;
|
|
if (isValueAndSentinel) {
|
|
return types.firstWhere((t) => t.name != 'Sentinel');
|
|
}
|
|
return null;
|
|
}
|
|
|
|
void parse(Parser parser) {
|
|
// foo|bar[]|baz
|
|
// (@Instance|Sentinel)[]
|
|
bool loop = true;
|
|
bool isMulti = false;
|
|
|
|
while (loop) {
|
|
parser.consume('(');
|
|
Token t = parser.expectName();
|
|
if (parser.consume(')')) isMulti = true;
|
|
TypeRef ref = TypeRef(_coerceRefType(t.text));
|
|
types.add(ref);
|
|
|
|
while (parser.consume('[')) {
|
|
parser.expect(']');
|
|
if (isMulti) {
|
|
types.forEach((t) => t.arrayDepth++);
|
|
} else {
|
|
ref.arrayDepth++;
|
|
}
|
|
}
|
|
|
|
loop = parser.consume('|');
|
|
}
|
|
}
|
|
}
|
|
|
|
class Method extends Member {
|
|
final String name;
|
|
final String? docs;
|
|
|
|
MemberType returnType = MemberType();
|
|
List<MethodArg> args = [];
|
|
|
|
Method(this.name, String definition, [this.docs]) {
|
|
_parse(Tokenizer(definition).tokenize());
|
|
}
|
|
|
|
String get consumerTypeName {
|
|
String? prefix;
|
|
if (returnType.isMultipleReturns) {
|
|
prefix = titleCase(name);
|
|
} else {
|
|
prefix = returnType.types.first.javaBoxedName;
|
|
}
|
|
return '$servicePackage.consumer.${prefix}Consumer';
|
|
}
|
|
|
|
bool get hasArgs => args.isNotEmpty;
|
|
|
|
bool get hasOptionalArgs => args.any((MethodArg arg) => arg.optional);
|
|
|
|
void generateConsumerInterface(JavaGenerator gen) {
|
|
gen.writeType(consumerTypeName, (TypeWriter writer) {
|
|
writer.javadoc = convertDocLinks(returnType.docs);
|
|
writer.interfaceNames.add('$servicePackage.consumer.Consumer');
|
|
writer.isInterface = true;
|
|
for (var t in returnType.types) {
|
|
writer.addImport(t.elementTypeName);
|
|
writer.addMethod(
|
|
"received", [JavaMethodArg('response', t.elementTypeName)], null);
|
|
}
|
|
});
|
|
}
|
|
|
|
void generateVmServiceForward(StatementWriter writer) {
|
|
var consumerName = classNameFor(consumerTypeName);
|
|
writer.addLine('if (consumer instanceof $consumerName) {');
|
|
List<Type?> types = List.from(returnType.types.map((ref) => ref.type));
|
|
for (int index = 0; index < types.length; ++index) {
|
|
types.addAll(types[index]!.subtypes);
|
|
}
|
|
types.sort((t1, t2) => t1!.name!.compareTo(t2!.name!));
|
|
for (var t in types) {
|
|
var responseName = classNameFor(t!.elementTypeName!);
|
|
writer.addLine(' if (responseType.equals("${t.rawName}")) {');
|
|
writer.addLine(
|
|
' (($consumerName) consumer).received(new $responseName(json));');
|
|
writer.addLine(' return;');
|
|
writer.addLine(' }');
|
|
}
|
|
writer.addLine('}');
|
|
}
|
|
|
|
void generateVmServiceMethod(TypeWriter writer, {includeOptional = false}) {
|
|
// TODO(danrubel) move this to the Consumer's javadoc
|
|
// String javadoc = docs == null ? '' : docs;
|
|
// if (returnType.isMultipleReturns) {
|
|
// javadoc += '\n\nThe return value can be one of '
|
|
// '${joinLast(returnType.types.map((t) => '[${t}]'), ', ', ' or ')}.';
|
|
// javadoc = javadoc.trim();
|
|
// }
|
|
|
|
// Update method docs
|
|
var javadoc = StringBuffer(docs == null ? '' : docs!);
|
|
bool firstParamDoc = true;
|
|
for (var a in args) {
|
|
if (!includeOptional && a.optional) continue;
|
|
var paramDoc = StringBuffer(a.docs ?? '');
|
|
if (paramDoc.isEmpty) {}
|
|
if (a.optional) {
|
|
if (paramDoc.isNotEmpty) paramDoc.write(' ');
|
|
paramDoc.write('This parameter is optional and may be null.');
|
|
}
|
|
if (paramDoc.isNotEmpty) {
|
|
if (firstParamDoc) {
|
|
javadoc.writeln();
|
|
firstParamDoc = false;
|
|
}
|
|
javadoc.writeln('@param ${a.name} $paramDoc');
|
|
}
|
|
}
|
|
|
|
if (args.any((MethodArg arg) => (arg.type.name == 'Map'))) {
|
|
writer.addImport('java.util.Map');
|
|
}
|
|
|
|
List<MethodArg> mthArgs = args;
|
|
if (!includeOptional) {
|
|
mthArgs = mthArgs.toList()..removeWhere((a) => a.optional);
|
|
}
|
|
|
|
List<JavaMethodArg> javaMethodArgs =
|
|
List.from(mthArgs.map((a) => a.asJavaMethodArg));
|
|
javaMethodArgs
|
|
.add(JavaMethodArg('consumer', classNameFor(consumerTypeName)));
|
|
writer.addMethod(name, javaMethodArgs, (StatementWriter writer) {
|
|
writer.addLine('final JsonObject params = new JsonObject();');
|
|
for (MethodArg arg in args) {
|
|
if (!includeOptional && arg.optional) continue;
|
|
var name = arg.name;
|
|
String op = arg.optional ? 'if (${name} != null) ' : '';
|
|
if (arg.isEnumType) {
|
|
writer.addLine('${op}params.addProperty("$name", $name.name());');
|
|
} else if (arg.type.name == 'Map') {
|
|
writer.addLine(
|
|
'${op}params.add("$name", convertMapToJsonObject($name));');
|
|
} else if (arg.type.arrayDepth > 0) {
|
|
writer.addLine(
|
|
'${op}params.add("$name", convertIterableToJsonArray($name));');
|
|
} else {
|
|
writer.addLine('${op}params.addProperty("$name", $name);');
|
|
}
|
|
}
|
|
writer.addLine('request("$name", params, consumer);');
|
|
}, javadoc: javadoc.toString());
|
|
}
|
|
|
|
void _parse(Token? token) {
|
|
MethodParser(token).parseInto(this);
|
|
}
|
|
}
|
|
|
|
class MethodArg extends Member {
|
|
final Method parent;
|
|
final TypeRef type;
|
|
String? name;
|
|
String? docs;
|
|
bool optional = false;
|
|
|
|
MethodArg(this.parent, this.type, this.name);
|
|
|
|
get asJavaMethodArg {
|
|
if (optional && type.ref == 'int') {
|
|
return JavaMethodArg(name, 'Integer');
|
|
}
|
|
if (optional && type.ref == 'double') {
|
|
return JavaMethodArg(name, 'Double');
|
|
}
|
|
if (optional && type.ref == 'boolean') {
|
|
return JavaMethodArg(name, 'Boolean');
|
|
}
|
|
return JavaMethodArg(name, type.ref);
|
|
}
|
|
|
|
/// TODO: Hacked enum arg type determination
|
|
bool get isEnumType => name == 'step' || name == 'mode';
|
|
}
|
|
|
|
class MethodParser extends Parser {
|
|
MethodParser(Token? startToken) : super(startToken);
|
|
|
|
void parseInto(Method method) {
|
|
// method is return type, name, (, args )
|
|
// args is type name, [optional], comma
|
|
|
|
method.returnType.parse(this);
|
|
|
|
Token t = expectName();
|
|
validate(
|
|
t.text == method.name, 'method name ${method.name} equals ${t.text}');
|
|
|
|
expect('(');
|
|
|
|
while (peek()!.text != ')') {
|
|
Token type = expectName();
|
|
TypeRef ref = TypeRef(_coerceRefType(type.text));
|
|
if (peek()!.text == '[') {
|
|
while (consume('[')) {
|
|
expect(']');
|
|
ref.arrayDepth++;
|
|
}
|
|
} else if (peek()!.text == '<') {
|
|
// handle generics
|
|
expect('<');
|
|
ref.genericTypes = [];
|
|
while (peek()!.text != '>') {
|
|
Token genericTypeName = expectName();
|
|
ref.genericTypes!.add(TypeRef(_coerceRefType(genericTypeName.text)));
|
|
consume(',');
|
|
}
|
|
expect('>');
|
|
}
|
|
Token name = expectName();
|
|
MethodArg arg = MethodArg(method, ref, name.text);
|
|
if (consume('[')) {
|
|
expect('optional');
|
|
expect(']');
|
|
arg.optional = true;
|
|
}
|
|
method.args.add(arg);
|
|
consume(',');
|
|
}
|
|
|
|
expect(')');
|
|
}
|
|
}
|
|
|
|
class TextOutputVisitor implements NodeVisitor {
|
|
StringBuffer buf = StringBuffer();
|
|
|
|
bool _inRef = false;
|
|
|
|
TextOutputVisitor();
|
|
|
|
String toString() => buf.toString().trim();
|
|
|
|
bool visitElementBefore(Element element) {
|
|
if (element.tag == 'em') {
|
|
buf.write('[');
|
|
_inRef = true;
|
|
} else if (element.tag == 'p') {
|
|
// Nothing to do.
|
|
} else if (element.tag == 'a') {
|
|
// Nothing to do - we're not writing out <a> refs (they won't resolve).
|
|
} else if (element.tag == 'code') {
|
|
buf.write(renderToHtml([element]));
|
|
} else {
|
|
print('unknown tag: ${element.tag}');
|
|
buf.write(renderToHtml([element]));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void visitText(Text text) {
|
|
String? t = text.text;
|
|
if (_inRef) t = _coerceRefType(t);
|
|
buf.write(t);
|
|
}
|
|
|
|
void visitElementAfter(Element element) {
|
|
if (element.tag == 'em') {
|
|
buf.write(']');
|
|
_inRef = false;
|
|
} else if (element.tag == 'p') {
|
|
buf.write('\n\n');
|
|
}
|
|
}
|
|
|
|
static String printText(Node node) {
|
|
TextOutputVisitor visitor = TextOutputVisitor();
|
|
node.accept(visitor);
|
|
return visitor.toString();
|
|
}
|
|
}
|
|
|
|
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());
|
|
}
|
|
|
|
String? get elementTypeName {
|
|
if (isSimple) return null;
|
|
return '$servicePackage.element.$name';
|
|
}
|
|
|
|
bool get isRef => name!.endsWith('Ref');
|
|
|
|
bool get isResponse {
|
|
if (superName == null) return false;
|
|
if (name == 'Response' || superName == 'Response') return true;
|
|
return parent.getType(superName)!.isResponse;
|
|
}
|
|
|
|
bool get isSimple => simpleTypes.contains(name);
|
|
|
|
get jsonTypeName {
|
|
if (name == 'ClassObj') return 'Class';
|
|
if (name == 'ErrorObj') return 'Error';
|
|
return name;
|
|
}
|
|
|
|
Iterable<Type?> get subtypes =>
|
|
api.types.toList()..retainWhere((t) => t!.superName == name);
|
|
|
|
void generateElement(JavaGenerator gen) {
|
|
gen.writeType('$servicePackage.element.$name', (TypeWriter writer) {
|
|
if (fields.any((f) => f.type.types.any((t) => t.isArray))) {
|
|
writer.addImport('com.google.gson.JsonObject');
|
|
}
|
|
writer.addImport('com.google.gson.JsonObject');
|
|
writer.javadoc = convertDocLinks(docs);
|
|
writer.superclassName = superName ?? 'Element';
|
|
writer.addConstructor(
|
|
<JavaMethodArg>[JavaMethodArg('json', 'com.google.gson.JsonObject')],
|
|
(StatementWriter writer) {
|
|
writer.addLine('super(json);');
|
|
});
|
|
|
|
if (name == 'InstanceRef' || name == 'Instance') {
|
|
writer.addMethod(
|
|
'isNull',
|
|
[],
|
|
(StatementWriter writer) {
|
|
writer.addLine('return getKind() == InstanceKind.Null;');
|
|
},
|
|
returnType: 'boolean',
|
|
javadoc: 'Returns whether this instance represents null.',
|
|
);
|
|
}
|
|
|
|
for (var field in fields) {
|
|
field.generateAccessor(writer);
|
|
}
|
|
});
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
Type? getSuper() => superName == null ? null : api.getType(superName);
|
|
|
|
bool hasField(String? name) {
|
|
if (fields.any((field) => field.name == name)) return true;
|
|
return getSuper()?.hasField(name) ?? false;
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// @Instance|@Error|Sentinel evaluate(
|
|
// string isolateId,
|
|
// string targetId [optional],
|
|
// string expression)
|
|
class TypeField extends Member {
|
|
static final Map<String, String> _nameRemap = {
|
|
'const': 'isConst',
|
|
'final': 'isFinal',
|
|
'static': 'isStatic',
|
|
'abstract': 'isAbstract',
|
|
'super': 'superClass',
|
|
'class': 'classRef'
|
|
};
|
|
|
|
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 accessorName {
|
|
var remappedName = _nameRemap[name];
|
|
if (remappedName != null) {
|
|
if (remappedName.startsWith('is')) return remappedName;
|
|
} else {
|
|
remappedName = name;
|
|
}
|
|
return 'get${titleCase(remappedName!)}';
|
|
}
|
|
|
|
String? get docs {
|
|
String str = _docs == null ? '' : _docs!;
|
|
if (type.isMultipleReturns) {
|
|
str += '\n\n@return one of '
|
|
'${joinLast(type.types.map((t) => '<code>${t}</code>'), ', ', ' or ')}';
|
|
str = str.trim();
|
|
}
|
|
if (optional) {
|
|
str += '\n\nCan return <code>null</code>.';
|
|
str = str.trim();
|
|
}
|
|
return str;
|
|
}
|
|
|
|
void generateAccessor(TypeWriter writer) {
|
|
if (type.isMultipleReturns && !type.isValueAndSentinel) {
|
|
writer.addMethod(accessorName, [], (StatementWriter w) {
|
|
w.addImport('com.google.gson.JsonObject');
|
|
w.addLine('final JsonObject elem = (JsonObject)json.get("$name");');
|
|
w.addLine('if (elem == null) return null;\n');
|
|
for (TypeRef t in type.types) {
|
|
String refName = t.name!;
|
|
if (refName.endsWith('Ref')) {
|
|
refName = "@" + refName.substring(0, refName.length - 3);
|
|
}
|
|
w.addLine('if (elem.get("type").getAsString().equals("${refName}")) '
|
|
'return new ${t.name}(elem);');
|
|
}
|
|
w.addLine('return null;');
|
|
}, javadoc: docs, returnType: 'Object');
|
|
} else {
|
|
String? returnType = type.valueType!.ref;
|
|
if (name == 'timestamp') {
|
|
returnType = 'long';
|
|
}
|
|
|
|
writer.addMethod(
|
|
accessorName,
|
|
[],
|
|
(StatementWriter writer) {
|
|
type.valueType!.generateAccessStatements(
|
|
writer,
|
|
name,
|
|
canBeSentinel: type.isValueAndSentinel,
|
|
defaultValue: defaultValue,
|
|
optional: optional,
|
|
);
|
|
},
|
|
javadoc: docs,
|
|
returnType: returnType,
|
|
isOverride: overrides,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
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(';');
|
|
}
|
|
|
|
expect('}');
|
|
}
|
|
}
|
|
|
|
class TypeRef {
|
|
String? name;
|
|
int arrayDepth = 0;
|
|
List<TypeRef>? genericTypes;
|
|
|
|
TypeRef(this.name);
|
|
|
|
String? get elementTypeName {
|
|
if (isSimple) return null;
|
|
return '$servicePackage.element.$name';
|
|
}
|
|
|
|
bool get isArray => arrayDepth > 0;
|
|
|
|
/// Hacked enum determination
|
|
bool get isEnum => name!.endsWith('Kind') || name!.endsWith('Mode');
|
|
|
|
bool get isSimple => simpleTypes.contains(name);
|
|
|
|
String? get javaBoxedName {
|
|
if (name == 'boolean') return 'Boolean';
|
|
if (name == 'int') return 'Integer';
|
|
if (name == 'double') return 'Double';
|
|
return name;
|
|
}
|
|
|
|
String? get ref {
|
|
if (genericTypes != null) {
|
|
return '$name<${genericTypes!.join(', ')}>';
|
|
} else if (isSimple) {
|
|
if (arrayDepth == 2) return 'List<List<${javaBoxedName}>>';
|
|
if (arrayDepth == 1) return 'List<${javaBoxedName}>';
|
|
} else {
|
|
if (arrayDepth == 2) return 'ElementList<ElementList<${javaBoxedName}>>';
|
|
if (arrayDepth == 1) return 'ElementList<${javaBoxedName}>';
|
|
}
|
|
return name;
|
|
}
|
|
|
|
Type? get type => api.types.firstWhere((t) => t!.name == name);
|
|
|
|
void generateAccessStatements(
|
|
StatementWriter writer,
|
|
String? propertyName, {
|
|
bool canBeSentinel = false,
|
|
String? defaultValue,
|
|
bool optional = false,
|
|
}) {
|
|
if (name == 'boolean') {
|
|
if (isArray) {
|
|
print('skipped accessor body for $propertyName');
|
|
} else {
|
|
if (defaultValue != null) {
|
|
writer.addImport('com.google.gson.JsonElement');
|
|
writer.addLine('final JsonElement elem = json.get("$propertyName");');
|
|
writer.addLine(
|
|
'return elem != null ? elem.getAsBoolean() : $defaultValue;');
|
|
} else {
|
|
writer.addLine('return getAsBoolean("$propertyName");');
|
|
}
|
|
}
|
|
} else if (name == 'int') {
|
|
if (arrayDepth > 1) {
|
|
writer.addImport('java.util.List');
|
|
writer.addLine('return getListListInt("$propertyName");');
|
|
} else if (arrayDepth == 1) {
|
|
writer.addImport('java.util.List');
|
|
writer.addLine('return getListInt("$propertyName");');
|
|
} else {
|
|
if (propertyName == 'timestamp') {
|
|
writer.addLine('return json.get("$propertyName") == null ? '
|
|
'-1 : json.get("$propertyName").getAsLong();');
|
|
} else {
|
|
writer.addLine('return getAsInt("$propertyName");');
|
|
}
|
|
}
|
|
} else if (name == 'double') {
|
|
writer.addLine('return json.get("$propertyName") == null ? '
|
|
'0.0 : json.get("$propertyName").getAsDouble();');
|
|
} else if (name == 'BigDecimal') {
|
|
if (isArray) {
|
|
print('skipped accessor body for $propertyName');
|
|
} else {
|
|
writer.addImport('java.math.BigDecimal');
|
|
writer.addLine('return json.get("$propertyName").getAsBigDecimal();');
|
|
}
|
|
} else if (name == 'String') {
|
|
if (isArray) {
|
|
writer.addImport('java.util.List');
|
|
if (optional) {
|
|
writer.addLine('return json.get("$propertyName") == null ? '
|
|
'null : getListString("$propertyName");');
|
|
} else {
|
|
writer.addLine('return getListString("$propertyName");');
|
|
}
|
|
} else {
|
|
writer.addLine('return getAsString("$propertyName");');
|
|
}
|
|
} else if (isEnum) {
|
|
if (isArray) {
|
|
print('skipped accessor body for $propertyName');
|
|
} else {
|
|
if (optional) {
|
|
writer.addLine('if (json.get("$propertyName") == null) return null;');
|
|
writer.addLine('');
|
|
}
|
|
writer.addImport('com.google.gson.JsonElement');
|
|
writer.addLine('final JsonElement value = json.get("$propertyName");');
|
|
writer.addLine('try {');
|
|
writer.addLine(' return value == null ? $name.Unknown'
|
|
' : $name.valueOf(value.getAsString());');
|
|
writer.addLine('} catch (IllegalArgumentException e) {');
|
|
writer.addLine(' return $name.Unknown;');
|
|
writer.addLine('}');
|
|
}
|
|
} else {
|
|
if (arrayDepth > 1) {
|
|
print('skipped accessor body for $propertyName');
|
|
} else if (arrayDepth == 1) {
|
|
writer.addImport('com.google.gson.JsonArray');
|
|
if (optional) {
|
|
writer.addLine('if (json.get("$propertyName") == null) return null;');
|
|
writer.addLine('');
|
|
}
|
|
writer.addLine(
|
|
'return new ElementList<$javaBoxedName>(json.get("$propertyName").getAsJsonArray()) {');
|
|
writer.addLine(' @Override');
|
|
writer.addLine(
|
|
' protected $javaBoxedName basicGet(JsonArray array, int index) {');
|
|
writer.addLine(
|
|
' return new $javaBoxedName(array.get(index).getAsJsonObject());');
|
|
writer.addLine(' }');
|
|
writer.addLine('};');
|
|
} else {
|
|
if (canBeSentinel) {
|
|
writer.addImport('com.google.gson.JsonElement');
|
|
writer.addLine('final JsonElement elem = json.get("$propertyName");');
|
|
writer.addLine('if (!elem.isJsonObject()) return null;');
|
|
writer.addLine('final JsonObject child = elem.getAsJsonObject();');
|
|
writer
|
|
.addLine('final String type = child.get("type").getAsString();');
|
|
writer.addLine('if ("Sentinel".equals(type)) return null;');
|
|
writer.addLine('return new $name(child);');
|
|
} else {
|
|
if (optional) {
|
|
writer.addLine(
|
|
'JsonObject obj = (JsonObject) json.get("$propertyName");');
|
|
writer.addLine('if (obj == null) return null;');
|
|
if ((name != 'InstanceRef') && (name != 'Instance')) {
|
|
writer.addLine(
|
|
'final String type = json.get("type").getAsString();');
|
|
writer.addLine(
|
|
'if ("Instance".equals(type) || "@Instance".equals(type)) {');
|
|
writer.addLine(
|
|
' final String kind = json.get("kind").getAsString();');
|
|
writer.addLine(' if ("Null".equals(kind)) return null;');
|
|
writer.addLine('}');
|
|
}
|
|
writer.addLine('return new $name(obj);');
|
|
} else {
|
|
writer.addLine(
|
|
'return new $name((JsonObject) json.get("$propertyName"));');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
String toString() => ref!;
|
|
}
|