| // 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 'package:meta/meta.dart'; |
| |
| import 'typescript.dart'; |
| |
| final formatter = new DartFormatter(); |
| Map<String, Interface> _interfaces = {}; |
| Map<String, Namespace> _namespaces = {}; |
| Map<String, TypeAlias> _typeAliases = {}; |
| |
| String generateDartForTypes(List<ApiItem> 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); |
| 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. |
| } |
| |
| List<String> _extractTypesFromUnion(String type) { |
| return type.split('|').map((t) => t.trim()).toList(); |
| } |
| |
| 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 |
| .map((name) => _getAllFields(_interfaces[name])) |
| .expand((ts) => ts)) |
| .toList(); |
| } |
| |
| String _getListType(String type) { |
| return type.substring('List<'.length, type.length - 1); |
| } |
| |
| /// Returns a copy of the list sorted by name. |
| List<ApiItem> _getSorted(List<ApiItem> items) { |
| final sortedList = items.toList(); |
| sortedList.sort((item1, item2) => item1.name.compareTo(item2.name)); |
| return sortedList; |
| } |
| |
| List<String> _getUnionTypes(String type) { |
| return type |
| .substring('EitherX<'.length, type.length - 1) |
| .split(',') |
| .map((s) => s.trim()) |
| .toList(); |
| } |
| |
| bool _isList(String type) { |
| return type.startsWith('List<') && type.endsWith('>'); |
| } |
| |
| bool _isLiteral(String type) { |
| const literals = ['num', 'String', 'bool']; |
| return literals.contains(type); |
| } |
| |
| bool _isSpecType(String type) { |
| return _interfaces.containsKey(type) || _namespaces.containsKey(type); |
| } |
| |
| bool _isUnion(String type) { |
| return type.startsWith('Either') && type.endsWith('>'); |
| } |
| |
| /// 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; |
| } |
| |
| /// Maps a TypeScript type on to a Dart type, including following TypeAliases. |
| @visibleForTesting |
| String mapType(List<String> types) { |
| const mapping = <String, String>{ |
| 'boolean': 'bool', |
| 'string': 'String', |
| 'number': 'num', |
| 'any': 'dynamic', |
| 'object': 'dynamic', |
| // Special cases that are hard to parse or anonymous types. |
| '{ [uri: string]: TextEdit[]; }': 'Map<String, List<TextEdit>>', |
| '{ language: string; value: string }': 'MarkedStringWithLanguage' |
| }; |
| if (types.length > 4) { |
| throw 'Unions of more than 4 types are not supported.'; |
| } |
| if (types.length >= 2) { |
| final typeArgs = types.map((t) => mapType([t])).join(', '); |
| return 'Either${types.length}<$typeArgs>'; |
| } |
| |
| final type = types.first; |
| if (type.endsWith('[]')) { |
| return 'List<${mapType([type.substring(0, type.length - 2)])}>'; |
| } else if (type.startsWith('Array<') && type.endsWith('>')) { |
| return 'List<${mapType([type.substring(6, type.length - 1)])}>'; |
| } else if (type.contains('<')) { |
| // For types with type args, we need to map the type and each type arg. |
| final declaredType = _stripTypeArgs(type); |
| final typeArgs = type |
| .substring(declaredType.length + 1, type.length - 1) |
| .split(',') |
| .map((t) => t.trim()); |
| return '${mapType([ |
| declaredType |
| ])}<${typeArgs.map((t) => mapType([t])).join(', ')}>'; |
| } else if (type.contains('|')) { |
| // It's possible we ended up with nested unions that the parsing. |
| // TODO(dantup): This is now partly done during parsing and partly done |
| // here. Maybe consider removing from typescript.dart and just carrying a |
| // String through so the logic is all in one place in this function? |
| return mapType(_extractTypesFromUnion(type)); |
| } else if (_typeAliases.containsKey(type)) { |
| return mapType([_typeAliases[type].baseType]); |
| } else if (mapping.containsKey(type)) { |
| return mapType([mapping[type]]); |
| } else { |
| return type; |
| } |
| } |
| |
| 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])'; |
| } |
| }); |
| } |
| |
| String _stripTypeArgs(String typeName) => typeName.contains('<') |
| ? typeName.substring(0, typeName.indexOf('<')) |
| : typeName; |
| |
| 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}']", mapType(field.types)); |
| } |
| buffer |
| ..writeln(';') |
| ..outdent() |
| ..writeIndentedln('}'); |
| } |
| |
| void _writeConst(IndentableStringBuffer buffer, Const cons) { |
| _writeDocCommentsAndAnnotations(buffer, cons); |
| buffer.writeIndentedln('static const ${cons.name} = ${cons.value};'); |
| } |
| |
| void _writeConstructor(IndentableStringBuffer buffer, Interface interface) { |
| final allFields = _getAllFields(interface); |
| if (allFields.isEmpty) { |
| return; |
| } |
| buffer |
| ..writeIndented('${_stripTypeArgs(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, ApiItem item) { |
| var comment = item.comment?.trim(); |
| if (comment == null || comment.length == 0) { |
| return; |
| } |
| 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 (item.isDeprecated) { |
| buffer.writeIndentedln('@core.deprecated'); |
| } |
| } |
| |
| void _writeEnumClass(IndentableStringBuffer buffer, Namespace namespace) { |
| _writeDocCommentsAndAnnotations(buffer, namespace); |
| buffer |
| ..writeln('class ${namespace.name} {') |
| ..indent() |
| ..writeIndentedln('const ${namespace.name}._(this._value);') |
| ..writeIndentedln('const ${namespace.name}.fromJson(this._value);') |
| ..writeln() |
| ..writeIndentedln('final Object _value;') |
| ..writeln() |
| ..writeIndentedln('static bool canParse(Object obj) {') |
| ..indent() |
| ..writeIndentedln('switch (obj) {') |
| ..indent(); |
| namespace.members.whereType<Const>().forEach((cons) { |
| buffer..writeIndentedln('case ${cons.value}:'); |
| }); |
| buffer |
| ..indent() |
| ..writeIndentedln('return true;') |
| ..outdent() |
| ..writeIndentedln('}') |
| ..writeIndentedln('return false;') |
| ..outdent() |
| ..writeIndentedln('}'); |
| namespace.members.whereType<Const>().forEach((cons) { |
| _writeDocCommentsAndAnnotations(buffer, cons); |
| buffer |
| ..writeIndentedln( |
| 'static const ${_makeValidIdentifier(cons.name)} = const ${namespace.name}._(${cons.value});'); |
| }); |
| 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 _writeField(IndentableStringBuffer buffer, Field field) { |
| _writeDocCommentsAndAnnotations(buffer, field); |
| buffer |
| ..writeIndented('final ') |
| ..write(mapType(field.types)) |
| ..writeln(' ${field.name};'); |
| } |
| |
| void _writeFromJsonCode( |
| IndentableStringBuffer buffer, List<String> types, String valueCode) { |
| final type = mapType(types); |
| if (_isLiteral(type)) { |
| buffer.write("$valueCode"); |
| } else if (_isSpecType(type)) { |
| // Our own types have fromJson() constructors we can call. |
| buffer.write("$valueCode != null ? new $type.fromJson($valueCode) : null"); |
| } else if (_isList(type)) { |
| // Lists need to be mapped so we can recursively call (they may need fromJson). |
| buffer.write("$valueCode?.map((item) => "); |
| final listType = _getListType(type); |
| _writeFromJsonCode(buffer, [listType], 'item'); |
| buffer.write(')?.cast<$listType>()?.toList()'); |
| } else if (_isUnion(type)) { |
| _writeFromJsonCodeForUnion(buffer, types, valueCode); |
| } else { |
| buffer.write("$valueCode"); |
| } |
| } |
| |
| void _writeFromJsonCodeForUnion( |
| IndentableStringBuffer buffer, List<String> types, String valueCode) { |
| final unionTypeName = mapType(types); |
| // 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 < types.length; i++) { |
| final dartType = mapType([types[i]]); |
| |
| // Dynamic matches all type checks, so only emit it if required. |
| if (dartType != 'dynamic') { |
| _writeTypeCheckCondition(buffer, valueCode, dartType); |
| buffer.write(' ? '); |
| } |
| |
| // The code to construct a value with this "side" of the union. |
| buffer.write('new $unionTypeName.t${i + 1}('); |
| _writeFromJsonCode(buffer, [dartType], valueCode); // Call recursively! |
| buffer.write(')'); |
| |
| // If we output the type condition at the top, prepare for the next condition. |
| if (dartType != 'dynamic') { |
| 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) { |
| buffer.write( |
| "throw '''\${$valueCode} was not one of (${types.join(', ')})'''"); |
| } |
| buffer.write(')' * unclosedParens); |
| } |
| |
| void _writeFromJsonConstructor( |
| IndentableStringBuffer buffer, Interface interface) { |
| final allFields = _getAllFields(interface); |
| if (allFields.isEmpty) { |
| return; |
| } |
| buffer |
| ..writeIndentedln( |
| 'factory ${_stripTypeArgs(interface.name)}.fromJson(Map<String, dynamic> json) {') |
| ..indent(); |
| for (final field in allFields) { |
| buffer.writeIndented('final ${field.name} = '); |
| _writeFromJsonCode(buffer, field.types, "json['${field.name}']"); |
| buffer.writeln(';'); |
| } |
| buffer |
| ..writeIndented('return new ${interface.name}(') |
| ..write(allFields.map((field) => '${field.name}').join(', ')) |
| ..writeln(');') |
| ..outdent() |
| ..writeIndented('}'); |
| } |
| |
| void _writeInterface(IndentableStringBuffer buffer, Interface interface) { |
| _writeDocCommentsAndAnnotations(buffer, interface); |
| |
| buffer.writeIndented('class ${interface.name} '); |
| var allBaseTypes = interface.baseTypes.followedBy(['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); |
| buffer |
| ..outdent() |
| ..writeIndentedln('}') |
| ..writeln(); |
| } |
| |
| void _writeJsonMapAssignment( |
| IndentableStringBuffer buffer, Field field, String mapName) { |
| // If we are allowed to be undefined (which essentially means required to be |
| // undefined and never explicitly null), we'll only add the value if set. |
| if (field.allowsUndefined) { |
| buffer |
| ..writeIndentedlnIf( |
| field.isDeprecated, '// ignore: deprecated_member_use') |
| ..writeIndentedln('if (${field.name} != null) {') |
| ..indent(); |
| } |
| buffer |
| ..writeIndentedlnIf(field.isDeprecated, '// ignore: deprecated_member_use') |
| ..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 (field.allowsUndefined) { |
| 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 _writeNamespace(IndentableStringBuffer buffer, Namespace namespace) { |
| // Namespaces are just groups of constants. For some uses we can write these |
| // as enum classes for extra type safety, but not for all - for example |
| // CodeActionKind can be an arbitrary String even though it also defines |
| // constants for common values. We can tell which can have their own values |
| // because they're marked with type aliases, with the exception of ErrorCodes! |
| if (!_typeAliases.containsKey(namespace.name) && |
| namespace.name != 'ErrorCodes') { |
| _writeEnumClass(buffer, namespace); |
| return; |
| } |
| |
| _writeDocCommentsAndAnnotations(buffer, namespace); |
| buffer |
| ..writeln('abstract class ${namespace.name} {') |
| ..indent(); |
| _writeMembers(buffer, namespace.members); |
| buffer |
| ..outdent() |
| ..writeln('}') |
| ..writeln(); |
| } |
| |
| 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 = {};'); |
| for (var field in _getAllFields(interface)) { |
| _writeJsonMapAssignment(buffer, field, '__result'); |
| } |
| buffer |
| ..writeIndentedln('return __result;') |
| ..outdent() |
| ..writeIndentedln('}'); |
| } |
| |
| void _writeType(IndentableStringBuffer buffer, ApiItem type) { |
| if (type is Interface) { |
| _writeInterface(buffer, type); |
| } else if (type is Namespace) { |
| _writeNamespace(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, String dartType) { |
| if (dartType == 'dynamic') { |
| buffer.write('true'); |
| } else if (_isLiteral(dartType)) { |
| buffer.write('$valueCode is $dartType'); |
| } else if (_isSpecType(dartType)) { |
| buffer.write('$dartType.canParse($valueCode)'); |
| } else if (_isList(dartType)) { |
| final listType = _getListType(dartType); |
| buffer.write('($valueCode is List'); |
| if (dartType != '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.length == 0 || $valueCode.every((item) => '); |
| _writeTypeCheckCondition(buffer, 'item', listType); |
| buffer.write('))'); |
| } |
| buffer.write(')'); |
| } else if (_isUnion(dartType)) { |
| // To type check a union, we just recursively check against each of its types. |
| final unionTypes = _getUnionTypes(dartType); |
| buffer.write('('); |
| for (var i = 0; i < unionTypes.length; i++) { |
| if (i != 0) { |
| buffer.write(' || '); |
| } |
| _writeTypeCheckCondition(buffer, valueCode, mapType([unionTypes[i]])); |
| } |
| buffer.write(')'); |
| } else { |
| throw 'Unable to type check $valueCode against $dartType'; |
| } |
| } |
| |
| 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); |
| } |
| |
| void writeIndentedlnIf(bool condition, Object obj) { |
| if (condition) { |
| writeIndentedln(obj); |
| } |
| } |
| } |