| // 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/analysis_options.dart'; | |
| import 'package:analyzer/src/analysis_options/code_style_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, | |
| }; | |
| } | |
| /// A helper to get the count of fields in an interface, including subtypes. | |
| int _getFieldCount(String interfaceName, {required bool required}) { | |
| var interface = _interfaces[interfaceName]!; | |
| var thisCount = interface.members | |
| .whereType<Field>() | |
| .where((field) => field.isRequired == required) | |
| .length; | |
| var baseCount = interface.baseTypes | |
| .map((baseType) => _getFieldCount(baseType.name, required: required)) | |
| .fold(0, (acc, item) => acc + item); | |
| return thisCount + baseCount; | |
| } | |
| /// 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) => | |
| _getFieldCount(interfaceName, required: true); | |
| int optionalFieldCount(String interfaceName) => | |
| _getFieldCount(interfaceName, required: false); | |
| 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); | |
| } |