[Analyzer] Split LSP completion resolution info into base/sub-classes

Change-Id: I57f61b8d0467c32562d865de3b87d75995cfb35f
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/174129
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart b/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart
index 5c97433..16daa2d 100644
--- a/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart
+++ b/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart
@@ -192,66 +192,30 @@
       CompletionItemResolutionInfo.canParse,
       CompletionItemResolutionInfo.fromJson);
 
-  CompletionItemResolutionInfo(
-      {@required this.file,
-      @required this.offset,
-      @required this.libId,
-      @required this.displayUri,
-      @required this.rOffset,
-      @required this.rLength}) {
+  CompletionItemResolutionInfo({@required this.file, @required this.offset}) {
     if (file == null) {
       throw 'file is required but was not provided';
     }
     if (offset == null) {
       throw 'offset is required but was not provided';
     }
-    if (libId == null) {
-      throw 'libId is required but was not provided';
-    }
-    if (displayUri == null) {
-      throw 'displayUri is required but was not provided';
-    }
-    if (rOffset == null) {
-      throw 'rOffset is required but was not provided';
-    }
-    if (rLength == null) {
-      throw 'rLength is required but was not provided';
-    }
   }
   static CompletionItemResolutionInfo fromJson(Map<String, dynamic> json) {
+    if (DartCompletionItemResolutionInfo.canParse(json, nullLspJsonReporter)) {
+      return DartCompletionItemResolutionInfo.fromJson(json);
+    }
     final file = json['file'];
     final offset = json['offset'];
-    final libId = json['libId'];
-    final displayUri = json['displayUri'];
-    final rOffset = json['rOffset'];
-    final rLength = json['rLength'];
-    return CompletionItemResolutionInfo(
-        file: file,
-        offset: offset,
-        libId: libId,
-        displayUri: displayUri,
-        rOffset: rOffset,
-        rLength: rLength);
+    return CompletionItemResolutionInfo(file: file, offset: offset);
   }
 
-  final String displayUri;
   final String file;
-  final num libId;
   final num offset;
-  final num rLength;
-  final num rOffset;
 
   Map<String, dynamic> toJson() {
     var __result = <String, dynamic>{};
     __result['file'] = file ?? (throw 'file is required but was not set');
     __result['offset'] = offset ?? (throw 'offset is required but was not set');
-    __result['libId'] = libId ?? (throw 'libId is required but was not set');
-    __result['displayUri'] =
-        displayUri ?? (throw 'displayUri is required but was not set');
-    __result['rOffset'] =
-        rOffset ?? (throw 'rOffset is required but was not set');
-    __result['rLength'] =
-        rLength ?? (throw 'rLength is required but was not set');
     return __result;
   }
 
@@ -291,6 +255,105 @@
       } finally {
         reporter.pop();
       }
