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:
Danny Tuppeny
2018-12-04 14:27:19 +00:00
committed by commit-bot@chromium.org
parent 43cd8ba822
commit 2dbf91ff13
7 changed files with 173 additions and 67 deletions
@@ -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) {