Handle generic types in LSP spec parsing

The regex wasn't picking up generate type args (in interface definitions or usages in field definitions). This adds <> characters to the regex and handles them in mapping of types (as well as adds some additional tests to cover this).

Change-Id: I8187b3d2abb2b35c0d7638839e3116700cfbf9e0
Reviewed-on: https://dart-review.googlesource.com/c/81004
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Danny Tuppeny <dantup@google.com>
diff --git a/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart b/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart
index 1c32810..d6fb0e5 100644
--- a/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart
+++ b/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart
@@ -3586,7 +3586,7 @@
 }
 
 class NotificationMessage implements Message {
-  NotificationMessage(this.method, this.jsonrpc) {
+  NotificationMessage(this.method, this.params, this.jsonrpc) {
     if (method == null) {
       throw 'method is required but was not provided';
     }
@@ -3596,8 +3596,14 @@
   }
   factory NotificationMessage.fromJson(Map<String, dynamic> json) {
     final method = json['method'];
+    final params = (json['params'] is List &&
+            (json['params'].length == 0 ||
+                json['params'].every((item) => true)))
+        ? new Either2<List<dynamic>, dynamic>.t1(
+            json['params']?.map((item) => item)?.cast<dynamic>()?.toList())
+        : (json['params']);
     final jsonrpc = json['jsonrpc'];
-    return new NotificationMessage(method, jsonrpc);
+    return new NotificationMessage(method, params, jsonrpc);
   }
 
   final String jsonrpc;
@@ -3605,9 +3611,15 @@
   /// The method to be invoked.
   final String method;
 
+  /// The notification's params.
+  final Either2<List<dynamic>, dynamic> params;
+
   Map<String, dynamic> toJson() {
     Map<String, dynamic> __result = {};
     __result['method'] = method ?? (throw 'method is required but was not set');
+    if (params != null) {
+      __result['params'] = params;
+    }
     __result['jsonrpc'] =
         jsonrpc ?? (throw 'jsonrpc is required but was not set');
     return __result;
@@ -4140,7 +4152,7 @@
 }
 
 class RequestMessage implements Message {
-  RequestMessage(this.id, this.method, this.jsonrpc) {
+  RequestMessage(this.id, this.method, this.params, this.jsonrpc) {
     if (id == null) {
       throw 'id is required but was not provided';
     }
@@ -4158,8 +4170,14 @@
             ? new Either2<num, String>.t2(json['id'])
             : (throw '''${json['id']} was not one of (number, string)'''));
     final method = json['method'];
+    final params = (json['params'] is List &&
+            (json['params'].length == 0 ||
+                json['params'].every((item) => true)))
+        ? new Either2<List<dynamic>, dynamic>.t1(
+            json['params']?.map((item) => item)?.cast<dynamic>()?.toList())
+        : (json['params']);
     final jsonrpc = json['jsonrpc'];
-    return new RequestMessage(id, method, jsonrpc);
+    return new RequestMessage(id, method, params, jsonrpc);
   }
 
   /// The request id.
@@ -4169,10 +4187,16 @@
   /// The method to be invoked.
   final String method;
 
+  /// The method's params.
+  final Either2<List<dynamic>, dynamic> params;
+
   Map<String, dynamic> toJson() {
     Map<String, dynamic> __result = {};
     __result['id'] = id ?? (throw 'id is required but was not set');
     __result['method'] = method ?? (throw 'method is required but was not set');
+    if (params != null) {
+      __result['params'] = params;
+    }
     __result['jsonrpc'] =
         jsonrpc ?? (throw 'jsonrpc is required but was not set');
     return __result;
@@ -4225,8 +4249,55 @@
   bool operator ==(o) => o is ResourceOperationKind && o._value == _value;
 }
 
+class ResponseError<D> {
+  ResponseError(this.code, this.message, this.data) {
+    if (code == null) {
+      throw 'code is required but was not provided';
+    }
+    if (message == null) {
+      throw 'message is required but was not provided';
+    }
+  }
+  factory ResponseError.fromJson(Map<String, dynamic> json) {
+    final code = json['code'];
+    final message = json['message'];
+    final data = json['data'];
+    return new ResponseError<D>(code, message, data);
+  }
+
+  /// A number indicating the error type that occurred.
+  final num code;
+
+  /// A Primitive or Structured value that contains
+  /// additional information about the error. Can be
+  /// omitted.
+  final D data;
+
+  /// A string providing a short description of the error.
+  final String message;
+
+  Map<String, dynamic> toJson() {
+    Map<String, dynamic> __result = {};
+    __result['code'] = code ?? (throw 'code is required but was not set');
+    __result['message'] =
+        message ?? (throw 'message is required but was not set');
+    if (data != null) {
+      __result['data'] = data;
+    }
+    return __result;
+  }
+
+  static bool canParse(Object obj) {
+    return obj is Map<String, dynamic> &&
+        obj.containsKey('code') &&
+        obj['code'] is num &&
+        obj.containsKey('message') &&
+        obj['message'] is String;
+  }
+}
+
 class ResponseMessage implements Message {
-  ResponseMessage(this.id, this.result, this.jsonrpc) {
+  ResponseMessage(this.id, this.result, this.error, this.jsonrpc) {
     if (jsonrpc == null) {
       throw 'jsonrpc is required but was not provided';
     }
@@ -4238,10 +4309,14 @@
             ? new Either2<num, String>.t2(json['id'])
             : (throw '''${json['id']} was not one of (number, string)'''));
     final result = json['result'];
+    final error = json['error'];
     final jsonrpc = json['jsonrpc'];
-    return new ResponseMessage(id, result, jsonrpc);
+    return new ResponseMessage(id, result, error, jsonrpc);
   }
 
+  /// The error object in case a request fails.
+  final ResponseError<dynamic> error;
+
   /// The request id.
   final Either2<num, String> id;
   final String jsonrpc;
@@ -4256,6 +4331,9 @@
     if (result != null) {
       __result['result'] = result;
     }
+    if (error != null) {
+      __result['error'] = error;
+    }
     __result['jsonrpc'] =
         jsonrpc ?? (throw 'jsonrpc is required but was not set');
     return __result;
diff --git a/pkg/analysis_server/test/tool/lsp_spec/dart_test.dart b/pkg/analysis_server/test/tool/lsp_spec/dart_test.dart
new file mode 100644
index 0000000..068063c
--- /dev/null
+++ b/pkg/analysis_server/test/tool/lsp_spec/dart_test.dart
@@ -0,0 +1,48 @@
+// Copyright (c) 2018, 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:test/test.dart';
+
+import '../../../tool/lsp_spec/codegen_dart.dart';
+
+main() {
+  group('mapType', () {
+    test('handles basic types', () {
+      expect(mapType(['string']), equals('String'));
+      expect(mapType(['boolean']), equals('bool'));
+      expect(mapType(['any']), equals('dynamic'));
+      expect(mapType(['object']), equals('dynamic'));
+      expect(mapType(['int']), equals('int'));
+      expect(mapType(['num']), equals('num'));
+    });
+
+    test('handles union types', () {
+      expect(mapType(['string', 'int']), equals('Either2<String, int>'));
+      expect(mapType(['string | int']), equals('Either2<String, int>'));
+    });
+
+    test('handles arrays', () {
+      expect(mapType(['string[]']), equals('List<String>'));
+      expect(mapType(['Array<string>']), equals('List<String>'));
+    });
+
+    test('handles types with args', () {
+      expect(mapType(['Class<string[]>']), equals('Class<List<String>>'));
+      expect(mapType(['Array<string | num>']),
+          equals('List<Either2<String, num>>'));
+    });
+
+    test('handles complex nested types', () {
+      expect(
+          mapType([
+            'Array<string>',
+            'any[]',
+            'Response<A>',
+            'Request<Array<string | num>>'
+          ]),
+          equals(
+              'Either4<List<String>, List<dynamic>, Response<A>, Request<List<Either2<String, num>>>>'));
+    });
+  });
+}
diff --git a/pkg/analysis_server/test/tool/lsp_spec/json_test.dart b/pkg/analysis_server/test/tool/lsp_spec/json_test.dart
index 646d49a..bc07300 100644
--- a/pkg/analysis_server/test/tool/lsp_spec/json_test.dart
+++ b/pkg/analysis_server/test/tool/lsp_spec/json_test.dart
@@ -18,7 +18,8 @@
     });
 
     test('returns correct output for union types', () {
-      final message = new RequestMessage(new Either2.t1(1), "test", "test");
+      final message =
+          new RequestMessage(new Either2.t1(1), "test", null, "test");
       String output = json.encode(message.toJson());
       expect(output, equals('{"id":1,"method":"test","jsonrpc":"test"}'));
     });
diff --git a/pkg/analysis_server/test/tool/lsp_spec/typescript_test.dart b/pkg/analysis_server/test/tool/lsp_spec/typescript_test.dart
index 77e3be7..1fad598 100644
--- a/pkg/analysis_server/test/tool/lsp_spec/typescript_test.dart
+++ b/pkg/analysis_server/test/tool/lsp_spec/typescript_test.dart
@@ -64,6 +64,48 @@
       });
     });
 
