Add experimental support for parsing variable definitions in fragments

ref: graphql/graphql-js#1141
This commit is contained in:
Daniel Tschinder 2018-02-15 13:37:45 +01:00
parent fde7df534d
commit 949b853678
7 changed files with 137 additions and 6 deletions

View File

@ -10,13 +10,21 @@ class FragmentDefinitionNode extends Node implements ExecutableDefinitionNode, H
*/ */
public $name; public $name;
/**
* Note: fragment variable definitions are experimental and may be changed
* or removed in the future.
*
* @var VariableDefinitionNode[]|NodeList
*/
public $variableDefinitions;
/** /**
* @var NamedTypeNode * @var NamedTypeNode
*/ */
public $typeCondition; public $typeCondition;
/** /**
* @var DirectiveNode[] * @var DirectiveNode[]|NodeList
*/ */
public $directives; public $directives;

View File

@ -49,7 +49,6 @@ use GraphQL\Language\AST\UnionTypeExtensionNode;
use GraphQL\Language\AST\VariableNode; use GraphQL\Language\AST\VariableNode;
use GraphQL\Language\AST\VariableDefinitionNode; use GraphQL\Language\AST\VariableDefinitionNode;
use GraphQL\Error\SyntaxError; use GraphQL\Error\SyntaxError;
use GraphQL\Type\TypeKind;
/** /**
* Parses string containing GraphQL query or [type definition](type-system/type-language.md) to Abstract Syntax Tree. * Parses string containing GraphQL query or [type definition](type-system/type-language.md) to Abstract Syntax Tree.
@ -67,10 +66,25 @@ class Parser
* in the source that they correspond to. This configuration flag * in the source that they correspond to. This configuration flag
* disables that behavior for performance or testing.) * disables that behavior for performance or testing.)
* *
* experimentalFragmentVariables: boolean,
* (If enabled, the parser will understand and parse variable definitions
* contained in a fragment definition. They'll be represented in the
* `variableDefinitions` field of the FragmentDefinitionNode.
*
* The syntax is identical to normal, query-defined variables. For example:
*
* fragment A($var: Boolean = false) on T {
* ...
* }
*
* Note: this feature is experimental and may change or be removed in the
* future.)
*
* @api * @api
* @param Source|string $source * @param Source|string $source
* @param array $options * @param array $options
* @return DocumentNode * @return DocumentNode
* @throws SyntaxError
*/ */
public static function parse($source, array $options = []) public static function parse($source, array $options = [])
{ {
@ -639,11 +653,19 @@ class Parser
$this->expectKeyword('fragment'); $this->expectKeyword('fragment');
$name = $this->parseFragmentName(); $name = $this->parseFragmentName();
// Experimental support for defining variables within fragments changes
// the grammar of FragmentDefinition:
// - fragment FragmentName VariableDefinitions? on TypeCondition Directives? SelectionSet
$variableDefinitions = null;
if (isset($this->lexer->options['experimentalFragmentVariables'])) {
$variableDefinitions = $this->parseVariableDefinitions();
}
$this->expectKeyword('on'); $this->expectKeyword('on');
$typeCondition = $this->parseNamedType(); $typeCondition = $this->parseNamedType();
return new FragmentDefinitionNode([ return new FragmentDefinitionNode([
'name' => $name, 'name' => $name,
'variableDefinitions' => $variableDefinitions,
'typeCondition' => $typeCondition, 'typeCondition' => $typeCondition,
'directives' => $this->parseDirectives(false), 'directives' => $this->parseDirectives(false),
'selectionSet' => $this->parseSelectionSet(), 'selectionSet' => $this->parseSelectionSet(),

View File

@ -131,7 +131,11 @@ class Printer
], ' '); ], ' ');
}, },
NodeKind::FRAGMENT_DEFINITION => function(FragmentDefinitionNode $node) { NodeKind::FRAGMENT_DEFINITION => function(FragmentDefinitionNode $node) {
return "fragment {$node->name} on {$node->typeCondition} " // Note: fragment variable definitions are experimental and may be changed
// or removed in the future.
return "fragment {$node->name}"
. $this->wrap('(', $this->join($node->variableDefinitions, ', '), ')')
. " on {$node->typeCondition} "
. $this->wrap('', $this->join($node->directives, ' '), ' ') . $this->wrap('', $this->join($node->directives, ' '), ' ')
. $node->selectionSet; . $node->selectionSet;
}, },

View File

