Updates to pase tuples in LSP spec

Change-Id: Ia1ce7579e954a8000b3460445859a2d221cf1a8c
Reviewed-on: https://dart-review.googlesource.com/c/87441
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 f38bab3..2d59611 100644
--- a/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart
+++ b/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart
@@ -4935,6 +4935,95 @@
   String toString() => jsonEncoder.convert(toJson());
 }
 
+class LocationLink implements ToJsonable {
+  LocationLink(this.originSelectionRange, this.targetUri, this.targetRange,
+      this.targetSelectionRange) {
+    if (targetUri == null) {
+      throw 'targetUri is required but was not provided';
+    }
+    if (targetRange == null) {
+      throw 'targetRange is required but was not provided';
+    }
+  }
+  static LocationLink fromJson(Map<String, dynamic> json) {
+    final originSelectionRange = json['originSelectionRange'] != null
+        ? Range.fromJson(json['originSelectionRange'])
+        : null;
+    final targetUri = json['targetUri'];
+    final targetRange = json['targetRange'] != null
+        ? Range.fromJson(json['targetRange'])
+        : null;
+    final targetSelectionRange = json['targetSelectionRange'] != null
+        ? Range.fromJson(json['targetSelectionRange'])
+        : null;
+    return new LocationLink(
+        originSelectionRange, targetUri, targetRange, targetSelectionRange);
+  }
+
+  /// Span of the origin of this link.
+  ///
+  /// Used as the underlined span for mouse interaction. Defaults to the word
+  /// range at the mouse position.
+  final Range originSelectionRange;
+
+  /// The full target range of this link.
+  final Range targetRange;
+
+  /// The span of this link.
+  final Range targetSelectionRange;
+
+  /// The target resource identifier of this link.
+  final String targetUri;
+
+  Map<String, dynamic> toJson() {
+    Map<String, dynamic> __result = {};
+    if (originSelectionRange != null) {
+      __result['originSelectionRange'] = originSelectionRange;
+    }
+    __result['targetUri'] =
+        targetUri ?? (throw 'targetUri is required but was not set');
+    __result['targetRange'] =
+        targetRange ?? (throw 'targetRange is required but was not set');
+    if (targetSelectionRange != null) {
+      __result['targetSelectionRange'] = targetSelectionRange;
+    }
+    return __result;
+  }
+
+  static bool canParse(Object obj) {
+    return obj is Map<String, dynamic> &&
+        obj.containsKey('targetUri') &&
+        obj['targetUri'] is String &&
+        obj.containsKey('targetRange') &&
+        Range.canParse(obj['targetRange']);
+  }
+
+  @override
+  bool operator ==(other) {
+    if (other is LocationLink) {
+      return originSelectionRange == other.originSelectionRange &&
+          targetUri == other.targetUri &&
+          targetRange == other.targetRange &&
+          targetSelectionRange == other.targetSelectionRange &&
+          true;
+    }
+    return false;
+  }
+
+  @override
+  int get hashCode {
+    int hash = 0;
+    hash = JenkinsSmiHash.combine(hash, originSelectionRange.hashCode);
+    hash = JenkinsSmiHash.combine(hash, targetUri.hashCode);
+    hash = JenkinsSmiHash.combine(hash, targetRange.hashCode);
+    hash = JenkinsSmiHash.combine(hash, targetSelectionRange.hashCode);
+    return JenkinsSmiHash.finish(hash);
+  }
+
+  @override
+  String toString() => jsonEncoder.convert(toJson());
+}
+
 class LogMessageParams implements ToJsonable {
   LogMessageParams(this.type, this.message) {
     if (type == null) {
@@ -5276,6 +5365,7 @@
       case r'completionItem/resolve':
       case r'textDocument/hover':
       case r'textDocument/signatureHelp':
+      case r'textDocument/declaration':
       case r'textDocument/definition':
       case r'textDocument/typeDefinition':
       case r'textDocument/implementation':
@@ -5397,6 +5487,10 @@
   static const textDocument_signatureHelp =
       const Method._(r'textDocument/signatureHelp');
 
+  /// Constant for the 'textDocument/declaration' method.
+  static const textDocument_declaration =
+      const Method._(r'textDocument/declaration');
+
   /// Constant for the 'textDocument/definition' method.
   static const textDocument_definition =
       const Method._(r'textDocument/definition');
@@ -5571,7 +5665,12 @@
   /// but can be omitted.
   final Either2<String, MarkupContent> documentation;
 
-  /// The label of this parameter. Will be shown in the UI.
+  /// The label of this parameter information.
+  ///
+  /// Either a string or an inclusive start and exclusive end offsets within its
+  /// containing signature label. (see SignatureInformation.label). *Note*: A
+  /// label of type string must be a substring of its containing signature
+  /// label.
   final String label;
 
   Map<String, dynamic> toJson() {
@@ -7866,6 +7965,7 @@
       this.formatting,
       this.rangeFormatting,
       this.onTypeFormatting,
+      this.declaration,
       this.definition,
       this.typeDefinition,
       this.implementation,
@@ -7913,6 +8013,10 @@
         ? TextDocumentClientCapabilitiesOnTypeFormatting.fromJson(
             json['onTypeFormatting'])
         : null;
+    final declaration = json['declaration'] != null
+        ? TextDocumentClientCapabilitiesDeclaration.fromJson(
+            json['declaration'])
+        : null;
     final definition = json['definition'] != null
         ? TextDocumentClientCapabilitiesDefinition.fromJson(json['definition'])
         : null;
@@ -7960,6 +8064,7 @@
         formatting,
         rangeFormatting,
         onTypeFormatting,
+        declaration,
         definition,
         typeDefinition,
         implementation,
@@ -7987,6 +8092,9 @@
   /// Capabilities specific to the `textDocument/completion`
   final TextDocumentClientCapabilitiesCompletion completion;
 
+  /// Capabilities specific to the `textDocument/declaration`
+  final TextDocumentClientCapabilitiesDeclaration declaration;
+
   /// Capabilities specific to the `textDocument/definition`
   final TextDocumentClientCapabilitiesDefinition definition;
 
@@ -8071,6 +8179,9 @@
     if (onTypeFormatting != null) {
       __result['onTypeFormatting'] = onTypeFormatting;
     }
+    if (declaration != null) {
+      __result['declaration'] = declaration;
+    }
     if (definition != null) {
       __result['definition'] = definition;
     }
@@ -8121,6 +8232,7 @@
           formatting == other.formatting &&
           rangeFormatting == other.rangeFormatting &&
           onTypeFormatting == other.onTypeFormatting &&
+          declaration == other.declaration &&
           definition == other.definition &&
           typeDefinition == other.typeDefinition &&
           implementation == other.implementation &&
@@ -8149,6 +8261,7 @@
     hash = JenkinsSmiHash.combine(hash, formatting.hashCode);
     hash = JenkinsSmiHash.combine(hash, rangeFormatting.hashCode);
     hash = JenkinsSmiHash.combine(hash, onTypeFormatting.hashCode);
+    hash = JenkinsSmiHash.combine(hash, declaration.hashCode);
     hash = JenkinsSmiHash.combine(hash, definition.hashCode);
     hash = JenkinsSmiHash.combine(hash, typeDefinition.hashCode);
     hash = JenkinsSmiHash.combine(hash, implementation.hashCode);
@@ -8652,22 +8765,88 @@
   String toString() => jsonEncoder.convert(toJson());
 }
 
-class TextDocumentClientCapabilitiesDefinition implements ToJsonable {
-  TextDocumentClientCapabilitiesDefinition(this.dynamicRegistration);
-  static TextDocumentClientCapabilitiesDefinition fromJson(
+class TextDocumentClientCapabilitiesDeclaration implements ToJsonable {
+  TextDocumentClientCapabilitiesDeclaration(
+      this.dynamicRegistration, this.linkSupport);
+  static TextDocumentClientCapabilitiesDeclaration fromJson(
       Map<String, dynamic> json) {
     final dynamicRegistration = json['dynamicRegistration'];
-    return new TextDocumentClientCapabilitiesDefinition(dynamicRegistration);
+    final linkSupport = json['linkSupport'];
+    return new TextDocumentClientCapabilitiesDeclaration(
+        dynamicRegistration, linkSupport);
   }
 
-  /// Whether definition supports dynamic registration.
+  /// Whether declaration supports dynamic registration. If this is set to
+  /// `true` the client supports the new `(TextDocumentRegistrationOptions &
+  /// StaticRegistrationOptions)` return value for the corresponding server
+  /// capability as well.
   final bool dynamicRegistration;
 
+  /// The client supports additional metadata in the form of declaration links.
+  final bool linkSupport;
+
   Map<String, dynamic> toJson() {
     Map<String, dynamic> __result = {};
     if (dynamicRegistration != null) {
       __result['dynamicRegistration'] = dynamicRegistration;
     }
+    if (linkSupport != null) {
+      __result['linkSupport'] = linkSupport;
+    }
+    return __result;
+  }
+
+  static bool canParse(Object obj) {
+    return obj is Map<String, dynamic>;
+  }
+
+  @override
+  bool operator ==(other) {
+    if (other is TextDocumentClientCapabilitiesDeclaration) {
+      return dynamicRegistration == other.dynamicRegistration &&
+          linkSupport == other.linkSupport &&
+          true;
+    }
+    return false;
+  }
+
+  @override
+  int get hashCode {
+    int hash = 0;
+    hash = JenkinsSmiHash.combine(hash, dynamicRegistration.hashCode);
+    hash = JenkinsSmiHash.combine(hash, linkSupport.hashCode);
+    return JenkinsSmiHash.finish(hash);
+  }
+
+  @override
+  String toString() => jsonEncoder.convert(toJson());
+}
+
+class TextDocumentClientCapabilitiesDefinition implements ToJsonable {
+  TextDocumentClientCapabilitiesDefinition(
+      this.dynamicRegistration, this.linkSupport);
+  static TextDocumentClientCapabilitiesDefinition fromJson(
+      Map<String, dynamic> json) {
+    final dynamicRegistration = json['dynamicRegistration'];
+    final linkSupport = json['linkSupport'];
+    return new TextDocumentClientCapabilitiesDefinition(
+        dynamicRegistration, linkSupport);
+  }
+
+  /// Whether definition supports dynamic registration.
+  final bool dynamicRegistration;
+
+  /// The client supports additional metadata in the form of definition links.
+  final bool linkSupport;
+
+  Map<String, dynamic> toJson() {
+    Map<String, dynamic> __result = {};
+    if (dynamicRegistration != null) {
+      __result['dynamicRegistration'] = dynamicRegistration;
+    }
+    if (linkSupport != null) {
+      __result['linkSupport'] = linkSupport;
+    }
     return __result;
   }
 
@@ -8678,7 +8857,9 @@
   @override
   bool operator ==(other) {
     if (other is TextDocumentClientCapabilitiesDefinition) {
-      return dynamicRegistration == other.dynamicRegistration && true;
+      return dynamicRegistration == other.dynamicRegistration &&
+          linkSupport == other.linkSupport &&
+          true;
     }
     return false;
   }
@@ -8687,6 +8868,7 @@
   int get hashCode {
     int hash = 0;
     hash = JenkinsSmiHash.combine(hash, dynamicRegistration.hashCode);
+    hash = JenkinsSmiHash.combine(hash, linkSupport.hashCode);
     return JenkinsSmiHash.finish(hash);
   }
 
@@ -9021,12 +9203,14 @@
 }
 
 class TextDocumentClientCapabilitiesImplementation implements ToJsonable {
-  TextDocumentClientCapabilitiesImplementation(this.dynamicRegistration);
+  TextDocumentClientCapabilitiesImplementation(
+      this.dynamicRegistration, this.linkSupport);
   static TextDocumentClientCapabilitiesImplementation fromJson(
       Map<String, dynamic> json) {
     final dynamicRegistration = json['dynamicRegistration'];
+    final linkSupport = json['linkSupport'];
     return new TextDocumentClientCapabilitiesImplementation(
-        dynamicRegistration);
+        dynamicRegistration, linkSupport);
   }
 
   /// Whether implementation supports dynamic registration. If this is set to
@@ -9035,11 +9219,17 @@
   /// capability as well.
   final bool dynamicRegistration;
 
+  /// The client supports additional metadata in the form of definition links.
+  final bool linkSupport;
+
   Map<String, dynamic> toJson() {
     Map<String, dynamic> __result = {};
     if (dynamicRegistration != null) {
       __result['dynamicRegistration'] = dynamicRegistration;
     }
+    if (linkSupport != null) {
+      __result['linkSupport'] = linkSupport;
+    }
     return __result;
   }
 
@@ -9050,7 +9240,9 @@
   @override
   bool operator ==(other) {
     if (other is TextDocumentClientCapabilitiesImplementation) {
-      return dynamicRegistration == other.dynamicRegistration && true;
+      return dynamicRegistration == other.dynamicRegistration &&
+          linkSupport == other.linkSupport &&
+          true;
     }
     return false;
   }
@@ -9059,6 +9251,7 @@
   int get hashCode {
     int hash = 0;
     hash = JenkinsSmiHash.combine(hash, dynamicRegistration.hashCode);
+    hash = JenkinsSmiHash.combine(hash, linkSupport.hashCode);
     return JenkinsSmiHash.finish(hash);
   }
 
@@ -9109,6 +9302,50 @@
   String toString() => jsonEncoder.convert(toJson());
 }
 
+class TextDocumentClientCapabilitiesParameterInformation implements ToJsonable {
+  TextDocumentClientCapabilitiesParameterInformation(this.labelOffsetSupport);
+  static TextDocumentClientCapabilitiesParameterInformation fromJson(
+      Map<String, dynamic> json) {
+    final labelOffsetSupport = json['labelOffsetSupport'];
+    return new TextDocumentClientCapabilitiesParameterInformation(
+        labelOffsetSupport);
+  }
+
+  /// The client supports processing label offsets instead of a simple label
+  /// string.
+  final bool labelOffsetSupport;
+
+  Map<String, dynamic> toJson() {
+    Map<String, dynamic> __result = {};
+    if (labelOffsetSupport != null) {
+      __result['labelOffsetSupport'] = labelOffsetSupport;
+    }
+    return __result;
+  }
+
+  static bool canParse(Object obj) {
+    return obj is Map<String, dynamic>;
+  }
+
+  @override
+  bool operator ==(other) {
+    if (other is TextDocumentClientCapabilitiesParameterInformation) {
+      return labelOffsetSupport == other.labelOffsetSupport && true;
+    }
+    return false;
+  }
+
+  @override
+  int get hashCode {
+    int hash = 0;
+    hash = JenkinsSmiHash.combine(hash, labelOffsetSupport.hashCode);
+    return JenkinsSmiHash.finish(hash);
+  }
+
+  @override
+  String toString() => jsonEncoder.convert(toJson());
+}
+
 class TextDocumentClientCapabilitiesPublishDiagnostics implements ToJsonable {
   TextDocumentClientCapabilitiesPublishDiagnostics(this.relatedInformation);
   static TextDocumentClientCapabilitiesPublishDiagnostics fromJson(
@@ -9351,26 +9588,37 @@
 }
 
 class TextDocumentClientCapabilitiesSignatureInformation implements ToJsonable {
-  TextDocumentClientCapabilitiesSignatureInformation(this.documentationFormat);
+  TextDocumentClientCapabilitiesSignatureInformation(
+      this.documentationFormat, this.parameterInformation);
   static TextDocumentClientCapabilitiesSignatureInformation fromJson(
       Map<String, dynamic> json) {
     final documentationFormat = json['documentationFormat']
         ?.map((item) => item != null ? MarkupKind.fromJson(item) : null)
         ?.cast<MarkupKind>()
         ?.toList();
+    final parameterInformation = json['parameterInformation'] != null
+        ? TextDocumentClientCapabilitiesParameterInformation.fromJson(
+            json['parameterInformation'])
+        : null;
     return new TextDocumentClientCapabilitiesSignatureInformation(
-        documentationFormat);
+        documentationFormat, parameterInformation);
   }
 
   /// The client supports the follow content formats for the documentation
   /// property. The order describes the preferred format of the client.
   final List<MarkupKind> documentationFormat;
 
+  /// Client capabilities specific to parameter information.
+  final TextDocumentClientCapabilitiesParameterInformation parameterInformation;
+
   Map<String, dynamic> toJson() {
     Map<String, dynamic> __result = {};
     if (documentationFormat != null) {
       __result['documentationFormat'] = documentationFormat;
     }
+    if (parameterInformation != null) {
+      __result['parameterInformation'] = parameterInformation;
+    }
     return __result;
   }
 
@@ -9383,6 +9631,7 @@
     if (other is TextDocumentClientCapabilitiesSignatureInformation) {
       return listEqual(documentationFormat, other.documentationFormat,
               (MarkupKind a, MarkupKind b) => a == b) &&
+          parameterInformation == other.parameterInformation &&
           true;
     }
     return false;
@@ -9392,6 +9641,7 @@
   int get hashCode {
     int hash = 0;
     hash = JenkinsSmiHash.combine(hash, documentationFormat.hashCode);
+    hash = JenkinsSmiHash.combine(hash, parameterInformation.hashCode);
     return JenkinsSmiHash.finish(hash);
   }
 
@@ -9526,12 +9776,14 @@
 }
 
 class TextDocumentClientCapabilitiesTypeDefinition implements ToJsonable {
-  TextDocumentClientCapabilitiesTypeDefinition(this.dynamicRegistration);
+  TextDocumentClientCapabilitiesTypeDefinition(
+      this.dynamicRegistration, this.linkSupport);
   static TextDocumentClientCapabilitiesTypeDefinition fromJson(
       Map<String, dynamic> json) {
     final dynamicRegistration = json['dynamicRegistration'];
+    final linkSupport = json['linkSupport'];
     return new TextDocumentClientCapabilitiesTypeDefinition(
-        dynamicRegistration);
+        dynamicRegistration, linkSupport);
   }
 
   /// Whether typeDefinition supports dynamic registration. If this is set to
@@ -9540,11 +9792,17 @@
   /// capability as well.
   final bool dynamicRegistration;
 
+  /// The client supports additional metadata in the form of definition links.
+  final bool linkSupport;
+
   Map<String, dynamic> toJson() {
     Map<String, dynamic> __result = {};
     if (dynamicRegistration != null) {
       __result['dynamicRegistration'] = dynamicRegistration;
     }
+    if (linkSupport != null) {
+      __result['linkSupport'] = linkSupport;
+    }
     return __result;
   }
 
@@ -9555,7 +9813,9 @@
   @override
   bool operator ==(other) {
     if (other is TextDocumentClientCapabilitiesTypeDefinition) {
-      return dynamicRegistration == other.dynamicRegistration && true;
+      return dynamicRegistration == other.dynamicRegistration &&
+          linkSupport == other.linkSupport &&
+          true;
     }
     return false;
   }
@@ -9564,6 +9824,7 @@
   int get hashCode {
     int hash = 0;
     hash = JenkinsSmiHash.combine(hash, dynamicRegistration.hashCode);
+    hash = JenkinsSmiHash.combine(hash, linkSupport.hashCode);
     return JenkinsSmiHash.finish(hash);
   }
 
diff --git a/pkg/analysis_server/test/lsp/server_abstract.dart b/pkg/analysis_server/test/lsp/server_abstract.dart
index 5c7c905..45e2ad9 100644
--- a/pkg/analysis_server/test/lsp/server_abstract.dart
+++ b/pkg/analysis_server/test/lsp/server_abstract.dart
@@ -520,6 +520,7 @@
           null,
           null,
           null,
+          null,
           null);
 
   final emptyWorkspaceClientCapabilities = new WorkspaceClientCapabilities(
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 d03b5fb..103392d 100644
--- a/pkg/analysis_server/test/tool/lsp_spec/typescript_test.dart
+++ b/pkg/analysis_server/test/tool/lsp_spec/typescript_test.dart
@@ -201,12 +201,12 @@
  * Blank lines should remain in-tact, as should:
  *   - Indented
  *   - Things
- * 
+ *
  * Some docs have:
  * - List items that are not indented
- * 
+ *
  * Sometimes after a blank line we'll have a note.
- * 
+ *
  * *Note* that something.
  */
 export interface A {
@@ -283,5 +283,26 @@
       expect(delete.commentText,
           equals('Supports deleting existing files and folders.'));
     });
+
+    test('parses a tuple in an array', () {
+      final String input = '''
+interface SomeInformation {
+	label: string | [number, number];
+}
+    ''';
+      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('label'));
+      expect(field.type, const TypeMatcher<UnionType>());
+      UnionType union = field.type;
+      expect(union.types, hasLength(2));
+      expect(union.types[0], isSimpleType('string'));
+      expect(union.types[1], isArrayOf(isSimpleType('number')));
+    });
   });
 }
