[analysis_server] Preserve type aliases from the LSP spec as typedefs in generated code

+ improve the use of LSPAny/LSPObject where

LSPAny = anything, including null or undefined
LSPObject = any object (equiv of non-null Map<String, Object?>)

Change-Id: I335b299aad8e58b1cb4ee33cf27dc0dd887ec916
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/248781
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
Danny Tuppeny
2022-06-16 14:15:41 +00:00
committed by Commit Bot
parent 73bc88f957
commit b46a2d3039
12 changed files with 547 additions and 306 deletions
@@ -16,6 +16,12 @@ import 'package:analysis_server/src/protocol/protocol_internal.dart';
const jsonEncoder = JsonEncoder.withIndent(' ');
typedef DocumentUri = String;
typedef LSPAny = Object?;
typedef LSPObject = Object;
typedef TextDocumentEditEdits
= List<Either3<AnnotatedTextEdit, SnippetTextEdit, TextEdit>>;
class AnalyzerStatusParams implements ToJsonable {
static const jsonHandler = LspJsonHandler(
AnalyzerStatusParams.canParse,
@@ -761,7 +767,7 @@ class IncomingMessage implements Message, ToJsonable {
@override
final String jsonrpc;
final Method method;
final Object? params;
final LSPAny params;
@override
Map<String, Object?> toJson() {
@@ -925,7 +931,7 @@ class NotificationMessage implements IncomingMessage, ToJsonable {
@override
final Method method;
@override
final Object? params;
final LSPAny params;
@override
Map<String, Object?> toJson() {
@@ -1363,7 +1369,7 @@ class RequestMessage implements IncomingMessage, ToJsonable {
@override
final Method method;
@override
final Object? params;
final LSPAny params;
@override
Map<String, Object?> toJson() {
@@ -1548,7 +1554,7 @@ class ResponseMessage implements Message, ToJsonable {
final Either2<int, String>? id;
@override
final String jsonrpc;
final Object? result;
final LSPAny result;
@override
Map<String, Object?> toJson() {
File diff suppressed because it is too large Load Diff
@@ -4,7 +4,7 @@
import 'dart:math' as math;
import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/lsp_protocol/protocol.dart' hide Declaration;
import 'package:analysis_server/protocol/protocol_generated.dart';
import 'package:analysis_server/src/domains/completion/available_suggestions.dart';
import 'package:analysis_server/src/lsp/client_capabilities.dart';
+1 -1
View File
@@ -5,7 +5,7 @@
import 'dart:io';
import 'dart:math';
import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/lsp_protocol/protocol.dart' hide Declaration;
import 'package:analysis_server/lsp_protocol/protocol.dart' as lsp;
import 'package:analysis_server/src/collections.dart';
import 'package:analysis_server/src/lsp/client_capabilities.dart';
@@ -1753,7 +1753,7 @@ mixin LspAnalysisServerTestMixin implements ClientCapabilitiesHelperMixin {
Future<ResponseMessage> sendDidChangeConfiguration() {
final request = makeRequest(
Method.workspace_didChangeConfiguration,
DidChangeConfigurationParams(),
DidChangeConfigurationParams(settings: {}),
);
return sendRequestToServer(request);
}
@@ -11,7 +11,6 @@ void main() {
test('handles basic types', () {
expect(_simple('string').dartType, equals('String'));
expect(_simple('boolean').dartType, equals('bool'));
expect(_simple('any').dartType, equals('Object?'));
expect(_simple('object').dartType, equals('Object?'));
expect(_simple('int').dartType, equals('int'));
expect(_simple('num').dartType, equals('num'));
@@ -4,11 +4,18 @@
import 'package:test/test.dart';
import '../../../tool/lsp_spec/codegen_dart.dart';
import '../../../tool/lsp_spec/generate_all.dart';
import '../../../tool/lsp_spec/meta_model.dart';
import 'matchers.dart';
void main() {
group('meta model reader', () {
setUpAll(() {
// Ensure any custom types like LSPAny are registered so that they can
// be resolved.
recordTypes(getCustomClasses());
});
test('reads an interface', () {
final input = {
"structures": [
@@ -445,7 +452,7 @@ Sometimes after a blank line we'll have a note.
expect(union.types[1], isSimpleType('string'));
});
test('reads an union including LSPObject into a single type', () {
test('reads an union including LSPAny into a single type', () {
final input = {
"structures": [
{
@@ -457,7 +464,7 @@ Sometimes after a blank line we'll have a note.
"kind": "or",
"items": [
{"kind": "base", "name": "string"},
{"kind": "base", "name": "LSPObject"},
{"kind": "base", "name": "LSPAny"},
]
},
},
@@ -473,7 +480,7 @@ Sometimes after a blank line we'll have a note.
final field = interface.members.first as Field;
expect(field, const TypeMatcher<Field>());
expect(field.name, equals('label'));
expect(field.type, isSimpleType('LSPObject'));
expect(field.type, isSimpleType('LSPAny'));
});
test('reads literal string values', () {
@@ -43,7 +43,15 @@ String generateDartForTypes(List<LspEntity> types) {
_canParseFunctions.clear();
_unionFunctions.clear();
final buffer = IndentableStringBuffer();
_getSortedUnique(types).forEach((t) => _writeType(buffer, t));
final sortedTypes = _getSortedUnique(types);
// Bump typedefs to the top.
final fileSortedTypes = [
...sortedTypes.whereType<TypeAlias>(),
...sortedTypes.where((type) => type is! TypeAlias),
];
for (var type in fileSortedTypes) {
_writeType(buffer, type);
}
for (var function in _canParseFunctions.values) {
buffer.writeln(function);
}
@@ -79,9 +87,16 @@ void recordTypes(List<LspEntity> types) {
_sortSubtypes();
}
TypeBase resolveTypeAlias(TypeBase type, {bool resolveEnumClasses = false}) {
/// 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 (resolveEnumClasses) {
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.
final enum_ = _namespaces[type.name];
@@ -89,19 +104,20 @@ TypeBase resolveTypeAlias(TypeBase type, {bool resolveEnumClasses = false}) {
return enum_.typeOfValues;
}
}
// The LSP spec contains type aliases for `integer` and `uinteger` that map
// into the `number` type, with comments stating they must be integers. To
// preserve the improved typing, do _not_ resolve them to the `number`
// type.
if (type.name == 'integer' || type.name == 'uinteger') {
return type;
}
final alias = _typeAliases[type.name];
// Only follow the type if we're not an enum, or we wanted to follow enums.
if (alias != null &&
(!_namespaces.containsKey(alias.name) || resolveEnumClasses)) {
return alias.baseType;
if (alias != null && (!onlyRenames || alias.isRename)) {
// Resolve aliases recursively.
var resolved = alias.baseType;
for (int i = 0; i < 10; i++) {
final 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;
@@ -215,7 +231,8 @@ bool _isSimpleType(TypeBase type) {
bool _isSpecType(TypeBase type) {
type = resolveTypeAlias(type);
return type is TypeReference &&
!isAnyType(type) &&
type != TypeReference.LspObject &&
type != TypeReference.LspAny &&
(_interfaces.containsKey(type.name) ||
(_namespaces.containsKey(type.name)));
}
@@ -246,14 +263,17 @@ String _memberNameForType(TypeBase type) {
if (type is TypeReference) {
type = resolveTypeAlias(type);
}
var 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('?', '_'));
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) {
@@ -308,8 +328,7 @@ void _sortSubtypes() {
String _specJsonType(TypeBase type) {
if (type is TypeReference && _namespaces.containsKey(type.name)) {
final valueType = _namespaces[type.name]!.typeOfValues;
return resolveTypeAlias(valueType, resolveEnumClasses: true)
.dartTypeWithTypeArgs;
return valueType.dartTypeWithTypeArgs;
}
return 'Map<String, Object?>';
}
@@ -346,8 +365,9 @@ void _writeCanParseMethod(IndentableStringBuffer buffer, Interface interface) {
// 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.
final fields =
_getAllFields(interface).whereNot((f) => isAnyType(f.type)).toList();
final fields = _getAllFields(interface)
.whereNot((f) => isNullableAnyType(f.type))
.toList();
for (var i = 0; i < fields.length; i++) {
final field = fields[i];
var type = field.type;
@@ -463,7 +483,7 @@ void _writeConstructor(IndentableStringBuffer buffer, Interface interface) {
final isRequired = !isLiteral &&
!field.allowsNull &&
!field.allowsUndefined &&
!isAnyType(field.type);
!isNullableAnyType(field.type);
final requiredKeyword = isRequired ? 'required' : '';
final valueCode =
isLiteral ? ' = ${(field.type as LiteralType).valueAsLiteral}' : '';
@@ -633,8 +653,8 @@ void _writeEqualsExpression(IndentableStringBuffer buffer, TypeBase type,
void _writeField(
IndentableStringBuffer buffer, Interface interface, Field field) {
_writeDocCommentsAndAnnotations(buffer, field);
final needsNullable =
(field.allowsNull || field.allowsUndefined) && !isAnyType(field.type);
final needsNullable = (field.allowsNull || field.allowsUndefined) &&
!isNullableAnyType(field.type);
if (_isOverride(interface, field)) {
buffer.writeIndentedln('@override');
}
@@ -654,7 +674,11 @@ void _writeFromJsonCode(
}) {
type = resolveTypeAlias(type);
final nullOperator = allowsNull ? '?' : '';
final cast = requiresCast && type.dartTypeWithTypeArgs != 'Object?'
final cast = requiresCast &&
// LSPAny
!isNullableAnyType(type) &&
// LSPObject marked as optional
!(isObjectType(type) && allowsNull)
? ' as ${type.dartTypeWithTypeArgs}$nullOperator'
: '';
@@ -741,7 +765,7 @@ void _writeFromJsonCodeForUnion(
for (var i = 0; i < union.types.length; i++) {
final type = union.types[i];
final isAny = isAnyType(type);
final isAny = isNullableAnyType(type);
// "any" matches all type checks, so only emit it if required.
if (!isAny) {
@@ -1028,15 +1052,20 @@ void _writeType(IndentableStringBuffer buffer, LspEntity type) {
} else if (type is LspEnum) {
_writeEnumClass(buffer, type);
} else if (type is TypeAlias) {
// For now type aliases are not supported, so are collected at the start
// of the process in a map, and just replaced with the aliased type during
// generation.
// _writeTypeAlias(buffer, type);
_writeTypeAlias(buffer, type);
} else {
throw 'Unknown type';
}
}
void _writeTypeAlias(IndentableStringBuffer buffer, TypeAlias alias) {
if (alias.isRename) return;
final baseType = alias.baseType;
final 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}) {
@@ -1049,8 +1078,11 @@ void _writeTypeCheckCondition(IndentableStringBuffer buffer,
final and = negation ? '||' : '&&';
final every = negation ? 'any' : 'every';
if (fullDartType == 'Object?') {
if (isNullableAnyType(type)) {
buffer.write(negation ? 'false' : 'true');
} else if (isObjectType(type)) {
final notEqual = negation ? '==' : '!=';
buffer.write('$valueCode $notEqual null');
} else if (_isSimpleType(type)) {
buffer.write('$valueCode is$operator $fullDartType');
} else if (type is LiteralType) {
@@ -142,11 +142,13 @@ List<LspEntity> getCustomClasses() {
final customTypes = <LspEntity>[
TypeAlias(
name: 'LSPAny',
baseType: TypeReference.Any,
baseType: TypeReference.LspAny,
isRename: false,
),
TypeAlias(
name: 'LSPObject',
baseType: TypeReference.Any,
baseType: TypeReference.LspObject,
isRename: false,
),
// The DocumentFilter more complex in v3.17's meta_model (to allow
// TextDocumentFilters to be guaranteed to have at least one of language,
@@ -156,6 +158,7 @@ List<LspEntity> getCustomClasses() {
TypeAlias(
name: 'DocumentFilter',
baseType: TypeReference('TextDocumentFilter2'),
isRename: true,
),
// Similarly, the meta_model includes String as an option for
// DocumentSelector which is deprecated and we never previously supported
@@ -165,6 +168,7 @@ List<LspEntity> getCustomClasses() {
TypeAlias(
name: 'DocumentSelector',
baseType: ArrayType(TypeReference('TextDocumentFilterWithScheme')),
isRename: true,
),
interface('Message', [
field('jsonrpc', type: 'string'),
@@ -231,6 +235,7 @@ List<LspEntity> getCustomClasses() {
TypeAlias(
name: 'DocumentUri',
baseType: TypeReference('string'),
isRename: false,
),
interface('DartDiagnosticServer', [field('port', type: 'int')]),
@@ -335,6 +340,7 @@ List<LspEntity> getCustomClasses() {
TypeReference('TextEdit'),
]),
),
isRename: false,
)
];
return customTypes;
@@ -9,19 +9,19 @@ import 'codegen_dart.dart';
export 'meta_model_cleaner.dart';
export 'meta_model_reader.dart';
/// Whether this type allows any value (including null).
bool isAnyType(TypeBase t) =>
t is TypeReference &&
(t.name == 'any' ||
t.name == 'LSPAny' ||
t.name == 'object' ||
t.name == 'LSPObject');
bool isLiteralType(TypeBase t) => t is LiteralType;
bool isNullType(TypeBase t) => t is TypeReference && t.name == 'null';
/// Whether this type is the equivalent of 'Object?' and may also be omitted
/// from JSON ("undefined").
bool isNullableAnyType(TypeBase t) =>
resolveTypeAlias(t).dartTypeWithTypeArgs == 'Object?';
bool isUndefinedType(TypeBase t) => t is TypeReference && t.name == 'undefined';
bool isNullType(TypeBase t) =>
resolveTypeAlias(t).dartTypeWithTypeArgs == 'Null';
/// Whether this type is the equivalent of (non-nullable) 'Object'.
bool isObjectType(TypeBase t) =>
resolveTypeAlias(t).dartTypeWithTypeArgs == 'Object';
class ArrayType extends TypeBase {
final TypeBase elementType;
@@ -202,12 +202,35 @@ abstract class Member extends LspEntity {
});
}
class NullableType extends TypeBase {
final TypeBase baseType;
NullableType(this.baseType);
@override
String get dartType => baseType.dartType;
@override
String get dartTypeWithTypeArgs => '${super.dartTypeWithTypeArgs}?';
@override
String get typeArgsString => baseType.typeArgsString;
}
class TypeAlias extends LspEntity {
final TypeBase baseType;
/// Whether this alias is just a simple rename and not a name for a more
/// complex type.
///
/// Renames will be followed when generating code, but other aliases may be
/// created as `typedef`s.
final bool isRename;
TypeAlias({
required super.name,
super.comment,
required this.baseType,
required this.isRename,
});
}
@@ -215,6 +238,7 @@ class TypeAlias extends LspEntity {
abstract class TypeBase {
String get dartType;
String get dartTypeWithTypeArgs => '$dartType$typeArgsString';
String get typeArgsString;
/// A unique identifier for this type. Used for folding types together
@@ -225,8 +249,13 @@ abstract class TypeBase {
/// A reference to a Type by name.
class TypeReference extends TypeBase {
static final TypeBase Undefined = TypeReference('undefined');
static final TypeBase Null_ = TypeReference('null');
static final TypeBase Any = TypeReference('any');
static final TypeBase Null_ = TypeReference('Null');
/// Any object (but not null).
static final TypeBase LspObject = TypeReference('Object');
/// Any object (or null/undefined).
static final TypeBase LspAny = NullableType(TypeReference('Object'));
final String name;
final List<TypeBase> typeArgs;
@@ -238,8 +267,8 @@ class TypeReference extends TypeBase {
@override
String get dartType {
// Always resolve type aliases when asked for our Dart type.
final resolvedType = resolveTypeAlias(this);
// Resolve any renames when asked for our type.
final resolvedType = resolveTypeAlias(this, onlyRenames: true);
if (resolvedType != this) {
return resolvedType.dartType;
}
@@ -249,14 +278,12 @@ class TypeReference extends TypeBase {
'string': 'String',
'number': 'num',
'integer': 'int',
'null': 'Null',
// Map decimal to num because clients may sent "1.0" or "1" and we want
// to consider both valid.
'decimal': 'num',
'uinteger': 'int',
'any': 'Object?',
'LSPAny': 'Object?',
'object': 'Object?',
'LSPObject': 'Object?',
// Simplify MarkedString from
// string | { language: string; value: string }
// to just String
@@ -269,8 +296,8 @@ class TypeReference extends TypeBase {
@override
String get typeArgsString {
// Always resolve type aliases when asked for our Dart type.
final resolvedType = resolveTypeAlias(this);
// Resolve any renames when asked for our type.
final resolvedType = resolveTypeAlias(this, onlyRenames: true);
if (resolvedType != this) {
return resolvedType.typeArgsString;
}
@@ -294,8 +321,6 @@ class UnionType extends TypeBase {
types.sortBy((type) => type.dartTypeWithTypeArgs.toLowerCase());
}
UnionType.nullable(TypeBase type) : this([type, TypeReference.Null_]);
@override
String get dartType {
if (types.length > 4) {
@@ -2,6 +2,8 @@
// 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.
@@ -162,7 +164,8 @@ class LspMetaModelCleaner {
return TypeAlias(
name: typeAlias.name,
comment: _cleanComment(typeAlias.comment),
baseType: typeAlias.baseType,
baseType: _cleanType(typeAlias.baseType),
isRename: typeAlias.isRename,
);
}
@@ -184,8 +187,8 @@ class LspMetaModelCleaner {
// 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(isAnyType)) {
return uniqueTypes.firstWhere(isAnyType);
if (uniqueTypes.any(isNullableAnyType)) {
return uniqueTypes.firstWhere(isNullableAnyType);
}
// Finally, sort the types by name so that we always generate the same type
@@ -196,11 +199,19 @@ class LspMetaModelCleaner {
// Recursively clean the inner types.
uniqueTypes = uniqueTypes.map(_cleanType).toList();
return uniqueTypes.length == 1
? uniqueTypes.single
: uniqueTypes.every(isLiteralType)
? LiteralUnionType(uniqueTypes.cast<LiteralType>())
: UnionType(uniqueTypes);
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:
@@ -235,7 +246,7 @@ class LspMetaModelCleaner {
? ArrayType(TypeReference(
improvedTypeName.substring(0, improvedTypeName.length - 2)))
: improvedTypeName.endsWith('?')
? UnionType.nullable(TypeReference(
? NullableType(TypeReference(
improvedTypeName.substring(0, improvedTypeName.length - 1)))
: TypeReference(improvedTypeName)
: null;
@@ -353,29 +364,42 @@ class LspMetaModelCleaner {
'SignatureInformationParameterInformation',
'TextDocumentFilter2': 'TextDocumentFilterWithScheme',
'PrepareRenameResult1': 'PlaceholderAndRange',
'URI': 'LspUri',
};
for (final type in types) {
if (type is Interface) {
final newName = renames[type.name];
if (newName != null) {
// Replace with renamed interface.
yield Interface(
name: newName,
comment: type.comment,
baseTypes: type.baseTypes,
members: type.members,
);
// Plus a TypeAlias for the old name.
yield TypeAlias(
name: type.name,
comment: type.comment,
baseType: TypeReference(newName),
);
continue;
}
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';
}
yield type;
}
}
}
@@ -48,6 +48,7 @@ class LspMetaModelReader {
...?enums?.map((e) => _readEnum(e)),
...?typeAliases?.map(_readTypeAlias),
].forEach(_addType);
final methodsEnum = _createMethodNamesEnum(methodNames);
if (methodsEnum != null) {
_addType(methodsEnum);
@@ -174,6 +175,7 @@ class LspMetaModelReader {
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
@@ -278,6 +280,7 @@ class LspMetaModelReader {
name: name,
comment: model['documentation'] as String?,
baseType: _extractType(name, null, model['type']),
isRename: false,
);
}
}