@ -115,7 +115,15 @@ class Visitor
NodeKind::ARGUMENT => ['name', 'value'], NodeKind::ARGUMENT => ['name', 'value'],
NodeKind::FRAGMENT_SPREAD => ['name', 'directives'], NodeKind::FRAGMENT_SPREAD => ['name', 'directives'],
NodeKind::INLINE_FRAGMENT => ['typeCondition', 'directives', 'selectionSet'], NodeKind::INLINE_FRAGMENT => ['typeCondition', 'directives', 'selectionSet'],
NodeKind::FRAGMENT_DEFINITION => ['name', 'typeCondition', 'directives', 'selectionSet'], NodeKind::FRAGMENT_DEFINITION => [
'name',
// Note: fragment variable definitions are experimental and may be changed
// or removed in the future.
'variableDefinitions',
'typeCondition',
'directives',
'selectionSet'
],
NodeKind::INT => [], NodeKind::INT => [],
NodeKind::FLOAT => [], NodeKind::FLOAT => [],

View File

@ -453,10 +453,23 @@ fragment $fragmentName on Type {
$this->assertEquals(null, $result->loc); $this->assertEquals(null, $result->loc);
} }
/**
* @it Experimental: allows parsing fragment defined variables
*/
public function testExperimentalAllowsParsingFragmentDefinedVariables()
{
$source = new Source('fragment a($v: Boolean = false) on t { f(v: $v) }');
// not throw
Parser::parse($source, ['experimentalFragmentVariables' => true]);
$this->setExpectedException(SyntaxError::class);
Parser::parse($source);
}
/** /**
* @it contains location information that only stringifys start/end * @it contains location information that only stringifys start/end
*/ */
public function testConvertToArray() public function testContainsLocationInformationThatOnlyStringifysStartEnd()
{ {
$source = new Source('{ id }'); $source = new Source('{ id }');
$result = Parser::parse($source); $result = Parser::parse($source);

View File

@ -132,6 +132,28 @@ class PrinterTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expected, Printer::doPrint($mutationAstWithArtifacts)); $this->assertEquals($expected, Printer::doPrint($mutationAstWithArtifacts));
} }
/**
* @it Experimental: correctly prints fragment defined variables
*/
public function testExperimentalCorrectlyPrintsFragmentDefinedVariables()
{
$fragmentWithVariable = Parser::parse('
fragment Foo($a: ComplexType, $b: Boolean = false) on TestType {
id
}
',
['experimentalFragmentVariables' => true]
);
$this->assertEquals(
Printer::doPrint($fragmentWithVariable),
'fragment Foo($a: ComplexType, $b: Boolean = false) on TestType {
id
}
'
);
}
/** /**
* @it correctly prints single-line with leading space and quotation * @it correctly prints single-line with leading space and quotation
*/ */

View File

@ -326,6 +326,60 @@ class VisitorTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expected, $visited); $this->assertEquals($expected, $visited);
} }
/**
* @it Experimental: visits variables defined in fragments
*/
public function testExperimentalVisitsVariablesDefinedInFragments()
{
$ast = Parser::parse(
'fragment a($v: Boolean = false) on t { f }',
['experimentalFragmentVariables' => true]
);
$visited = [];
Visitor::visit($ast, [
'enter' => function($node) use (&$visited) {
$visited[] = ['enter', $node->kind, isset($node->value) ? $node->value : null];
},
'leave' => function($node) use (&$visited) {
$visited[] = ['leave', $node->kind, isset($node->value) ? $node->value : null];
},
]);
$expected = [
['enter', 'Document', null],
['enter', 'FragmentDefinition', null],
['enter', 'Name', 'a'],
['leave', 'Name', 'a'],
['enter', 'VariableDefinition', null],
['enter', 'Variable', null],
['enter', 'Name', 'v'],
['leave', 'Name', 'v'],
['leave', 'Variable', null],
['enter', 'NamedType', null],
['enter', 'Name', 'Boolean'],
['leave', 'Name', 'Boolean'],
['leave', 'NamedType', null],
['enter', 'BooleanValue', false],
['leave', 'BooleanValue', false],
['leave', 'VariableDefinition', null],
['enter', 'NamedType', null],
['enter', 'Name', 't'],
['leave', 'Name', 't'],
['leave', 'NamedType', null],
['enter', 'SelectionSet', null],
['enter', 'Field', null],
['enter', 'Name', 'f'],
['leave', 'Name', 'f'],
['leave', 'Field', null],
['leave', 'SelectionSet', null],
['leave', 'FragmentDefinition', null],
['leave', 'Document', null],
];
$this->assertEquals($expected, $visited);
}
/** /**
* @it visits kitchen sink * @it visits kitchen sink
*/ */