diff --git a/pkg/analysis_server/tool/lsp_spec/typescript.dart b/pkg/analysis_server/tool/lsp_spec/typescript.dart
index dd6e854..6f70d2b 100644
--- a/pkg/analysis_server/tool/lsp_spec/typescript.dart
+++ b/pkg/analysis_server/tool/lsp_spec/typescript.dart
@@ -70,6 +70,9 @@
     "SymbolInformation": {
       "kind": "SymbolKind",
     },
+    "ParameterInformation": {
+      "label": "String",
+    }
   };
 
   final interface = _improvedTypeMappings[interfaceName];
diff --git a/pkg/analysis_server/tool/lsp_spec/typescript_parser.dart b/pkg/analysis_server/tool/lsp_spec/typescript_parser.dart
index 5a6a844..30083e3 100644
--- a/pkg/analysis_server/tool/lsp_spec/typescript_parser.dart
+++ b/pkg/analysis_server/tool/lsp_spec/typescript_parser.dart
@@ -379,13 +379,16 @@
     _consume(TokenType.LEFT_BRACE, 'Expected {');
     final members = <Member>[];
     while (!_check(TokenType.RIGHT_BRACE)) {
-      members.add(_member(name.lexeme));
+      final member = _member(name.lexeme);
+      // TODO(dantup): Remove this temp workaround once spec is fixed/clarified.
+      // https://github.com/Microsoft/language-server-protocol/issues/643
+      if (members.any((m) => m.name == member.name)) {
+        print('Skipping duplicate member ${member.name} in ${name.lexeme}');
+        continue;
+      }
+      members.add(member);
     }
 
