blob: 6c87b90f07d1db2037c7dcd7b7291f838c162731 [file] [log] [blame]
// Copyright (c) 2018, 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.
import 'package:dart_style/dart_style.dart';
import 'typescript.dart';
import 'typescript_parser.dart';
final formatter = new DartFormatter();
Map<String, Interface> _interfaces = {};
Map<String, List<String>> _subtypes = {};
// TODO(dantup): Rename namespaces -> enums since they're always that now.
Map<String, Namespace> _namespaces = {};
Map<String, TypeAlias> _typeAliases = {};
/// Whether our enum class allows any value (eg. should always return true
/// from canParse() for the correct type). This is to allow us to have some
/// type safety for these values but without restricting which values are allowed.
/// This is to support things like custom error codes and also future changes
/// in the spec (it's important the server doesn't crash on deserialising
/// newer values).
bool enumClassAllowsAnyValue(String name) {
// TODO(dantup): This should return true by default, and allow opt-out for
// those things we know are not supported. This behaviour matches the old
// code in order to simplify diffs while migrating.
return name == 'ErrorCodes' || name == 'CodeActionKind';
}
String generateDartForTypes(List<AstNode> types) {
// Keep maps of items we may need to look up quickly later.
types
.whereType<TypeAlias>()
.forEach((alias) => _typeAliases[alias.name] = alias);
types.whereType<Interface>().forEach((interface) {
_interfaces[interface.name] = interface;
// Keep track of our base classes so they can look up their super classes
// later in their fromJson() to deserialise into the most specific type.
interface.baseTypes.forEach((base) {
final subTypes = _subtypes[base.dartType] ??= new List<String>();
subTypes.add(interface.name);
});
});
types
.whereType<Namespace>()
.forEach((namespace) => _namespaces[namespace.name] = namespace);
final buffer = new IndentableStringBuffer();
_getSorted(types).forEach((t) => _writeType(buffer, t));
final formattedCode = _formatCode(buffer.toString());
return formattedCode.trim() + '\n'; // Ensure a single trailing newline.
}
TypeBase resolveTypeAlias(TypeBase type, {resolveEnumClasses: false}) {
if (type is Type && _typeAliases.containsKey(type.name)) {
final alias = _typeAliases[type.name];
// Only follow the type if we're not an enum, or we wanted to follow enums.
if (!_namespaces.containsKey(alias.name) || resolveEnumClasses) {
return alias.baseType;
}
}
return type;
}
String _formatCode(String code) {
try {
code = formatter.format(code);
} catch (e) {
print('Failed to format code, returning unformatted code.');
}
return code;
}
/// Recursively gets all members from superclasses.
List<Field> _getAllFields(Interface interface) {
// Handle missing interfaces (such as special cased interfaces that won't
// be included in this model).
if (interface == null) {
return [];
}
return interface.members
.whereType<Field>()
.followedBy(interface.baseTypes
// This cast is safe because base types are always real types.
.map((type) => _getAllFields(_interfaces[(type as Type).name]))
.expand((ts) => ts))
.toList();
}
/// Returns a copy of the list sorted by name.
List<AstNode> _getSorted(List<AstNode> items) {
final sortedList = items.toList();
sortedList.sort((item1, item2) => item1.name.compareTo(item2.name));
return sortedList;
}
bool _isSimpleType(TypeBase type) {
const literals = ['num', 'String', 'bool'];
return type is Type && literals.contains(type.dartType);
}
bool _isSpecType(TypeBase type) {
return type is Type &&
(_interfaces.containsKey(type.name) ||
(_namespaces.containsKey(type.name)));
}
/// Maps reserved words and identifiers that cause issues in field names.
String _makeValidIdentifier(String identifier) {
// The SymbolKind class has uses these names which cause issues for code that
// uses them as types.
const map = {
'Object': 'Obj',
'String': 'Str',
};
return map[identifier] ?? identifier;
}
String _rewriteCommentReference(String comment) {
final commentReferencePattern = new RegExp(r'\[([\w ]+)\]\(#(\w+)\)');
return comment.replaceAllMapped(commentReferencePattern, (m) {
final description = m.group(1);
final reference = m.group(2);
if (description == reference) {
return '[$reference]';
} else {
return '$description ([$reference])';
}
});
}
Iterable<String> _wrapLines(List<String> lines, int maxLength) sync* {
lines = lines.map((l) => l.trimRight()).toList();
for (var line in lines) {
while (true) {
if (line.length <= maxLength) {
yield line;
break;
} else {
int lastSpace = line.lastIndexOf(' ', maxLength);
// If there was no valid place to wrap, yield the whole string.
if (lastSpace == -1) {
yield line;
break;
} else {
yield line.substring(0, lastSpace);
line = line.substring(lastSpace + 1);
}
}
}
}
}
void _writeCanParseMethod(IndentableStringBuffer buffer, Interface interface) {
buffer
..writeIndentedln('static bool canParse(Object obj) {')
..indent()
..writeIndented('return obj is Map<String, dynamic>');
// In order to consider this valid for parsing, all fields that may not be
// undefined must be present and also type check for the correct type.
final requiredFields =
_getAllFields(interface).where((f) => !f.allowsUndefined);
for (var field in requiredFields) {
buffer.write(" && obj.containsKey('${field.name}') && ");
_writeTypeCheckCondition(buffer, "obj['${field.name}']", field.type);
}
buffer
..writeln(';')
..outdent()
..writeIndentedln('}');
}
void _writeConst(IndentableStringBuffer buffer, Const cons) {
_writeDocCommentsAndAnnotations(buffer, cons);
buffer.writeIndentedln('static const ${cons.name} = ${cons.valueAsLiteral};');
}
void _writeConstructor(IndentableStringBuffer buffer, Interface interface) {
final allFields = _getAllFields(interface);
if (allFields.isEmpty) {
return;
}
buffer
..writeIndented('${interface.name}(')
..write(allFields.map((field) => 'this.${field.name}').join(', '))
..write(')');
final fieldsWithValidation =
allFields.where((f) => !f.allowsNull && !f.allowsUndefined).toList();
if (fieldsWithValidation.isNotEmpty) {
buffer
..writeIndentedln(' {')
..indent();
for (var field in fieldsWithValidation) {
buffer
..writeIndentedln('if (${field.name} == null) {')
..indent()
..writeIndentedln(
"throw '${field.name} is required but was not provided';")
..outdent()
..writeIndentedln('}');
}
buffer
..outdent()
..writeIndentedln('}');
} else {
buffer.writeln(';');
}
}
void _writeDocCommentsAndAnnotations(
IndentableStringBuffer buffer, AstNode node) {
var comment = node.commentText?.trim();
if (comment != null && comment.isNotEmpty) {
comment = _rewriteCommentReference(comment);
Iterable<String> lines = comment.split('\n');
// Wrap at 80 - 4 ('/// ') - indent characters.
lines = _wrapLines(lines, (80 - 4 - buffer.totalIndent).clamp(0, 80));
lines.forEach((l) => buffer.writeIndentedln('/// $l'.trim()));
}
if (node.isDeprecated) {
buffer.writeIndentedln('@core.deprecated');
}
}
void _writeEnumClass(IndentableStringBuffer buffer, Namespace namespace) {
_writeDocCommentsAndAnnotations(buffer, namespace);
final consts = namespace.members.cast<Const>().toList();
final allowsAnyValue = enumClassAllowsAnyValue(namespace.name);
final constructorName = allowsAnyValue ? '' : '._';
final typeOfValues =
resolveTypeAlias(consts.first.type, resolveEnumClasses: true);
buffer
..writeln('class ${namespace.name} {')
..indent()
..writeIndentedln('const ${namespace.name}$constructorName(this._value);')
..writeIndentedln('const ${namespace.name}.fromJson(this._value);')
..writeln()
..writeIndentedln('final ${typeOfValues.dartTypeWithTypeArgs} _value;')
..writeln()
..writeIndentedln('static bool canParse(Object obj) {')
..indent();
if (allowsAnyValue) {
buffer.writeIndentedln('return ');
_writeTypeCheckCondition(buffer, 'obj', consts.first.type);
buffer.writeln(';');
} else {
buffer
..writeIndentedln('switch (obj) {')
..indent();
consts.forEach((cons) {
buffer..writeIndentedln('case ${cons.valueAsLiteral}:');
});
buffer
..indent()
..writeIndentedln('return true;')
..outdent()
..outdent()
..writeIndentedln('}')
..writeIndentedln('return false;');
}
buffer
..outdent()
..writeIndentedln('}');
namespace.members.whereType<Const>().forEach((cons) {
_writeDocCommentsAndAnnotations(buffer, cons);
buffer
..writeIndentedln(
'static const ${_makeValidIdentifier(cons.name)} = const ${namespace.name}$constructorName(${cons.valueAsLiteral});');
});
buffer
..writeln()
..writeIndentedln('Object toJson() => _value;')
..writeln()
..writeIndentedln('@override String toString() => _value.toString();')
..writeln()
..writeIndentedln('@override get hashCode => _value.hashCode;')
..writeln()
..writeIndentedln(
'bool operator ==(o) => o is ${namespace.name} && o._value == _value;')
..outdent()
..writeln('}')
..writeln();
}
void _writeEquals(IndentableStringBuffer buffer, Interface interface) {
buffer
..writeIndentedln('@override')
..writeIndentedln('bool operator ==(other) {')
..indent()
..writeIndentedln('if (other is ${interface.name}) {')
..indent()
..writeIndented('return ');
for (var field in _getAllFields(interface)) {
final type = field.type;
if (type is ArrayType) {
final elementType = type.elementType;
final elementDartType = elementType.dartTypeWithTypeArgs;
buffer.write(
'listEqual(${field.name}, other.${field.name}, ($elementDartType a, $elementDartType b) => a == b) && ');
} else if (type is MapType) {
final valueType = type.valueType;
final valueDartType = valueType.dartTypeWithTypeArgs;
buffer.write(
'mapEqual(${field.name}, other.${field.name}, ($valueDartType a, $valueDartType b) => a == b) && ');
} else {
buffer.write('${field.name} == other.${field.name} && ');
}
}
buffer
..writeln('true;')
..outdent()
..writeIndentedln('}')
..writeIndentedln('return false;')
..outdent()
..writeIndentedln('}');
}
void _writeField(IndentableStringBuffer buffer, Field field) {
_writeDocCommentsAndAnnotations(buffer, field);
buffer
..writeIndented('final ')
..write(field.type.dartTypeWithTypeArgs)
..writeln(' ${field.name};');
}
void _writeFromJsonCode(
IndentableStringBuffer buffer, TypeBase type, String valueCode,
{bool allowsNull}) {
type = resolveTypeAlias(type);
if (_isSimpleType(type)) {
buffer.write("$valueCode");
} else if (_isSpecType(type)) {
// Our own types have fromJson() constructors we can call.
buffer.write(
"$valueCode != null ? ${type.dartType}.fromJson${type.typeArgsString}($valueCode) : null");
} else if (type is ArrayType) {
// Lists need to be map()'d so we can recursively call writeFromJsonCode
// as they may need fromJson on each element.
buffer.write("$valueCode?.map((item) => ");
_writeFromJsonCode(buffer, type.elementType, 'item',
allowsNull: allowsNull);
buffer
.write(')?.cast<${type.elementType.dartTypeWithTypeArgs}>()?.toList()');
} else if (type is MapType) {
// Maps need to be map()'d so we can recursively call writeFromJsonCode as
// they may need fromJson on each key or value.
buffer.write('$valueCode?.map((key, value) => new MapEntry(');
_writeFromJsonCode(buffer, type.indexType, 'key', allowsNull: allowsNull);
buffer.write(', ');
_writeFromJsonCode(buffer, type.valueType, 'value', allowsNull: allowsNull);
buffer.write(
'))?.cast<${type.indexType.dartTypeWithTypeArgs}, ${type.valueType.dartTypeWithTypeArgs}>()');
} else if (type is UnionType) {
_writeFromJsonCodeForUnion(buffer, type, valueCode, allowsNull: allowsNull);
} else {
buffer.write("$valueCode");
}
}
void _writeFromJsonCodeForUnion(
IndentableStringBuffer buffer, UnionType union, String valueCode,
{bool allowsNull}) {
// Write a check against each type, eg.:
// x is y ? new Either.tx(x) : (...)
var hasIncompleteCondition = false;
var unclosedParens = 0;
for (var i = 0; i < union.types.length; i++) {
final type = union.types[i];
final isDynamic = type.dartType == 'dynamic';
// Dynamic matches all type checks, so only emit it if required.
if (!isDynamic) {
_writeTypeCheckCondition(buffer, valueCode, type);
buffer.write(' ? ');
}
// The code to construct a value with this "side" of the union.
buffer.write('new ${union.dartTypeWithTypeArgs}.t${i + 1}(');
_writeFromJsonCode(buffer, type, valueCode,
allowsNull: allowsNull); // Call recursively!
buffer.write(')');
// If we output the type condition at the top, prepare for the next condition.
if (!isDynamic) {
buffer.write(' : (');
hasIncompleteCondition = true;
unclosedParens++;
} else {
hasIncompleteCondition = false;
}
}
// Fill the final parens with a throw because if we fell through all of the
// cases then the value we had didn't match any of the types in the union.
if (hasIncompleteCondition) {
if (allowsNull) {
buffer.write('$valueCode == null ? null : (');
unclosedParens++;
}
buffer.write(
"throw '''\${$valueCode} was not one of (${union.types.map((t) => t.dartTypeWithTypeArgs).join(', ')})'''");
}
buffer.write(')' * unclosedParens);
}
void _writeFromJsonConstructor(
IndentableStringBuffer buffer, Interface interface) {
final allFields = _getAllFields(interface);
buffer
..writeIndentedln('static ${interface.nameWithTypeArgs} '
'fromJson${interface.typeArgsString}(Map<String, dynamic> json) {')
..indent();
// First check whether any of our subclasses can deserialise this.
for (final subclassName in _subtypes[interface.name] ?? const <String>[]) {
final subclass = _interfaces[subclassName];
buffer
..writeIndentedln('if (${subclass.nameWithTypeArgs}.canParse(json)) {')
..indent()
..writeln('return ${subclass.nameWithTypeArgs}.fromJson(json);')
..outdent()
..writeIndentedln('}');
}
for (final field in allFields) {
buffer.writeIndented('final ${field.name} = ');
_writeFromJsonCode(buffer, field.type, "json['${field.name}']",
allowsNull: field.allowsNull || field.allowsUndefined);
buffer.writeln(';');
}
buffer
..writeIndented('return new ${interface.nameWithTypeArgs}(')
..write(allFields.map((field) => '${field.name}').join(', '))
..writeln(');')
..outdent()
..writeIndented('}');
}
void _writeHashCode(IndentableStringBuffer buffer, Interface interface) {
buffer
..writeIndentedln('@override')
..writeIndentedln('int get hashCode {')
..indent()
..writeIndented('int hash = 0;');
for (var field in _getAllFields(interface)) {
buffer
.write('hash = JenkinsSmiHash.combine(hash, ${field.name}.hashCode);');
}
buffer
..writeln('return JenkinsSmiHash.finish(hash);')
..outdent()
..writeIndentedln('}');
}
void _writeInterface(IndentableStringBuffer buffer, Interface interface) {
_writeDocCommentsAndAnnotations(buffer, interface);
buffer.writeIndented('class ${interface.nameWithTypeArgs} ');
final allBaseTypes =
interface.baseTypes.map((t) => t.dartTypeWithTypeArgs).toList();
allBaseTypes.addAll(getSpecialBaseTypes(interface));
allBaseTypes.add('ToJsonable');
if (allBaseTypes.isNotEmpty) {
buffer.writeIndented('implements ${allBaseTypes.join(', ')} ');
}
buffer
..writeln('{')
..indent();
_writeConstructor(buffer, interface);
_writeFromJsonConstructor(buffer, interface);
// Handle Consts and Fields separately, since we need to include superclass
// Fields.
final consts = interface.members.whereType<Const>().toList();
final fields = _getAllFields(interface);
buffer.writeln();
_writeMembers(buffer, consts);
buffer.writeln();
_writeMembers(buffer, fields);
buffer.writeln();
_writeToJsonMethod(buffer, interface);
_writeCanParseMethod(buffer, interface);
_writeEquals(buffer, interface);
_writeHashCode(buffer, interface);
_writeToString(buffer, interface);
buffer
..outdent()
..writeIndentedln('}')
..writeln();
}
void _writeJsonMapAssignment(
IndentableStringBuffer buffer, Field field, String mapName) {
// If we are allowed to be undefined, we'll only add the value if set.
final shouldBeOmittedIfNoValue = field.allowsUndefined;
if (shouldBeOmittedIfNoValue) {
buffer
..writeIndentedln('if (${field.name} != null) {')
..indent();
}
buffer..writeIndented('''$mapName['${field.name}'] = ${field.name}''');
if (!field.allowsUndefined && !field.allowsNull) {
buffer.write(''' ?? (throw '${field.name} is required but was not set')''');
}
buffer.writeln(';');
if (shouldBeOmittedIfNoValue) {
buffer
..outdent()
..writeIndentedln('}');
}
}
void _writeMember(IndentableStringBuffer buffer, Member member) {
if (member is Field) {
_writeField(buffer, member);
} else if (member is Const) {
_writeConst(buffer, member);
} else {
throw 'Unknown type';
}
}
void _writeMembers(IndentableStringBuffer buffer, List<Member> members) {
_getSorted(members).forEach((m) => _writeMember(buffer, m));
}
void _writeToJsonMethod(IndentableStringBuffer buffer, Interface interface) {
// It's important the name we use for the map here isn't in use in the object
// already. 'result' was, so we prefix it with some underscores.
buffer
..writeIndentedln('Map<String, dynamic> toJson() {')
..indent()
..writeIndentedln('Map<String, dynamic> __result = {};');
// ResponseMessage must confirm to JSON-RPC which says only one of
// result/error can be included. Since this isn't encoded in the types we
// need to special-case it's toJson generation.
if (interface.name == "ResponseMessage") {
_writeToJsonFieldsForResponseMessage(buffer, interface);
} else {
for (var field in _getAllFields(interface)) {
_writeJsonMapAssignment(buffer, field, '__result');
}
}
buffer
..writeIndentedln('return __result;')
..outdent()
..writeIndentedln('}');
}
void _writeToJsonFieldsForResponseMessage(
IndentableStringBuffer buffer, Interface interface) {
const mapName = '__result';
final allFields = _getAllFields(interface);
final standardFields =
allFields.where((f) => f.name != 'error' && f.name != 'result');
for (var field in standardFields) {
_writeJsonMapAssignment(buffer, field, mapName);
}
// Write special code for result/error so that only one is populated.
buffer
..writeIndentedln('if (error != null && result != null) {')
..indent()
..writeIndentedln('''throw 'result and error cannot both be set';''')
..outdent()
..writeIndentedln('} else if (error != null) {')
..indent()
..writeIndentedln('''$mapName['error'] = error;''')
..outdent()
..writeIndentedln('} else {')
..indent()
..writeIndentedln('''$mapName['result'] = result;''')
..outdent()
..writeIndentedln('}');
}
void _writeToString(IndentableStringBuffer buffer, Interface interface) {
buffer
..writeIndentedln('@override')
..writeIndentedln('String toString() => jsonEncoder.convert(toJson());');
}
void _writeType(IndentableStringBuffer buffer, AstNode type) {
if (type is Interface) {
_writeInterface(buffer, type);
} else if (type is Namespace) {
_writeEnumClass(buffer, type);
} else if (type is TypeAlias) {
// For now type aliases are not supported, so are collected at the start
// of the process in a map, and just replaced with the aliased type during
// generation.
// _writeTypeAlias(buffer, type);
} else {
throw 'Unknown type';
}
}
void _writeTypeCheckCondition(
IndentableStringBuffer buffer, String valueCode, TypeBase type) {
type = resolveTypeAlias(type, resolveEnumClasses: true);
final resolvedDartType = type.dartTypeWithTypeArgs;
if (resolvedDartType == 'dynamic') {
buffer.write('true');
} else if (_isSimpleType(type)) {
buffer.write('$valueCode is $resolvedDartType');
} else if (_isSpecType(type)) {
buffer.write('$resolvedDartType.canParse($valueCode)');
} else if (type is ArrayType) {
buffer.write('($valueCode is List');
if (resolvedDartType != 'dynamic') {
// TODO(dantup): If we're happy to assume we never have two lists in a union
// we could skip this bit.
buffer.write(' && ($valueCode.every((item) => ');
_writeTypeCheckCondition(buffer, 'item', type.elementType);
buffer.write('))');
}
buffer.write(')');
} else if (type is MapType) {
buffer.write('($valueCode is Map');
if (resolvedDartType != 'dynamic') {
buffer..write(' && ((')..write('$valueCode.keys.every((item) => ');
_writeTypeCheckCondition(buffer, 'item', type.indexType);
buffer..write('&& $valueCode.values.every((item) => ');
_writeTypeCheckCondition(buffer, 'item', type.valueType);
buffer.write(')))');
}
buffer.write(')');
} else if (type is UnionType) {
// To type check a union, we just recursively check against each of its types.
buffer.write('(');
for (var i = 0; i < type.types.length; i++) {
if (i != 0) {
buffer.write(' || ');
}
_writeTypeCheckCondition(buffer, valueCode, type.types[i]);
}
buffer.write(')');
} else {
throw 'Unable to type check $valueCode against $resolvedDartType';
}
}
class IndentableStringBuffer extends StringBuffer {
int _indentLevel = 0;
int _indentSpaces = 2;
int get totalIndent => _indentLevel * _indentSpaces;
String get _indentString => ' ' * totalIndent;
void indent() => _indentLevel++;
void outdent() => _indentLevel--;
void writeIndented(Object obj) {
write(_indentString);
write(obj);
}
void writeIndentedln(Object obj) {
write(_indentString);
writeln(obj);
}
}