| // Copyright (c) 2022, 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:convert'; |
| import 'dart:io'; |
| |
| import 'package:analysis_server/src/utilities/strings.dart'; |
| import 'package:collection/collection.dart'; |
| |
| import 'meta_model.dart'; |
| |
| /// Reads the LSP 'meta_model.json' file and returns its types. |
| class LspMetaModelReader { |
| final _types = <LspEntity>[]; |
| |
| /// A set of names already used (or reserved) by types that have been read. |
| final Set<String> _typeNames = {}; |
| |
| /// Characters to strip from member names. |
| final _memberNameInvalidCharPattern = RegExp(r'\$_?'); |
| |
| /// Patterns to replace with '_' in member names. |
| final _memberNameSeparatorPattern = RegExp(r'/'); |
| |
| /// Gets all types that have been read from the model JSON. |
| List<LspEntity> get types => _types.toList(); |
| |
| /// Reads all spec types from [file]. |
| LspMetaModel readFile(File file) { |
| final modelJson = file.readAsStringSync(); |
| final model = jsonDecode(modelJson) as Map<String, Object?>; |
| return readMap(model); |
| } |
| |
| /// Reads all spec types from [model]. |
| LspMetaModel readMap(Map<String, dynamic> model) { |
| final requests = model['requests'] as List?; |
| final notifications = model['notifications'] as List?; |
| final structures = model['structures'] as List?; |
| final enums = model['enumerations'] as List?; |
| final typeAliases = model['typeAliases'] as List?; |
| final methodNames = [...?requests, ...?notifications] |
| .map((item) => item['method'] as String) |
| .toList(); |
| [ |
| ...?structures?.map(_readStructure), |
| ...?enums?.map((e) => _readEnum(e)), |
| ...?typeAliases?.map(_readTypeAlias), |
| ].forEach(_addType); |
| |
| // Requests and notifications may have inline union types as their |
| // params/result. We can create TypeAliases for those using sensible |
| // names to simplify their use in the handlers. |
| requests?.forEach(_readRequest); |
| notifications?.forEach(_readNotification); |
| |
| final methodsEnum = _createMethodNamesEnum(methodNames); |
| if (methodsEnum != null) { |
| _addType(methodsEnum); |
| } |
| |
| return LspMetaModel(types: types, methods: methodNames); |
| } |
| |
| /// Adds [type] to the current list and prevents its name from being used |
| /// by generated interfaces. |
| void _addType(LspEntity type) { |
| _typeNames.add(type.name); |
| _types.add(type); |
| } |
| |
| String _camelCase(String str) => |
| str.substring(0, 1).toLowerCase() + str.substring(1); |
| |
| /// Creates an enum for all LSP method names. |
| LspEnum? _createMethodNamesEnum(List<String> methods) { |
| Constant toConstant(String value) { |
| final comment = '''Constant for the '$value' method.'''; |
| return Constant( |
| name: _generateMemberName(value, camelCase: true), |
| comment: comment, |
| type: TypeReference('string'), |
| value: value, |
| ); |
| } |
| |
| final methodConstants = methods.map(toConstant).toList(); |
| |
| if (methodConstants.isEmpty) { |
| return null; |
| } |
| |
| final comment = 'All standard LSP Methods read from the JSON spec.'; |
| return LspEnum( |
| name: 'Method', |
| comment: comment, |
| typeOfValues: TypeReference('string'), |
| members: methodConstants, |
| ); |
| } |
| |
| /// Creates a type alias for a top-level union, such as those used for |
| /// request parameters/results that don't have named types in the spec. |
| void _createUnionAlias(String name, dynamic model, String? documentation) { |
| if (model == null) { |
| return; |
| } |
| |
| // We don't currently support reading the two top-level intersection types. |
| // These can just be skipped because the types we're generating here are |
| // just for convenience (to produce better names for use in handlers rather |
| // than referencing `EitherX<Y>` everywhere). |
| if (model['kind'] == 'and') { |
| return; |
| } |
| |
| final type = _extractType(name, '', model); |
| if (type is UnionType) { |
| _addType(TypeAlias( |
| name: name, |
| comment: documentation, |
| baseType: type, |
| isRename: false, |
| )); |
| } |
| } |
| |
| Constant _extractEnumValue(TypeBase parentType, dynamic model) { |
| final name = model['name'] as String; |
| return Constant( |
| name: _generateMemberName(name), |
| comment: model['documentation'] as String?, |
| type: parentType, |
| value: model['value'].toString(), |
| ); |
| } |
| |
| Member _extractMember(String parentName, dynamic model) { |
| final name = model['name'] as String; |
| var type = _extractType(parentName, name, model['type']); |
| |
| // Unions may contain `null` types which we promote up to the field. |
| var allowsNull = false; |
| if (type is UnionType) { |
| final types = type.types; |
| |
| // Extract and strip `null`s from the union. |
| if (types.any(isNullType)) { |
| allowsNull = true; |
| type = UnionType(types.whereNot(isNullType).toList()); |
| } |
| } |
| |
| return Field( |
| name: _generateMemberName(name), |
| comment: model['documentation'] as String?, |
| type: type, |
| allowsNull: allowsNull, |
| allowsUndefined: model['optional'] == true, |
| ); |
| } |
| |
| /// Reads the type of [model]. |
| TypeBase _extractType(String parentName, String? fieldName, dynamic model) { |
| if (model['kind'] == 'reference' || model['kind'] == 'base') { |
| // Reference kinds are other named interfaces defined in the spec, base are |
| // other named types defined elsewhere. |
| return TypeReference(model['name'] as String); |
| } else if (model['kind'] == 'array') { |
| return ArrayType( |
| _extractType(parentName, fieldName, model['element']!), |
| ); |
| } else if (model['kind'] == 'map') { |
| final name = fieldName ?? ''; |
| return MapType( |
| _extractType(parentName, '${name}Key', model['key']!), |
| _extractType(parentName, '${name}Value', model['value']!), |
| ); |
| } else if (model['kind'] == 'literal') { |
| // "Literal" here means an inline/anonymous type. |
| final inlineTypeName = _generateTypeName( |
| parentName, |
| fieldName ?? '', |
| ); |
| |
| // First record the definition of the anonymous type itself. |
| final members = (model['value']['properties'] as List) |
| .map((p) => _extractMember(inlineTypeName, p)) |
| .toList(); |
| _addType(Interface.inline(inlineTypeName, members)); |
| |
| // Then return its name. |
| return TypeReference(inlineTypeName); |
| } else if (model['kind'] == 'stringLiteral') { |
| return LiteralType( |
| TypeReference('string'), |
| model['value'] as String, |
| ); |
| } else if (model['kind'] == 'or') { |
| // Ensure the parent name is reserved so we don't try to reuse its name |
| // if we're parsing something without a field name. |
| _typeNames.add(parentName); |
| |
| final itemTypes = model['items'] as List; |
| final types = itemTypes.map((item) { |
| final generatedName = _generateAvailableTypeName(parentName, fieldName); |
| return _extractType(generatedName, null, item); |
| }).toList(); |
| |
| return UnionType(types); |
| } else if (model['kind'] == 'tuple') { |
| // We currently just map tuples to an array of any of the types. The |
| // LSP 3.17 spec only has one tuple which is `[number, number]`. |
| final itemTypes = model['items'] as List; |
| final types = itemTypes.mapIndexed((index, item) { |
| final suffix = index + 1; |
| final name = fieldName ?? ''; |
| final thisName = '$name$suffix'; |
| return _extractType(parentName, thisName, item); |
| }).toList(); |
| return ArrayType(UnionType(types)); |
| } else { |
| throw 'Unable to extract type from $model'; |
| } |
| } |
| |
| /// Generates an available name for a node. |
| /// |
| /// If the computed name is already used, a number will be appended to the |
| /// end. |
| String _generateAvailableTypeName(String containerName, String? fieldName) { |
| final name = _generateTypeName(containerName, fieldName ?? ''); |
| final requiresSuffix = fieldName == null; |
| // If the name has already been taken, try appending a number and try |
| // again. |
| String generatedName; |
| var suffixIndex = 1; |
| do { |
| if (suffixIndex > 20) { |
| throw 'Failed to generate an available name for $name'; |
| } |
| generatedName = |
| requiresSuffix || suffixIndex > 1 ? '$name$suffixIndex' : name; |
| suffixIndex++; |
| } while (_typeNames.contains(generatedName)); |
| return generatedName; |
| } |
| |
| /// Generates a valid name for a member. |
| String _generateMemberName(String name, {bool camelCase = false}) { |
| // Replace any separators like `/` with `_`. |
| name = name.replaceAll(_memberNameSeparatorPattern, '_'); |
| |
| // Replace out any characters we don't want in member names. |
| name = name.replaceAll(_memberNameInvalidCharPattern, ''); |
| |
| // TODO(dantup): Remove this condition and always do camelCase in a future |
| // CL to reduce the migration diff. |
| if (camelCase) { |
| name = _camelCase(name); |
| } |
| return name; |
| } |
| |
| /// Generates a valid name for a type. |
| String _generateTypeName(String parent, String child) { |
| // Some classes are private (`_InitializeParams`) but still exposed via |
| // other classes (`InitializeParams`) but the child types still need to be |
| // exposed, so remove any leading underscores. |
| if (parent.startsWith('_')) { |
| parent = parent.substring(1); |
| } |
| return '${capitalize(parent)}${capitalize(child)}'; |
| } |
| |
| LspEnum _readEnum(dynamic model) { |
| final name = model['name'] as String; |
| final type = TypeReference(name); |
| final baseType = _extractType(name, null, model['type']); |
| |
| return LspEnum( |
| name: name, |
| comment: model['documentation'] as String?, |
| typeOfValues: baseType, |
| members: [ |
| ...?(model['values'] as List?)?.map((p) => _extractEnumValue(type, p)), |
| ], |
| ); |
| } |
| |
| void _readNotification(dynamic model) { |
| final method = model['method'] as String; |
| final namePrefix = method.split('/').map(capitalize).join(); |
| final documentation = model['documentation'] as String?; |
| |
| final paramsDoc = documentation != null |
| ? 'Parameters for ${_camelCase(documentation)}' |
| : null; |
| |
| _createUnionAlias('${namePrefix}Params', model['params'], paramsDoc); |
| } |
| |
| void _readRequest(dynamic model) { |
| final method = model['method'] as String; |
| final namePrefix = method.split('/').map(capitalize).join(); |
| final documentation = model['documentation'] as String?; |
| |
| final paramsDoc = documentation != null |
| ? 'Parameters for ${_camelCase(documentation)}' |
| : null; |
| |
| final resultDoc = documentation != null |
| ? 'Result for ${_camelCase(documentation)}' |
| : null; |
| |
| _createUnionAlias('${namePrefix}Params', model['params'], paramsDoc); |
| _createUnionAlias('${namePrefix}Result', model['result'], resultDoc); |
| } |
| |
| LspEntity _readStructure(dynamic model) { |
| final name = model['name'] as String; |
| return Interface( |
| name: name, |
| comment: model['documentation'] as String?, |
| baseTypes: [ |
| ...?(model['extends'] as List?) |
| ?.map((e) => TypeReference(e['name'] as String)), |
| ...?(model['mixins'] as List?) |
| ?.map((e) => TypeReference(e['name'] as String)), |
| ], |
| members: [ |
| ...?(model['properties'] as List?)?.map((p) => _extractMember(name, p)), |
| ], |
| ); |
| } |
| |
| TypeAlias _readTypeAlias(dynamic model) { |
| final name = model['name'] as String; |
| return TypeAlias( |
| name: name, |
| comment: model['documentation'] as String?, |
| baseType: _extractType(name, null, model['type']), |
| isRename: false, |
| ); |
| } |
| } |