-    // TODO(dantup): Temporary hack until we handle indexers. Remove nulls, which
-    // are (currently) returned by _field() for indexers.
-    members.removeWhere((m) => m == null);
-
     _consume(TokenType.RIGHT_BRACE, 'Expected }');
 
     return new Interface(leadingComment, name, typeArgs, baseTypes, members);
@@ -478,10 +481,6 @@
           members.add(_member(containerName));
         }
 
-        // TODO(dantup): Temporary hack until we handle indexers. Remove nulls, which
-        // are (currently) returned by _field() for indexers.
-        members.removeWhere((m) => m == null);
-
         _consume(TokenType.RIGHT_BRACE, 'Expected }');
         // Some of the inline interfaces have trailing commas (and some do not!)
         _match([TokenType.COMMA]);
@@ -512,6 +511,21 @@
         // export const Invoked: 1 = 1;
         // the best we can do is use their base type (number).
         type = Type.identifier('number');
+      } else if (_match([TokenType.LEFT_BRACKET])) {
+        // Tuples will just be converted to List/Array.
+        final tupleElementTypes = <TypeBase>[];
+        while (!_check(TokenType.RIGHT_BRACKET)) {
+          tupleElementTypes.add(_type(containerName, fieldName));
+          // Remove commas in between.
+          _match([TokenType.COMMA]);
+        }
+        _consume(TokenType.RIGHT_BRACKET, 'Expected ]');
+
+        final uniqueTypes = _getUniqueTypes(tupleElementTypes);
+        var tupleType = uniqueTypes.length == 1
+            ? uniqueTypes.single
+            : new UnionType(uniqueTypes);
+        type = new ArrayType(tupleType);
       } else {
         var typeName = _consume(TokenType.IDENTIFIER, 'Expected identifier');
         final typeArgs = <Type>[];
@@ -553,12 +567,7 @@
       }
     }
 
-    // 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();
+    final uniqueTypes = _getUniqueTypes(types);
 
     var type = uniqueTypes.length == 1
         ? uniqueTypes.single
@@ -574,6 +583,16 @@
     return type;
   }
 
+  /// 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.
+  List<TypeBase> _getUniqueTypes(List<TypeBase> types) {
+    final uniqueTypes = new Map.fromEntries(
+      types.map((t) => new MapEntry(t.dartTypeWithTypeArgs, t)),
+    ).values.toList();
+    return uniqueTypes;
+  }
+
   TypeAlias _typeAlias(Comment leadingComment) {
     final name = _consume(TokenType.IDENTIFIER, 'Expected identifier');
     _consume(TokenType.EQUAL, 'Expected =');