d6c87cc934
Change-Id: I4d62eacbf97c5f83e0eab378bc7d239ab934e6b8 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/505623 Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
1439 lines
44 KiB
Dart
1439 lines
44 KiB
Dart
// 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 'dart:collection';
|
|
|
|
import 'package:analysis_server/src/services/correction/sort_members.dart';
|
|
import 'package:analyzer/dart/analysis/utilities.dart';
|
|
import 'package:analyzer/src/analysis_options/code_style_options.dart';
|
|
import 'package:analyzer/src/dart/analysis/analysis_options.dart';
|
|
import 'package:analyzer_plugin/protocol/protocol_common.dart';
|
|
import 'package:analyzer_utilities/tools.dart';
|
|
import 'package:collection/collection.dart';
|
|
import 'package:dart_style/dart_style.dart';
|
|
|
|
import 'meta_model.dart';
|
|
|
|
final formatter = DartFormatter(
|
|
// The language_server_protocol package is currently still using a lower
|
|
// SDK constraint so pre-commit hooks require it matches short style.
|
|
languageVersion: DartFormatter.latestShortStyleLanguageVersion,
|
|
);
|
|
|
|
final _canParseFunctions = SplayTreeMap<String, String>();
|
|
Map<String, Interface> _interfaces = {};
|
|
Map<String, LspEnum> _namespaces = {};
|
|
Map<String, List<String>> _subtypes = {};
|
|
Map<String, TypeAlias> _typeAliases = {};
|
|
final _unionFunctions = SplayTreeMap<String, String>();
|
|
|
|
/// 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 deserializing
|
|
/// newer values).
|
|
bool enumClassAllowsAnyValue(String name) {
|
|
// The types listed here are the ones that have a guaranteed restricted type
|
|
// in the LSP spec, for example:
|
|
//
|
|
// export type CompletionTriggerKind = 1 | 2 | 3;
|
|
//
|
|
// The other enum types use string/number/etc. in the referencing classes.
|
|
return name != 'CompletionTriggerKind' &&
|
|
name != 'FailureHandlingKind' &&
|
|
name != 'InsertTextFormat' &&
|
|
name != 'MarkupKind' &&
|
|
name != 'ResourceOperationKind' &&
|
|
name != 'TraceValue';
|
|
}
|
|
|
|
/// Generates the ultimate Dart code for [types], sorted and formatted.
|
|
String generateDartForTypes(List<LspEntity> types) {
|
|
var content = _sortContent(_buildContent(types));
|
|
|
|
var stopwatch = Stopwatch()..start();
|
|
var formattedCode = _formatCode(content);
|
|
stopwatch.stop();
|
|
if (stopwatch.elapsed.inSeconds > 3) {
|
|
print('WARN: Formatting took ${stopwatch.elapsed} (${types.length} types)');
|
|
}
|
|
return '${formattedCode.trim()}\n'; // Ensure a single trailing newline.
|
|
}
|
|
|
|
void recordTypes(List<LspEntity> types) {
|
|
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 deserialize into the most specific type.
|
|
for (var base in interface.baseTypes) {
|
|
var subTypes = _subtypes[base.dartType] ??= <String>[];
|
|
subTypes.add(interface.name);
|
|
}
|
|
});
|
|
types.whereType<LspEnum>().forEach(
|
|
(namespace) => _namespaces[namespace.name] = namespace,
|
|
);
|
|
_sortSubtypes();
|
|
}
|
|
|
|
/// Resolves [type] to its base type if it is a reference to another type.
|
|
///
|
|
/// If [resolveEnums] is `true`, will resolve them to the type of their values.
|
|
///
|
|
/// If [onlyRenames] is true, references to [TypeAlias]es will only be resolved
|
|
/// if they are renames.
|
|
TypeBase resolveTypeAlias(
|
|
TypeBase type, {
|
|
bool resolveEnums = false,
|
|
bool onlyRenames = false,
|
|
}) {
|
|
if (type is TypeReference) {
|
|
if (resolveEnums) {
|
|
// Enums are no longer recorded with TypeAliases (as they were in the
|
|
// Markdown/TS spec) so must be resolved explicitly to their base types.
|
|
var enum_ = _namespaces[type.name];
|
|
if (enum_ != null) {
|
|
return enum_.typeOfValues;
|
|
}
|
|
}
|
|
|
|
var alias = _typeAliases[type.name];
|
|
if (alias != null && (!onlyRenames || alias.renameReferences)) {
|
|
// Resolve aliases recursively.
|
|
var resolved = alias.baseType;
|
|
for (int i = 0; i < 10; i++) {
|
|
var newResolved = resolveTypeAlias(
|
|
resolved,
|
|
resolveEnums: resolveEnums,
|
|
onlyRenames: onlyRenames,
|
|
);
|
|
if (newResolved == resolved) {
|
|
return resolved;
|
|
}
|
|
resolved = newResolved;
|
|
}
|
|
throw 'Failed to resolve type after 10 iterations: ${alias.name}';
|
|
}
|
|
}
|
|
return type;
|
|
}
|
|
|
|
/// Builds the Dart code for [types].
|
|
String _buildContent(List<LspEntity> types) {
|
|
_canParseFunctions.clear();
|
|
_unionFunctions.clear();
|
|
var buffer = IndentableStringBuffer();
|
|
for (var type in types) {
|
|
_writeType(buffer, type);
|
|
}
|
|
for (var function in _canParseFunctions.values) {
|
|
buffer.writeln(function);
|
|
}
|
|
for (var function in _unionFunctions.values) {
|
|
buffer.writeln(function);
|
|
}
|
|
return buffer.toString();
|
|
}
|
|
|
|
String _determineVariableName(
|
|
Interface interface,
|
|
Iterable<String> suggestions,
|
|
) {
|
|
var fieldNames = _getAllFields(interface).map((f) => f.name).toList();
|
|
var suggestion = suggestions.firstWhereOrNull((s) => !fieldNames.contains(s));
|
|
if (suggestion != null) {
|
|
return suggestion;
|
|
}
|
|
var first = suggestions.firstOrNull ?? 'var';
|
|
for (var i = 1; true; i++) {
|
|
var suggestion = '$first$i';
|
|
if (!fieldNames.contains(suggestion)) {
|
|
return suggestion;
|
|
}
|
|
}
|
|
}
|
|
|
|
String _formatCode(String code) {
|
|
try {
|
|
code = formatter.format(code);
|
|
} catch (e) {
|
|
print('Failed to format code, returning unformatted code: $e');
|
|
}
|
|
return code;
|
|
}
|
|
|
|
/// Recursively gets all members from superclasses and returns them sorted
|
|
/// alphabetically.
|
|
List<Field> _getAllFields(Interface? interface) =>
|
|
_getSortedUnique(_getAllFieldsMap(interface).values.toList());
|
|
|
|
/// Recursively gets all members from superclasses keyed by field name.
|
|
Map<String, Field> _getAllFieldsMap(Interface? interface) {
|
|
// Handle missing interfaces (such as special cased interfaces that won't
|
|
// be included in this model).
|
|
if (interface == null) {
|
|
return {};
|
|
}
|
|
|
|
// It's possible our interface redefines something in a base type (for example
|
|
// where the base has `String` but this type overrides it with a literal such
|
|
// as `ResourceOperation`) so use a map to keep the most-specific by name.
|
|
return {
|
|
for (final baseType in interface.baseTypes)
|
|
..._getAllFieldsMap(_interfaces[baseType.name]),
|
|
for (final field in interface.members.whereType<Field>()) field.name: field,
|
|
};
|
|
}
|
|
|
|
/// Returns a copy of the list sorted by name with duplicates (by name+type) removed.
|
|
List<N> _getSortedUnique<N extends LspEntity>(List<N> items) {
|
|
var uniqueByName = <String, N>{};
|
|
for (var item in items) {
|
|
// It's fine to have the same name used for different types (eg. namespace +
|
|
// type alias) but some types are just duplicated entirely in the spec in
|
|
// different positions which should not be emitted twice.
|
|
var nameTypeKey = '${item.name}|${item.runtimeType}';
|
|
if (uniqueByName.containsKey(nameTypeKey)) {
|
|
// At the time of writing, there were two duplicated types:
|
|
// - TextDocumentSyncKind (same definition in both places)
|
|
// - TextDocumentSyncOptions (first definition is just a subset)
|
|
// If this list grows, consider handling this better - or try to have the
|
|
// spec updated to be unambiguous.
|
|
print('WARN: More than one definition for $nameTypeKey.');
|
|
}
|
|
|
|
// Keep the last one as in some cases the first definition is less specific.
|
|
uniqueByName[nameTypeKey] = item;
|
|
}
|
|
var sortedList = uniqueByName.values.toList();
|
|
sortedList.sort((item1, item2) => item1.name.compareTo(item2.name));
|
|
return sortedList;
|
|
}
|
|
|
|
String _getTypeCheckFailureMessage(TypeBase type) {
|
|
type = resolveTypeAlias(type);
|
|
|
|
if (type is LiteralType) {
|
|
return "must be the literal '\$literal'";
|
|
} else if (type is LiteralUnionType) {
|
|
return "must be one of the \${literals.map((e) => \"'\$e'\").join(', ')}";
|
|
} else {
|
|
return 'must be of type ${type.dartTypeWithTypeArgs}';
|
|
}
|
|
}
|
|
|
|
bool _isOverride(Interface interface, Field field) {
|
|
for (var parentType in interface.baseTypes) {
|
|
var parent = _interfaces[parentType.name];
|
|
if (parent != null) {
|
|
if (parent.members.any((m) => m.name == field.name)) {
|
|
return true;
|
|
}
|
|
if (_isOverride(parent, field)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool _isSimpleType(TypeBase type) {
|
|
const literals = ['num', 'String', 'bool', 'int', 'double'];
|
|
return type is TypeReference && literals.contains(type.dartType);
|
|
}
|
|
|
|
bool _isSpecType(TypeBase type) {
|
|
type = resolveTypeAlias(type);
|
|
return type is TypeReference &&
|
|
type != TypeReference.lspObject &&
|
|
type != TypeReference.lspAny &&
|
|
(_interfaces.containsKey(type.name) ||
|
|
(_namespaces.containsKey(type.name)));
|
|
}
|
|
|
|
bool _isUnionType(TypeBase type) {
|
|
type = resolveTypeAlias(type);
|
|
return type is UnionType;
|
|
}
|
|
|
|
bool _isUriType(TypeBase type) {
|
|
type = resolveTypeAlias(type);
|
|
return type is TypeReference && type.dartType == 'Uri';
|
|
}
|
|
|
|
/// Maps reserved words and identifiers that cause issues in field names.
|
|
String _makeValidIdentifier(String identifier) {
|
|
// Some identifiers used in LSP are reserved words in Dart, so map them to
|
|
// other values.
|
|
const map = {
|
|
'Object': 'Obj',
|
|
'String': 'Str',
|
|
'class': 'class_',
|
|
'default': 'defaultValue',
|
|
'enum': 'enum_',
|
|
'new': 'new_',
|
|
'null': 'null_',
|
|
};
|
|
return map[identifier] ?? identifier;
|
|
}
|
|
|
|
/// Returns the name of the possibly enclosed types,
|
|
/// to be used as a unique name for that type.
|
|
String _memberNameForType(TypeBase type) {
|
|
if (type is LiteralType) {
|
|
return 'Literal';
|
|
}
|
|
if (type is LiteralUnionType) {
|
|
return 'LiteralUnion';
|
|
}
|
|
if (type is TypeReference) {
|
|
type = resolveTypeAlias(type);
|
|
}
|
|
|
|
var dartType = type is NullableType
|
|
? '${type.dartType}?'
|
|
: type is UnionType
|
|
? type.types.map(_memberNameForType).join()
|
|
: type is ArrayType
|
|
? 'List${_memberNameForType(type.elementType)}'
|
|
: type is MapType
|
|
? 'Map${_memberNameForType(type.indexType)}${_memberNameForType(type.valueType)}'
|
|
: type.dartType;
|
|
return capitalize(dartType.replaceAll('?', 'Nullable'));
|
|
}
|
|
|
|
String _rewriteCommentReference(String comment) {
|
|
var commentReferencePattern = RegExp(r'\[([\w ]+)\]\(#(\w+)\)');
|
|
return comment.replaceAllMapped(commentReferencePattern, (m) {
|
|
var description = m[1];
|
|
var reference = m[2];
|
|
if (description == reference) {
|
|
return '[$reference]';
|
|
} else {
|
|
return '$description ([$reference])';
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Sorts [content] as a Dart library.
|
|
String _sortContent(String content) {
|
|
try {
|
|
var parseResult = parseString(content: content);
|
|
var codeOptions = CodeStyleOptionsImpl(useFormatter: true);
|
|
codeOptions.options = AnalysisOptionsImpl();
|
|
var sorter = MemberSorter(
|
|
content,
|
|
parseResult.unit,
|
|
codeOptions,
|
|
parseResult.lineInfo,
|
|
);
|
|
var edits = sorter.sort();
|
|
return SourceEdit.applySequence(content, edits);
|
|
} catch (e) {
|
|
print('Failed to sort code, returning unsorted code: $e');
|
|
}
|
|
return content;
|
|
}
|
|
|
|
/// Sorts subtypes into a consistent order.
|
|
///
|
|
/// Subtypes will be sorted such that types with the most required fields appear
|
|
/// first to ensure `fromJson` constructors delegate to the most specific type.
|
|
void _sortSubtypes() {
|
|
int requiredFieldCount(String interfaceName) => _interfaces[interfaceName]!
|
|
.members
|
|
.whereType<Field>()
|
|
.where((field) => !field.allowsUndefined && !field.allowsNull)
|
|
.length;
|
|
int optionalFieldCount(String interfaceName) => _interfaces[interfaceName]!
|
|
.members
|
|
.whereType<Field>()
|
|
.where((field) => field.allowsUndefined || field.allowsNull)
|
|
.length;
|
|
for (var entry in _subtypes.entries) {
|
|
var subtypes = entry.value;
|
|
subtypes.sort((subtype1, subtype2) {
|
|
var requiredFields1 = requiredFieldCount(subtype1);
|
|
var requiredFields2 = requiredFieldCount(subtype2);
|
|
var optionalFields1 = optionalFieldCount(subtype1);
|
|
var optionalFields2 = optionalFieldCount(subtype2);
|
|
return requiredFields1 != requiredFields2
|
|
? requiredFields2.compareTo(requiredFields1)
|
|
: optionalFields1 != optionalFields2
|
|
? optionalFields2.compareTo(optionalFields1)
|
|
: subtype1.compareTo(subtype2);
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Returns a String representing the underlying Dart type for the provided
|
|
/// spec [type].
|
|
///
|
|
/// This is `Map<String, Object?>` for complex types but can be a simple type
|
|
/// for enums.
|
|
String _specJsonType(TypeBase type) {
|
|
if (type is TypeReference && _namespaces.containsKey(type.name)) {
|
|
var valueType = _namespaces[type.name]!.typeOfValues;
|
|
return valueType.dartTypeWithTypeArgs;
|
|
}
|
|
return 'Map<String, Object?>';
|
|
}
|
|
|
|
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 {
|
|
// First try to wrap at the last space before the max length, and fall
|
|
// back to wrapping at the first space if there were none before the
|
|
// max length.
|
|
var lastSpace = line.lastIndexOf(' ', maxLength);
|
|
if (lastSpace == -1) {
|
|
lastSpace = line.indexOf(' ');
|
|
}
|
|
|
|
// If we still don't have one, the whole line has no spaces to split on.
|
|
if (lastSpace == -1) {
|
|
yield line;
|
|
break;
|
|
}
|
|
|
|
yield line.substring(0, lastSpace);
|
|
line = line.substring(lastSpace + 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void _writeCanParseMethod(IndentableStringBuffer buffer, Interface interface) {
|
|
buffer
|
|
..writeIndentedln(
|
|
'static bool canParse(Object? obj, LspJsonReporter reporter) {',
|
|
)
|
|
..indent()
|
|
..writeIndentedln('if (obj is Map<String, Object?>) {')
|
|
..indent();
|
|
// In order to consider this valid for parsing, all fields that must not be
|
|
// undefined must be present and also type check for the correct type.
|
|
// Any fields that are optional but present, must still type check.
|
|
var fields = _getAllFields(
|
|
interface,
|
|
).whereNot((f) => isNullableAnyType(f.type)).toList();
|
|
for (var i = 0; i < fields.length; i++) {
|
|
var field = fields[i];
|
|
var type = field.type;
|
|
var functionName = '_canParse${_memberNameForType(type)}';
|
|
var invocation =
|
|
"$functionName(obj, reporter, '${field.name}', "
|
|
'allowsUndefined: ${field.allowsUndefined}, allowsNull: ${field.allowsNull}'
|
|
'${type is LiteralType ? ', literal: ${type.valueAsLiteral}' : ''}'
|
|
'${type is LiteralUnionType ? ', literals: {${type.literalTypes.map((t) => t.valueAsLiteral).join(', ')}}' : ''}'
|
|
')';
|
|
|
|
if (i == fields.length - 1) {
|
|
buffer.writeIndentedln('return $invocation;');
|
|
} else {
|
|
buffer
|
|
..writeIndentedln('if (!$invocation) {')
|
|
..indent()
|
|
..writeIndentedln('return false;')
|
|
..outdent()
|
|
..writeIndentedln('}');
|
|
}
|
|
if (!_canParseFunctions.containsKey(functionName)) {
|
|
var temp = IndentableStringBuffer();
|
|
_writeCanParseType(temp, interface, type, functionName);
|
|
_canParseFunctions[functionName] = temp.toString();
|
|
}
|
|
}
|
|
if (fields.isEmpty) {
|
|
buffer.writeIndentedln('return true;');
|
|
}
|
|
buffer
|
|
..outdent()
|
|
..writeIndentedln('} else {')
|
|
..indent()
|
|
..writeIndentedln(
|
|
"reporter.reportError('must be of type ${interface.name}');",
|
|
)
|
|
..writeIndentedln('return false;')
|
|
..outdent()
|
|
..writeIndentedln('}')
|
|
..outdent()
|
|
..writeIndentedln('}');
|
|
}
|
|
|
|
void _writeCanParseType(
|
|
IndentableStringBuffer buffer,
|
|
Interface? interface,
|
|
TypeBase type,
|
|
String functionName,
|
|
) {
|
|
buffer.writeln(
|
|
'bool $functionName(Map<String, Object?> map, LspJsonReporter reporter, '
|
|
'String fieldName, {required bool allowsUndefined, required bool allowsNull'
|
|
'${type is LiteralType ? ', required String literal' : ''}'
|
|
'${type is LiteralUnionType ? ', required Iterable<String> literals' : ''}'
|
|
'}) {',
|
|
);
|
|
|
|
buffer
|
|
..writeIndentedln('reporter.push(fieldName);')
|
|
..writeIndentedln('try {')
|
|
..indent();
|
|
buffer
|
|
..writeIndentedln('if (!allowsUndefined && !map.containsKey(fieldName)) {')
|
|
..indent()
|
|
..writeIndentedln("reporter.reportError('must not be undefined');")
|
|
..writeIndentedln('return false;')
|
|
..outdent()
|
|
..writeIndentedln('}');
|
|
|
|
buffer.writeIndentedln('final value = map[fieldName];');
|
|
buffer.writeIndentedln('final nullCheck = allowsNull || allowsUndefined;');
|
|
buffer
|
|
..writeIndentedln('if (!nullCheck && value == null) {')
|
|
..indent()
|
|
..writeIndentedln("reporter.reportError('must not be null');")
|
|
..writeIndentedln('return false;')
|
|
..outdent()
|
|
..writeIndentedln('}');
|
|
|
|
buffer.writeIndented('if ((!nullCheck || value != null) && ');
|
|
_writeTypeCheckCondition(
|
|
buffer,
|
|
interface,
|
|
'value',
|
|
type,
|
|
'reporter',
|
|
negation: true,
|
|
parenForCollection: true,
|
|
);
|
|
|
|
var failureMessage = _getTypeCheckFailureMessage(type);
|
|
var quote = failureMessage.contains("'") ? '"' : "'";
|
|
|
|
buffer
|
|
..write(') {')
|
|
..indent();
|
|
if (!_isSpecType(type)) {
|
|
// Only report an error for non-spec types, as spec types will have reported
|
|
// their own error in the nested canParse() call.
|
|
buffer.writeIndentedln(
|
|
'reporter.reportError($quote$failureMessage$quote);',
|
|
);
|
|
}
|
|
buffer
|
|
..writeIndentedln('return false;')
|
|
..outdent()
|
|
..writeIndentedln('}')
|
|
..outdent()
|
|
..writeIndentedln('} finally {')
|
|
..indent()
|
|
..writeIndentedln('reporter.pop();')
|
|
..outdent()
|
|
..writeIndentedln('}')
|
|
..writeIndentedln('return true;');
|
|
|
|
buffer.writeln('}');
|
|
}
|
|
|
|
void _writeConst(IndentableStringBuffer buffer, Constant constant) {
|
|
_writeDocCommentsAndAnnotations(buffer, constant);
|
|
buffer.writeIndentedln(
|
|
'static const ${constant.name} = ${constant.valueAsLiteral};',
|
|
);
|
|
}
|
|
|
|
void _writeConstructor(IndentableStringBuffer buffer, Interface interface) {
|
|
var allFields = _getAllFields(interface);
|
|
if (allFields.isEmpty) {
|
|
return;
|
|
}
|
|
buffer
|
|
..writeIndented('${interface.name}({')
|
|
..write(
|
|
allFields.map((field) {
|
|
var isLiteral = field.type is LiteralType;
|
|
var isRequired =
|
|
!isLiteral &&
|
|
!field.allowsNull &&
|
|
!field.allowsUndefined &&
|
|
!isNullableAnyType(field.type);
|
|
var requiredKeyword = isRequired ? 'required' : '';
|
|
var valueCode = isLiteral
|
|
? ' = ${(field.type as LiteralType).valueAsLiteral}'
|
|
: '';
|
|
return '$requiredKeyword this.${field.dartSafeName}$valueCode, ';
|
|
}).join(),
|
|
)
|
|
..write('})');
|
|
var fieldsWithValidation = allFields
|
|
.where((f) => f.type is LiteralType)
|
|
.toList();
|
|
if (fieldsWithValidation.isNotEmpty) {
|
|
buffer
|
|
..writeIndentedln(' {')
|
|
..indent();
|
|
for (var field in fieldsWithValidation) {
|
|
var type = field.type;
|
|
if (type is LiteralType) {
|
|
buffer
|
|
..writeIndentedln('if (${field.name} != ${type.valueAsLiteral}) {')
|
|
..indent()
|
|
..writeIndentedln(
|
|
"throw '${field.name} may only be the literal ${type.valueAsLiteral.replaceAll("'", "\\'")}';",
|
|
)
|
|
..outdent()
|
|
..writeIndentedln('}');
|
|
}
|
|
}
|
|
buffer
|
|
..outdent()
|
|
..writeIndentedln('}');
|
|
} else {
|
|
buffer.writeln(';');
|
|
}
|
|
}
|
|
|
|
void _writeDocCommentsAndAnnotations(
|
|
IndentableStringBuffer buffer,
|
|
LspEntity node,
|
|
) {
|
|
var comment = node.comment?.trim();
|
|
if (comment != null && comment.isNotEmpty) {
|
|
comment = _rewriteCommentReference(comment);
|
|
var originalLines = comment.split('\n');
|
|
// Wrap at 80 - 4 ('/// ') - indent characters.
|
|
var wrappedLines = _wrapLines(
|
|
originalLines,
|
|
(80 - 4 - buffer.totalIndent).clamp(0, 80),
|
|
);
|
|
for (var l in wrappedLines) {
|
|
buffer.writeIndentedln('/// $l'.trim());
|
|
}
|
|
}
|
|
// Marking LSP-deprecated fields as deprecated in Dart results in a lot
|
|
// of warnings because we still often populate these fields for clients that
|
|
// may still be using them. This code is useful for enabling temporarily
|
|
// and reviewing which deprecated fields we should still support but isn't
|
|
// generally useful to keep enabled.
|
|
// if (node.isDeprecated) {
|
|
// buffer.writeIndentedln('@core.deprecated');
|
|
// }
|
|
}
|
|
|
|
void _writeEnumClass(IndentableStringBuffer buffer, LspEnum namespace) {
|
|
_writeDocCommentsAndAnnotations(buffer, namespace);
|
|
var consts = namespace.constants;
|
|
var namespaceName = namespace.name;
|
|
var typeOfValues = namespace.typeOfValues;
|
|
var allowsAnyValue = enumClassAllowsAnyValue(namespaceName);
|
|
var constructorName = allowsAnyValue ? '' : '._';
|
|
var dartType = typeOfValues.dartTypeWithTypeArgs;
|
|
|
|
buffer
|
|
..writeln('class $namespaceName implements ToJsonable {')
|
|
..indent()
|
|
..writeIndentedln('const $namespaceName$constructorName(this._value);')
|
|
..writeIndentedln('const $namespaceName.fromJson(this._value);')
|
|
..writeln()
|
|
..writeIndentedln('final $dartType _value;')
|
|
..writeln()
|
|
..writeIndented(
|
|
'static bool canParse(Object? obj, LspJsonReporter reporter) ',
|
|
);
|
|
if (allowsAnyValue) {
|
|
buffer.writeIndentedln('=> ');
|
|
_writeTypeCheckCondition(buffer, null, 'obj', typeOfValues, 'reporter');
|
|
buffer.writeln(';');
|
|
} else {
|
|
buffer
|
|
..writeIndentedln('{')
|
|
..indent()
|
|
..writeIndentedln('switch (obj) {')
|
|
..indent();
|
|
for (var cons in consts) {
|
|
buffer.writeIndentedln('case ${cons.valueAsLiteral}:');
|
|
}
|
|
buffer
|
|
..indent()
|
|
..writeIndentedln('return true;')
|
|
..outdent()
|
|
..outdent()
|
|
..writeIndentedln('}')
|
|
..writeIndentedln('return false;')
|
|
..outdent()
|
|
..writeIndentedln('}');
|
|
}
|
|
for (var cons in consts) {
|
|
// We don't use any deprecated enum values, so omit them entirely.
|
|
if (cons.isDeprecated) {
|
|
continue;
|
|
}
|
|
_writeDocCommentsAndAnnotations(buffer, cons);
|
|
var memberName = cons.dartSafeName;
|
|
var value = cons.valueAsLiteral;
|
|
buffer.writeIndentedln(
|
|
'static const $memberName = $namespaceName$constructorName($value);',
|
|
);
|
|
}
|
|
if (namespace.flags) {
|
|
buffer
|
|
..writeln()
|
|
..writeIndentedln(
|
|
'static $namespaceName combine(List<$namespaceName> values) =>',
|
|
)
|
|
..indent()
|
|
..writeIndentedln(
|
|
'$namespaceName$constructorName(values.fold<$dartType>(0, (combinedValue, value) => combinedValue | value._value));',
|
|
)
|
|
..outdent()
|
|
..writeln()
|
|
..writeIndentedln(
|
|
'bool hasFlag($namespaceName value) => (_value & value._value) == value._value;',
|
|
);
|
|
}
|
|
buffer
|
|
..writeln()
|
|
..writeIndentedln('@override $dartType toJson() => _value;')
|
|
..writeln()
|
|
..writeIndentedln('@override String toString() => _value.toString();')
|
|
..writeln()
|
|
..writeIndentedln('@override int get hashCode => _value.hashCode;')
|
|
..writeln()
|
|
..writeIndentedln(
|
|
'@override bool operator ==(Object other) => other is $namespaceName && other._value == _value;',
|
|
)
|
|
..outdent()
|
|
..writeln('}')
|
|
..writeln();
|
|
}
|
|
|
|
void _writeEquals(IndentableStringBuffer buffer, Interface interface) {
|
|
buffer
|
|
..writeIndentedln('@override')
|
|
..writeIndentedln('bool operator ==(Object other) {')
|
|
..indent()
|
|
// We want an exact type match, but also need `is` to have the analyzer
|
|
// promote the type to allow access to the fields on `other`.
|
|
..writeIndentedln(
|
|
'return other is ${interface.name} && other.runtimeType == ${interface.name}',
|
|
)
|
|
..indent()
|
|
..writeIndented('');
|
|
for (var field in _getAllFields(interface)) {
|
|
buffer.write(' && ');
|
|
var type = resolveTypeAlias(field.type);
|
|
var thisName = field.dartSafeName;
|
|
var otherName = 'other.${field.dartSafeName}';
|
|
if (type is ArrayType || type is MapType) {
|
|
buffer.write(
|
|
'const DeepCollectionEquality().equals($thisName, $otherName)',
|
|
);
|
|
} else {
|
|
buffer.write('$thisName == $otherName');
|
|
}
|
|
}
|
|
buffer
|
|
..writeln(';')
|
|
..outdent()
|
|
..outdent()
|
|
..writeIndentedln('}');
|
|
}
|
|
|
|
void _writeField(
|
|
IndentableStringBuffer buffer,
|
|
Interface interface,
|
|
Field field,
|
|
) {
|
|
_writeDocCommentsAndAnnotations(buffer, field);
|
|
var fieldName = field.dartSafeName;
|
|
var needsNullable =
|
|
(field.allowsNull || field.allowsUndefined) &&
|
|
!isNullableAnyType(field.type);
|
|
if (_isOverride(interface, field)) {
|
|
buffer.writeIndentedln('@override');
|
|
}
|
|
buffer
|
|
..writeIndented('final ')
|
|
..write(field.type.dartTypeWithTypeArgs)
|
|
..write(needsNullable ? '?' : '')
|
|
..writeln(' $fieldName;');
|
|
}
|
|
|
|
void _writeFromJsonCode(
|
|
IndentableStringBuffer buffer,
|
|
TypeBase type,
|
|
String valueCode, {
|
|
required bool allowsNull,
|
|
bool requiresCast = true,
|
|
}) {
|
|
type = resolveTypeAlias(type);
|
|
var nullOperator = allowsNull ? '?' : '';
|
|
var cast =
|
|
requiresCast &&
|
|
// LSPAny
|
|
!isNullableAnyType(type) &&
|
|
// LSPObject marked as optional
|
|
!(isObjectType(type) && allowsNull)
|
|
? ' as ${type.dartTypeWithTypeArgs}$nullOperator'
|
|
: '';
|
|
|
|
if (_isSimpleType(type)) {
|
|
buffer.write('$valueCode$cast');
|
|
} else if (_isUriType(type)) {
|
|
if (allowsNull) {
|
|
buffer.write('$valueCode != null ? ');
|
|
}
|
|
buffer
|
|
..write('Uri.parse(')
|
|
..write(requiresCast ? '$valueCode as String' : valueCode)
|
|
..write(')');
|
|
if (allowsNull) {
|
|
buffer.write(': null');
|
|
}
|
|
} else if (_isSpecType(type)) {
|
|
// Our own types have fromJson() constructors we can call.
|
|
if (allowsNull) {
|
|
buffer.write('$valueCode != null ? ');
|
|
}
|
|
buffer
|
|
..write('${type.dartType}.fromJson${type.typeArgsString}')
|
|
..write('($valueCode as ${_specJsonType(type)})');
|
|
if (allowsNull) {
|
|
buffer.write(': 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.
|
|
var listCast = requiresCast ? ' as List<Object?>$nullOperator' : '';
|
|
var leftParen = requiresCast ? '(' : '';
|
|
var rightParen = requiresCast ? ')' : '';
|
|
buffer.write(
|
|
'$leftParen$valueCode$listCast$rightParen$nullOperator.map((item) => ',
|
|
);
|
|
_writeFromJsonCode(buffer, type.elementType, 'item', allowsNull: false);
|
|
buffer.write(').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.
|
|
var mapCast = requiresCast ? ' as Map<Object, Object?>$nullOperator' : '';
|
|
buffer
|
|
..write('($valueCode$mapCast)$nullOperator.map(')
|
|
..write('(key, value) => MapEntry(');
|
|
_writeFromJsonCode(buffer, type.indexType, 'key', allowsNull: false);
|
|
buffer.write(', ');
|
|
_writeFromJsonCode(buffer, type.valueType, 'value', allowsNull: false);
|
|
buffer.write('))');
|
|
} else if (type is LiteralUnionType) {
|
|
_writeFromJsonCodeForLiteralUnion(
|
|
buffer,
|
|
type,
|
|
valueCode,
|
|
allowsNull: allowsNull,
|
|
);
|
|
} else if (type is UnionType) {
|
|
var functionName = type.types.map(_memberNameForType).join();
|
|
|
|
functionName = '_either$functionName';
|
|
|
|
if (allowsNull) {
|
|
buffer.write('$valueCode == null ? null : ');
|
|
}
|
|
buffer.write('$functionName($valueCode)');
|
|
if (!_unionFunctions.containsKey(functionName)) {
|
|
var temp = IndentableStringBuffer();
|
|
_writeFromJsonCodeForUnion(temp, type, functionName);
|
|
_unionFunctions[functionName] = temp.toString();
|
|
}
|
|
} else {
|
|
buffer.write('$valueCode$cast');
|
|
}
|
|
}
|
|
|
|
void _writeFromJsonCodeForLiteralUnion(
|
|
IndentableStringBuffer buffer,
|
|
LiteralUnionType union,
|
|
String valueCode, {
|
|
required bool allowsNull,
|
|
}) {
|
|
var allowedValues = [
|
|
if (allowsNull) null,
|
|
...union.literalTypes.map((t) => t.valueAsLiteral),
|
|
];
|
|
var valueType = union.literalTypes.first.dartTypeWithTypeArgs;
|
|
var cast = ' as $valueType${allowsNull ? '?' : ''}';
|
|
buffer.write(
|
|
"const {${allowedValues.join(', ')}}.contains($valueCode) ? $valueCode$cast : "
|
|
"throw \"\$$valueCode was not one of (${allowedValues.join(', ')})\"",
|
|
);
|
|
}
|
|
|
|
void _writeFromJsonCodeForUnion(
|
|
IndentableStringBuffer buffer,
|
|
UnionType union,
|
|
String functionName,
|
|
) {
|
|
buffer
|
|
..writeln('${union.dartTypeWithTypeArgs} $functionName(Object? value) {')
|
|
..indent()
|
|
..writeIndented('return ');
|
|
|
|
// Write a check against each type, eg.:
|
|
// x is y ? Either.tx(x) : (...)
|
|
var hasIncompleteCondition = false;
|
|
|
|
for (var i = 0; i < union.types.length; i++) {
|
|
var type = union.types[i];
|
|
var isAny = isNullableAnyType(type);
|
|
|
|
// "any" matches all type checks, so only emit it if required.
|
|
if (!isAny) {
|
|
_writeTypeCheckCondition(
|
|
buffer,
|
|
null,
|
|
'value',
|
|
type,
|
|
'nullLspJsonReporter',
|
|
);
|
|
buffer.write(' ? ');
|
|
}
|
|
|
|
// The code to construct a value with this "side" of the union.
|
|
buffer.write('${union.dartType}.t${i + 1}(');
|
|
// Call recursively as unions may be nested.
|
|
_writeFromJsonCode(
|
|
buffer,
|
|
type,
|
|
'value',
|
|
// null + type checks are already handled above this loop
|
|
allowsNull: false,
|
|
requiresCast: false,
|
|
);
|
|
buffer.write(')');
|
|
|
|
// If we output the type condition at the top, prepare for the next condition.
|
|
if (!isAny) {
|
|
buffer.write(' : ');
|
|
hasIncompleteCondition = true;
|
|
} 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 '\$value was not one of (${union.types.map((t) => t.dartTypeWithTypeArgs).join(', ')})'",
|
|
);
|
|
}
|
|
buffer
|
|
..writeln(';')
|
|
..outdent()
|
|
..writeln('}');
|
|
}
|
|
|
|
void _writeFromJsonConstructor(
|
|
IndentableStringBuffer buffer,
|
|
Interface interface,
|
|
) {
|
|
var allFields = _getAllFields(interface);
|
|
buffer
|
|
..writeIndentedln(
|
|
'static ${interface.name} '
|
|
'fromJson(Map<String, Object?> json) {',
|
|
)
|
|
..indent();
|
|
// First check whether any of our subclasses can deserialize this.
|
|
for (var subclassName in _subtypes[interface.name] ?? const <String>[]) {
|
|
var subclass = _interfaces[subclassName]!;
|
|
buffer
|
|
..writeIndentedln(
|
|
'if (${subclass.name}.canParse(json, nullLspJsonReporter)) {',
|
|
)
|
|
..indent()
|
|
..writeIndentedln('return ${subclass.name}.fromJson(json);')
|
|
..outdent()
|
|
..writeIndentedln('}');
|
|
}
|
|
if (interface.abstract || interface.sealed) {
|
|
buffer.writeIndentedln(
|
|
'throw ArgumentError('
|
|
"'Supplied map is not valid for any subclass of ${interface.name}'"
|
|
');',
|
|
);
|
|
} else {
|
|
for (var field in allFields) {
|
|
// Add a local variable to allow type promotion (and avoid multiple lookups).
|
|
var localName = field.dartSafeName;
|
|
var localNameJson = '${localName}Json';
|
|
buffer.writeIndentedln("final $localNameJson = json['${field.name}'];");
|
|
buffer.writeIndented('final $localName = ');
|
|
_writeFromJsonCode(
|
|
buffer,
|
|
field.type,
|
|
localNameJson,
|
|
allowsNull: field.allowsNull || field.allowsUndefined,
|
|
);
|
|
buffer.writeln(';');
|
|
}
|
|
buffer
|
|
..writeIndented('return ${interface.name}(')
|
|
..write(
|
|
allFields
|
|
.map((field) => '${field.dartSafeName}: ${field.dartSafeName}, ')
|
|
.join(),
|
|
)
|
|
..writeln(');');
|
|
}
|
|
buffer
|
|
..outdent()
|
|
..writeIndented('}');
|
|
}
|
|
|
|
void _writeGetter(IndentableStringBuffer buffer, AbstractGetter getter) {
|
|
_writeDocCommentsAndAnnotations(buffer, getter);
|
|
buffer
|
|
..writeIndented(getter.type.dartTypeWithTypeArgs)
|
|
..writeln(' get ${getter.name};');
|
|
}
|
|
|
|
void _writeHashCode(IndentableStringBuffer buffer, Interface interface) {
|
|
buffer
|
|
..writeIndentedln('@override')
|
|
..writeIndented('int get hashCode => ');
|
|
|
|
var fields = _getAllFields(interface);
|
|
|
|
String endWith;
|
|
if (fields.isEmpty) {
|
|
buffer.write('42');
|
|
endWith = ';';
|
|
} else if (fields.length == 1) {
|
|
endWith = ';';
|
|
} else if (fields.length > 20) {
|
|
buffer.write('Object.hashAll([');
|
|
endWith = ',]);';
|
|
} else {
|
|
buffer.write('Object.hash(');
|
|
endWith = ',);';
|
|
}
|
|
|
|
buffer.writeAll(
|
|
fields.map((field) {
|
|
var type = resolveTypeAlias(field.type);
|
|
if (type is ArrayType || type is MapType) {
|
|
return 'lspHashCode(${field.name})';
|
|
} else {
|
|
if (fields.length == 1) {
|
|
return '${field.dartSafeName}.hashCode';
|
|
}
|
|
return field.dartSafeName;
|
|
}
|
|
}),
|
|
',',
|
|
);
|
|
buffer
|
|
..writeln(endWith)
|
|
..writeln();
|
|
}
|
|
|
|
void _writeInterface(IndentableStringBuffer buffer, Interface interface) {
|
|
var isPrivate = interface.name.startsWith('_');
|
|
_writeDocCommentsAndAnnotations(buffer, interface);
|
|
|
|
buffer
|
|
..writeIndented(interface.abstract ? 'abstract ' : '')
|
|
..write(interface.sealed ? 'sealed ' : '')
|
|
..write('class ${interface.name} ');
|
|
var allBaseTypes = interface.baseTypes
|
|
.map((t) => t.dartTypeWithTypeArgs)
|
|
.toList();
|
|
allBaseTypes.add('ToJsonable');
|
|
if (allBaseTypes.isNotEmpty) {
|
|
buffer.writeIndented('implements ${allBaseTypes.join(', ')} ');
|
|
}
|
|
buffer
|
|
..writeln('{')
|
|
..indent();
|
|
if (!isPrivate) {
|
|
_writeJsonHandler(buffer, interface);
|
|
buffer.writeln();
|
|
}
|
|
_writeConstructor(buffer, interface);
|
|
buffer.writeln();
|
|
_writeFromJsonConstructor(buffer, interface);
|
|
buffer.writeln();
|
|
// Handle Consts and Fields separately, since we need to include superclass
|
|
// Fields.
|
|
var consts = interface.members.whereType<Constant>().toList();
|
|
var getters = interface.members.whereType<AbstractGetter>().toList();
|
|
var fields = _getAllFields(interface);
|
|
_writeMembers(buffer, interface, getters);
|
|
buffer.writeln();
|
|
_writeMembers(buffer, interface, consts);
|
|
buffer.writeln();
|
|
_writeMembers(buffer, interface, fields);
|
|
buffer.writeln();
|
|
_writeToJsonMethod(buffer, interface);
|
|
buffer.writeln();
|
|
_writeCanParseMethod(buffer, interface);
|
|
buffer.writeln();
|
|
_writeEquals(buffer, interface);
|
|
buffer.writeln();
|
|
_writeHashCode(buffer, interface);
|
|
buffer.writeln();
|
|
_writeToString(buffer, interface);
|
|
buffer
|
|
..outdent()
|
|
..writeIndentedln('}')
|
|
..writeln();
|
|
}
|
|
|
|
void _writeJsonHandler(IndentableStringBuffer buffer, Interface interface) {
|
|
buffer
|
|
..writeIndented('static const jsonHandler = ')
|
|
..write('LspJsonHandler(')
|
|
..write('${interface.name}.canParse, ${interface.name}.fromJson,')
|
|
..writeln(');')
|
|
..writeln();
|
|
}
|
|
|
|
void _writeJsonMapAssignment(
|
|
IndentableStringBuffer buffer,
|
|
Field field,
|
|
String mapName,
|
|
) {
|
|
// If we are allowed to be undefined, we'll only add the value if set.
|
|
var shouldBeOmittedIfNoValue = field.allowsUndefined;
|
|
if (shouldBeOmittedIfNoValue) {
|
|
buffer
|
|
..writeIndentedln('if (${field.dartSafeName} != null) {')
|
|
..indent();
|
|
}
|
|
// Use the correct null operator depending on whether the value could be null.
|
|
var nullOp = field.allowsNull || field.allowsUndefined ? '?' : '';
|
|
buffer.writeIndented('''$mapName['${field.name}'] = ''');
|
|
_writeToJsonCode(buffer, field.type, field.dartSafeName, nullOp);
|
|
buffer.writeln(';');
|
|
if (shouldBeOmittedIfNoValue) {
|
|
buffer
|
|
..outdent()
|
|
..writeIndentedln('}');
|
|
}
|
|
}
|
|
|
|
void _writeMember(
|
|
IndentableStringBuffer buffer,
|
|
Interface interface,
|
|
Member member,
|
|
) {
|
|
if (member is Field) {
|
|
_writeField(buffer, interface, member);
|
|
} else if (member is Constant) {
|
|
_writeConst(buffer, member);
|
|
} else if (member is AbstractGetter) {
|
|
_writeGetter(buffer, member);
|
|
} else {
|
|
throw 'Unknown type';
|
|
}
|
|
}
|
|
|
|
void _writeMembers(
|
|
IndentableStringBuffer buffer,
|
|
Interface interface,
|
|
List<Member> members,
|
|
) {
|
|
_getSortedUnique(members).forEach((m) => _writeMember(buffer, interface, m));
|
|
}
|
|
|
|
void _writeToJsonCode(
|
|
IndentableStringBuffer buffer,
|
|
TypeBase type,
|
|
String valueCode,
|
|
String nullOp,
|
|
) {
|
|
if (_isSpecType(type)) {
|
|
buffer.write('$valueCode$nullOp.toJson()');
|
|
} else if (type is ArrayType && _isSpecType(type.elementType)) {
|
|
buffer.write('$valueCode$nullOp.map((item) => ');
|
|
_writeToJsonCode(buffer, type.elementType, 'item', '');
|
|
buffer.write(').toList()');
|
|
} else if (type is MapType &&
|
|
(_isUriType(type.indexType) || _isUriType(type.valueType))) {
|
|
buffer.write('$valueCode$nullOp.map((key, value) => MapEntry(');
|
|
_writeToJsonCode(buffer, type.indexType, 'key', '');
|
|
buffer.write(', ');
|
|
_writeToJsonCode(buffer, type.valueType, 'value', '');
|
|
buffer.write('))');
|
|
} else if (_isUnionType(type)) {
|
|
// We must call toJson() on EitherX classes to ensure the generated Map
|
|
// contains the raw value and not the EitherX. We don't need to handle
|
|
// LiteralUnionTypes because those are stored as a single underlying type
|
|
// without a wrapper class anyway.
|
|
buffer.write('$valueCode$nullOp.toJson()');
|
|
} else if (_isUriType(type)) {
|
|
buffer.write('$valueCode$nullOp.toString()');
|
|
} else if (type is ArrayType && _isUriType(type.elementType)) {
|
|
buffer.write('$valueCode$nullOp.map((uri) => uri.toString()).toList()');
|
|
} else {
|
|
buffer.write(valueCode);
|
|
}
|
|
}
|
|
|
|
void _writeToJsonFieldsForResponseMessage(
|
|
IndentableStringBuffer buffer,
|
|
Interface interface,
|
|
String mapName,
|
|
) {
|
|
var allFields = _getAllFields(interface);
|
|
var 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 if (result case ToJsonable result?) {')
|
|
..indent()
|
|
..writeIndentedln('''$mapName['result'] = result.toJson();''')
|
|
..outdent()
|
|
..writeIndentedln('} else {')
|
|
..indent()
|
|
..writeIndentedln('''$mapName['result'] = result;''')
|
|
..outdent()
|
|
..writeIndentedln('}');
|
|
}
|
|
|
|
void _writeToJsonMethod(IndentableStringBuffer buffer, Interface interface) {
|
|
var fields = _getAllFields(interface);
|
|
|
|
buffer
|
|
..writeIndentedln('@override')
|
|
..writeIndented('Map<String, Object?> toJson() ');
|
|
if (fields.isEmpty) {
|
|
buffer
|
|
..writeln('=> {};')
|
|
..writeln();
|
|
return;
|
|
}
|
|
|
|
var mapName = _determineVariableName(interface, [
|
|
'result',
|
|
'map',
|
|
'json',
|
|
'toReturn',
|
|
'results',
|
|
'value',
|
|
'values',
|
|
]);
|
|
buffer
|
|
..writeln('{')
|
|
..indent()
|
|
..writeIndentedln('var $mapName = <String, Object?>{};');
|
|
// 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, mapName);
|
|
} else {
|
|
for (var field in fields) {
|
|
_writeJsonMapAssignment(buffer, field, mapName);
|
|
}
|
|
}
|
|
buffer
|
|
..writeIndentedln('return $mapName;')
|
|
..outdent()
|
|
..writeIndentedln('}');
|
|
}
|
|
|
|
void _writeToString(IndentableStringBuffer buffer, Interface interface) {
|
|
buffer
|
|
..writeIndentedln('@override')
|
|
..writeIndentedln('String toString() => jsonEncoder.convert(toJson());');
|
|
}
|
|
|
|
void _writeType(IndentableStringBuffer buffer, LspEntity type) {
|
|
if (type is Interface) {
|
|
_writeInterface(buffer, type);
|
|
} else if (type is LspEnum) {
|
|
_writeEnumClass(buffer, type);
|
|
} else if (type is TypeAlias) {
|
|
_writeTypeAlias(buffer, type);
|
|
} else {
|
|
throw 'Unknown type';
|
|
}
|
|
}
|
|
|
|
void _writeTypeAlias(IndentableStringBuffer buffer, TypeAlias alias) {
|
|
if (!alias.generateTypeDef) return;
|
|
var baseType = alias.baseType;
|
|
var typeName = baseType.dartTypeWithTypeArgs;
|
|
_writeDocCommentsAndAnnotations(buffer, alias);
|
|
buffer.writeIndentedln('typedef ${alias.name} = $typeName;');
|
|
}
|
|
|
|
void _writeTypeCheckCondition(
|
|
IndentableStringBuffer buffer,
|
|
Interface? interface,
|
|
String valueCode,
|
|
TypeBase type,
|
|
String reporter, {
|
|
bool negation = false,
|
|
bool parenForCollection = false,
|
|
}) {
|
|
type = resolveTypeAlias(type);
|
|
|
|
var dartType = type.dartType;
|
|
var fullDartType = type.dartTypeWithTypeArgs;
|
|
|
|
var operator = negation ? '!' : '';
|
|
var and = negation ? '||' : '&&';
|
|
var or = negation ? '&&' : '||';
|
|
var every = negation ? 'any' : 'every';
|
|
var equals = negation ? '!=' : '==';
|
|
var notEqual = negation ? '==' : '!=';
|
|
var true_ = negation ? 'false' : 'true';
|
|
|
|
if (isNullableAnyType(type)) {
|
|
buffer.write(true_);
|
|
} else if (isObjectType(type)) {
|
|
buffer.write('$valueCode $notEqual null');
|
|
} else if (_isSimpleType(type)) {
|
|
buffer.write('$valueCode is$operator $fullDartType');
|
|
} else if (_isUriType(type)) {
|
|
buffer.write(
|
|
'($valueCode is$operator String $and '
|
|
'Uri.tryParse($valueCode) $notEqual null)',
|
|
);
|
|
} else if (type is LiteralType) {
|
|
buffer.write('$valueCode $equals literal');
|
|
} else if (type is LiteralUnionType) {
|
|
buffer.write('${operator}literals.contains(value)');
|
|
} else if (_isSpecType(type)) {
|
|
buffer.write('$operator$dartType.canParse($valueCode, $reporter)');
|
|
} else if (type is ArrayType) {
|
|
if (parenForCollection) {
|
|
buffer.write('(');
|
|
}
|
|
buffer.write('$valueCode is$operator List<Object?>');
|
|
if (fullDartType != 'Object?') {
|
|
buffer.write(' $and $valueCode.$every((item) => ');
|
|
_writeTypeCheckCondition(
|
|
buffer,
|
|
interface,
|
|
'item',
|
|
type.elementType,
|
|
reporter,
|
|
negation: negation,
|
|
);
|
|
buffer.write(')');
|
|
}
|
|
if (parenForCollection) {
|
|
buffer.write(')');
|
|
}
|
|
} else if (type is MapType) {
|
|
if (parenForCollection) {
|
|
buffer.write('(');
|
|
}
|
|
buffer.write('$valueCode is$operator Map');
|
|
if (fullDartType != 'Object?') {
|
|
buffer
|
|
..write(' $and (')
|
|
..write('$valueCode.keys.$every((item) => ');
|
|
_writeTypeCheckCondition(
|
|
buffer,
|
|
interface,
|
|
'item',
|
|
type.indexType,
|
|
reporter,
|
|
negation: negation,
|
|
);
|
|
buffer.write('$and $valueCode.values.$every((item) => ');
|
|
_writeTypeCheckCondition(
|
|
buffer,
|
|
interface,
|
|
'item',
|
|
type.valueType,
|
|
reporter,
|
|
negation: negation,
|
|
);
|
|
buffer.write(')))');
|
|
}
|
|
if (parenForCollection) {
|
|
buffer.write(')');
|
|
}
|
|
} else if (type is UnionType) {
|
|
if (parenForCollection) {
|
|
buffer.write('(');
|
|
}
|
|
|
|
// To type check a union, we just recursively check against each of its types.
|
|
for (var i = 0; i < type.types.length; i++) {
|
|
if (i != 0) {
|
|
buffer.write(' $or ');
|
|
}
|
|
_writeTypeCheckCondition(
|
|
buffer,
|
|
interface,
|
|
valueCode,
|
|
type.types[i],
|
|
reporter,
|
|
negation: negation,
|
|
);
|
|
}
|
|
if (parenForCollection) {
|
|
buffer.write(')');
|
|
}
|
|
} else {
|
|
throw 'Unable to type check $valueCode against $fullDartType';
|
|
}
|
|
}
|
|
|
|
class IndentableStringBuffer extends StringBuffer {
|
|
int _indentLevel = 0;
|
|
final 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);
|
|
}
|
|
}
|
|
|
|
extension on LspEntity {
|
|
String get dartSafeName => _makeValidIdentifier(name);
|
|
}
|