From b716c730cb906fd926d32e943f9e7ede413c054c Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Sat, 23 Dec 2023 23:56:39 -0600 Subject: [PATCH 1/8] test: Adds a few missing description parser tests --- .../LanguageTests/SchemaParserTests.swift | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/Tests/GraphQLTests/LanguageTests/SchemaParserTests.swift b/Tests/GraphQLTests/LanguageTests/SchemaParserTests.swift index 45d29f4e..d5c7f93a 100644 --- a/Tests/GraphQLTests/LanguageTests/SchemaParserTests.swift +++ b/Tests/GraphQLTests/LanguageTests/SchemaParserTests.swift @@ -111,6 +111,63 @@ class SchemaParserTests: XCTestCase { XCTAssert(result == expected) } + func testParsesTypeWithDescriptionString() throws { + let doc = try parse(source: """ + "Description" + type Hello { + world: String + } + """) + + let type = try XCTUnwrap(doc.definitions[0] as? ObjectTypeDefinition) + + XCTAssertEqual( + type.description?.value, + "Description" + ) + } + + func testParsesTypeWithDescriptionMultiLineString() throws { + let doc = try parse(source: #""" + """ + Description + """ + # Even with comments between them + type Hello { + world: String + } + """#) + + let type = try XCTUnwrap(doc.definitions[0] as? ObjectTypeDefinition) + + XCTAssertEqual( + type.description?.value, + "Description" + ) + } + + func testParsesSchemaWithDescriptionMultiLineString() throws { + let doc = try parse(source: """ + "Description" + schema { + query: Foo + } + """) + + let type = try XCTUnwrap(doc.definitions[0] as? SchemaDefinition) + + XCTAssertEqual( + type.description?.value, + "Description" + ) + } + + func testDescriptionFollowedBySomethingOtherThanTypeSystemDefinitionThrows() throws { + XCTAssertThrowsError( + try parse(source: #""Description" 1"#) + ) + } + func testSchemeExtension() throws { // Based on Apollo Federation example schema: https://github.com/apollographql/apollo-federation-subgraph-compatibility/blob/main/COMPATIBILITY.md#products-schema-to-be-implemented-by-library-maintainers let source = From a1871c451b8afe27f23187f35f5aa4be3ac82689 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Sat, 23 Dec 2023 23:57:09 -0600 Subject: [PATCH 2/8] fix: Fixes description printing --- Sources/GraphQL/Language/BlockString.swift | 60 +++++++++++++++ .../GraphQL/Language/CharacterClasses.swift | 14 ++++ Sources/GraphQL/Language/PrintString.swift | 43 +++++++++++ Sources/GraphQL/Language/Printer.swift | 3 +- .../LanguageTests/BlockStringTests.swift | 75 +++++++++++++++++++ .../LanguageTests/PrintStringTests.swift | 75 +++++++++++++++++++ 6 files changed, 268 insertions(+), 2 deletions(-) create mode 100644 Sources/GraphQL/Language/BlockString.swift create mode 100644 Sources/GraphQL/Language/CharacterClasses.swift create mode 100644 Sources/GraphQL/Language/PrintString.swift create mode 100644 Tests/GraphQLTests/LanguageTests/BlockStringTests.swift create mode 100644 Tests/GraphQLTests/LanguageTests/PrintStringTests.swift diff --git a/Sources/GraphQL/Language/BlockString.swift b/Sources/GraphQL/Language/BlockString.swift new file mode 100644 index 00000000..1a819b40 --- /dev/null +++ b/Sources/GraphQL/Language/BlockString.swift @@ -0,0 +1,60 @@ +import Foundation + +/** + * Print a block string in the indented block form by adding a leading and + * trailing blank line. However, if a block string starts with whitespace and is + * a single-line, adding a leading blank line would strip that whitespace. + * + * @internal + */ +func printBlockString( + _ value: String, + minimize: Bool = false +) -> String { + let escapedValue = value.replacingOccurrences(of: "\"\"\"", with: "\\\"\"\"") + + // Expand a block string's raw value into independent lines. + let lines = splitLines(string: escapedValue) + let isSingleLine = lines.count == 1 + + // If common indentation is found we can fix some of those cases by adding leading new line + let forceLeadingNewLine = + lines.count > 1 && + lines[1 ... (lines.count - 1)].allSatisfy { line in + line.count == 0 || isWhiteSpace(line.charCode(at: 0)) + } + + // Trailing triple quotes just looks confusing but doesn't force trailing new line + let hasTrailingTripleQuotes = escapedValue.hasSuffix("\\\"\"\"") + + // Trailing quote (single or double) or slash forces trailing new line + let hasTrailingQuote = value.hasSuffix("\"") && !hasTrailingTripleQuotes + let hasTrailingSlash = value.hasSuffix("\\") + let forceTrailingNewline = hasTrailingQuote || hasTrailingSlash + + let printAsMultipleLines = + !minimize && + // add leading and trailing new lines only if it improves readability + ( + !isSingleLine || + value.count > 70 || + forceTrailingNewline || + forceLeadingNewLine || + hasTrailingTripleQuotes + ) + + var result = "" + + // Format a multi-line block quote to account for leading space. + let skipLeadingNewLine = isSingleLine && isWhiteSpace(value.charCode(at: 0)) + if (printAsMultipleLines && !skipLeadingNewLine) || forceLeadingNewLine { + result += "\n" + } + + result += escapedValue + if printAsMultipleLines || forceTrailingNewline { + result += "\n" + } + + return "\"\"\"" + result + "\"\"\"" +} diff --git a/Sources/GraphQL/Language/CharacterClasses.swift b/Sources/GraphQL/Language/CharacterClasses.swift new file mode 100644 index 00000000..e0b8a84e --- /dev/null +++ b/Sources/GraphQL/Language/CharacterClasses.swift @@ -0,0 +1,14 @@ +/** + * ``` + * WhiteSpace :: + * - "Horizontal Tab (U+0009)" + * - "Space (U+0020)" + * ``` + * @internal + */ +func isWhiteSpace(_ code: UInt8?) -> Bool { + guard let code = code else { + return false + } + return code == 0x0009 || code == 0x0020 +} diff --git a/Sources/GraphQL/Language/PrintString.swift b/Sources/GraphQL/Language/PrintString.swift new file mode 100644 index 00000000..6b5c6389 --- /dev/null +++ b/Sources/GraphQL/Language/PrintString.swift @@ -0,0 +1,43 @@ +import Foundation + +/** + * Prints a string as a GraphQL StringValue literal. Replaces control characters + * and excluded characters (" U+0022 and \\ U+005C) with escape sequences. + */ +func printString(_ str: String) -> String { + let replacedString = str.unicodeScalars.map { char in + if + char.value <= 0x1F || // \x00-\x1f + char.value == 0x22 || // \x22 + char.value == 0x5C || // \x5c + (char.value >= 0x7F && char.value <= 0x9F) // \x7f-\x9f + { + return escapeSequences[Int(char.value)] + } + return String(char) + }.joined() + return "\"\(replacedString)\"" +} + +let escapeSequences = [ + "\\u0000", "\\u0001", "\\u0002", "\\u0003", "\\u0004", "\\u0005", "\\u0006", "\\u0007", + "\\b", "\\t", "\\n", "\\u000B", "\\f", "\\r", "\\u000E", "\\u000F", + "\\u0010", "\\u0011", "\\u0012", "\\u0013", "\\u0014", "\\u0015", "\\u0016", "\\u0017", + "\\u0018", "\\u0019", "\\u001A", "\\u001B", "\\u001C", "\\u001D", "\\u001E", "\\u001F", + "", "", "\\\"", "", "", "", "", "", + "", "", "", "", "", "", "", "", // 2F + "", "", "", "", "", "", "", "", + "", "", "", "", "", "", "", "", // 3F + "", "", "", "", "", "", "", "", + "", "", "", "", "", "", "", "", // 4F + "", "", "", "", "", "", "", "", + "", "", "", "", "\\\\", "", "", "", // 5F + "", "", "", "", "", "", "", "", + "", "", "", "", "", "", "", "", // 6F + "", "", "", "", "", "", "", "", + "", "", "", "", "", "", "", "\\u007F", + "\\u0080", "\\u0081", "\\u0082", "\\u0083", "\\u0084", "\\u0085", "\\u0086", "\\u0087", + "\\u0088", "\\u0089", "\\u008A", "\\u008B", "\\u008C", "\\u008D", "\\u008E", "\\u008F", + "\\u0090", "\\u0091", "\\u0092", "\\u0093", "\\u0094", "\\u0095", "\\u0096", "\\u0097", + "\\u0098", "\\u0099", "\\u009A", "\\u009B", "\\u009C", "\\u009D", "\\u009E", "\\u009F", +] diff --git a/Sources/GraphQL/Language/Printer.swift b/Sources/GraphQL/Language/Printer.swift index 4807a62e..a91d85a6 100644 --- a/Sources/GraphQL/Language/Printer.swift +++ b/Sources/GraphQL/Language/Printer.swift @@ -141,8 +141,7 @@ extension FloatValue: Printable { extension StringValue: Printable { var printed: String { - block == true ? value : "\"\(value)\"" - // TODO: isBlockString === true ? printBlockString(value) : printString(value), + block == true ? printBlockString(value) : printString(value) } } diff --git a/Tests/GraphQLTests/LanguageTests/BlockStringTests.swift b/Tests/GraphQLTests/LanguageTests/BlockStringTests.swift new file mode 100644 index 00000000..1897de5d --- /dev/null +++ b/Tests/GraphQLTests/LanguageTests/BlockStringTests.swift @@ -0,0 +1,75 @@ +@testable import GraphQL +import XCTest + +class PrintBlockStringTests: XCTestCase { + func testDoesNotEscapeCharacters() { + let str = "\" \\ / \n \r \t" + XCTAssertEqual(printBlockString(str), "\"\"\"\n" + str + "\n\"\"\"") + XCTAssertEqual(printBlockString(str, minimize: true), "\"\"\"\n" + str + "\"\"\"") + } + + func testByDefaultPrintBlockStringsAsSingleLine() { + XCTAssertEqual(printBlockString("one liner"), "\"\"\"one liner\"\"\"") + } + + func testByDefaultPrintBlockStringsEndingWithTripleQuotationAsMultiLine() { + let str = "triple quotation \"\"\"" + XCTAssertEqual(printBlockString(str), "\"\"\"\ntriple quotation \\\"\"\"\n\"\"\"") + XCTAssertEqual( + printBlockString(str, minimize: true), + "\"\"\"triple quotation \\\"\"\"\"\"\"" + ) + } + + func testCorrectlyPrintsSingleLineWithLeadingSpace() { + XCTAssertEqual( + printBlockString(" space-led value \"quoted string\""), + "\"\"\" space-led value \"quoted string\"\n\"\"\"" + ) + } + + func testCorrectlyPrintsSingleLineWithTrailingBackslash() { + let str = "backslash \\" + XCTAssertEqual(printBlockString(str), "\"\"\"\nbackslash \\\n\"\"\"") + XCTAssertEqual(printBlockString(str, minimize: true), "\"\"\"backslash \\\n\"\"\"") + } + + func testCorrectlyPrintsMultiLineWithInternalIndent() { + let str = "no indent\n with indent" + XCTAssertEqual(printBlockString(str), "\"\"\"\nno indent\n with indent\n\"\"\"") + XCTAssertEqual( + printBlockString(str, minimize: true), + "\"\"\"\nno indent\n with indent\"\"\"" + ) + } + + func testCorrectlyPrintsStringWithAFirstLineIndentation() { + let str = [ + " first ", + " line ", + "indentation", + " string", + ].joined(separator: "\n") + + XCTAssertEqual( + printBlockString(str), + [ + "\"\"\"", + " first ", + " line ", + "indentation", + " string", + "\"\"\"", + ].joined(separator: "\n") + ) + XCTAssertEqual( + printBlockString(str, minimize: true), + [ + "\"\"\" first ", + " line ", + "indentation", + " string\"\"\"", + ].joined(separator: "\n") + ) + } +} diff --git a/Tests/GraphQLTests/LanguageTests/PrintStringTests.swift b/Tests/GraphQLTests/LanguageTests/PrintStringTests.swift new file mode 100644 index 00000000..db06b4ce --- /dev/null +++ b/Tests/GraphQLTests/LanguageTests/PrintStringTests.swift @@ -0,0 +1,75 @@ +@testable import GraphQL +import XCTest + +class PrintStringTests: XCTestCase { + func testPrintsASimpleString() { + XCTAssertEqual(printString("hello world"), "\"hello world\"") + } + + func testEscapesQutoes() { + XCTAssertEqual(printString("\"hello world\""), "\"\\\"hello world\\\"\"") + } + + func testDoesNotEscapeSingleQuote() { + XCTAssertEqual(printString("who's test"), "\"who's test\"") + } + + func testEscapesBackslashes() { + XCTAssertEqual(printString("escape: \\"), "\"escape: \\\\\"") + } + + func testEscapesWellKnownControlChars() { + XCTAssertEqual(printString("\n\r\t"), "\"\\n\\r\\t\"") + } + + func testEscapesZeroByte() { + XCTAssertEqual(printString("\u{0000}"), "\"\\u0000\"") + } + + func testDoesNotEscapeSpace() { + XCTAssertEqual(printString(" "), "\" \"") + } + + // TODO: We only support UTF8 + func testDoesNotEscapeSupplementaryCharacter() { + XCTAssertEqual(printString("\u{1f600}"), "\"\u{1f600}\"") + } + + func testEscapesAllControlChars() { + XCTAssertEqual( + printString( + "\u{0000}\u{0001}\u{0002}\u{0003}\u{0004}\u{0005}\u{0006}\u{0007}" + + "\u{0008}\u{0009}\u{000A}\u{000B}\u{000C}\u{000D}\u{000E}\u{000F}" + + "\u{0010}\u{0011}\u{0012}\u{0013}\u{0014}\u{0015}\u{0016}\u{0017}" + + "\u{0018}\u{0019}\u{001A}\u{001B}\u{001C}\u{001D}\u{001E}\u{001F}" + + "\u{0020}\u{0021}\u{0022}\u{0023}\u{0024}\u{0025}\u{0026}\u{0027}" + + "\u{0028}\u{0029}\u{002A}\u{002B}\u{002C}\u{002D}\u{002E}\u{002F}" + + "\u{0030}\u{0031}\u{0032}\u{0033}\u{0034}\u{0035}\u{0036}\u{0037}" + + "\u{0038}\u{0039}\u{003A}\u{003B}\u{003C}\u{003D}\u{003E}\u{003F}" + + "\u{0040}\u{0041}\u{0042}\u{0043}\u{0044}\u{0045}\u{0046}\u{0047}" + + "\u{0048}\u{0049}\u{004A}\u{004B}\u{004C}\u{004D}\u{004E}\u{004F}" + + "\u{0050}\u{0051}\u{0052}\u{0053}\u{0054}\u{0055}\u{0056}\u{0057}" + + "\u{0058}\u{0059}\u{005A}\u{005B}\u{005C}\u{005D}\u{005E}\u{005F}" + + "\u{0060}\u{0061}\u{0062}\u{0063}\u{0064}\u{0065}\u{0066}\u{0067}" + + "\u{0068}\u{0069}\u{006A}\u{006B}\u{006C}\u{006D}\u{006E}\u{006F}" + + "\u{0070}\u{0071}\u{0072}\u{0073}\u{0074}\u{0075}\u{0076}\u{0077}" + + "\u{0078}\u{0079}\u{007A}\u{007B}\u{007C}\u{007D}\u{007E}\u{007F}" + + "\u{0080}\u{0081}\u{0082}\u{0083}\u{0084}\u{0085}\u{0086}\u{0087}" + + "\u{0088}\u{0089}\u{008A}\u{008B}\u{008C}\u{008D}\u{008E}\u{008F}" + + "\u{0090}\u{0091}\u{0092}\u{0093}\u{0094}\u{0095}\u{0096}\u{0097}" + + "\u{0098}\u{0099}\u{009A}\u{009B}\u{009C}\u{009D}\u{009E}\u{009F}" + ), + "\"\\u0000\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007" + + "\\b\\t\\n\\u000B\\f\\r\\u000E\\u000F" + + "\\u0010\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017" + + "\\u0018\\u0019\\u001A\\u001B\\u001C\\u001D\\u001E\\u001F" + + " !\\\"#$%&\'()*+,-./0123456789:;<=>?" + + "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_" + + "`abcdefghijklmnopqrstuvwxyz{|}~\\u007F" + + "\\u0080\\u0081\\u0082\\u0083\\u0084\\u0085\\u0086\\u0087" + + "\\u0088\\u0089\\u008A\\u008B\\u008C\\u008D\\u008E\\u008F" + + "\\u0090\\u0091\\u0092\\u0093\\u0094\\u0095\\u0096\\u0097" + + "\\u0098\\u0099\\u009A\\u009B\\u009C\\u009D\\u009E\\u009F\"" + ) + } +} From e6d3e56c9f1c10a2792b323bd5c70e3f29e726ed Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Mon, 25 Dec 2023 14:10:14 -0600 Subject: [PATCH 3/8] fix: Schema extension parser captures operations --- Sources/GraphQL/Language/Parser.swift | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/Sources/GraphQL/Language/Parser.swift b/Sources/GraphQL/Language/Parser.swift index 6604c0dd..42e3e446 100644 --- a/Sources/GraphQL/Language/Parser.swift +++ b/Sources/GraphQL/Language/Parser.swift @@ -1038,16 +1038,28 @@ func parseTypeExtensionDefinition(lexer: Lexer) throws -> TypeExtensionDefinitio func parseSchemaExtensionDefinition(lexer: Lexer) throws -> SchemaExtensionDefinition { let start = lexer.token try expectKeyword(lexer: lexer, value: "extend") - let description = try parseDescription(lexer: lexer) try expectKeyword(lexer: lexer, value: "schema") let directives = try parseDirectives(lexer: lexer) + let operationTypes = try optionalMany( + lexer: lexer, + openKind: .openingBrace, + closeKind: .closingBrace, + parse: parseOperationTypeDefinition + ) + if directives.isEmpty, operationTypes.isEmpty { + throw syntaxError( + source: lexer.source, + position: lexer.token.start, + description: "expected schema extend to have directive or operation" + ) + } return SchemaExtensionDefinition( loc: loc(lexer: lexer, startToken: start), definition: SchemaDefinition( loc: loc(lexer: lexer, startToken: start), - description: description, + description: nil, directives: directives, - operationTypes: [] + operationTypes: operationTypes ) ) } From 2ac84b220ac18e268b0bbceea5ffedc1dabe637a Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Sun, 24 Dec 2023 23:13:03 -0600 Subject: [PATCH 4/8] test: Adds schema printer tests --- .../LanguageTests/SchemaPrinterTests.swift | 174 ++++++++++++++++++ .../LanguageTests/schema-kitchen-sink.graphql | 14 +- 2 files changed, 181 insertions(+), 7 deletions(-) create mode 100644 Tests/GraphQLTests/LanguageTests/SchemaPrinterTests.swift diff --git a/Tests/GraphQLTests/LanguageTests/SchemaPrinterTests.swift b/Tests/GraphQLTests/LanguageTests/SchemaPrinterTests.swift new file mode 100644 index 00000000..234a3bdf --- /dev/null +++ b/Tests/GraphQLTests/LanguageTests/SchemaPrinterTests.swift @@ -0,0 +1,174 @@ +@testable import GraphQL +import XCTest + +class SchemaPrinterTests: XCTestCase { + func testPrintsMinimalAST() { + let ast = ScalarTypeDefinition( + name: .init(value: "foo") + ) + XCTAssertEqual(print(ast: ast), "scalar foo") + } + + func testPrintsKitchenSinkWithoutAlteringAST() throws { + guard + let url = Bundle.module.url( + forResource: "schema-kitchen-sink", + withExtension: "graphql" + ), + let kitchenSink = try? String(contentsOf: url) + else { + XCTFail("Could not load kitchen sink") + return + } + + let ast = try parse(source: kitchenSink, noLocation: true) + + let printed = print(ast: ast) + let printedAST = try parse(source: printed, noLocation: true) + + XCTAssertEqual(printed, print(ast: printedAST)) + XCTAssertEqual(printed, #""" + """This is a description of the schema as a whole.""" + schema { + query: QueryType + mutation: MutationType + } + + """ + This is a description + of the `Foo` type. + """ + type Foo implements Bar & Baz & Two { + "Description of the `one` field." + one: Type + """This is a description of the `two` field.""" + two( + """This is a description of the `argument` argument.""" + argument: InputType! + ): Type + """This is a description of the `three` field.""" + three(argument: InputType, other: String): Int + four(argument: String = "string"): String + five(argument: [String] = ["string", "string"]): String + six(argument: InputType = { key: "value" }): Type + seven(argument: Int = null): Type + eight(argument: OneOfInputType): Type + } + + type AnnotatedObject @onObject(arg: "value") { + annotatedField(arg: Type = "default" @onArgumentDefinition): Type @onField + } + + type UndefinedType + + extend type Foo { + seven(argument: [String]): Type + } + + extend type Foo @onType + + interface Bar { + one: Type + four(argument: String = "string"): String + } + + interface AnnotatedInterface @onInterface { + annotatedField(arg: Type @onArgumentDefinition): Type @onField + } + + interface UndefinedInterface + + extend interface Bar implements Two { + two(argument: InputType!): Type + } + + extend interface Bar @onInterface + + interface Baz implements Bar & Two { + one: Type + two(argument: InputType!): Type + four(argument: String = "string"): String + } + + union Feed = Story | Article | Advert + + union AnnotatedUnion @onUnion = A | B + + union AnnotatedUnionTwo @onUnion = A | B + + union UndefinedUnion + + extend union Feed = Photo | Video + + extend union Feed @onUnion + + scalar CustomScalar + + scalar AnnotatedScalar @onScalar + + extend scalar CustomScalar @onScalar + + enum Site { + """This is a description of the `DESKTOP` value""" + DESKTOP + """This is a description of the `MOBILE` value""" + MOBILE + "This is a description of the `WEB` value" + WEB + } + + enum AnnotatedEnum @onEnum { + ANNOTATED_VALUE @onEnumValue + OTHER_VALUE + } + + enum UndefinedEnum + + extend enum Site { + VR + } + + extend enum Site @onEnum + + input InputType { + key: String! + answer: Int = 42 + } + + input OneOfInputType @oneOf { + string: String + int: Int + } + + input AnnotatedInput @onInputObject { + annotatedField: Type @onInputFieldDefinition + } + + input UndefinedInput + + extend input InputType { + other: Float = 1.23e4 @onInputFieldDefinition + } + + extend input InputType @onInputObject + + """This is a description of the `@skip` directive""" + directive @skip( + """This is a description of the `if` argument""" + if: Boolean! @onArgumentDefinition + ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + + directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + + directive @include2(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + + directive @myRepeatableDir(name: String!) repeatable on OBJECT | INTERFACE + + extend schema @onSchema + + extend schema @onSchema { + subscription: SubscriptionType + } + """#) + } +} diff --git a/Tests/GraphQLTests/LanguageTests/schema-kitchen-sink.graphql b/Tests/GraphQLTests/LanguageTests/schema-kitchen-sink.graphql index 82145538..c1d9d06e 100644 --- a/Tests/GraphQLTests/LanguageTests/schema-kitchen-sink.graphql +++ b/Tests/GraphQLTests/LanguageTests/schema-kitchen-sink.graphql @@ -1,10 +1,4 @@ -# Copyright (c) 2015, Facebook, Inc. -# All rights reserved. -# -# This source code is licensed under the BSD-style license found in the -# LICENSE file in the root directory of this source tree. An additional grant -# of patent rights can be found in the PATENTS file in the same directory. - +"""This is a description of the schema as a whole.""" schema { query: QueryType mutation: MutationType @@ -32,6 +26,7 @@ type Foo implements Bar & Baz & Two { five(argument: [String] = ["string", "string"]): String six(argument: InputType = {key: "value"}): Type seven(argument: Int = null): Type + eight(argument: OneOfInputType): Type } type AnnotatedObject @onObject(arg: "value") { @@ -121,6 +116,11 @@ input InputType { answer: Int = 42 } +input OneOfInputType @oneOf { + string: String + int: Int +} + input AnnotatedInput @onInputObject { annotatedField: Type @onInputFieldDefinition } From 3f0be1dd6f42f2495a39eeba23d37616f6d404bd Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Mon, 25 Dec 2023 15:25:35 -0600 Subject: [PATCH 5/8] fix: Parser errors on empty extends --- Sources/GraphQL/Language/Parser.swift | 132 ++++++++++++++++++++++---- 1 file changed, 115 insertions(+), 17 deletions(-) diff --git a/Sources/GraphQL/Language/Parser.swift b/Sources/GraphQL/Language/Parser.swift index 42e3e446..6dc2004d 100644 --- a/Sources/GraphQL/Language/Parser.swift +++ b/Sources/GraphQL/Language/Parser.swift @@ -1025,10 +1025,31 @@ func parseExtensionDefinition(lexer: Lexer) throws -> TypeSystemDefinition { func parseTypeExtensionDefinition(lexer: Lexer) throws -> TypeExtensionDefinition { let start = lexer.token try expectKeyword(lexer: lexer, value: "extend") - let definition = try parseObjectTypeDefinition(lexer: lexer) + try expectKeyword(lexer: lexer, value: "type") + let name = try parseName(lexer: lexer) + let interfaces = try parseImplementsInterfaces(lexer: lexer) + let directives = try parseDirectives(lexer: lexer) + let fields = try optionalMany( + lexer: lexer, + openKind: .openingBrace, + closeKind: .closingBrace, + parse: parseFieldDefinition + ) + if + interfaces.isEmpty, + directives.isEmpty, + fields.isEmpty + { + throw unexpected(lexer: lexer) + } return TypeExtensionDefinition( loc: loc(lexer: lexer, startToken: start), - definition: definition + definition: ObjectTypeDefinition( + name: name, + interfaces: interfaces, + directives: directives, + fields: fields + ) ) } @@ -1047,11 +1068,7 @@ func parseSchemaExtensionDefinition(lexer: Lexer) throws -> SchemaExtensionDefin parse: parseOperationTypeDefinition ) if directives.isEmpty, operationTypes.isEmpty { - throw syntaxError( - source: lexer.source, - position: lexer.token.start, - description: "expected schema extend to have directive or operation" - ) + throw unexpected(lexer: lexer) } return SchemaExtensionDefinition( loc: loc(lexer: lexer, startToken: start), @@ -1070,10 +1087,31 @@ func parseSchemaExtensionDefinition(lexer: Lexer) throws -> SchemaExtensionDefin func parseInterfaceExtensionDefinition(lexer: Lexer) throws -> InterfaceExtensionDefinition { let start = lexer.token try expectKeyword(lexer: lexer, value: "extend") - let interfaceDefinition = try parseInterfaceTypeDefinition(lexer: lexer) + try expectKeyword(lexer: lexer, value: "interface") + let name = try parseName(lexer: lexer) + let interfaces = try parseImplementsInterfaces(lexer: lexer) + let directives = try parseDirectives(lexer: lexer) + let fields = try optionalMany( + lexer: lexer, + openKind: .openingBrace, + closeKind: .closingBrace, + parse: parseFieldDefinition + ) + if + interfaces.isEmpty, + directives.isEmpty, + fields.isEmpty + { + throw unexpected(lexer: lexer) + } return InterfaceExtensionDefinition( loc: loc(lexer: lexer, startToken: start), - definition: interfaceDefinition + definition: InterfaceTypeDefinition( + name: name, + interfaces: interfaces, + directives: directives, + fields: fields + ) ) } @@ -1083,10 +1121,18 @@ func parseInterfaceExtensionDefinition(lexer: Lexer) throws -> InterfaceExtensio func parseScalarExtensionDefinition(lexer: Lexer) throws -> ScalarExtensionDefinition { let start = lexer.token try expectKeyword(lexer: lexer, value: "extend") - let scalarDefinition = try parseScalarTypeDefinition(lexer: lexer) + try expectKeyword(lexer: lexer, value: "scalar") + let name = try parseName(lexer: lexer) + let directives = try parseDirectives(lexer: lexer) + if (directives.isEmpty) { + throw unexpected(lexer: lexer) + } return ScalarExtensionDefinition( loc: loc(lexer: lexer, startToken: start), - definition: scalarDefinition + definition: ScalarTypeDefinition( + name: name, + directives: directives + ) ) } @@ -1096,10 +1142,24 @@ func parseScalarExtensionDefinition(lexer: Lexer) throws -> ScalarExtensionDefin func parseUnionExtensionDefinition(lexer: Lexer) throws -> UnionExtensionDefinition { let start = lexer.token try expectKeyword(lexer: lexer, value: "extend") - let definition = try parseUnionTypeDefinition(lexer: lexer) + try expectKeyword(lexer: lexer, value: "union") + let name = try parseName(lexer: lexer) + let directives = try parseDirectives(lexer: lexer) + let types = try parseUnionMembers(lexer: lexer) + if + directives.isEmpty, + types.isEmpty + { + throw unexpected(lexer: lexer) + } return UnionExtensionDefinition( loc: loc(lexer: lexer, startToken: start), - definition: definition + definition: UnionTypeDefinition( + loc: loc(lexer: lexer, startToken: start), + name: name, + directives: directives, + types: types + ) ) } @@ -1109,10 +1169,29 @@ func parseUnionExtensionDefinition(lexer: Lexer) throws -> UnionExtensionDefinit func parseEnumExtensionDefinition(lexer: Lexer) throws -> EnumExtensionDefinition { let start = lexer.token try expectKeyword(lexer: lexer, value: "extend") - let definition = try parseEnumTypeDefinition(lexer: lexer) + try expectKeyword(lexer: lexer, value: "enum") + let name = try parseName(lexer: lexer) + let directives = try parseDirectives(lexer: lexer) + let values = try optionalMany( + lexer: lexer, + openKind: .openingBrace, + closeKind: .closingBrace, + parse: parseEnumValueDefinition + ) + if + directives.isEmpty, + values.isEmpty + { + throw unexpected(lexer: lexer) + } return EnumExtensionDefinition( loc: loc(lexer: lexer, startToken: start), - definition: definition + definition: EnumTypeDefinition( + loc: loc(lexer: lexer, startToken: start), + name: name, + directives: directives, + values: values + ) ) } @@ -1122,10 +1201,29 @@ func parseEnumExtensionDefinition(lexer: Lexer) throws -> EnumExtensionDefinitio func parseInputObjectExtensionDefinition(lexer: Lexer) throws -> InputObjectExtensionDefinition { let start = lexer.token try expectKeyword(lexer: lexer, value: "extend") - let definition = try parseInputObjectTypeDefinition(lexer: lexer) + try expectKeyword(lexer: lexer, value: "input") + let name = try parseName(lexer: lexer) + let directives = try parseDirectives(lexer: lexer) + let fields = try optionalMany( + lexer: lexer, + openKind: .openingBrace, + closeKind: .closingBrace, + parse: parseInputValueDef + ) + if + directives.isEmpty, + fields.isEmpty + { + throw unexpected(lexer: lexer) + } return InputObjectExtensionDefinition( loc: loc(lexer: lexer, startToken: start), - definition: definition + definition: InputObjectTypeDefinition( + loc: loc(lexer: lexer, startToken: start), + name: name, + directives: directives, + fields: fields + ) ) } From 88b71a0400eb90e07242773a43b81a42ca39507c Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Mon, 25 Dec 2023 15:25:56 -0600 Subject: [PATCH 6/8] test: Adds extends schema parser tests --- .../LanguageTests/SchemaParserTests.swift | 194 +++++++++++++++--- 1 file changed, 170 insertions(+), 24 deletions(-) diff --git a/Tests/GraphQLTests/LanguageTests/SchemaParserTests.swift b/Tests/GraphQLTests/LanguageTests/SchemaParserTests.swift index d5c7f93a..618be62a 100644 --- a/Tests/GraphQLTests/LanguageTests/SchemaParserTests.swift +++ b/Tests/GraphQLTests/LanguageTests/SchemaParserTests.swift @@ -88,29 +88,6 @@ class SchemaParserTests: XCTestCase { XCTAssert(result == expected) } - func testSimpleExtension() throws { - let source = "extend type Hello { world: String }" - - let expected = Document( - definitions: [ - TypeExtensionDefinition( - definition: ObjectTypeDefinition( - name: nameNode("Hello"), - fields: [ - fieldNode( - nameNode("world"), - typeNode("String") - ), - ] - ) - ), - ] - ) - - let result = try parse(source: source) - XCTAssert(result == expected) - } - func testParsesTypeWithDescriptionString() throws { let doc = try parse(source: """ "Description" @@ -168,6 +145,172 @@ class SchemaParserTests: XCTestCase { ) } + func testSimpleExtension() throws { + let source = "extend type Hello { world: String }" + + let expected = Document( + definitions: [ + TypeExtensionDefinition( + definition: ObjectTypeDefinition( + name: nameNode("Hello"), + fields: [ + fieldNode( + nameNode("world"), + typeNode("String") + ), + ] + ) + ), + ] + ) + + let result = try parse(source: source) + XCTAssert(result == expected) + } + + func testObjectExtensionWithoutFields() throws { + XCTAssertEqual( + try parse(source: "extend type Hello implements Greeting"), + Document( + definitions: [ + TypeExtensionDefinition( + definition: ObjectTypeDefinition( + name: nameNode("Hello"), + interfaces: [typeNode("Greeting")], + directives: [], + fields: [] + ) + ), + ] + ) + ) + } + + func testInterfaceExtensionWithoutFields() throws { + XCTAssertEqual( + try parse(source: "extend interface Hello implements Greeting"), + Document( + definitions: [ + InterfaceExtensionDefinition( + definition: InterfaceTypeDefinition( + name: nameNode("Hello"), + interfaces: [typeNode("Greeting")], + directives: [], + fields: [] + ) + ), + ] + ) + ) + } + + func testObjectExtensionWithoutFieldsFollowedByExtension() throws { + XCTAssertEqual( + try parse(source: """ + extend type Hello implements Greeting + + extend type Hello implements SecondGreeting + """), + Document( + definitions: [ + TypeExtensionDefinition( + definition: ObjectTypeDefinition( + name: nameNode("Hello"), + interfaces: [typeNode("Greeting")], + directives: [], + fields: [] + ) + ), + TypeExtensionDefinition( + definition: ObjectTypeDefinition( + name: nameNode("Hello"), + interfaces: [typeNode("SecondGreeting")], + directives: [], + fields: [] + ) + ), + ] + ) + ) + } + + func testExtensionWithoutAnythingThrows() throws { + try XCTAssertThrowsError(parse(source: "extend scalar Hello")) + try XCTAssertThrowsError(parse(source: "extend type Hello")) + try XCTAssertThrowsError(parse(source: "extend interface Hello")) + try XCTAssertThrowsError(parse(source: "extend union Hello")) + try XCTAssertThrowsError(parse(source: "extend enum Hello")) + try XCTAssertThrowsError(parse(source: "extend input Hello")) + } + + func testInterfaceExtensionWithoutFieldsFollowedByExtension() throws { + XCTAssertEqual( + try parse(source: """ + extend interface Hello implements Greeting + + extend interface Hello implements SecondGreeting + """), + Document( + definitions: [ + InterfaceExtensionDefinition( + definition: InterfaceTypeDefinition( + name: nameNode("Hello"), + interfaces: [typeNode("Greeting")], + directives: [], + fields: [] + ) + ), + InterfaceExtensionDefinition( + definition: InterfaceTypeDefinition( + name: nameNode("Hello"), + interfaces: [typeNode("SecondGreeting")], + directives: [], + fields: [] + ) + ), + ] + ) + ) + } + + func testObjectExtensionDoNotIncludeDescriptions() throws { + XCTAssertThrowsError( + try parse(source: """ + "Description" + extend type Hello { + world: String + } + """) + ) + + XCTAssertThrowsError( + try parse(source: """ + extend "Description" type Hello { + world: String + } + """) + ) + } + + func testInterfaceExtensionDoNotIncludeDescriptions() throws { + XCTAssertThrowsError( + try parse(source: """ + "Description" + extend interface Hello { + world: String + } + """) + ) + + XCTAssertThrowsError( + try parse(source: """ + extend "Description" interface Hello { + world: String + } + """) + ) + } + func testSchemeExtension() throws { // Based on Apollo Federation example schema: https://github.com/apollographql/apollo-federation-subgraph-compatibility/blob/main/COMPATIBILITY.md#products-schema-to-be-implemented-by-library-maintainers let source = @@ -1084,13 +1227,16 @@ class SchemaParserTests: XCTestCase { } func testInputExtension() throws { - let source = #"extend input InputType"# + let source = #"extend input InputType @include"# let expected = Document( definitions: [ InputObjectExtensionDefinition( definition: InputObjectTypeDefinition( name: nameNode("InputType"), + directives: [ + Directive(name: Name(value: "include")), + ], fields: [] ) ), From 06d0266c8f7e89cac5fed09b7e9014873aa395bc Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Mon, 25 Dec 2023 15:52:10 -0600 Subject: [PATCH 7/8] fix: Parser errors when disallowed description provided --- Sources/GraphQL/Language/Parser.swift | 92 +++++++++++---------------- 1 file changed, 36 insertions(+), 56 deletions(-) diff --git a/Sources/GraphQL/Language/Parser.swift b/Sources/GraphQL/Language/Parser.swift index 6dc2004d..21b1eec5 100644 --- a/Sources/GraphQL/Language/Parser.swift +++ b/Sources/GraphQL/Language/Parser.swift @@ -173,24 +173,45 @@ func parseDefinition(lexer: Lexer) throws -> Definition { return try parseOperationDefinition(lexer: lexer) } - if peek(lexer: lexer, kind: .name) { - guard let value = lexer.token.value else { + // Many definitions begin with a description and require a lookahead. + let hasDescription = peekDescription(lexer: lexer) + let keywordToken = hasDescription + ? try lexer.lookahead() + : lexer.token + + if keywordToken.kind == .name { + guard let value = keywordToken.value else { throw GraphQLError(message: "Expected name token to have value: \(lexer.token)") } + switch value { - case "query", "mutation", "subscription": - return try parseOperationDefinition(lexer: lexer) - case "fragment": - return try parseFragmentDefinition(lexer: lexer) - // Note: the Type System IDL is an experimental non-spec addition. - case "schema", "scalar", "type", "interface", "union", "enum", "input", "extend", - "directive": - return try parseTypeSystemDefinition(lexer: lexer) + case "schema": return try parseSchemaDefinition(lexer: lexer) + case "scalar": return try parseScalarTypeDefinition(lexer: lexer) + case "type": return try parseObjectTypeDefinition(lexer: lexer) + case "interface": return try parseInterfaceTypeDefinition(lexer: lexer) + case "union": return try parseUnionTypeDefinition(lexer: lexer) + case "enum": return try parseEnumTypeDefinition(lexer: lexer) + case "input": return try parseInputObjectTypeDefinition(lexer: lexer) + case "directive": return try parseDirectiveDefinition(lexer: lexer) default: - break + if hasDescription { + throw syntaxError( + source: lexer.source, + position: lexer.token.start, + description: "Unexpected description, descriptions are supported only on type definitions." + ) + } + switch value { + case "query", "mutation", "subscription": + return try parseOperationDefinition(lexer: lexer) + case "fragment": + return try parseFragmentDefinition(lexer: lexer) + case "extend": + return try parseExtensionDefinition(lexer: lexer) + default: + break + } } - } else if peekDescription(lexer: lexer) { - return try parseTypeSystemDefinition(lexer: lexer) } throw unexpected(lexer: lexer) @@ -675,47 +696,6 @@ func parseNamedType(lexer: Lexer) throws -> NamedType { // Implements the parsing rules in the Type Definition section. -/** - * TypeSystemDefinition : - * - SchemaDefinition - * - TypeDefinition - * - TypeExtensionDefinition - * - DirectiveDefinition - * - * TypeDefinition : - * - ScalarTypeDefinition - * - ObjectTypeDefinition - * - InterfaceTypeDefinition - * - UnionTypeDefinition - * - EnumTypeDefinition - * - InputObjectTypeDefinition - */ -func parseTypeSystemDefinition(lexer: Lexer) throws -> TypeSystemDefinition { - let keywordToken = peekDescription(lexer: lexer) - ? try lexer.lookahead() - : lexer.token - - if keywordToken.kind == .name { - guard let value = keywordToken.value else { - throw GraphQLError(message: "Expected keyword token to have value: \(keywordToken)") - } - switch value { - case "schema": return try parseSchemaDefinition(lexer: lexer) - case "scalar": return try parseScalarTypeDefinition(lexer: lexer) - case "type": return try parseObjectTypeDefinition(lexer: lexer) - case "interface": return try parseInterfaceTypeDefinition(lexer: lexer) - case "union": return try parseUnionTypeDefinition(lexer: lexer) - case "enum": return try parseEnumTypeDefinition(lexer: lexer) - case "input": return try parseInputObjectTypeDefinition(lexer: lexer) - case "extend": return try parseExtensionDefinition(lexer: lexer) - case "directive": return try parseDirectiveDefinition(lexer: lexer) - default: break - } - } - - throw unexpected(lexer: lexer, atToken: keywordToken) -} - /** * SchemaDefinition : schema Directives? { OperationTypeDefinition+ } * @@ -1124,8 +1104,8 @@ func parseScalarExtensionDefinition(lexer: Lexer) throws -> ScalarExtensionDefin try expectKeyword(lexer: lexer, value: "scalar") let name = try parseName(lexer: lexer) let directives = try parseDirectives(lexer: lexer) - if (directives.isEmpty) { - throw unexpected(lexer: lexer) + if directives.isEmpty { + throw unexpected(lexer: lexer) } return ScalarExtensionDefinition( loc: loc(lexer: lexer, startToken: start), From 83b2c7021774120eb0d65e6f7fb1e45c123a3c28 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Mon, 25 Dec 2023 16:07:28 -0600 Subject: [PATCH 8/8] test: Adds missing schema parser tests --- .../LanguageTests/SchemaParserTests.swift | 254 ++++++++++++++---- 1 file changed, 197 insertions(+), 57 deletions(-) diff --git a/Tests/GraphQLTests/LanguageTests/SchemaParserTests.swift b/Tests/GraphQLTests/LanguageTests/SchemaParserTests.swift index 618be62a..a84c4833 100644 --- a/Tests/GraphQLTests/LanguageTests/SchemaParserTests.swift +++ b/Tests/GraphQLTests/LanguageTests/SchemaParserTests.swift @@ -311,67 +311,59 @@ class SchemaParserTests: XCTestCase { ) } - func testSchemeExtension() throws { - // Based on Apollo Federation example schema: https://github.com/apollographql/apollo-federation-subgraph-compatibility/blob/main/COMPATIBILITY.md#products-schema-to-be-implemented-by-library-maintainers - let source = - """ - extend schema - @link( - url: "https://specs.apollo.dev/federation/v2.0", - import: [ - "@extends", - "@external", - "@key", - "@inaccessible", - "@override", - "@provides", - "@requires", - "@shareable", - "@tag" + func testSchemaExtension() throws { + XCTAssertEqual( + try parse(source: """ + extend schema { + mutation: Mutation + } + """), + Document( + definitions: [ + SchemaExtensionDefinition( + definition: SchemaDefinition( + directives: [], + operationTypes: [ + OperationTypeDefinition( + operation: .mutation, + type: .init(name: .init(value: "Mutation")) + ), + ] + ) + ), ] - ) - """ + ) + ) + } - let expected = Document( - definitions: [ - SchemaExtensionDefinition( - definition: SchemaDefinition( - directives: [ - Directive( - name: nameNode("link"), - arguments: [ - Argument( - name: nameNode("url"), - value: StringValue( - value: "https://specs.apollo.dev/federation/v2.0", - block: false - ) - ), - Argument( - name: nameNode("import"), - value: ListValue(values: [ - StringValue(value: "@extends", block: false), - StringValue(value: "@external", block: false), - StringValue(value: "@key", block: false), - StringValue(value: "@inaccessible", block: false), - StringValue(value: "@override", block: false), - StringValue(value: "@provides", block: false), - StringValue(value: "@requires", block: false), - StringValue(value: "@shareable", block: false), - StringValue(value: "@tag", block: false), - ]) - ), - ] - ), - ], - operationTypes: [] - ) - ), - ] + func testSchemaExtensionWithOnlyDirectives() throws { + XCTAssertEqual( + try parse(source: "extend schema @directive"), + Document( + definitions: [ + SchemaExtensionDefinition( + definition: SchemaDefinition( + directives: [ + Directive(name: .init(value: "directive")), + ], + operationTypes: [] + ) + ), + ] + ) ) + } - let result = try parse(source: source) - XCTAssert(result == expected) + func testSchemaExtensionWithoutAnythingThrows() throws { + XCTAssertThrowsError( + try parse(source: "extend schema") + ) + } + + func testSchemaExtensionWithInvalidOperationTypeThrows() throws { + XCTAssertThrowsError( + try parse(source: "extend schema { unknown: SomeType }") + ) } func testSimpleNonNullType() throws { @@ -397,6 +389,26 @@ class SchemaParserTests: XCTestCase { XCTAssert(result == expected) } + func testSimpleInterfaceInheritingInterface() throws { + XCTAssertEqual( + try parse(source: "interface Hello implements World { field: String }"), + Document( + definitions: [ + InterfaceTypeDefinition( + name: nameNode("Hello"), + interfaces: [typeNode("World")], + fields: [ + FieldDefinition( + name: .init(value: "field"), + type: NamedType(name: .init(value: "String")) + ), + ] + ), + ] + ) + ) + } + func testSimpleTypeInheritingInterface() throws { let source = "type Hello implements World { }" @@ -432,6 +444,71 @@ class SchemaParserTests: XCTestCase { XCTAssert(result == expected) } + func testSimpleInterfaceInheritingMultipleInterfaces() throws { + XCTAssertEqual( + try parse(source: "interface Hello implements Wo & rld { field: String }"), + Document( + definitions: [ + InterfaceTypeDefinition( + name: nameNode("Hello"), + interfaces: [ + typeNode("Wo"), + typeNode("rld"), + ], + fields: [ + FieldDefinition( + name: .init(value: "field"), + type: NamedType(name: .init(value: "String")) + ), + ] + ), + ] + ) + ) + } + + func testSimpleTypeInheritingMultipleInterfacesWithLeadingAmbersand() throws { + let source = "type Hello implements & Wo & rld { }" + + let expected = Document( + definitions: [ + ObjectTypeDefinition( + name: nameNode("Hello"), + interfaces: [ + typeNode("Wo"), + typeNode("rld"), + ] + ), + ] + ) + + let result = try parse(source: source) + XCTAssert(result == expected) + } + + func testSimpleInterfaceInheritingMultipleInterfacesWithLeadingAmbersand() throws { + XCTAssertEqual( + try parse(source: "interface Hello implements & Wo & rld { field: String }"), + Document( + definitions: [ + InterfaceTypeDefinition( + name: nameNode("Hello"), + interfaces: [ + typeNode("Wo"), + typeNode("rld"), + ], + fields: [ + FieldDefinition( + name: .init(value: "field"), + type: NamedType(name: .init(value: "String")) + ), + ] + ), + ] + ) + ) + } + func testSingleValueEnum() throws { let source = "enum Hello { WORLD }" @@ -1310,4 +1387,67 @@ class SchemaParserTests: XCTestCase { _ = try parse(source: kitchenSink) } + + func testSchemeExtension() throws { + // Based on Apollo Federation example schema: https://github.com/apollographql/apollo-federation-subgraph-compatibility/blob/main/COMPATIBILITY.md#products-schema-to-be-implemented-by-library-maintainers + let source = + """ + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.0", + import: [ + "@extends", + "@external", + "@key", + "@inaccessible", + "@override", + "@provides", + "@requires", + "@shareable", + "@tag" + ] + ) + """ + + let expected = Document( + definitions: [ + SchemaExtensionDefinition( + definition: SchemaDefinition( + directives: [ + Directive( + name: nameNode("link"), + arguments: [ + Argument( + name: nameNode("url"), + value: StringValue( + value: "https://specs.apollo.dev/federation/v2.0", + block: false + ) + ), + Argument( + name: nameNode("import"), + value: ListValue(values: [ + StringValue(value: "@extends", block: false), + StringValue(value: "@external", block: false), + StringValue(value: "@key", block: false), + StringValue(value: "@inaccessible", block: false), + StringValue(value: "@override", block: false), + StringValue(value: "@provides", block: false), + StringValue(value: "@requires", block: false), + StringValue(value: "@shareable", block: false), + StringValue(value: "@tag", block: false), + ]) + ), + ] + ), + ], + operationTypes: [] + ) + ), + ] + ) + + let result = try parse(source: source) + XCTAssert(result == expected) + } }