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 45a7fc1e8d78..fef2206d8b6b 100644 --- a/pkg/analysis_server/test/tool/lsp_spec/typescript_test.dart +++ b/pkg/analysis_server/test/tool/lsp_spec/typescript_test.dart @@ -242,6 +242,39 @@ export type DocumentSelector = DocumentFilter[]; expect(typeAlias.baseType, isArrayOf(isSimpleType('DocumentFilter'))); }); + test('parses a type alias that is a union of unnamed types', () { + final input = ''' +export type NameOrLength = { name: string } | { length: number }; + '''; + final output = parseString(input); + expect(output, hasLength(3)); + + // Results should be the two inline interfaces followed by the type alias. + + expect(output[0], const TypeMatcher()); + final InlineInterface interface1 = output[0]; + expect(interface1.name, equals('NameOrLength1')); + expect(interface1.members, hasLength(1)); + expect(interface1.members[0].name, equals('name')); + + expect(output[1], const TypeMatcher()); + final InlineInterface interface2 = output[1]; + expect(interface2.name, equals('NameOrLength2')); + expect(interface2.members, hasLength(1)); + expect(interface2.members[0].name, equals('length')); + + expect(output[2], const TypeMatcher()); + final TypeAlias typeAlias = output[2]; + expect(typeAlias.name, equals('NameOrLength')); + expect(typeAlias.baseType, const TypeMatcher()); + + // The type alias should be a union of the two types above. + UnionType union = typeAlias.baseType; + expect(union.types, hasLength(2)); + expect(union.types[0], isSimpleType(interface1.name)); + expect(union.types[1], isSimpleType(interface2.name)); + }); + test('parses a namespace of constants', () { final input = ''' export namespace ResourceOperationKind { @@ -321,5 +354,18 @@ interface SomeInformation { expect(field.name, equals('label')); expect(field.type, isSimpleType('object')); }); + + test('parses multiple single-line comments into a single token', () { + final input = ''' +// This is line 1 +// This is line 2 +interface SomeInformation { +} + '''; + final output = parseString(input); + expect(output, hasLength(1)); + expect(output[0].commentNode.token.lexeme, equals('''// This is line 1 +// This is line 2''')); + }); }); } diff --git a/pkg/analysis_server/tool/lsp_spec/codegen_dart.dart b/pkg/analysis_server/tool/lsp_spec/codegen_dart.dart index 6fd16917ef2f..f33ab22afed4 100644 --- a/pkg/analysis_server/tool/lsp_spec/codegen_dart.dart +++ b/pkg/analysis_server/tool/lsp_spec/codegen_dart.dart @@ -36,7 +36,7 @@ bool enumClassAllowsAnyValue(String name) { String generateDartForTypes(List types) { final buffer = IndentableStringBuffer(); - _getSorted(types).forEach((t) => _writeType(buffer, t)); + _getSortedUnique(types).forEach((t) => _writeType(buffer, t)); final formattedCode = _formatCode(buffer.toString()); return formattedCode.trim() + '\n'; // Ensure a single trailing newline. } @@ -95,9 +95,27 @@ List _getAllFields(Interface interface) { .toList(); } -/// Returns a copy of the list sorted by name. -List _getSorted(List items) { - final sortedList = items.toList(); +/// Returns a copy of the list sorted by name with duplicates (by name+type) removed. +List _getSortedUnique(List items) { + final uniqueByName = {}; + items.forEach((item) { + // It's fine to have the same name used for different types (eg. namespace + + // type alias) but some types are just duplicated entirely in the spec in + // different positions which should not be emitted twice. + final nameTypeKey = '${item.name}|${item.runtimeType}'; + if (uniqueByName.containsKey(nameTypeKey)) { + // At the time of writing, there were two duplicated types: + // - TextDocumentSyncKind (same defintion in both places) + // - TextDocumentSyncOptions (first definition is just a subset) + // If this list grows, consider handling this better - or try to have the + // spec updated to be unambigious. + print('WARN: More than one definition for $nameTypeKey.'); + } + + // Keep the last one as in some cases the first definition is less specific. + uniqueByName[nameTypeKey] = item; + }); + final sortedList = uniqueByName.values.toList(); sortedList.sort((item1, item2) => item1.name.compareTo(item2.name)); return sortedList; } @@ -199,7 +217,7 @@ void _writeCanParseMethod(IndentableStringBuffer buffer, Interface interface) { } buffer.write('!('); _writeTypeCheckCondition( - buffer, "obj['${field.name}']", field.type, 'reporter'); + buffer, interface, "obj['${field.name}']", field.type, 'reporter'); buffer ..write(')) {') ..indent() @@ -302,7 +320,7 @@ void _writeEnumClass(IndentableStringBuffer buffer, Namespace namespace) { ..indent(); if (allowsAnyValue) { buffer.writeIndentedln('return '); - _writeTypeCheckCondition(buffer, 'obj', typeOfValues, 'reporter'); + _writeTypeCheckCondition(buffer, null, 'obj', typeOfValues, 'reporter'); buffer.writeln(';'); } else { buffer @@ -445,7 +463,8 @@ void _writeFromJsonCodeForUnion( // Dynamic matches all type checks, so only emit it if required. if (!isDynamic) { - _writeTypeCheckCondition(buffer, valueCode, type, 'nullLspJsonReporter'); + _writeTypeCheckCondition( + buffer, null, valueCode, type, 'nullLspJsonReporter'); buffer.write(' ? '); } @@ -609,7 +628,7 @@ void _writeMember(IndentableStringBuffer buffer, Member member) { } void _writeMembers(IndentableStringBuffer buffer, List members) { - _getSorted(members).forEach((m) => _writeMember(buffer, m)); + _getSortedUnique(members).forEach((m) => _writeMember(buffer, m)); } void _writeToJsonFieldsForResponseMessage( @@ -685,8 +704,8 @@ void _writeType(IndentableStringBuffer buffer, AstNode type) { } } -void _writeTypeCheckCondition(IndentableStringBuffer buffer, String valueCode, - TypeBase type, String reporter) { +void _writeTypeCheckCondition(IndentableStringBuffer buffer, + Interface interface, String valueCode, TypeBase type, String reporter) { type = resolveTypeAlias(type); final dartType = type.dartType; @@ -703,7 +722,8 @@ void _writeTypeCheckCondition(IndentableStringBuffer buffer, String valueCode, // TODO(dantup): If we're happy to assume we never have two lists in a union // we could skip this bit. buffer.write(' && ($valueCode.every((item) => '); - _writeTypeCheckCondition(buffer, 'item', type.elementType, reporter); + _writeTypeCheckCondition( + buffer, interface, 'item', type.elementType, reporter); buffer.write('))'); } buffer.write(')'); @@ -711,9 +731,11 @@ void _writeTypeCheckCondition(IndentableStringBuffer buffer, String valueCode, buffer.write('($valueCode is Map'); if (fullDartType != 'dynamic') { buffer..write(' && (')..write('$valueCode.keys.every((item) => '); - _writeTypeCheckCondition(buffer, 'item', type.indexType, reporter); + _writeTypeCheckCondition( + buffer, interface, 'item', type.indexType, reporter); buffer..write('&& $valueCode.values.every((item) => '); - _writeTypeCheckCondition(buffer, 'item', type.valueType, reporter); + _writeTypeCheckCondition( + buffer, interface, 'item', type.valueType, reporter); buffer.write(')))'); } buffer.write(')'); @@ -724,9 +746,18 @@ void _writeTypeCheckCondition(IndentableStringBuffer buffer, String valueCode, if (i != 0) { buffer.write(' || '); } - _writeTypeCheckCondition(buffer, valueCode, type.types[i], reporter); + _writeTypeCheckCondition( + buffer, interface, valueCode, type.types[i], reporter); } buffer.write(')'); + } else if (interface != null && + interface.typeArgs != null && + interface.typeArgs.any((typeArg) => typeArg.lexeme == fullDartType)) { + final comment = '/* $fullDartType.canParse($valueCode) */'; + print( + 'WARN: Unable to write a type check for $valueCode with generic type $fullDartType. ' + 'Please review the generated code annotated with $comment'); + buffer.write('true $comment'); } else { throw 'Unable to type check $valueCode against $fullDartType'; } diff --git a/pkg/analysis_server/tool/lsp_spec/typescript_parser.dart b/pkg/analysis_server/tool/lsp_spec/typescript_parser.dart index d3038866d1bc..8121590a9ac2 100644 --- a/pkg/analysis_server/tool/lsp_spec/typescript_parser.dart +++ b/pkg/analysis_server/tool/lsp_spec/typescript_parser.dart @@ -151,6 +151,7 @@ class Interface extends AstNode { this.baseTypes, this.members, ) : super(comment); + @override String get name => nameToken.lexeme; String get nameWithTypeArgs => '$name$typeArgsString'; @@ -502,7 +503,9 @@ class Parser { if (includeUndefined) { types.add(Type.Undefined); } + var typeIndex = 0; while (true) { + typeIndex++; TypeBase type; if (_match([TokenType.LEFT_BRACE])) { // Inline interfaces. @@ -521,7 +524,12 @@ class Parser { type = 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); + // If we have no fieldName to base the synthetic name from, we should use + // the index of this type, for example in: + // type Foo = { [..] } | { [...] } + // we will generate Foo1 and Foo2 for the types. + final nameSuffix = fieldName ?? '$typeIndex'; + final generatedName = _joinNames(containerName, nameSuffix); _nodes.add(InlineInterface(generatedName, members)); // Record the type as a simple type that references this interface. type = Type.identifier(generatedName); @@ -616,7 +624,9 @@ class Parser { final name = _consume(TokenType.IDENTIFIER, 'Expected identifier'); _consume(TokenType.EQUAL, 'Expected ='); final type = _type(name.lexeme, null); - _consume(TokenType.SEMI_COLON, 'Expected ;'); + if (!_isAtEnd) { + _consume(TokenType.SEMI_COLON, 'Expected ;'); + } return TypeAlias(leadingComment, name, type); } @@ -640,8 +650,16 @@ class Scanner { return _tokens; } - void _addToken(TokenType type) { - final text = _source.substring(_startOfToken, _currentPos); + void _addToken(TokenType type, {bool mergeSameTypes = false}) { + var text = _source.substring(_startOfToken, _currentPos); + + // Consecutive tokens of some types (for example Comments) are merged + // together. + if (mergeSameTypes && _tokens.isNotEmpty && type == _tokens.last.type) { + text = '${_tokens.last.lexeme}\n$text'; + _tokens.removeLast(); + } + _tokens.add(Token(type, text)); } @@ -744,13 +762,13 @@ class Scanner { _advance(); _advance(); } - _addToken(TokenType.COMMENT); + _addToken(TokenType.COMMENT, mergeSameTypes: true); } else if (_match('/')) { // Single line comment. while (_peek() != '\n' && !_isAtEnd) { _advance(); } - _addToken(TokenType.COMMENT); + _addToken(TokenType.COMMENT, mergeSameTypes: true); } else { _addToken(TokenType.SLASH); }