diff --git a/docs/reference.md b/docs/reference.md index 7cdc37fca..14c24d7ba 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -696,6 +696,14 @@ Parses string containing GraphQL query or [type definition](type-system/type-lan * (By default, the parser creates AST nodes that know the location * in the source that they correspond to. This configuration flag * disables that behavior for performance or testing.) + * + * allowLegacySDLImplementsInterfaces: boolean, + * (If enabled, the parser will parse implemented interfaces with no `&` + * character between each interface. Otherwise, the parser will follow the + * current specification. + * + * This option is provided to ease adoption of the final SDL specification + * and will be removed in a future major release.) * * @api * @param Source|string $source diff --git a/src/Language/Lexer.php b/src/Language/Lexer.php index 6c1bc8206..05f8db5d5 100644 --- a/src/Language/Lexer.php +++ b/src/Language/Lexer.php @@ -150,6 +150,8 @@ private function readToken(Token $prev) return $this->readComment($line, $col, $prev); case 36: // $ return new Token(Token::DOLLAR, $position, $position + 1, $line, $col, $prev); + case 38: // & + return new Token(Token::AMP, $position, $position + 1, $line, $col, $prev); case 40: // ( return new Token(Token::PAREN_L, $position, $position + 1, $line, $col, $prev); case 41: // ) diff --git a/src/Language/Parser.php b/src/Language/Parser.php index 2a7c7f0f1..4531a962c 100644 --- a/src/Language/Parser.php +++ b/src/Language/Parser.php @@ -58,6 +58,14 @@ class Parser * (By default, the parser creates AST nodes that know the location * in the source that they correspond to. This configuration flag * disables that behavior for performance or testing.) + * + * allowLegacySDLImplementsInterfaces: boolean, + * (If enabled, the parser will parse implemented interfaces with no `&` + * character between each interface. Otherwise, the parser will follow the + * current specification. + * + * This option is provided to ease adoption of the final SDL specification + * and will be removed in a future major release.) * * @api * @param Source|string $source @@ -966,9 +974,19 @@ function parseImplementsInterfaces() $types = []; if ($this->lexer->token->value === 'implements') { $this->lexer->advance(); + // Optional leading ampersand + $this->skip(Token::AMP); do { $types[] = $this->parseNamedType(); - } while ($this->peek(Token::NAME)); + } while ( + $this->skip(Token::AMP) || + // Legacy support for the SDL? + ( + isset($this->lexer->options['allowLegacySDLImplementsInterfaces']) && + $this->peek(Token::NAME) + ) + + ); } return $types; } diff --git a/src/Language/Printer.php b/src/Language/Printer.php index 7e7336b2f..712e828a4 100644 --- a/src/Language/Printer.php +++ b/src/Language/Printer.php @@ -195,7 +195,7 @@ public function printAST($ast) return $this->join([ 'type', $def->name, - $this->wrap('implements ', $this->join($def->interfaces, ', ')), + $this->wrap('implements ', $this->join($def->interfaces, ' & ')), $this->join($def->directives, ' '), $this->block($def->fields) ], ' '); diff --git a/src/Language/Token.php b/src/Language/Token.php index f908a5d25..5de7871b9 100644 --- a/src/Language/Token.php +++ b/src/Language/Token.php @@ -12,6 +12,7 @@ class Token const EOF = ''; const BANG = '!'; const DOLLAR = '$'; + const AMP = '&'; const PAREN_L = '('; const PAREN_R = ')'; const SPREAD = '...'; @@ -42,6 +43,7 @@ public static function getKindDescription($kind) $description[self::EOF] = ''; $description[self::BANG] = '!'; $description[self::DOLLAR] = '$'; + $description[self::AMP] = '&'; $description[self::PAREN_L] = '('; $description[self::PAREN_R] = ')'; $description[self::SPREAD] = '...'; diff --git a/tests/Language/SchemaParserTest.php b/tests/Language/SchemaParserTest.php index 7b0324e5e..ef7a1578c 100644 --- a/tests/Language/SchemaParserTest.php +++ b/tests/Language/SchemaParserTest.php @@ -165,7 +165,7 @@ public function testSimpleTypeInheritingInterface() */ public function testSimpleTypeInheritingMultipleInterfaces() { - $body = 'type Hello implements Wo, rld { }'; + $body = 'type Hello implements Wo & rld { field: String }'; $loc = function($start, $end) {return TestUtils::locArray($start, $end);}; $doc = Parser::parse($body); @@ -177,15 +177,58 @@ public function testSimpleTypeInheritingMultipleInterfaces() 'name' => $this->nameNode('Hello', $loc(5, 10)), 'interfaces' => [ $this->typeNode('Wo', $loc(22,24)), - $this->typeNode('rld', $loc(26,29)) + $this->typeNode('rld', $loc(27,30)), ], 'directives' => [], - 'fields' => [], - 'loc' => $loc(0, 33), + 'fields' => [ + $this->fieldNode( + $this->nameNode('field', $loc(33,38)), + $this->typeNode('String', $loc(40,46)), + $loc(33,46) + ), + ], + 'loc' => $loc(0, 48), 'description' => null ] ], - 'loc' => $loc(0, 33) + 'loc' => $loc(0, 48) + ]; + + $this->assertEquals($expected, TestUtils::nodeToArray($doc)); + } + + /** + * @it Simple type inheriting multiple interfaces with leading ampersand + */ + public function testSimpleTypeInheritingMultipleInterfacesWithLeadingAmpersand() + { + $body = 'type Hello implements & Wo & rld { field: String }'; + $loc = function($start, $end) {return TestUtils::locArray($start, $end);}; + $doc = Parser::parse($body); + + $expected = [ + 'kind' => NodeKind::DOCUMENT, + 'definitions' => [ + [ + 'kind' => NodeKind::OBJECT_TYPE_DEFINITION, + 'name' => $this->nameNode('Hello', $loc(5, 10)), + 'interfaces' => [ + $this->typeNode('Wo', $loc(24,26)), + $this->typeNode('rld', $loc(29,32)), + ], + 'directives' => [], + 'fields' => [ + $this->fieldNode( + $this->nameNode('field', $loc(35,40)), + $this->typeNode('String', $loc(42,48)), + $loc(35,48) + ), + ], + 'loc' => $loc(0, 50), + 'description' => null + ] + ], + 'loc' => $loc(0, 50) ]; $this->assertEquals($expected, TestUtils::nodeToArray($doc)); @@ -705,6 +748,31 @@ public function testSimpleTypeDescriptionInComments() $this->assertEquals($expected, TestUtils::nodeToArray($doc)); } + /** + * @it Option: allowLegacySDLImplementsInterfaces + */ + public function testOptionAllowLegacySDLImplementationsInterfaces() + { + $body = 'type Hello implements Wo rld { field: String }'; + $loc = function($start, $end) {return TestUtils::locArray($start, $end);}; + $thrown = false; + try { + Parser::parse($body); + } catch (SyntaxError $e) { + $thrown = true; + } + + $this->assertTrue($thrown, 'Parsing old SDL should throw without legacy option'); + + $doc = Parser::parse($body, ['allowLegacySDLImplementsInterfaces' => true]); + $expected = [ + $this->typeNode('Wo', $loc(22,24)), + $this->typeNode('rld', $loc(25,28)), + ]; + + $this->assertEquals($expected, TestUtils::nodeToArray($doc)['definitions'][0]['interfaces']); + } + private function typeNode($name, $loc) { return [ diff --git a/tests/Language/SchemaPrinterTest.php b/tests/Language/SchemaPrinterTest.php index a649cedcd..9c5b54cfd 100644 --- a/tests/Language/SchemaPrinterTest.php +++ b/tests/Language/SchemaPrinterTest.php @@ -56,7 +56,7 @@ public function testPrintsKitchenSink() mutation: MutationType } -type Foo implements Bar { +type Foo implements Bar & Baz { one: Type two(argument: InputType!): Type three(argument: InputType, other: String): Int diff --git a/tests/Language/schema-kitchen-sink.graphql b/tests/Language/schema-kitchen-sink.graphql index 0544266fa..5bb0b01fd 100644 --- a/tests/Language/schema-kitchen-sink.graphql +++ b/tests/Language/schema-kitchen-sink.graphql @@ -10,7 +10,7 @@ schema { mutation: MutationType } -type Foo implements Bar { +type Foo implements Bar & Baz { one: Type two(argument: InputType!): Type three(argument: InputType, other: String): Int