+    test('parses an interface with type args', () {
+      final String input = '''
+interface ResponseError<D> {
+	data?: D;
+}
+    ''';
+      final List<ApiItem> output = extractTypes(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('data'));
+      expect(field.allowsUndefined, true);
+      expect(field.allowsNull, false);
+      expect(field.types, equals(['D']));
+    });
+
+    test('parses an interface with Arrays in Array<T> format', () {
+      final String input = '''
+export interface RequestMessage {
+	/**
+	 * The method's params.
+	 */
+	params?: Array<any> | object;
+}
+    ''';
+      final List<ApiItem> output = extractTypes(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('params'));
+      expect(field.comment, equals('''The method's params.'''));
+      expect(field.allowsUndefined, true);
+      expect(field.allowsNull, false);
+      expect(field.types, equals(['Array<any>', 'object']));
+    });
+
     test('flags nullable undefined values', () {
       final String input = '''
 export interface A {
diff --git a/pkg/analysis_server/tool/lsp_spec/codegen_dart.dart b/pkg/analysis_server/tool/lsp_spec/codegen_dart.dart
index 1f96c64..54b0ee1 100644
--- a/pkg/analysis_server/tool/lsp_spec/codegen_dart.dart
+++ b/pkg/analysis_server/tool/lsp_spec/codegen_dart.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:dart_style/dart_style.dart';
+import 'package:meta/meta.dart';
 
 import 'typescript.dart';
 
@@ -28,6 +29,10 @@
   return formattedCode.trim() + '\n'; // Ensure a single trailing newline.
 }
 
+List<String> _extractTypesFromUnion(String type) {
+  return type.split('|').map((t) => t.trim()).toList();
+}
+
 String _formatCode(String code) {
   try {
     code = formatter.format(code);
@@ -100,12 +105,14 @@
 }
 
 /// Maps a TypeScript type on to a Dart type, including following TypeAliases.
-String _mapType(List<String> types) {
+@visibleForTesting
+String mapType(List<String> types) {
   const mapping = <String, String>{
     'boolean': 'bool',
     'string': 'String',
     'number': 'num',
     'any': 'dynamic',
+    'object': 'dynamic',
     // Special cases that are hard to parse or anonymous types.
     '{ [uri: string]: TextEdit[]; }': 'Map<String, List<TextEdit>>',
     '{ language: string; value: string }': 'MarkedStringWithLanguage'
@@ -114,21 +121,38 @@
     throw 'Unions of more than 4 types are not supported.';
   }
   if (types.length >= 2) {
-    final typeArgs = types.map((t) => _mapType([t])).join(', ');
+    final typeArgs = types.map((t) => mapType([t])).join(', ');
     return 'Either${types.length}<$typeArgs>';
   }
 
   final type = types.first;
   if (type.endsWith('[]')) {
-    return 'List<${_mapType([type.substring(0, type.length - 2)])}>';
+    return 'List<${mapType([type.substring(0, type.length - 2)])}>';
+  } else if (type.startsWith('Array<') && type.endsWith('>')) {
+    return 'List<${mapType([type.substring(6, type.length - 1)])}>';
+  } else if (type.contains('<')) {
+    // For types with type args, we need to map the type and each type arg.
+    final declaredType = _stripTypeArgs(type);
+    final typeArgs = type
+        .substring(declaredType.length + 1, type.length - 1)
+        .split(',')
+        .map((t) => t.trim());
+    return '${mapType([
+      declaredType
+    ])}<${typeArgs.map((t) => mapType([t])).join(', ')}>';
+  } else if (type.contains('|')) {
+    // It's possible we ended up with nested unions that the parsing.
+    // TODO(dantup): This is now partly done during parsing and partly done
+    // here. Maybe consider removing from typescript.dart and just carrying a
+    // String through so the logic is all in one place in this function?
+    return mapType(_extractTypesFromUnion(type));
+  } else if (_typeAliases.containsKey(type)) {
+    return mapType([_typeAliases[type].baseType]);
+  } else if (mapping.containsKey(type)) {
+    return mapType([mapping[type]]);
+  } else {
+    return type;
   }
-  if (_typeAliases.containsKey(type)) {
-    return _mapType([_typeAliases[type].baseType]);
-  }
-  if (mapping.containsKey(type)) {
-    return _mapType([mapping[type]]);
-  }
-  return type;
 }
 
 String _rewriteCommentReference(String comment) {
@@ -144,6 +168,10 @@
   });
 }
 
+String _stripTypeArgs(String typeName) => typeName.contains('<')
+    ? typeName.substring(0, typeName.indexOf('<'))
+    : typeName;
+
 Iterable<String> _wrapLines(List<String> lines, int maxLength) sync* {
   lines = lines.map((l) => l.trimRight()).toList();
   for (var line in lines) {
@@ -170,15 +198,15 @@
   buffer
     ..writeIndentedln('static bool canParse(Object obj) {')
     ..indent()
-    ..writeIndentedln('return obj is Map<String, dynamic>');
+    ..writeIndented('return obj is Map<String, dynamic>');
   // In order to consider this valid for parsing, all fields that may not be
   // undefined must be present and also type check for the correct type.
   final requiredFields =
       _getAllFields(interface).where((f) => !f.allowsUndefined);
   for (var field in requiredFields) {
-    buffer.write("&& obj.containsKey('${field.name}') && ");
+    buffer.write(" && obj.containsKey('${field.name}') && ");
     _writeTypeCheckCondition(
-        buffer, "obj['${field.name}']", _mapType(field.types));
+        buffer, "obj['${field.name}']", mapType(field.types));
   }
   buffer
     ..writeln(';')
@@ -197,7 +225,7 @@
     return;
   }
   buffer
-    ..writeIndented('${interface.name}(')
+    ..writeIndented('${_stripTypeArgs(interface.name)}(')
     ..write(allFields.map((field) => 'this.${field.name}').join(', '))
     ..write(')');
   final fieldsWithValidation =
@@ -289,13 +317,13 @@
   _writeDocCommentsAndAnnotations(buffer, field);
   buffer
     ..writeIndented('final ')
-    ..write(_mapType(field.types))
+    ..write(mapType(field.types))
     ..writeln(' ${field.name};');
 }
 
 void _writeFromJsonCode(
     IndentableStringBuffer buffer, List<String> types, String valueCode) {
-  final type = _mapType(types);
+  final type = mapType(types);
   if (_isLiteral(type)) {
     buffer.write("$valueCode");
   } else if (_isSpecType(type)) {
@@ -316,22 +344,33 @@
 
 void _writeFromJsonCodeForUnion(
     IndentableStringBuffer buffer, List<String> types, String valueCode) {
-  final unionTypeName = _mapType(types);
+  final unionTypeName = mapType(types);
   // Write a check against each type, eg.:
   // x is y ? new Either.tx(x) : (...)
+  var hasIncompleteCondition = false;
+  var unclosedParens = 0;
   for (var i = 0; i < types.length; i++) {
-    final dartType = _mapType([types[i]]);
+    final dartType = mapType([types[i]]);
 
-    _writeTypeCheckCondition(buffer, valueCode, dartType);
-    buffer.write(' ? new $unionTypeName.t${i + 1}(');
-    _writeFromJsonCode(buffer, [dartType], valueCode); // Call recursively!
-    buffer.write(') : (');
+    if (dartType != 'dynamic') {
+      _writeTypeCheckCondition(buffer, valueCode, dartType);
+      buffer.write(' ? new $unionTypeName.t${i + 1}(');
+      _writeFromJsonCode(buffer, [dartType], valueCode); // Call recursively!
+      buffer.write(') : (');
+      hasIncompleteCondition = true;
+      unclosedParens++;
+    } else {
+      _writeFromJsonCode(buffer, [dartType], valueCode);
+      hasIncompleteCondition = false;
+    }
   }
   // Fill the final parens with a throw because if we fell through all of the
   // cases then the value we had didn't match any of the types in the union.
-  buffer
-      .write("throw '''\${$valueCode} was not one of (${types.join(', ')})'''");
-  buffer.write(')' * types.length);
+  if (hasIncompleteCondition) {
+    buffer.write(
+        "throw '''\${$valueCode} was not one of (${types.join(', ')})'''");
+  }
+  buffer.write(')' * unclosedParens);
 }
 
 void _writeFromJsonConstructor(
@@ -342,7 +381,7 @@
   }
   buffer
     ..writeIndentedln(
-        'factory ${interface.name}.fromJson(Map<String, dynamic> json) {')
+        'factory ${_stripTypeArgs(interface.name)}.fromJson(Map<String, dynamic> json) {')
     ..indent();
   for (final field in allFields) {
     buffer.writeIndented('final ${field.name} = ');
@@ -354,7 +393,7 @@
     ..write(allFields.map((field) => '${field.name}').join(', '))
     ..writeln(');')
     ..outdent()
-    ..write('}');
+    ..writeIndented('}');
 }
 
 void _writeInterface(IndentableStringBuffer buffer, Interface interface) {
@@ -488,12 +527,17 @@
   } else if (_isSpecType(dartType)) {
     buffer.write('$dartType.canParse($valueCode)');
   } else if (_isList(dartType)) {
-    // TODO(dantup): If we're happy to assume we never have two lists in a union
-    // we could simplify this to '$valueCode is List'.
-    buffer.write(
-        '($valueCode is List && ($valueCode.length == 0 || $valueCode.every((item) => ');
-    _writeTypeCheckCondition(buffer, 'item', _getListType(dartType));
-    buffer.write(')))');
+    final listType = _getListType(dartType);
+    buffer.write('($valueCode is List');
+    if (dartType != 'dynamic') {
+      // TODO(dantup): If we're happy to assume we never have two lists in a union
+      // we could skip this bit.
+      buffer
+          .write(' && ($valueCode.length == 0 || $valueCode.every((item) => ');
+      _writeTypeCheckCondition(buffer, 'item', listType);
+      buffer.write('))');
+    }
+    buffer.write(')');
   } else if (_isUnion(dartType)) {
     // To type check a union, we just recursively check against each of its types.
     final unionTypes = _getUnionTypes(dartType);
@@ -502,7 +546,7 @@
       if (i != 0) {
         buffer.write(' || ');
       }
-      _writeTypeCheckCondition(buffer, valueCode, _mapType([unionTypes[i]]));
+      _writeTypeCheckCondition(buffer, valueCode, mapType([unionTypes[i]]));
     }
     buffer.write(')');
   } else {
diff --git a/pkg/analysis_server/tool/lsp_spec/typescript.dart b/pkg/analysis_server/tool/lsp_spec/typescript.dart
index 286b0e2..017c666 100644
--- a/pkg/analysis_server/tool/lsp_spec/typescript.dart
+++ b/pkg/analysis_server/tool/lsp_spec/typescript.dart
@@ -159,8 +159,8 @@
       : super(name, comment);
 
   static List<Field> extractFrom(String interfaceName, String code) {
-    final RegExp _fieldPattern = new RegExp(
-        _comment + r'([\w\[\]]+\??)\s*:\s*([\w\[\] \|\{\}\(\):;]+)\s*(?:;|$)');
+    final RegExp _fieldPattern = new RegExp(_comment +
+        r'([\w\[\]]+\??)\s*:\s*([\w\[\] \|\{\}\(\)<>:;]+)\s*(?:;|$)');
 
     final fields = _fieldPattern.allMatches(code).where((m) {
       // Skip over the indexer in FormattingOptions since we don't need this
@@ -220,7 +220,7 @@
 
   static List<Interface> extractFrom(String code) {
     final RegExp _interfacePattern = new RegExp(_comment +
-        r'(?:export\s+)?(?:interface|class)\s+(\w+)(?:\s+extends\s+([\w, ]+?))?\s*' +
+        r'(?:export\s+)?(?:interface|class)\s+([\w<>]+)(?:\s+extends\s+([\w, ]+?))?\s*' +
         _blockBody);
 
     final interfaces = _interfacePattern.allMatches(code).map((match) {