blob: b1b77efd8034c3f0349c29d978a181de3b342293 [file] [log] [blame]
// 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 'package:collection/collection.dart';
import 'meta_model.dart';
/// Helper methods to clean the meta model to produce better Dart classes.
///
/// Cleaning includes:
///
/// - Unwrapping comments that have been wrapped in the source model
/// - Removing relative hyperlinks from comments that assume rendering in an
/// HTML page with anchors
/// - Merging types that are distinct in the meta model but we want as one
/// - Removing types in the spec that we will never use
/// - Renaming types that may have long or sub-optimal generated names
/// - Simplifying union types that contain duplicates/overlaps
class LspMetaModelCleaner {
/// A pattern to match newlines in source comments that are likely for
/// wrapping and not formatting. This allows us to rewrap based on our indent
/// level/line length without potentially introducing very short lines.
final _sourceCommentWrappingNewlinesPattern =
RegExp(r'[\w`\]\).]\n[\w`\[\(]');
final _sourceCommentDocumentLinksPattern =
RegExp(r'\[([`\w \-.]+)\] ?\((#[^)]+)\)');
/// Cleans an entire [LspMetaModel].
LspMetaModel cleanModel(LspMetaModel model) {
final types = cleanTypes(model.types);
return LspMetaModel(types: types, methods: model.methods);
}
/// Cleans a List of types.
List<LspEntity> cleanTypes(List<LspEntity> types) {
types = _mergeTypes(types);
types = types
.where((type) => _includeTypeInOutput(type.name))
.map(_clean)
.toList();
types = _renameTypes(types).toList();
return types;
}
/// Whether a type should be retained type signatures in generated code.
bool _allowTypeInUnions(TypeBase type) {
// Don't allow arrays of MarkedStrings, but do allow simple MarkedStrings.
// The only place that uses these are Hovers and we only send one value
// (to match the MarkupString equiv) so the array just makes the types
// unnecessarily complicated.
if (type is ArrayType) {
// TODO(dantup): Consider removing this, it's not adding much.
final elementType = type.elementType;
if (elementType is TypeReference && elementType.name == 'MarkedString') {
return false;
}
}
return true;
}
/// Cleans a single [LspEntity].
LspEntity _clean(LspEntity type) {
if (type is Interface) {
return _cleanInterface(type);
} else if (type is LspEnum) {
return _cleanNamespace(type);
} else if (type is TypeAlias) {
return _cleanTypeAlias(type);
} else {
throw 'Cleaning $type is not implemented.';
}
}
String? _cleanComment(String? text) {
if (text == null) {
return text;
}
// Unwrap any wrapping in the source by replacing any matching newlines with
// spaces.
text = text.replaceAllMapped(
_sourceCommentWrappingNewlinesPattern,
(match) => match.group(0)!.replaceAll('\n', ' '),
);
// Strip any relative links that are intended for displaying online in the
// HTML spec.
text = text.replaceAllMapped(
_sourceCommentDocumentLinksPattern,
(match) => match.group(1)!,
);
return text;
}
Constant _cleanConst(Constant const_) {
return Constant(
name: const_.name,
comment: _cleanComment(const_.comment),
type: _cleanType(const_.type),
value: const_.value,
);
}
Field _cleanField(String parentName, Field field) {
final improvedType = _getImprovedType(parentName, field.name);
final type = improvedType ?? field.type;
return Field(
name: field.name,
comment: _cleanComment(field.comment),
type: _cleanType(type),
allowsNull: field.allowsNull,
allowsUndefined: field.allowsUndefined,
);
}
Interface _cleanInterface(Interface interface) {
return Interface(
name: interface.name,
comment: _cleanComment(interface.comment),
baseTypes: interface.baseTypes
.where((type) => _includeTypeInOutput(type.name))
.toList(),
members: interface.members
.map((member) => _cleanMember(interface.name, member))
.toList(),
);
}
Member _cleanMember(String parentName, Member member) {
if (member is Field) {
return _cleanField(parentName, member);
} else if (member is Constant) {
return _cleanConst(member);
} else {
throw 'Cleaning $member is not implemented.';
}
}
LspEnum _cleanNamespace(LspEnum namespace) {
return LspEnum(
name: namespace.name,
comment: _cleanComment(namespace.comment),
typeOfValues: namespace.typeOfValues,
members: namespace.members
.map((member) => _cleanMember(namespace.name, member))
.toList(),
);
}
TypeBase _cleanType(TypeBase type) {
if (type is UnionType) {
return _cleanUnionType(type);
} else if (type is ArrayType) {
return ArrayType(_cleanType(type.elementType));
} else {
return type;
}
}
TypeAlias _cleanTypeAlias(TypeAlias typeAlias) {
return TypeAlias(
name: typeAlias.name,
comment: _cleanComment(typeAlias.comment),
baseType: _cleanType(typeAlias.baseType),
isRename: typeAlias.isRename,
);
}
/// Removes any duplicate types in a union.
///
/// For example, if we map multiple types into `Object?` we don't want to end
/// up with `Either2<Object?, Object?>`.
///
/// Key on `dartType` to ensure we combine different types that will map down
/// to the same type.
TypeBase _cleanUnionType(UnionType type) {
var uniqueTypes = Map.fromEntries(
type.types
.where(_allowTypeInUnions)
.map((t) => MapEntry(t.uniqueTypeIdentifier, t)),
).values.toList();
// If our list includes something that maps to Object? as well as other
// types, we should just treat the whole thing as Object? as we get no value
// typing Either4<bool, String, num, Object?> but it becomes much more
// difficult to use.
if (uniqueTypes.any(isNullableAnyType)) {
return uniqueTypes.firstWhere(isNullableAnyType);
}
// Finally, sort the types by name so that we always generate the same type
// for the same combination to improve reuse of helper methods used in
// multiple handlers.
uniqueTypes.sort((t1, t2) => t1.dartType.compareTo(t2.dartType));
// Recursively clean the inner types.
uniqueTypes = uniqueTypes.map(_cleanType).toList();
if (uniqueTypes.length == 1) {
return uniqueTypes.single;
} else if (uniqueTypes.every(isLiteralType)) {
return LiteralUnionType(uniqueTypes.cast<LiteralType>());
} else if (uniqueTypes.any(isNullType)) {
final remainingTypes = uniqueTypes.whereNot(isNullType).toList();
final nonNullType = remainingTypes.length == 1
? remainingTypes.single
: UnionType(remainingTypes);
return NullableType(nonNullType);
} else {
return UnionType(uniqueTypes);
}
}
/// Improves types in code generated from the LSP model, including:
///
/// - Making some untyped fields (like `CompletionItem.data`) strong typed for
/// our use.
///
/// - Simplifying unions for types generated only by the server to avoid a lot
/// of wrapping in `EitherX<Y,Z>.tX()`.
TypeBase? _getImprovedType(String interfaceName, String? fieldName) {
const improvedTypeMappings = <String, Map<String, String>>{
'Diagnostic': {
'code': 'String',
},
'CompletionItem': {
'data': 'CompletionItemResolutionInfo',
},
'ParameterInformation': {
'label': 'String',
},
'TextDocumentEdit': {
'edits': 'TextDocumentEditEdits',
}
};
final interface = improvedTypeMappings[interfaceName];
final improvedTypeName = interface != null ? interface[fieldName] : null;
return improvedTypeName != null
? improvedTypeName.endsWith('[]')
? ArrayType(TypeReference(
improvedTypeName.substring(0, improvedTypeName.length - 2)))
: improvedTypeName.endsWith('?')
? NullableType(TypeReference(
improvedTypeName.substring(0, improvedTypeName.length - 1)))
: TypeReference(improvedTypeName)
: null;
}
/// Some types are merged together. This method returns the type that [name]s
/// members should be merged into.
String? _getMergeTarget(String name) {
switch (name) {
// The meta model defines both `LSPErrorCodes` and `ErrorCodes`. The
// intention was that one is JSONRPC and one is LSP codes, but some codes
// were defined in the wrong enum with the wrong values, but kept for
// backwards compatibility. For simplicity, we merge them all into `ErrorCodes`.
case 'LSPErrorCodes':
return 'ErrorCodes';
// In the model, `InitializeParams` is defined as by two classes,
// `_InitializeParams` and `WorkspaceFoldersInitializeParams`. This
// split doesn't add anything but makes the types less clear so we
// merge them into `InitializeParams`.
case '_InitializeParams':
return 'InitializeParams';
case 'WorkspaceFoldersInitializeParams':
return 'InitializeParams';
default:
return null;
}
}
/// Removes types that are in the spec that we don't want to emit.
bool _includeTypeInOutput(String name) {
const ignoredTypes = {
// InitializeError is not used for v3.0 (Feb 2017) and by dropping it we
// don't have to handle any cases where both a namespace and interfaces
// are declared with the same name.
'InitializeError',
// Merged into InitializeParams.
'_InitializeParams',
'WorkspaceFoldersInitializeParams',
// We don't use these classes and they weren't in the TS version of the
// spec so continue to not generate them until required.
'DidChangeConfigurationRegistrationOptions',
// LSPAny/LSPObject are used by the LSP spec for unions of basic types.
// We map these onto Object? and don't use this type (and don't support
// unions with so many types).
'LSPAny',
'LSPObject',
// The meta model currently includes an unwanted type named 'T' that we
// don't want to create a class for.
// TODO(dantup): Remove this once it's gone from the JSON model.
'T',
};
const ignoredPrefixes = {
// We don't emit MarkedString because it gets mapped to a simple String
// when getting the .dartType for it.
'MarkedString'
};
final shouldIgnore = ignoredTypes.contains(name) ||
ignoredPrefixes.any((ignore) => name.startsWith(ignore));
return !shouldIgnore;
}
LspEntity _merge(LspEntity source, LspEntity dest) {
if (source.runtimeType != dest.runtimeType) {
throw 'Cannot merge ${source.runtimeType} into ${dest.runtimeType}';
}
if (source is LspEnum && dest is LspEnum) {
return LspEnum(
name: dest.name,
comment: dest.comment ?? source.comment,
typeOfValues: dest.typeOfValues,
members: [...dest.members, ...source.members],
);
} else if (source is Interface && dest is Interface) {
return Interface(
name: dest.name,
comment: dest.comment ?? source.comment,
baseTypes: [...dest.baseTypes, ...source.baseTypes],
members: [...dest.members, ...source.members],
);
}
throw 'Merging ${source.runtimeType}s is not yet supported';
}
List<LspEntity> _mergeTypes(List<LspEntity> types) {
final typesByName = {
for (final type in types) type.name: type,
};
assert(types.length == typesByName.length);
final typeNames = typesByName.keys.toList();
for (final typeName in typeNames) {
final targetName = _getMergeTarget(typeName);
if (targetName != null) {
final type = typesByName[typeName]!;
final target = typesByName[targetName]!;
typesByName[targetName] = _merge(type, target);
typesByName.remove(typeName);
}
}
return typesByName.values.toList();
}
/// Renames types that may have been generated with bad (or long) names.
Iterable<LspEntity> _renameTypes(List<LspEntity> types) sync* {
const renames = <String, String>{
'CodeActionClientCapabilitiesCodeActionLiteralSupportCodeActionKind':
'CodeActionLiteralSupportCodeActionKind',
'CompletionClientCapabilitiesCompletionItemInsertTextModeSupport':
'CompletionItemInsertTextModeSupport',
'CompletionClientCapabilitiesCompletionItemTagSupport':
'CompletionItemTagSupport',
'CompletionClientCapabilitiesCompletionItemResolveSupport':
'CompletionItemResolveSupport',
'CompletionListItemDefaultsEditRange': 'CompletionItemEditRange',
'SignatureHelpClientCapabilitiesSignatureInformationParameterInformation':
'SignatureInformationParameterInformation',
'TextDocumentFilter2': 'TextDocumentFilterWithScheme',
'PrepareRenameResult1': 'PlaceholderAndRange',
'Pattern': 'LspPattern',
'URI': 'LspUri',
};
for (final type in types) {
final newName = renames[type.name];
if (newName == null) {
yield type;
continue;
}
// Add a TypeAlias for the old name.
yield TypeAlias(
name: type.name,
comment: type.comment,
baseType: TypeReference(newName),
isRename: true,
);
// Replace the type with an equivalent with the same name.
if (type is Interface) {
yield Interface(
name: newName,
comment: type.comment,
baseTypes: type.baseTypes,
members: type.members,
);
} else if (type is TypeAlias) {
yield TypeAlias(
name: newName,
comment: type.comment,
baseType: type.baseType,
isRename: type.isRename,
);
} else {
throw 'Renaming ${type.runtimeType} is not implemented';
}
}
}
}