+      return true;
+    } else {
+      reporter.reportError('must be of type CompletionItemResolutionInfo');
+      return false;
+    }
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (other is CompletionItemResolutionInfo &&
+        other.runtimeType == CompletionItemResolutionInfo) {
+      return file == other.file && offset == other.offset && true;
+    }
+    return false;
+  }
+
+  @override
+  int get hashCode {
+    var hash = 0;
+    hash = JenkinsSmiHash.combine(hash, file.hashCode);
+    hash = JenkinsSmiHash.combine(hash, offset.hashCode);
+    return JenkinsSmiHash.finish(hash);
+  }
+
+  @override
+  String toString() => jsonEncoder.convert(toJson());
+}
+
+class DartCompletionItemResolutionInfo
+    implements CompletionItemResolutionInfo, ToJsonable {
+  static const jsonHandler = LspJsonHandler(
+      DartCompletionItemResolutionInfo.canParse,
+      DartCompletionItemResolutionInfo.fromJson);
+
+  DartCompletionItemResolutionInfo(
+      {@required this.libId,
+      @required this.displayUri,
+      @required this.rOffset,
+      @required this.rLength,
+      @required this.file,
+      @required this.offset}) {
+    if (libId == null) {
+      throw 'libId is required but was not provided';
+    }
+    if (displayUri == null) {
+      throw 'displayUri is required but was not provided';
+    }
+    if (rOffset == null) {
+      throw 'rOffset is required but was not provided';
+    }
+    if (rLength == null) {
+      throw 'rLength is required but was not provided';
+    }
+    if (file == null) {
+      throw 'file is required but was not provided';
+    }
+    if (offset == null) {
+      throw 'offset is required but was not provided';
+    }
+  }
+  static DartCompletionItemResolutionInfo fromJson(Map<String, dynamic> json) {
+    final libId = json['libId'];
+    final displayUri = json['displayUri'];
+    final rOffset = json['rOffset'];
+    final rLength = json['rLength'];
+    final file = json['file'];
+    final offset = json['offset'];
+    return DartCompletionItemResolutionInfo(
+        libId: libId,
+        displayUri: displayUri,
+        rOffset: rOffset,
+        rLength: rLength,
+        file: file,
+        offset: offset);
+  }
+
+  final String displayUri;
+  final String file;
+  final num libId;
+  final num offset;
+  final num rLength;
+  final num rOffset;
+
+  Map<String, dynamic> toJson() {
+    var __result = <String, dynamic>{};
+    __result['libId'] = libId ?? (throw 'libId is required but was not set');
+    __result['displayUri'] =
+        displayUri ?? (throw 'displayUri is required but was not set');
+    __result['rOffset'] =
+        rOffset ?? (throw 'rOffset is required but was not set');
+    __result['rLength'] =
+        rLength ?? (throw 'rLength is required but was not set');
+    __result['file'] = file ?? (throw 'file is required but was not set');
+    __result['offset'] = offset ?? (throw 'offset is required but was not set');
+    return __result;
+  }
+
+  static bool canParse(Object obj, LspJsonReporter reporter) {
+    if (obj is Map<String, dynamic>) {
       reporter.push('libId');
       try {
         if (!obj.containsKey('libId')) {
@@ -359,23 +422,57 @@
       } finally {
         reporter.pop();
       }
+      reporter.push('file');
+      try {
+        if (!obj.containsKey('file')) {
+          reporter.reportError('must not be undefined');
+          return false;
+        }
+        if (obj['file'] == null) {
+          reporter.reportError('must not be null');
+          return false;
+        }
+        if (!(obj['file'] is String)) {
+          reporter.reportError('must be of type String');
+          return false;
+        }
+      } finally {
+        reporter.pop();
+      }
+      reporter.push('offset');
+      try {
+        if (!obj.containsKey('offset')) {
+          reporter.reportError('must not be undefined');
+          return false;
+        }
+        if (obj['offset'] == null) {
+          reporter.reportError('must not be null');
+          return false;
+        }
+        if (!(obj['offset'] is num)) {
+          reporter.reportError('must be of type num');
+          return false;
+        }
+      } finally {
+        reporter.pop();
+      }
       return true;
     } else {
-      reporter.reportError('must be of type CompletionItemResolutionInfo');
+      reporter.reportError('must be of type DartCompletionItemResolutionInfo');
       return false;
     }
   }
 
   @override
   bool operator ==(Object other) {
-    if (other is CompletionItemResolutionInfo &&
-        other.runtimeType == CompletionItemResolutionInfo) {
-      return file == other.file &&
-          offset == other.offset &&
-          libId == other.libId &&
+    if (other is DartCompletionItemResolutionInfo &&
+        other.runtimeType == DartCompletionItemResolutionInfo) {
+      return libId == other.libId &&
           displayUri == other.displayUri &&
           rOffset == other.rOffset &&
           rLength == other.rLength &&
+          file == other.file &&
+          offset == other.offset &&
           true;
     }
     return false;
@@ -384,12 +481,12 @@
   @override
   int get hashCode {
     var hash = 0;
-    hash = JenkinsSmiHash.combine(hash, file.hashCode);
-    hash = JenkinsSmiHash.combine(hash, offset.hashCode);
     hash = JenkinsSmiHash.combine(hash, libId.hashCode);
     hash = JenkinsSmiHash.combine(hash, displayUri.hashCode);
     hash = JenkinsSmiHash.combine(hash, rOffset.hashCode);
     hash = JenkinsSmiHash.combine(hash, rLength.hashCode);
+    hash = JenkinsSmiHash.combine(hash, file.hashCode);
+    hash = JenkinsSmiHash.combine(hash, offset.hashCode);
     return JenkinsSmiHash.finish(hash);
   }
 
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_completion_resolve.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_completion_resolve.dart
index d7f1610..c7be4e9 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_completion_resolve.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_completion_resolve.dart
@@ -2,6 +2,7 @@
 // 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:analysis_server/lsp_protocol/protocol_custom_generated.dart';
 import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
 import 'package:analysis_server/lsp_protocol/protocol_special.dart';
 import 'package:analysis_server/src/lsp/constants.dart';
@@ -15,10 +16,11 @@
 
 class CompletionResolveHandler
     extends MessageHandler<CompletionItem, CompletionItem> {
+  /// The last completion item we asked to be resolved.
   ///
-  /// The latest completion item we were asked to resolve. We use it to abort
-  /// previous requests.
-  ///
+  /// Used to abort previous requests in async handlers if another resolve request
+  /// arrives while the previous is being processed (for clients that don't send
+  /// cancel events).
   CompletionItem _latestCompletionItem;
 
   CompletionResolveHandler(LspAnalysisServer server) : super(server);
@@ -31,13 +33,23 @@
 
   @override
   Future<ErrorOr<CompletionItem>> handle(
-      CompletionItem item, CancellationToken token) async {
-    // If this isn't an item with resolution data, return the same item back.
-    if (item.data == null) {
+    CompletionItem item,
+    CancellationToken token,
+  ) async {
+    final resolutionInfo = item.data;
+
+    if (resolutionInfo is DartCompletionItemResolutionInfo) {
+      return resolveDartCompletion(item, resolutionInfo, token);
+    } else {
       return success(item);
     }
+  }
 
-    final data = item.data;
+  Future<ErrorOr<CompletionItem>> resolveDartCompletion(
+    CompletionItem item,
+    DartCompletionItemResolutionInfo data,
+    CancellationToken token,
+  ) async {
     final lineInfo = server.getLineInfo(data.file);
     if (lineInfo == null) {
       return error(
@@ -166,7 +178,7 @@
           textEdit: TextEdit(
             // TODO(dantup): If `clientSupportsSnippets == true` then we should map
             // `selection` in to a snippet (see how Dart Code does this).
-            range: toRange(lineInfo, item.data.rOffset, item.data.rLength),
+            range: toRange(lineInfo, data.rOffset, data.rLength),
             newText: newInsertText,
           ),
           additionalTextEdits: thisFilesChanges
diff --git a/pkg/analysis_server/lib/src/lsp/mapping.dart b/pkg/analysis_server/lib/src/lsp/mapping.dart
index ce49475..56c7ee0 100644
--- a/pkg/analysis_server/lib/src/lsp/mapping.dart
+++ b/pkg/analysis_server/lib/src/lsp/mapping.dart
@@ -301,7 +301,7 @@
         ? insertText
         : null, // insertText uses label if not set
     // data, used for completionItem/resolve.
-    data: lsp.CompletionItemResolutionInfo(
+    data: lsp.DartCompletionItemResolutionInfo(
         file: file,
         offset: offset,
         libId: includedSuggestionSet.id,
@@ -783,6 +783,7 @@
   int replacementLength, {
   @required bool includeCommitCharacters,
   @required bool completeFunctionCalls,
+  Object resolutionData,
 }) {
   // Build display labels and text to insert. insertText and filterText may
   // differ from label (for ex. if the label includes things like (…)). If
@@ -876,6 +877,7 @@
         : null,
     commitCharacters:
         includeCommitCharacters ? dartCompletionCommitCharacters : null,
+    data: resolutionData,
     detail: getCompletionDetail(suggestion, completionKind,
         supportsDeprecatedFlag || supportsDeprecatedTag),
     documentation:
diff --git a/pkg/analysis_server/tool/lsp_spec/generate_all.dart b/pkg/analysis_server/tool/lsp_spec/generate_all.dart
index 26b8916..2998c8f 100644
--- a/pkg/analysis_server/tool/lsp_spec/generate_all.dart
+++ b/pkg/analysis_server/tool/lsp_spec/generate_all.dart
@@ -182,8 +182,14 @@
 ''';
 
 List<AstNode> getCustomClasses() {
-  Interface interface(String name, List<Member> fields) {
-    return Interface(null, Token.identifier(name), [], [], fields);
+  Interface interface(String name, List<Member> fields, {String baseType}) {
+    return Interface(
+      null,
+      Token.identifier(name),
+      [],
+      [if (baseType != null) Type.identifier(baseType)],
+      fields,
+    );
   }
 
   Field field(String name,
@@ -250,11 +256,17 @@
       [
         field('file', type: 'string'),
         field('offset', type: 'number'),
+      ],
+    ),
+    interface(
+      'DartCompletionItemResolutionInfo',
+      [
         field('libId', type: 'number'),
         field('displayUri', type: 'string'),
         field('rOffset', type: 'number'),
-        field('rLength', type: 'number')
+        field('rLength', type: 'number'),
       ],
+      baseType: 'CompletionItemResolutionInfo',
     ),
   ];
   return customTypes;