Add support for parsing Maps from the LSP spec
Change-Id: Ieafcacb3ba2f6be8d3232025d22fa5ec94de5352 Reviewed-on: https://dart-review.googlesource.com/c/85768 Commit-Queue: Danny Tuppeny <dantup@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
committed by
commit-bot@chromium.org
parent
43cd8ba822
commit
2dbf91ff13
@@ -14,7 +14,7 @@ import 'dart:core' as core show deprecated;
|
||||
import 'dart:convert' show JsonEncoder;
|
||||
import 'package:analysis_server/lsp_protocol/protocol_special.dart';
|
||||
import 'package:analysis_server/src/protocol/protocol_internal.dart'
|
||||
show listEqual;
|
||||
show listEqual, mapEqual;
|
||||
import 'package:analyzer/src/generated/utilities_general.dart';
|
||||
|
||||
const jsonEncoder = const JsonEncoder.withIndent(' ');
|
||||
@@ -10916,9 +10916,14 @@ class WorkspaceClientCapabilitiesWorkspaceEdit implements ToJsonable {
|
||||
class WorkspaceEdit implements ToJsonable {
|
||||
WorkspaceEdit(this.changes, this.documentChanges);
|
||||
static WorkspaceEdit fromJson(Map<String, dynamic> json) {
|
||||
final changes = json['changes'] != null
|
||||
? WorkspaceEditChanges.fromJson(json['changes'])
|
||||
: null;
|
||||
final changes = json['changes']
|
||||
?.map((key, value) => new MapEntry(
|
||||
key,
|
||||
value
|
||||
?.map((item) => item != null ? TextEdit.fromJson(item) : null)
|
||||
?.cast<TextEdit>()
|
||||
?.toList()))
|
||||
?.cast<String, List<TextEdit>>();
|
||||
final documentChanges = (json['documentChanges'] is List && (json['documentChanges'].length == 0 || json['documentChanges'].every((item) => TextDocumentEdit.canParse(item))))
|
||||
? new Either2<List<TextDocumentEdit>, List<Either4<TextDocumentEdit, CreateFile, RenameFile, DeleteFile>>>.t1(json['documentChanges']
|
||||
?.map(
|
||||
@@ -10941,7 +10946,7 @@ class WorkspaceEdit implements ToJsonable {
|
||||
}
|
||||
|
||||
/// Holds changes to existing resources.
|
||||
final WorkspaceEditChanges changes;
|
||||
final Map<String, List<TextEdit>> changes;
|
||||
|
||||
/// Depending on the client capability
|
||||
/// `workspace.workspaceEdit.resourceOperations` document changes are either
|
||||
@@ -10978,7 +10983,8 @@ class WorkspaceEdit implements ToJsonable {
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
if (other is WorkspaceEdit) {
|
||||
return changes == other.changes &&
|
||||
return mapEqual(changes, other.changes,
|
||||
(List<TextEdit> a, List<TextEdit> b) => a == b) &&
|
||||
documentChanges == other.documentChanges &&
|
||||
true;
|
||||
}
|
||||
@@ -10997,38 +11003,6 @@ class WorkspaceEdit implements ToJsonable {
|
||||
String toString() => jsonEncoder.convert(toJson());
|
||||
}
|
||||
|
||||
class WorkspaceEditChanges implements ToJsonable {
|
||||
static WorkspaceEditChanges fromJson(Map<String, dynamic> json) {
|
||||
return new WorkspaceEditChanges();
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
Map<String, dynamic> __result = {};
|
||||
return __result;
|
||||
}
|
||||
|
||||
static bool canParse(Object obj) {
|
||||
return obj is Map<String, dynamic>;
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
if (other is WorkspaceEditChanges) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
int hash = 0;
|
||||
return JenkinsSmiHash.finish(hash);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => jsonEncoder.convert(toJson());
|
||||
}
|
||||
|
||||
class WorkspaceFolder implements ToJsonable {
|
||||
WorkspaceFolder(this.uri, this.name) {
|
||||
if (uri == null) {
|
||||
|
||||
@@ -153,4 +153,21 @@ main() {
|
||||
expect(restoredObj.endCharacter, equals(obj.endCharacter));
|
||||
expect(restoredObj.kind, equals(obj.kind));
|
||||
});
|
||||
|
||||
test('objects with maps can round-trip through to json and back', () {
|
||||
final start = new Position(1, 1);
|
||||
final end = new Position(2, 2);
|
||||
final range = new Range(start, end);
|
||||
final obj = new WorkspaceEdit(<String, List<TextEdit>>{
|
||||
'fileA': [new TextEdit(range, 'text A')],
|
||||
'fileB': [new TextEdit(range, 'text B')]
|
||||
}, null);
|
||||
final String json = jsonEncode(obj);
|
||||
final restoredObj = WorkspaceEdit.fromJson(jsonDecode(json));
|
||||
|
||||
expect(restoredObj.documentChanges, equals(obj.documentChanges));
|
||||
expect(restoredObj.changes, equals(obj.changes));
|
||||
expect(restoredObj.changes.keys, equals(obj.changes.keys));
|
||||
expect(restoredObj.changes.values, equals(obj.changes.values));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ class ArrayTypeMatcher extends Matcher {
|
||||
}
|
||||
|
||||
Description describe(Description description) =>
|
||||
description.add('an ArrayType').addDescriptionOf(_elementTypeMatcher);
|
||||
description.add('an array of ').addDescriptionOf(_elementTypeMatcher);
|
||||
|
||||
Description describeMismatch(
|
||||
item, Description mismatchDescription, Map matchState, bool verbose) {
|
||||
@@ -58,5 +58,25 @@ class ArrayTypeMatcher extends Matcher {
|
||||
}
|
||||
}
|
||||
|
||||
Matcher isMapOf(Matcher indexMatcher, Matcher valueMatcher) =>
|
||||
new MapTypeMatcher(wrapMatcher(indexMatcher), wrapMatcher(valueMatcher));
|
||||
|
||||
class MapTypeMatcher extends Matcher {
|
||||
final Matcher _indexMatcher, _valueMatcher;
|
||||
const MapTypeMatcher(this._indexMatcher, this._valueMatcher);
|
||||
|
||||
bool matches(item, Map matchState) {
|
||||
return item is MapType &&
|
||||
_indexMatcher.matches(item.indexType, matchState) &&
|
||||
_valueMatcher.matches(item.valueType, matchState);
|
||||
}
|
||||
|
||||
Description describe(Description description) => description
|
||||
.add('a MapType where index is ')
|
||||
.addDescriptionOf(_indexMatcher)
|
||||
.add(' and value is ')
|
||||
.addDescriptionOf(_valueMatcher);
|
||||
}
|
||||
|
||||
Matcher isResponseError(ErrorCodes code) => const TypeMatcher<ResponseError>()
|
||||
.having((e) => e.code, 'code', equals(code));
|
||||
|
||||
@@ -146,6 +146,24 @@ export interface MyMessage {
|
||||
expect(union.types[1], isSimpleType('object'));
|
||||
});
|
||||
|
||||
test('parses an interface with a map into a MapType', () {
|
||||
final String input = '''
|
||||
export interface WorkspaceEdit {
|
||||
changes: { [uri: string]: TextEdit[]; };
|
||||
}
|
||||
''';
|
||||
final List<AstNode> output = parseFile(input);
|
||||
expect(output, hasLength(1));
|
||||
expect(output[0], const TypeMatcher<Interface>());
|
||||
final Interface interface = output[0];
|
||||
expect(interface.members, hasLength(1));
|
||||
final Field field = interface.members.first;
|
||||
expect(field, const TypeMatcher<Field>());
|
||||
expect(field.name, equals('changes'));
|
||||
expect(field.type,
|
||||
isMapOf(isSimpleType('string'), isArrayOf(isSimpleType('TextEdit'))));
|
||||
});
|
||||
|
||||
test('flags nullable undefined values', () {
|
||||
final String input = '''
|
||||
export interface A {
|
||||
|
||||
@@ -290,6 +290,11 @@ void _writeEquals(IndentableStringBuffer buffer, Interface interface) {
|
||||
final elementDartType = elementType.dartTypeWithTypeArgs;
|
||||
buffer.write(
|
||||
'listEqual(${field.name}, other.${field.name}, ($elementDartType a, $elementDartType b) => a == b) && ');
|
||||
} else if (type is MapType) {
|
||||
final valueType = type.valueType;
|
||||
final valueDartType = valueType.dartTypeWithTypeArgs;
|
||||
buffer.write(
|
||||
'mapEqual(${field.name}, other.${field.name}, ($valueDartType a, $valueDartType b) => a == b) && ');
|
||||
} else {
|
||||
buffer.write('${field.name} == other.${field.name} && ');
|
||||
}
|
||||
@@ -323,12 +328,22 @@ void _writeFromJsonCode(
|
||||
buffer.write(
|
||||
"$valueCode != null ? ${type.dartType}.fromJson${type.typeArgsString}($valueCode) : null");
|
||||
} else if (type is ArrayType) {
|
||||
// Lists need to be mapped so we can recursively call (they may need fromJson).
|
||||
// Lists need to be map()'d so we can recursively call writeFromJsonCode
|
||||
// as they may need fromJson on each element.
|
||||
buffer.write("$valueCode?.map((item) => ");
|
||||
_writeFromJsonCode(buffer, type.elementType, 'item',
|
||||
allowsNull: allowsNull);
|
||||
buffer
|
||||
.write(')?.cast<${type.elementType.dartTypeWithTypeArgs}>()?.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.
|
||||
buffer.write('$valueCode?.map((key, value) => new MapEntry(');
|
||||
_writeFromJsonCode(buffer, type.indexType, 'key', allowsNull: allowsNull);
|
||||
buffer.write(', ');
|
||||
_writeFromJsonCode(buffer, type.valueType, 'value', allowsNull: allowsNull);
|
||||
buffer.write(
|
||||
'))?.cast<${type.indexType.dartTypeWithTypeArgs}, ${type.valueType.dartTypeWithTypeArgs}>()');
|
||||
} else if (type is UnionType) {
|
||||
_writeFromJsonCodeForUnion(buffer, type, valueCode, allowsNull: allowsNull);
|
||||
} else {
|
||||
@@ -548,6 +563,18 @@ void _writeTypeCheckCondition(
|
||||
buffer.write('))');
|
||||
}
|
||||
buffer.write(')');
|
||||
} else if (type is MapType) {
|
||||
buffer.write('($valueCode is Map');
|
||||
if (resolvedDartType != 'dynamic') {
|
||||
buffer
|
||||
..write(' && ($valueCode.length == 0 || (')
|
||||
..write('$valueCode.keys.every((item) => ');
|
||||
_writeTypeCheckCondition(buffer, 'item', type.indexType);
|
||||
buffer..write('&& $valueCode.values.every((item) => ');
|
||||
_writeTypeCheckCondition(buffer, 'item', type.valueType);
|
||||
buffer.write(')))');
|
||||
}
|
||||
buffer.write(')');
|
||||
} else if (type is UnionType) {
|
||||
// To type check a union, we just recursively check against each of its types.
|
||||
buffer.write('(');
|
||||
|
||||
@@ -79,7 +79,7 @@ import 'dart:core' hide deprecated;
|
||||
import 'dart:core' as core show deprecated;
|
||||
import 'dart:convert' show JsonEncoder;
|
||||
import 'package:analysis_server/lsp_protocol/protocol_special.dart';
|
||||
import 'package:analysis_server/src/protocol/protocol_internal.dart' show listEqual;
|
||||
import 'package:analysis_server/src/protocol/protocol_internal.dart' show listEqual, mapEqual;
|
||||
import 'package:analyzer/src/generated/utilities_general.dart';
|
||||
|
||||
const jsonEncoder = const JsonEncoder.withIndent(' ');
|
||||
|
||||
@@ -16,6 +16,10 @@ bool isNullType(TypeBase t) => t is Type && t.name == 'null';
|
||||
|
||||
bool isUndefinedType(TypeBase t) => t is Type && t.name == 'undefined';
|
||||
|
||||
/// A fabricated field name for indexers in case they result in generation
|
||||
/// of type names for inline types.
|
||||
const fieldNameForIndexer = 'indexer';
|
||||
|
||||
List<AstNode> parseFile(String input) {
|
||||
final scanner = new Scanner(input);
|
||||
final tokens = scanner.scan();
|
||||
@@ -43,6 +47,19 @@ class ArrayType extends TypeBase {
|
||||
String get typeArgsString => '<${elementType.dartTypeWithTypeArgs}>';
|
||||
}
|
||||
|
||||
class MapType extends TypeBase {
|
||||
final TypeBase indexType;
|
||||
final TypeBase valueType;
|
||||
|
||||
MapType(this.indexType, this.valueType);
|
||||
|
||||
@override
|
||||
String get dartType => 'Map';
|
||||
@override
|
||||
String get typeArgsString =>
|
||||
'<${indexType.dartTypeWithTypeArgs}, ${valueType.dartTypeWithTypeArgs}>';
|
||||
}
|
||||
|
||||
class AstNode {
|
||||
final Comment commentNode;
|
||||
final bool isDeprecated;
|
||||
@@ -117,6 +134,18 @@ class InlineInterface extends Interface {
|
||||
) : super(null, new Token.identifier(name), [], [], members);
|
||||
}
|
||||
|
||||
class Indexer extends Member {
|
||||
final TypeBase indexType;
|
||||
final TypeBase valueType;
|
||||
Indexer(
|
||||
Comment comment,
|
||||
this.indexType,
|
||||
this.valueType,
|
||||
) : super(comment);
|
||||
|
||||
String get name => fieldNameForIndexer;
|
||||
}
|
||||
|
||||
class Interface extends AstNode {
|
||||
final Token nameToken;
|
||||
final List<Token> typeArgs;
|
||||
@@ -202,6 +231,20 @@ class Parser {
|
||||
return Const(leadingComment, name, type, value);
|
||||
}
|
||||
|
||||
Indexer _indexer(String containerName, Comment leadingComment) {
|
||||
final indexer = _field(containerName, leadingComment);
|
||||
_consume(TokenType.RIGHT_BRACKET, 'Expected ]');
|
||||
_consume(TokenType.COLON, 'Expected :');
|
||||
|
||||
TypeBase type;
|
||||
type = _type(containerName, fieldNameForIndexer, improveTypes: true);
|
||||
|
||||
//_consume(TokenType.RIGHT_BRACE, 'Expected }');
|
||||
_match([TokenType.SEMI_COLON]);
|
||||
|
||||
return new Indexer(leadingComment, indexer.type, type);
|
||||
}
|
||||
|
||||
/// Ensures the next token is [type] and moves to the next, throwing [message]
|
||||
/// if not.
|
||||
Token _consume(TokenType type, String message) {
|
||||
@@ -259,13 +302,8 @@ class Parser {
|
||||
_consume(TokenType.COLON, 'Expected :');
|
||||
TypeBase type;
|
||||
Token value;
|
||||
type = _type(containerName, name.lexeme, includeUndefined: canBeUndefined);
|
||||
|
||||
// Handle improved type mappings for things that aren't very tight in the spec.
|
||||
final improvedTypeName = getImprovedType(containerName, name.lexeme);
|
||||
if (improvedTypeName != null) {
|
||||
type = new Type.identifier(improvedTypeName);
|
||||
}
|
||||
type = _type(containerName, name.lexeme,
|
||||
includeUndefined: canBeUndefined, improveTypes: true);
|
||||
|
||||
// Some fields have weird comments like this in the spec:
|
||||
// {@link MessageType}
|
||||
@@ -373,14 +411,7 @@ class Parser {
|
||||
if (_match([TokenType.CONST_KEYWORD])) {
|
||||
return _const(containerName, leadingComment);
|
||||
} else if (_match([TokenType.LEFT_BRACKET])) {
|
||||
// TODO(dantup): Support (or not?) indexers...
|
||||
while (true) {
|
||||
if (_match([TokenType.SEMI_COLON])) {
|
||||
break;
|
||||
}
|
||||
_advance();
|
||||
}
|
||||
return null;
|
||||
return _indexer(containerName, leadingComment);
|
||||
} else {
|
||||
return _field(containerName, leadingComment);
|
||||
}
|
||||
@@ -428,8 +459,12 @@ class Parser {
|
||||
}
|
||||
}
|
||||
|
||||
TypeBase _type(String containerName, String fieldName,
|
||||
{bool includeUndefined = false}) {
|
||||
TypeBase _type(
|
||||
String containerName,
|
||||
String fieldName, {
|
||||
bool includeUndefined = false,
|
||||
bool improveTypes = false,
|
||||
}) {
|
||||
var types = <TypeBase>[];
|
||||
if (includeUndefined) {
|
||||
types.add(Type.Undefined);
|
||||
@@ -451,11 +486,17 @@ class Parser {
|
||||
// Some of the inline interfaces have trailing commas (and some do not!)
|
||||
_match([TokenType.COMMA]);
|
||||
|
||||
// Add a synthetic interface to the parsers list of nodes to represent this type.
|
||||
final generatedName = _joinNames(containerName, fieldName);
|
||||
_nodes.add(new InlineInterface(generatedName, members));
|
||||
// Record the type as a simple type that references this interface.
|
||||
type = new Type.identifier(generatedName);
|
||||
// If we have a single member that is an indexer type, we can use a Map.
|
||||
if (members.length == 1 && members.single is Indexer) {
|
||||
Indexer indexer = members.single;
|
||||
type = new MapType(indexer.indexType, indexer.valueType);
|
||||
} else {
|
||||
// Add a synthetic interface to the parsers list of nodes to represent this type.
|
||||
final generatedName = _joinNames(containerName, fieldName);
|
||||
_nodes.add(new InlineInterface(generatedName, members));
|
||||
// Record the type as a simple type that references this interface.
|
||||
type = new Type.identifier(generatedName);
|
||||
}
|
||||
} else if (_match([TokenType.LEFT_PAREN])) {
|
||||
// Some types are in (parens), so we just parse the contents as a nested type.
|
||||
type = _type(containerName, fieldName);
|
||||
@@ -511,17 +552,26 @@ class Parser {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any duplicate types (for ex. if we map multiple types into dynamic)
|
||||
// we don't want to end up with `dynamic | dynamic`. Key on dartType to
|
||||
// ensure we different types that will map down to the same type.
|
||||
final uniqueTypes = new Map.fromEntries(
|
||||
types.map((t) => new MapEntry(t.dartTypeWithTypeArgs, t)),
|
||||
).values.toList();
|
||||
if (uniqueTypes.length == 1) {
|
||||
return uniqueTypes.single;
|
||||
} else {
|
||||
return new UnionType(uniqueTypes);
|
||||
|
||||
var type = uniqueTypes.length == 1
|
||||
? uniqueTypes.single
|
||||
: new UnionType(uniqueTypes);
|
||||
|
||||
// Handle improved type mappings for things that aren't very tight in the spec.
|
||||
if (improveTypes) {
|
||||
final improvedTypeName = getImprovedType(containerName, fieldName);
|
||||
if (improvedTypeName != null) {
|
||||
type = new Type.identifier(improvedTypeName);
|
||||
}
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
TypeAlias _typeAlias(Comment leadingComment) {
|
||||
|
||||
Reference in New Issue
Block a user