diff --git a/CHANGELOG.md b/CHANGELOG.md index 100a1db..3a6599e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## dev-master - Spec compliance: error extensions are displayed under `extensions` key +- `AbstractValidationRule` renamed to `ValidationRule` (NS `GraphQL\Validator\Rules`) +- `AbstractQuerySecurity` renamed to `QuerySecurityRule` (NS `GraphQL\Validator\Rules`) #### v0.12.5 - Execution performance optimization for lists diff --git a/benchmarks/HugeSchemaBench.php b/benchmarks/HugeSchemaBench.php index 7146e7f..31073e2 100644 --- a/benchmarks/HugeSchemaBench.php +++ b/benchmarks/HugeSchemaBench.php @@ -2,10 +2,9 @@ namespace GraphQL\Benchmarks; use GraphQL\GraphQL; -use GraphQL\Schema; use GraphQL\Benchmarks\Utils\QueryGenerator; use GraphQL\Benchmarks\Utils\SchemaGenerator; -use GraphQL\Type\LazyResolution; +use GraphQL\Type\Schema; /** * @BeforeMethods({"setUp"}) @@ -23,8 +22,6 @@ class HugeSchemaBench private $schema; - private $lazySchema; - /** * @var string */ diff --git a/benchmarks/Utils/QueryGenerator.php b/benchmarks/Utils/QueryGenerator.php index e99f43a..7312e30 100644 --- a/benchmarks/Utils/QueryGenerator.php +++ b/benchmarks/Utils/QueryGenerator.php @@ -7,11 +7,11 @@ use GraphQL\Language\AST\NameNode; use GraphQL\Language\AST\OperationDefinitionNode; use GraphQL\Language\AST\SelectionSetNode; use GraphQL\Language\Printer; -use GraphQL\Schema; use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\WrappingType; +use GraphQL\Type\Schema; use GraphQL\Utils\Utils; class QueryGenerator diff --git a/benchmarks/Utils/SchemaGenerator.php b/benchmarks/Utils/SchemaGenerator.php index 6cc9145..8ee39e7 100644 --- a/benchmarks/Utils/SchemaGenerator.php +++ b/benchmarks/Utils/SchemaGenerator.php @@ -1,11 +1,11 @@ 'Email', 'serialize' => function($value) {/* See function body above */}, 'parseValue' => function($value) {/* See function body above */}, - 'parseLiteral' => function($valueNode) {/* See function body above */}, + 'parseLiteral' => function($valueNode, array $variables = null) {/* See function body above */}, ]); ``` diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 4be54dd..aba1cb2 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -90,4 +90,11 @@ /> + + + + + + + diff --git a/src/Error/Error.php b/src/Error/Error.php index 02c26ec..35019b2 100644 --- a/src/Error/Error.php +++ b/src/Error/Error.php @@ -8,9 +8,9 @@ use GraphQL\Language\AST\Node; use GraphQL\Language\Source; use GraphQL\Language\SourceLocation; use GraphQL\Utils\Utils; +use Traversable; use function array_filter; use function array_map; -use function array_merge; use function is_array; use function iterator_to_array; @@ -81,12 +81,12 @@ class Error extends \Exception implements \JsonSerializable, ClientAware protected $extensions; /** - * @param string $message - * @param Node[]|null $nodes - * @param mixed[]|null $positions - * @param mixed[]|null $path - * @param \Throwable $previous - * @param mixed[] $extensions + * @param string $message + * @param Node|Node[]|Traversable|null $nodes + * @param mixed[]|null $positions + * @param mixed[]|null $path + * @param \Throwable $previous + * @param mixed[] $extensions */ public function __construct( $message, @@ -271,7 +271,7 @@ class Error extends \Exception implements \JsonSerializable, ClientAware $this->locations = array_filter( array_map( function ($node) { - if ($node->loc) { + if ($node->loc && $node->loc->source) { return $node->loc->source->getLocation($node->loc->start); } }, diff --git a/src/GraphQL.php b/src/GraphQL.php index ce863d6..d5c6237 100644 --- a/src/GraphQL.php +++ b/src/GraphQL.php @@ -13,7 +13,7 @@ use GraphQL\Executor\Promise\PromiseAdapter; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\Type; use GraphQL\Validator\DocumentValidator; -use GraphQL\Validator\Rules\AbstractValidationRule; +use GraphQL\Validator\Rules\ValidationRule; use GraphQL\Validator\Rules\QueryComplexity; /** @@ -272,7 +272,7 @@ class GraphQL * Returns standard validation rules implementing GraphQL spec * * @api - * @return AbstractValidationRule[] + * @return ValidationRule[] */ public static function getStandardValidationRules() { diff --git a/src/Language/AST/ArgumentNode.php b/src/Language/AST/ArgumentNode.php index 3c942e7..1519429 100644 --- a/src/Language/AST/ArgumentNode.php +++ b/src/Language/AST/ArgumentNode.php @@ -1,17 +1,19 @@ start = $start; - $tmp->end = $end; + $tmp->end = $end; return $tmp; } - public function __construct(Token $startToken = null, Token $endToken = null, Source $source = null) + public function __construct(?Token $startToken = null, ?Token $endToken = null, ?Source $source = null) { $this->startToken = $startToken; - $this->endToken = $endToken; - $this->source = $source; + $this->endToken = $endToken; + $this->source = $source; - if ($startToken && $endToken) { - $this->start = $startToken->start; - $this->end = $endToken->end; + if (! $startToken || ! $endToken) { + return; } + + $this->start = $startToken->start; + $this->end = $endToken->end; } } diff --git a/src/Language/AST/NameNode.php b/src/Language/AST/NameNode.php index 7860775..91f6d05 100644 --- a/src/Language/AST/NameNode.php +++ b/src/Language/AST/NameNode.php @@ -1,12 +1,15 @@ $arrValue) { $cloned[$key] = $this->cloneValue($arrValue); } - } else if ($value instanceof Node) { + } elseif ($value instanceof self) { $cloned = clone $value; foreach (get_object_vars($cloned) as $prop => $propValue) { $cloned->{$prop} = $this->cloneValue($propValue); @@ -84,34 +88,34 @@ abstract class Node public function __toString() { $tmp = $this->toArray(true); + return (string) json_encode($tmp); } /** * @param bool $recursive - * @return array + * @return mixed[] */ public function toArray($recursive = false) { if ($recursive) { return $this->recursiveToArray($this); - } else { - $tmp = (array) $this; - - if ($this->loc) { - $tmp['loc'] = [ - 'start' => $this->loc->start, - 'end' => $this->loc->end - ]; - } - - return $tmp; } + + $tmp = (array) $this; + + if ($this->loc) { + $tmp['loc'] = [ + 'start' => $this->loc->start, + 'end' => $this->loc->end, + ]; + } + + return $tmp; } /** - * @param Node $node - * @return array + * @return mixed[] */ private function recursiveToArray(Node $node) { @@ -122,25 +126,27 @@ abstract class Node if ($node->loc) { $result['loc'] = [ 'start' => $node->loc->start, - 'end' => $node->loc->end + 'end' => $node->loc->end, ]; } foreach (get_object_vars($node) as $prop => $propValue) { - if (isset($result[$prop])) + if (isset($result[$prop])) { continue; + } - if ($propValue === null) + if ($propValue === null) { continue; + } if (is_array($propValue) || $propValue instanceof NodeList) { $tmp = []; foreach ($propValue as $tmp1) { $tmp[] = $tmp1 instanceof Node ? $this->recursiveToArray($tmp1) : (array) $tmp1; } - } else if ($propValue instanceof Node) { + } elseif ($propValue instanceof Node) { $tmp = $this->recursiveToArray($propValue); - } else if (is_scalar($propValue) || null === $propValue) { + } elseif (is_scalar($propValue) || $propValue === null) { $tmp = $propValue; } else { $tmp = null; @@ -148,6 +154,7 @@ abstract class Node $result[$prop] = $tmp; } + return $result; } } diff --git a/src/Language/AST/NodeKind.php b/src/Language/AST/NodeKind.php index 1091c6e..d321cdc 100644 --- a/src/Language/AST/NodeKind.php +++ b/src/Language/AST/NodeKind.php @@ -1,5 +1,7 @@ NameNode::class, + self::NAME => NameNode::class, // Document - NodeKind::DOCUMENT => DocumentNode::class, - NodeKind::OPERATION_DEFINITION => OperationDefinitionNode::class, - NodeKind::VARIABLE_DEFINITION => VariableDefinitionNode::class, - NodeKind::VARIABLE => VariableNode::class, - NodeKind::SELECTION_SET => SelectionSetNode::class, - NodeKind::FIELD => FieldNode::class, - NodeKind::ARGUMENT => ArgumentNode::class, + self::DOCUMENT => DocumentNode::class, + self::OPERATION_DEFINITION => OperationDefinitionNode::class, + self::VARIABLE_DEFINITION => VariableDefinitionNode::class, + self::VARIABLE => VariableNode::class, + self::SELECTION_SET => SelectionSetNode::class, + self::FIELD => FieldNode::class, + self::ARGUMENT => ArgumentNode::class, // Fragments - NodeKind::FRAGMENT_SPREAD => FragmentSpreadNode::class, - NodeKind::INLINE_FRAGMENT => InlineFragmentNode::class, - NodeKind::FRAGMENT_DEFINITION => FragmentDefinitionNode::class, + self::FRAGMENT_SPREAD => FragmentSpreadNode::class, + self::INLINE_FRAGMENT => InlineFragmentNode::class, + self::FRAGMENT_DEFINITION => FragmentDefinitionNode::class, // Values - NodeKind::INT => IntValueNode::class, - NodeKind::FLOAT => FloatValueNode::class, - NodeKind::STRING => StringValueNode::class, - NodeKind::BOOLEAN => BooleanValueNode::class, - NodeKind::ENUM => EnumValueNode::class, - NodeKind::NULL => NullValueNode::class, - NodeKind::LST => ListValueNode::class, - NodeKind::OBJECT => ObjectValueNode::class, - NodeKind::OBJECT_FIELD => ObjectFieldNode::class, + self::INT => IntValueNode::class, + self::FLOAT => FloatValueNode::class, + self::STRING => StringValueNode::class, + self::BOOLEAN => BooleanValueNode::class, + self::ENUM => EnumValueNode::class, + self::NULL => NullValueNode::class, + self::LST => ListValueNode::class, + self::OBJECT => ObjectValueNode::class, + self::OBJECT_FIELD => ObjectFieldNode::class, // Directives - NodeKind::DIRECTIVE => DirectiveNode::class, + self::DIRECTIVE => DirectiveNode::class, // Types - NodeKind::NAMED_TYPE => NamedTypeNode::class, - NodeKind::LIST_TYPE => ListTypeNode::class, - NodeKind::NON_NULL_TYPE => NonNullTypeNode::class, + self::NAMED_TYPE => NamedTypeNode::class, + self::LIST_TYPE => ListTypeNode::class, + self::NON_NULL_TYPE => NonNullTypeNode::class, // Type System Definitions - NodeKind::SCHEMA_DEFINITION => SchemaDefinitionNode::class, - NodeKind::OPERATION_TYPE_DEFINITION => OperationTypeDefinitionNode::class, + self::SCHEMA_DEFINITION => SchemaDefinitionNode::class, + self::OPERATION_TYPE_DEFINITION => OperationTypeDefinitionNode::class, // Type Definitions - NodeKind::SCALAR_TYPE_DEFINITION => ScalarTypeDefinitionNode::class, - NodeKind::OBJECT_TYPE_DEFINITION => ObjectTypeDefinitionNode::class, - NodeKind::FIELD_DEFINITION => FieldDefinitionNode::class, - NodeKind::INPUT_VALUE_DEFINITION => InputValueDefinitionNode::class, - NodeKind::INTERFACE_TYPE_DEFINITION => InterfaceTypeDefinitionNode::class, - NodeKind::UNION_TYPE_DEFINITION => UnionTypeDefinitionNode::class, - NodeKind::ENUM_TYPE_DEFINITION => EnumTypeDefinitionNode::class, - NodeKind::ENUM_VALUE_DEFINITION => EnumValueDefinitionNode::class, - NodeKind::INPUT_OBJECT_TYPE_DEFINITION =>InputObjectTypeDefinitionNode::class, + self::SCALAR_TYPE_DEFINITION => ScalarTypeDefinitionNode::class, + self::OBJECT_TYPE_DEFINITION => ObjectTypeDefinitionNode::class, + self::FIELD_DEFINITION => FieldDefinitionNode::class, + self::INPUT_VALUE_DEFINITION => InputValueDefinitionNode::class, + self::INTERFACE_TYPE_DEFINITION => InterfaceTypeDefinitionNode::class, + self::UNION_TYPE_DEFINITION => UnionTypeDefinitionNode::class, + self::ENUM_TYPE_DEFINITION => EnumTypeDefinitionNode::class, + self::ENUM_VALUE_DEFINITION => EnumValueDefinitionNode::class, + self::INPUT_OBJECT_TYPE_DEFINITION => InputObjectTypeDefinitionNode::class, // Type Extensions - NodeKind::SCALAR_TYPE_EXTENSION => ScalarTypeExtensionNode::class, - NodeKind::OBJECT_TYPE_EXTENSION => ObjectTypeExtensionNode::class, - NodeKind::INTERFACE_TYPE_EXTENSION => InterfaceTypeExtensionNode::class, - NodeKind::UNION_TYPE_EXTENSION => UnionTypeExtensionNode::class, - NodeKind::ENUM_TYPE_EXTENSION => EnumTypeExtensionNode::class, - NodeKind::INPUT_OBJECT_TYPE_EXTENSION => InputObjectTypeExtensionNode::class, + self::SCALAR_TYPE_EXTENSION => ScalarTypeExtensionNode::class, + self::OBJECT_TYPE_EXTENSION => ObjectTypeExtensionNode::class, + self::INTERFACE_TYPE_EXTENSION => InterfaceTypeExtensionNode::class, + self::UNION_TYPE_EXTENSION => UnionTypeExtensionNode::class, + self::ENUM_TYPE_EXTENSION => EnumTypeExtensionNode::class, + self::INPUT_OBJECT_TYPE_EXTENSION => InputObjectTypeExtensionNode::class, // Directive Definitions - NodeKind::DIRECTIVE_DEFINITION => DirectiveDefinitionNode::class + self::DIRECTIVE_DEFINITION => DirectiveDefinitionNode::class, ]; } diff --git a/src/Language/AST/NodeList.php b/src/Language/AST/NodeList.php index b0adb81..5028022 100644 --- a/src/Language/AST/NodeList.php +++ b/src/Language/AST/NodeList.php @@ -1,22 +1,22 @@ nodes; } return new NodeList(array_merge($this->nodes, $list)); diff --git a/src/Language/AST/NonNullTypeNode.php b/src/Language/AST/NonNullTypeNode.php index e450404..da0ce3d 100644 --- a/src/Language/AST/NonNullTypeNode.php +++ b/src/Language/AST/NonNullTypeNode.php @@ -1,12 +1,15 @@ self::QUERY, self::MUTATION => self::MUTATION, diff --git a/src/Language/Lexer.php b/src/Language/Lexer.php index 835b947..223b6f8 100644 --- a/src/Language/Lexer.php +++ b/src/Language/Lexer.php @@ -1,9 +1,16 @@ source = $source; - $this->options = $options; + $this->source = $source; + $this->options = $options; $this->lastToken = $startOfFileToken; - $this->token = $startOfFileToken; - $this->line = 1; + $this->token = $startOfFileToken; + $this->line = 1; $this->lineStart = 0; - $this->position = $this->byteStreamPosition = 0; + $this->position = $this->byteStreamPosition = 0; } /** @@ -93,7 +93,8 @@ class Lexer public function advance() { $this->lastToken = $this->token; - $token = $this->token = $this->lookahead(); + $token = $this->token = $this->lookahead(); + return $token; } @@ -105,11 +106,11 @@ class Lexer $token = $token->next ?: ($token->next = $this->readToken($token)); } while ($token->kind === Token::COMMENT); } + return $token; } /** - * @param Token $prev * @return Token * @throws SyntaxError */ @@ -121,7 +122,7 @@ class Lexer $position = $this->position; $line = $this->line; - $col = 1 + $position - $this->lineStart; + $col = 1 + $position - $this->lineStart; if ($position >= $bodyLength) { return new Token(Token::EOF, $bodyLength, $bodyLength, $line, $col, $prev); @@ -144,6 +145,7 @@ class Lexer return new Token(Token::BANG, $position, $position + 1, $line, $col, $prev); case 35: // # $this->moveStringCursor(-1, -1 * $bytes); + return $this->readComment($line, $col, $prev); case 36: // $ return new Token(Token::DOLLAR, $position, $position + 1, $line, $col, $prev); @@ -178,30 +180,82 @@ class Lexer case 125: // } return new Token(Token::BRACE_R, $position, $position + 1, $line, $col, $prev); // A-Z - case 65: case 66: case 67: case 68: case 69: case 70: case 71: case 72: - case 73: case 74: case 75: case 76: case 77: case 78: case 79: case 80: - case 81: case 82: case 83: case 84: case 85: case 86: case 87: case 88: - case 89: case 90: - // _ + case 65: + case 66: + case 67: + case 68: + case 69: + case 70: + case 71: + case 72: + case 73: + case 74: + case 75: + case 76: + case 77: + case 78: + case 79: + case 80: + case 81: + case 82: + case 83: + case 84: + case 85: + case 86: + case 87: + case 88: + case 89: + case 90: + // _ case 95: - // a-z - case 97: case 98: case 99: case 100: case 101: case 102: case 103: case 104: - case 105: case 106: case 107: case 108: case 109: case 110: case 111: - case 112: case 113: case 114: case 115: case 116: case 117: case 118: - case 119: case 120: case 121: case 122: + // a-z + case 97: + case 98: + case 99: + case 100: + case 101: + case 102: + case 103: + case 104: + case 105: + case 106: + case 107: + case 108: + case 109: + case 110: + case 111: + case 112: + case 113: + case 114: + case 115: + case 116: + case 117: + case 118: + case 119: + case 120: + case 121: + case 122: return $this->moveStringCursor(-1, -1 * $bytes) ->readName($line, $col, $prev); // - case 45: - // 0-9 - case 48: case 49: case 50: case 51: case 52: - case 53: case 54: case 55: case 56: case 57: + // 0-9 + case 48: + case 49: + case 50: + case 51: + case 52: + case 53: + case 54: + case 55: + case 56: + case 57: return $this->moveStringCursor(-1, -1 * $bytes) ->readNumber($line, $col, $prev); // " case 34: - list(,$nextCode) = $this->readChar(); - list(,$nextNextCode) = $this->moveStringCursor(1, 1)->readChar(); + list(, $nextCode) = $this->readChar(); + list(, $nextNextCode) = $this->moveStringCursor(1, 1)->readChar(); if ($nextCode === 34 && $nextNextCode === 34) { return $this->moveStringCursor(-2, (-1 * $bytes) - 1) @@ -213,7 +267,7 @@ class Lexer } $errMessage = $code === 39 - ? "Unexpected single quote character ('), did you mean to use ". 'a double quote (")?' + ? "Unexpected single quote character ('), did you mean to use " . 'a double quote (")?' : 'Cannot parse the unexpected character ' . Utils::printCharCode($code) . '.'; throw new SyntaxError( @@ -230,24 +284,24 @@ class Lexer * * @param int $line * @param int $col - * @param Token $prev * @return Token */ private function readName($line, $col, Token $prev) { - $value = ''; - $start = $this->position; + $value = ''; + $start = $this->position; list ($char, $code) = $this->readChar(); while ($code && ( - $code === 95 || // _ - $code >= 48 && $code <= 57 || // 0-9 - $code >= 65 && $code <= 90 || // A-Z - $code >= 97 && $code <= 122 // a-z - )) { - $value .= $char; + $code === 95 || // _ + $code >= 48 && $code <= 57 || // 0-9 + $code >= 65 && $code <= 90 || // A-Z + $code >= 97 && $code <= 122 // a-z + )) { + $value .= $char; list ($char, $code) = $this->moveStringCursor(1, 1)->readChar(); } + return new Token( Token::NAME, $start, @@ -268,33 +322,36 @@ class Lexer * * @param int $line * @param int $col - * @param Token $prev * @return Token * @throws SyntaxError */ private function readNumber($line, $col, Token $prev) { - $value = ''; - $start = $this->position; + $value = ''; + $start = $this->position; list ($char, $code) = $this->readChar(); $isFloat = false; if ($code === 45) { // - - $value .= $char; + $value .= $char; list ($char, $code) = $this->moveStringCursor(1, 1)->readChar(); } // guard against leading zero's if ($code === 48) { // 0 - $value .= $char; + $value .= $char; list ($char, $code) = $this->moveStringCursor(1, 1)->readChar(); if ($code >= 48 && $code <= 57) { - throw new SyntaxError($this->source, $this->position, "Invalid number, unexpected digit after 0: " . Utils::printCharCode($code)); + throw new SyntaxError( + $this->source, + $this->position, + 'Invalid number, unexpected digit after 0: ' . Utils::printCharCode($code) + ); } } else { - $value .= $this->readDigits(); + $value .= $this->readDigits(); list ($char, $code) = $this->readChar(); } @@ -302,14 +359,14 @@ class Lexer $isFloat = true; $this->moveStringCursor(1, 1); - $value .= $char; - $value .= $this->readDigits(); + $value .= $char; + $value .= $this->readDigits(); list ($char, $code) = $this->readChar(); } if ($code === 69 || $code === 101) { // E e - $isFloat = true; - $value .= $char; + $isFloat = true; + $value .= $char; list ($char, $code) = $this->moveStringCursor(1, 1)->readChar(); if ($code === 43 || $code === 45) { // + - @@ -341,7 +398,7 @@ class Lexer $value = ''; do { - $value .= $char; + $value .= $char; list ($char, $code) = $this->moveStringCursor(1, 1)->readChar(); } while ($code >= 48 && $code <= 57); // 0 - 9 @@ -362,7 +419,6 @@ class Lexer /** * @param int $line * @param int $col - * @param Token $prev * @return Token * @throws SyntaxError */ @@ -371,13 +427,12 @@ class Lexer $start = $this->position; // Skip leading quote and read first string char: - list ($char, $code, $bytes) = $this->moveStringCursor(1, 1)->readChar(); + [$char, $code, $bytes] = $this->moveStringCursor(1, 1)->readChar(); $chunk = ''; $value = ''; - while ( - $code !== null && + while ($code !== null && // not LineTerminator $code !== 10 && $code !== 13 ) { @@ -403,22 +458,38 @@ class Lexer $this->moveStringCursor(1, $bytes); if ($code === 92) { // \ - $value .= $chunk; + $value .= $chunk; list (, $code) = $this->readChar(true); switch ($code) { - case 34: $value .= '"'; break; - case 47: $value .= '/'; break; - case 92: $value .= '\\'; break; - case 98: $value .= chr(8); break; // \b (backspace) - case 102: $value .= "\f"; break; - case 110: $value .= "\n"; break; - case 114: $value .= "\r"; break; - case 116: $value .= "\t"; break; + case 34: + $value .= '"'; + break; + case 47: + $value .= '/'; + break; + case 92: + $value .= '\\'; + break; + case 98: + $value .= chr(8); + break; // \b (backspace) + case 102: + $value .= "\f"; + break; + case 110: + $value .= "\n"; + break; + case 114: + $value .= "\r"; + break; + case 116: + $value .= "\t"; + break; case 117: - $position = $this->position; + $position = $this->position; list ($hex) = $this->readChars(4, true); - if (!preg_match('/[0-9a-fA-F]{4}/', $hex)) { + if (! preg_match('/[0-9a-fA-F]{4}/', $hex)) { throw new SyntaxError( $this->source, $position - 1, @@ -470,8 +541,8 @@ class Lexer // Closing Triple-Quote (""") if ($code === 34) { // Move 2 quotes - list(,$nextCode) = $this->moveStringCursor(1, 1)->readChar(); - list(,$nextNextCode) = $this->moveStringCursor(1, 1)->readChar(); + list(, $nextCode) = $this->moveStringCursor(1, 1)->readChar(); + list(, $nextNextCode) = $this->moveStringCursor(1, 1)->readChar(); if ($nextCode === 34 && $nextNextCode === 34) { $value .= $chunk; @@ -496,9 +567,9 @@ class Lexer $this->assertValidBlockStringCharacterCode($code, $this->position); $this->moveStringCursor(1, $bytes); - list(,$nextCode) = $this->readChar(); - list(,$nextNextCode) = $this->moveStringCursor(1, 1)->readChar(); - list(,$nextNextNextCode) = $this->moveStringCursor(1, 1)->readChar(); + list(, $nextCode) = $this->readChar(); + list(, $nextNextCode) = $this->moveStringCursor(1, 1)->readChar(); + list(, $nextNextNextCode) = $this->moveStringCursor(1, 1)->readChar(); // Escape Triple-Quote (\""") if ($code === 92 && @@ -508,7 +579,7 @@ class Lexer ) { $this->moveStringCursor(1, 1); $value .= $chunk . '"""'; - $chunk = ''; + $chunk = ''; } else { $this->moveStringCursor(-2, -2); $chunk .= $char; @@ -561,11 +632,11 @@ class Lexer // tab | space | comma | BOM if ($code === 9 || $code === 32 || $code === 44 || $code === 0xFEFF) { $this->moveStringCursor(1, $bytes); - } else if ($code === 10) { // new line + } elseif ($code === 10) { // new line $this->moveStringCursor(1, $bytes); $this->line++; $this->lineStart = $this->position; - } else if ($code === 13) { // carriage return + } elseif ($code === 13) { // carriage return list(, $nextCode, $nextBytes) = $this->moveStringCursor(1, $bytes)->readChar(); if ($nextCode === 10) { // lf after cr @@ -584,9 +655,8 @@ class Lexer * * #[\u0009\u0020-\uFFFF]* * - * @param $line - * @param $col - * @param Token $prev + * @param int $line + * @param int $col * @return Token */ private function readComment($line, $col, Token $prev) @@ -597,11 +667,10 @@ class Lexer do { list ($char, $code, $bytes) = $this->moveStringCursor(1, $bytes)->readChar(); - $value .= $char; - } while ( - $code && - // SourceCharacter but not LineTerminator - ($code > 0x001F || $code === 0x0009) + $value .= $char; + } while ($code && + // SourceCharacter but not LineTerminator + ($code > 0x001F || $code === 0x0009) ); return new Token( @@ -619,8 +688,8 @@ class Lexer * Reads next UTF8Character from the byte stream, starting from $byteStreamPosition. * * @param bool $advance - * @param int $byteStreamPosition - * @return array + * @param int $byteStreamPosition + * @return (string|int)[] */ private function readChar($advance = false, $byteStreamPosition = null) { @@ -628,9 +697,9 @@ class Lexer $byteStreamPosition = $this->byteStreamPosition; } - $code = null; - $utf8char = ''; - $bytes = 0; + $code = null; + $utf8char = ''; + $bytes = 0; $positionOffset = 0; if (isset($this->source->body[$byteStreamPosition])) { @@ -638,7 +707,7 @@ class Lexer if ($ord < 128) { $bytes = 1; - } else if ($ord < 224) { + } elseif ($ord < 224) { $bytes = 2; } elseif ($ord < 240) { $bytes = 3; @@ -651,7 +720,7 @@ class Lexer $utf8char .= $this->source->body[$pos]; } $positionOffset = 1; - $code = $bytes === 1 ? $ord : Utils::ord($utf8char); + $code = $bytes === 1 ? $ord : Utils::ord($utf8char); } if ($advance) { @@ -664,40 +733,42 @@ class Lexer /** * Reads next $numberOfChars UTF8 characters from the byte stream, starting from $byteStreamPosition. * - * @param $numberOfChars + * @param int $charCount * @param bool $advance * @param null $byteStreamPosition - * @return array + * @return (string|int)[] */ - private function readChars($numberOfChars, $advance = false, $byteStreamPosition = null) + private function readChars($charCount, $advance = false, $byteStreamPosition = null) { - $result = ''; + $result = ''; $totalBytes = 0; $byteOffset = $byteStreamPosition ?: $this->byteStreamPosition; - for ($i = 0; $i < $numberOfChars; $i++) { + for ($i = 0; $i < $charCount; $i++) { list ($char, $code, $bytes) = $this->readChar(false, $byteOffset); - $totalBytes += $bytes; - $byteOffset += $bytes; - $result .= $char; + $totalBytes += $bytes; + $byteOffset += $bytes; + $result .= $char; } if ($advance) { - $this->moveStringCursor($numberOfChars, $totalBytes); + $this->moveStringCursor($charCount, $totalBytes); } + return [$result, $totalBytes]; } /** * Moves internal string cursor position * - * @param $positionOffset - * @param $byteStreamOffset + * @param int $positionOffset + * @param int $byteStreamOffset * @return self */ private function moveStringCursor($positionOffset, $byteStreamOffset) { - $this->position += $positionOffset; + $this->position += $positionOffset; $this->byteStreamPosition += $byteStreamOffset; + return $this; } } diff --git a/src/Language/Parser.php b/src/Language/Parser.php index b920583..6f0ce0a 100644 --- a/src/Language/Parser.php +++ b/src/Language/Parser.php @@ -1,38 +1,43 @@ parseDocument(); } @@ -121,16 +127,17 @@ class Parser * * @api * @param Source|string $source - * @param array $options + * @param bool[] $options * @return BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|ListValueNode|ObjectValueNode|StringValueNode|VariableNode */ public static function parseValue($source, array $options = []) { $sourceObj = $source instanceof Source ? $source : new Source($source); - $parser = new Parser($sourceObj, $options); + $parser = new Parser($sourceObj, $options); $parser->expect(Token::SOF); $value = $parser->parseValueLiteral(false); $parser->expect(Token::EOF); + return $value; } @@ -146,30 +153,28 @@ class Parser * * @api * @param Source|string $source - * @param array $options + * @param bool[] $options * @return ListTypeNode|NameNode|NonNullTypeNode */ public static function parseType($source, array $options = []) { $sourceObj = $source instanceof Source ? $source : new Source($source); - $parser = new Parser($sourceObj, $options); + $parser = new Parser($sourceObj, $options); $parser->expect(Token::SOF); $type = $parser->parseTypeReference(); $parser->expect(Token::EOF); + return $type; } - /** - * @var Lexer - */ + /** @var Lexer */ private $lexer; /** - * Parser constructor. - * @param Source $source - * @param array $options + * + * @param bool[] $options */ - function __construct(Source $source, array $options = []) + public function __construct(Source $source, array $options = []) { $this->lexer = new Lexer($source, $options); } @@ -178,24 +183,24 @@ class Parser * Returns a location object, used to identify the place in * the source that created a given parsed object. * - * @param Token $startToken * @return Location|null */ - function loc(Token $startToken) + private function loc(Token $startToken) { if (empty($this->lexer->options['noLocation'])) { return new Location($startToken, $this->lexer->lastToken, $this->lexer->source); } + return null; } /** * Determines if the next token is of a given kind * - * @param $kind + * @param string $kind * @return bool */ - function peek($kind) + private function peek($kind) { return $this->lexer->token->kind === $kind; } @@ -204,16 +209,17 @@ class Parser * If the next token is of the given kind, return true after advancing * the parser. Otherwise, do not change the parser state and return false. * - * @param $kind + * @param string $kind * @return bool */ - function skip($kind) + private function skip($kind) { $match = $this->lexer->token->kind === $kind; if ($match) { $this->lexer->advance(); } + return $match; } @@ -224,19 +230,20 @@ class Parser * @return Token * @throws SyntaxError */ - function expect($kind) + private function expect($kind) { $token = $this->lexer->token; if ($token->kind === $kind) { $this->lexer->advance(); + return $token; } throw new SyntaxError( $this->lexer->source, $token->start, - "Expected $kind, found " . $token->getDescription() + sprintf('Expected %s, found %s', $kind, $token->getDescription()) ); } @@ -249,12 +256,13 @@ class Parser * @return Token * @throws SyntaxError */ - function expectKeyword($value) + private function expectKeyword($value) { $token = $this->lexer->token; if ($token->kind === Token::NAME && $token->value === $value) { $this->lexer->advance(); + return $token; } throw new SyntaxError( @@ -265,13 +273,13 @@ class Parser } /** - * @param Token|null $atToken * @return SyntaxError */ - function unexpected(Token $atToken = null) + private function unexpected(?Token $atToken = null) { $token = $atToken ?: $this->lexer->token; - return new SyntaxError($this->lexer->source, $token->start, "Unexpected " . $token->getDescription()); + + return new SyntaxError($this->lexer->source, $token->start, 'Unexpected ' . $token->getDescription()); } /** @@ -280,20 +288,21 @@ class Parser * and ends with a lex token of closeKind. Advances the parser * to the next lex token after the closing token. * - * @param int $openKind + * @param string $openKind * @param callable $parseFn - * @param int $closeKind + * @param string $closeKind * @return NodeList * @throws SyntaxError */ - function any($openKind, $parseFn, $closeKind) + private function any($openKind, $parseFn, $closeKind) { $this->expect($openKind); $nodes = []; - while (!$this->skip($closeKind)) { + while (! $this->skip($closeKind)) { $nodes[] = $parseFn($this); } + return new NodeList($nodes); } @@ -303,20 +312,21 @@ class Parser * and ends with a lex token of closeKind. Advances the parser * to the next lex token after the closing token. * - * @param $openKind - * @param $parseFn - * @param $closeKind + * @param string $openKind + * @param callable $parseFn + * @param string $closeKind * @return NodeList * @throws SyntaxError */ - function many($openKind, $parseFn, $closeKind) + private function many($openKind, $parseFn, $closeKind) { $this->expect($openKind); $nodes = [$parseFn($this)]; - while (!$this->skip($closeKind)) { + while (! $this->skip($closeKind)) { $nodes[] = $parseFn($this); } + return new NodeList($nodes); } @@ -326,13 +336,13 @@ class Parser * @return NameNode * @throws SyntaxError */ - function parseName() + private function parseName() { $token = $this->expect(Token::NAME); return new NameNode([ 'value' => $token->value, - 'loc' => $this->loc($token) + 'loc' => $this->loc($token), ]); } @@ -342,7 +352,7 @@ class Parser * @return DocumentNode * @throws SyntaxError */ - function parseDocument() + private function parseDocument() { $start = $this->lexer->token; $this->expect(Token::SOF); @@ -350,11 +360,11 @@ class Parser $definitions = []; do { $definitions[] = $this->parseDefinition(); - } while (!$this->skip(Token::EOF)); + } while (! $this->skip(Token::EOF)); return new DocumentNode([ 'definitions' => new NodeList($definitions), - 'loc' => $this->loc($start) + 'loc' => $this->loc($start), ]); } @@ -362,7 +372,7 @@ class Parser * @return ExecutableDefinitionNode|TypeSystemDefinitionNode * @throws SyntaxError */ - function parseDefinition() + private function parseDefinition() { if ($this->peek(Token::NAME)) { switch ($this->lexer->token->value) { @@ -385,9 +395,9 @@ class Parser // Note: The schema definition language is an experimental addition. return $this->parseTypeSystemDefinition(); } - } else if ($this->peek(Token::BRACE_L)) { + } elseif ($this->peek(Token::BRACE_L)) { return $this->parseExecutableDefinition(); - } else if ($this->peekDescription()) { + } elseif ($this->peekDescription()) { // Note: The schema definition language is an experimental addition. return $this->parseTypeSystemDefinition(); } @@ -399,7 +409,7 @@ class Parser * @return ExecutableDefinitionNode * @throws SyntaxError */ - function parseExecutableDefinition() + private function parseExecutableDefinition() { if ($this->peek(Token::NAME)) { switch ($this->lexer->token->value) { @@ -411,7 +421,7 @@ class Parser case 'fragment': return $this->parseFragmentDefinition(); } - } else if ($this->peek(Token::BRACE_L)) { + } elseif ($this->peek(Token::BRACE_L)) { return $this->parseOperationDefinition(); } @@ -424,17 +434,17 @@ class Parser * @return OperationDefinitionNode * @throws SyntaxError */ - function parseOperationDefinition() + private function parseOperationDefinition() { $start = $this->lexer->token; if ($this->peek(Token::BRACE_L)) { return new OperationDefinitionNode([ - 'operation' => 'query', - 'name' => null, + 'operation' => 'query', + 'name' => null, 'variableDefinitions' => new NodeList([]), - 'directives' => new NodeList([]), - 'selectionSet' => $this->parseSelectionSet(), - 'loc' => $this->loc($start) + 'directives' => new NodeList([]), + 'selectionSet' => $this->parseSelectionSet(), + 'loc' => $this->loc($start), ]); } @@ -446,12 +456,12 @@ class Parser } return new OperationDefinitionNode([ - 'operation' => $operation, - 'name' => $name, + 'operation' => $operation, + 'name' => $name, 'variableDefinitions' => $this->parseVariableDefinitions(), - 'directives' => $this->parseDirectives(false), - 'selectionSet' => $this->parseSelectionSet(), - 'loc' => $this->loc($start) + 'directives' => $this->parseDirectives(false), + 'selectionSet' => $this->parseSelectionSet(), + 'loc' => $this->loc($start), ]); } @@ -459,13 +469,16 @@ class Parser * @return string * @throws SyntaxError */ - function parseOperationType() + private function parseOperationType() { $operationToken = $this->expect(Token::NAME); switch ($operationToken->value) { - case 'query': return 'query'; - case 'mutation': return 'mutation'; - case 'subscription': return 'subscription'; + case 'query': + return 'query'; + case 'mutation': + return 'mutation'; + case 'subscription': + return 'subscription'; } throw $this->unexpected($operationToken); @@ -474,12 +487,14 @@ class Parser /** * @return VariableDefinitionNode[]|NodeList */ - function parseVariableDefinitions() + private function parseVariableDefinitions() { return $this->peek(Token::PAREN_L) ? $this->many( Token::PAREN_L, - [$this, 'parseVariableDefinition'], + function () { + return $this->parseVariableDefinition(); + }, Token::PAREN_R ) : new NodeList([]); @@ -489,20 +504,20 @@ class Parser * @return VariableDefinitionNode * @throws SyntaxError */ - function parseVariableDefinition() + private function parseVariableDefinition() { $start = $this->lexer->token; - $var = $this->parseVariable(); + $var = $this->parseVariable(); $this->expect(Token::COLON); $type = $this->parseTypeReference(); return new VariableDefinitionNode([ - 'variable' => $var, - 'type' => $type, + 'variable' => $var, + 'type' => $type, 'defaultValue' => ($this->skip(Token::EQUALS) ? $this->parseValueLiteral(true) : null), - 'loc' => $this->loc($start) + 'loc' => $this->loc($start), ]); } @@ -510,27 +525,36 @@ class Parser * @return VariableNode * @throws SyntaxError */ - function parseVariable() + private function parseVariable() { $start = $this->lexer->token; $this->expect(Token::DOLLAR); return new VariableNode([ 'name' => $this->parseName(), - 'loc' => $this->loc($start) + 'loc' => $this->loc($start), ]); } /** * @return SelectionSetNode */ - function parseSelectionSet() + private function parseSelectionSet() { $start = $this->lexer->token; - return new SelectionSetNode([ - 'selections' => $this->many(Token::BRACE_L, [$this, 'parseSelection'], Token::BRACE_R), - 'loc' => $this->loc($start) - ]); + + return new SelectionSetNode( + [ + 'selections' => $this->many( + Token::BRACE_L, + function () { + return $this->parseSelection(); + }, + Token::BRACE_R + ), + 'loc' => $this->loc($start), + ] + ); } /** @@ -541,7 +565,7 @@ class Parser * * @return mixed */ - function parseSelection() + private function parseSelection() { return $this->peek(Token::SPREAD) ? $this->parseFragment() : @@ -552,26 +576,26 @@ class Parser * @return FieldNode * @throws SyntaxError */ - function parseField() + private function parseField() { - $start = $this->lexer->token; + $start = $this->lexer->token; $nameOrAlias = $this->parseName(); if ($this->skip(Token::COLON)) { $alias = $nameOrAlias; - $name = $this->parseName(); + $name = $this->parseName(); } else { $alias = null; - $name = $nameOrAlias; + $name = $nameOrAlias; } return new FieldNode([ - 'alias' => $alias, - 'name' => $name, - 'arguments' => $this->parseArguments(false), - 'directives' => $this->parseDirectives(false), + 'alias' => $alias, + 'name' => $name, + 'arguments' => $this->parseArguments(false), + 'directives' => $this->parseDirectives(false), 'selectionSet' => $this->peek(Token::BRACE_L) ? $this->parseSelectionSet() : null, - 'loc' => $this->loc($start) + 'loc' => $this->loc($start), ]); } @@ -580,11 +604,18 @@ class Parser * @return ArgumentNode[]|NodeList * @throws SyntaxError */ - function parseArguments($isConst) + private function parseArguments($isConst) { - $item = $isConst ? 'parseConstArgument' : 'parseArgument'; + $parseFn = $isConst ? + function () { + return $this->parseConstArgument(); + } : + function () { + return $this->parseArgument(); + }; + return $this->peek(Token::PAREN_L) ? - $this->many(Token::PAREN_L, [$this, $item], Token::PAREN_R) : + $this->many(Token::PAREN_L, $parseFn, Token::PAREN_R) : new NodeList([]); } @@ -592,18 +623,18 @@ class Parser * @return ArgumentNode * @throws SyntaxError */ - function parseArgument() + private function parseArgument() { $start = $this->lexer->token; - $name = $this->parseName(); + $name = $this->parseName(); $this->expect(Token::COLON); $value = $this->parseValueLiteral(false); return new ArgumentNode([ - 'name' => $name, + 'name' => $name, 'value' => $value, - 'loc' => $this->loc($start) + 'loc' => $this->loc($start), ]); } @@ -611,18 +642,18 @@ class Parser * @return ArgumentNode * @throws SyntaxError */ - function parseConstArgument() + private function parseConstArgument() { $start = $this->lexer->token; - $name = $this->parseName(); + $name = $this->parseName(); $this->expect(Token::COLON); $value = $this->parseConstValue(); return new ArgumentNode([ - 'name' => $name, + 'name' => $name, 'value' => $value, - 'loc' => $this->loc($start) + 'loc' => $this->loc($start), ]); } @@ -632,16 +663,16 @@ class Parser * @return FragmentSpreadNode|InlineFragmentNode * @throws SyntaxError */ - function parseFragment() + private function parseFragment() { $start = $this->lexer->token; $this->expect(Token::SPREAD); if ($this->peek(Token::NAME) && $this->lexer->token->value !== 'on') { return new FragmentSpreadNode([ - 'name' => $this->parseFragmentName(), + 'name' => $this->parseFragmentName(), 'directives' => $this->parseDirectives(false), - 'loc' => $this->loc($start) + 'loc' => $this->loc($start), ]); } @@ -653,9 +684,9 @@ class Parser return new InlineFragmentNode([ 'typeCondition' => $typeCondition, - 'directives' => $this->parseDirectives(false), - 'selectionSet' => $this->parseSelectionSet(), - 'loc' => $this->loc($start) + 'directives' => $this->parseDirectives(false), + 'selectionSet' => $this->parseSelectionSet(), + 'loc' => $this->loc($start), ]); } @@ -663,7 +694,7 @@ class Parser * @return FragmentDefinitionNode * @throws SyntaxError */ - function parseFragmentDefinition() + private function parseFragmentDefinition() { $start = $this->lexer->token; $this->expectKeyword('fragment'); @@ -679,13 +710,14 @@ class Parser } $this->expectKeyword('on'); $typeCondition = $this->parseNamedType(); + return new FragmentDefinitionNode([ - 'name' => $name, + 'name' => $name, 'variableDefinitions' => $variableDefinitions, - 'typeCondition' => $typeCondition, - 'directives' => $this->parseDirectives(false), - 'selectionSet' => $this->parseSelectionSet(), - 'loc' => $this->loc($start) + 'typeCondition' => $typeCondition, + 'directives' => $this->parseDirectives(false), + 'selectionSet' => $this->parseSelectionSet(), + 'loc' => $this->loc($start), ]); } @@ -693,11 +725,12 @@ class Parser * @return NameNode * @throws SyntaxError */ - function parseFragmentName() + private function parseFragmentName() { if ($this->lexer->token->value === 'on') { throw $this->unexpected(); } + return $this->parseName(); } @@ -721,11 +754,11 @@ class Parser * * EnumValue : Name but not `true`, `false` or `null` * - * @param $isConst + * @param bool $isConst * @return BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|StringValueNode|VariableNode|ListValueNode|ObjectValueNode|NullValueNode * @throws SyntaxError */ - function parseValueLiteral($isConst) + private function parseValueLiteral($isConst) { $token = $this->lexer->token; switch ($token->kind) { @@ -735,15 +768,17 @@ class Parser return $this->parseObject($isConst); case Token::INT: $this->lexer->advance(); + return new IntValueNode([ 'value' => $token->value, - 'loc' => $this->loc($token) + 'loc' => $this->loc($token), ]); case Token::FLOAT: $this->lexer->advance(); + return new FloatValueNode([ 'value' => $token->value, - 'loc' => $this->loc($token) + 'loc' => $this->loc($token), ]); case Token::STRING: case Token::BLOCK_STRING: @@ -751,26 +786,29 @@ class Parser case Token::NAME: if ($token->value === 'true' || $token->value === 'false') { $this->lexer->advance(); + return new BooleanValueNode([ 'value' => $token->value === 'true', - 'loc' => $this->loc($token) + 'loc' => $this->loc($token), ]); - } else if ($token->value === 'null') { + } elseif ($token->value === 'null') { $this->lexer->advance(); + return new NullValueNode([ - 'loc' => $this->loc($token) + 'loc' => $this->loc($token), ]); } else { $this->lexer->advance(); + return new EnumValueNode([ 'value' => $token->value, - 'loc' => $this->loc($token) + 'loc' => $this->loc($token), ]); } break; case Token::DOLLAR: - if (!$isConst) { + if (! $isConst) { return $this->parseVariable(); } break; @@ -781,14 +819,15 @@ class Parser /** * @return StringValueNode */ - function parseStringLiteral() { + private function parseStringLiteral() + { $token = $this->lexer->token; $this->lexer->advance(); return new StringValueNode([ 'value' => $token->value, 'block' => $token->kind === Token::BLOCK_STRING, - 'loc' => $this->loc($token) + 'loc' => $this->loc($token), ]); } @@ -796,7 +835,7 @@ class Parser * @return BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|StringValueNode|VariableNode * @throws SyntaxError */ - function parseConstValue() + private function parseConstValue() { return $this->parseValueLiteral(true); } @@ -804,7 +843,7 @@ class Parser /** * @return BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|ListValueNode|ObjectValueNode|StringValueNode|VariableNode */ - function parseVariableValue() + private function parseVariableValue() { return $this->parseValueLiteral(false); } @@ -813,49 +852,57 @@ class Parser * @param bool $isConst * @return ListValueNode */ - function parseArray($isConst) + private function parseArray($isConst) { - $start = $this->lexer->token; - $item = $isConst ? 'parseConstValue' : 'parseVariableValue'; - return new ListValueNode([ - 'values' => $this->any(Token::BRACKET_L, [$this, $item], Token::BRACKET_R), - 'loc' => $this->loc($start) - ]); + $start = $this->lexer->token; + $parseFn = $isConst ? function () { + return $this->parseConstValue(); + } : function () { + return $this->parseVariableValue(); + }; + + return new ListValueNode( + [ + 'values' => $this->any(Token::BRACKET_L, $parseFn, Token::BRACKET_R), + 'loc' => $this->loc($start), + ] + ); } /** - * @param $isConst + * @param bool $isConst * @return ObjectValueNode */ - function parseObject($isConst) + private function parseObject($isConst) { $start = $this->lexer->token; $this->expect(Token::BRACE_L); $fields = []; - while (!$this->skip(Token::BRACE_R)) { + while (! $this->skip(Token::BRACE_R)) { $fields[] = $this->parseObjectField($isConst); } + return new ObjectValueNode([ 'fields' => new NodeList($fields), - 'loc' => $this->loc($start) + 'loc' => $this->loc($start), ]); } /** - * @param $isConst + * @param bool $isConst * @return ObjectFieldNode */ - function parseObjectField($isConst) + private function parseObjectField($isConst) { $start = $this->lexer->token; - $name = $this->parseName(); + $name = $this->parseName(); $this->expect(Token::COLON); return new ObjectFieldNode([ - 'name' => $name, + 'name' => $name, 'value' => $this->parseValueLiteral($isConst), - 'loc' => $this->loc($start) + 'loc' => $this->loc($start), ]); } @@ -866,12 +913,13 @@ class Parser * @return DirectiveNode[]|NodeList * @throws SyntaxError */ - function parseDirectives($isConst) + private function parseDirectives($isConst) { $directives = []; while ($this->peek(Token::AT)) { $directives[] = $this->parseDirective($isConst); } + return new NodeList($directives); } @@ -880,14 +928,15 @@ class Parser * @return DirectiveNode * @throws SyntaxError */ - function parseDirective($isConst) + private function parseDirective($isConst) { $start = $this->lexer->token; $this->expect(Token::AT); + return new DirectiveNode([ - 'name' => $this->parseName(), + 'name' => $this->parseName(), 'arguments' => $this->parseArguments($isConst), - 'loc' => $this->loc($start) + 'loc' => $this->loc($start), ]); } @@ -899,7 +948,7 @@ class Parser * @return ListTypeNode|NameNode|NonNullTypeNode * @throws SyntaxError */ - function parseTypeReference() + private function parseTypeReference() { $start = $this->lexer->token; @@ -908,7 +957,7 @@ class Parser $this->expect(Token::BRACKET_R); $type = new ListTypeNode([ 'type' => $type, - 'loc' => $this->loc($start) + 'loc' => $this->loc($start), ]); } else { $type = $this->parseNamedType(); @@ -916,20 +965,20 @@ class Parser if ($this->skip(Token::BANG)) { return new NonNullTypeNode([ 'type' => $type, - 'loc' => $this->loc($start) + 'loc' => $this->loc($start), ]); - } + return $type; } - function parseNamedType() + private function parseNamedType() { $start = $this->lexer->token; return new NamedTypeNode([ 'name' => $this->parseName(), - 'loc' => $this->loc($start) + 'loc' => $this->loc($start), ]); } @@ -953,7 +1002,7 @@ class Parser * @return TypeSystemDefinitionNode * @throws SyntaxError */ - function parseTypeSystemDefinition() + private function parseTypeSystemDefinition() { // Many definitions begin with a description and require a lookahead. $keywordToken = $this->peekDescription() @@ -962,15 +1011,24 @@ class Parser if ($keywordToken->kind === Token::NAME) { switch ($keywordToken->value) { - case 'schema': return $this->parseSchemaDefinition(); - case 'scalar': return $this->parseScalarTypeDefinition(); - case 'type': return $this->parseObjectTypeDefinition(); - case 'interface': return $this->parseInterfaceTypeDefinition(); - case 'union': return $this->parseUnionTypeDefinition(); - case 'enum': return $this->parseEnumTypeDefinition(); - case 'input': return $this->parseInputObjectTypeDefinition(); - case 'extend': return $this->parseTypeExtension(); - case 'directive': return $this->parseDirectiveDefinition(); + case 'schema': + return $this->parseSchemaDefinition(); + case 'scalar': + return $this->parseScalarTypeDefinition(); + case 'type': + return $this->parseObjectTypeDefinition(); + case 'interface': + return $this->parseInterfaceTypeDefinition(); + case 'union': + return $this->parseUnionTypeDefinition(); + case 'enum': + return $this->parseEnumTypeDefinition(); + case 'input': + return $this->parseInputObjectTypeDefinition(); + case 'extend': + return $this->parseTypeExtension(); + case 'directive': + return $this->parseDirectiveDefinition(); } } @@ -980,14 +1038,16 @@ class Parser /** * @return bool */ - function peekDescription() { + private function peekDescription() + { return $this->peek(Token::STRING) || $this->peek(Token::BLOCK_STRING); } /** * @return StringValueNode|null */ - function parseDescription() { + private function parseDescription() + { if ($this->peekDescription()) { return $this->parseStringLiteral(); } @@ -997,7 +1057,7 @@ class Parser * @return SchemaDefinitionNode * @throws SyntaxError */ - function parseSchemaDefinition() + private function parseSchemaDefinition() { $start = $this->lexer->token; $this->expectKeyword('schema'); @@ -1005,14 +1065,16 @@ class Parser $operationTypes = $this->many( Token::BRACE_L, - [$this, 'parseOperationTypeDefinition'], + function () { + return $this->parseOperationTypeDefinition(); + }, Token::BRACE_R ); return new SchemaDefinitionNode([ - 'directives' => $directives, + 'directives' => $directives, 'operationTypes' => $operationTypes, - 'loc' => $this->loc($start) + 'loc' => $this->loc($start), ]); } @@ -1020,17 +1082,17 @@ class Parser * @return OperationTypeDefinitionNode * @throws SyntaxError */ - function parseOperationTypeDefinition() + private function parseOperationTypeDefinition() { - $start = $this->lexer->token; + $start = $this->lexer->token; $operation = $this->parseOperationType(); $this->expect(Token::COLON); $type = $this->parseNamedType(); return new OperationTypeDefinitionNode([ 'operation' => $operation, - 'type' => $type, - 'loc' => $this->loc($start) + 'type' => $type, + 'loc' => $this->loc($start), ]); } @@ -1038,19 +1100,19 @@ class Parser * @return ScalarTypeDefinitionNode * @throws SyntaxError */ - function parseScalarTypeDefinition() + private function parseScalarTypeDefinition() { - $start = $this->lexer->token; + $start = $this->lexer->token; $description = $this->parseDescription(); $this->expectKeyword('scalar'); - $name = $this->parseName(); + $name = $this->parseName(); $directives = $this->parseDirectives(true); return new ScalarTypeDefinitionNode([ - 'name' => $name, - 'directives' => $directives, - 'loc' => $this->loc($start), - 'description' => $description + 'name' => $name, + 'directives' => $directives, + 'loc' => $this->loc($start), + 'description' => $description, ]); } @@ -1058,23 +1120,23 @@ class Parser * @return ObjectTypeDefinitionNode * @throws SyntaxError */ - function parseObjectTypeDefinition() + private function parseObjectTypeDefinition() { - $start = $this->lexer->token; + $start = $this->lexer->token; $description = $this->parseDescription(); $this->expectKeyword('type'); - $name = $this->parseName(); + $name = $this->parseName(); $interfaces = $this->parseImplementsInterfaces(); $directives = $this->parseDirectives(true); - $fields = $this->parseFieldsDefinition(); + $fields = $this->parseFieldsDefinition(); return new ObjectTypeDefinitionNode([ - 'name' => $name, - 'interfaces' => $interfaces, - 'directives' => $directives, - 'fields' => $fields, - 'loc' => $this->loc($start), - 'description' => $description + 'name' => $name, + 'interfaces' => $interfaces, + 'directives' => $directives, + 'fields' => $fields, + 'loc' => $this->loc($start), + 'description' => $description, ]); } @@ -1085,7 +1147,7 @@ class Parser * * @return NamedTypeNode[] */ - function parseImplementsInterfaces() + private function parseImplementsInterfaces() { $types = []; if ($this->lexer->token->value === 'implements') { @@ -1094,12 +1156,12 @@ class Parser $this->skip(Token::AMP); do { $types[] = $this->parseNamedType(); - } while ( - $this->skip(Token::AMP) || - // Legacy support for the SDL? - (!empty($this->lexer->options['allowLegacySDLImplementsInterfaces']) && $this->peek(Token::NAME)) + } while ($this->skip(Token::AMP) || + // Legacy support for the SDL? + (! empty($this->lexer->options['allowLegacySDLImplementsInterfaces']) && $this->peek(Token::NAME)) ); } + return $types; } @@ -1107,22 +1169,25 @@ class Parser * @return FieldDefinitionNode[]|NodeList * @throws SyntaxError */ - function parseFieldsDefinition() + private function parseFieldsDefinition() { // Legacy support for the SDL? - if ( - !empty($this->lexer->options['allowLegacySDLEmptyFields']) && + if (! empty($this->lexer->options['allowLegacySDLEmptyFields']) && $this->peek(Token::BRACE_L) && $this->lexer->lookahead()->kind === Token::BRACE_R ) { $this->lexer->advance(); $this->lexer->advance(); + return []; } + return $this->peek(Token::BRACE_L) ? $this->many( Token::BRACE_L, - [$this, 'parseFieldDefinition'], + function () { + return $this->parseFieldDefinition(); + }, Token::BRACE_R ) : new NodeList([]); @@ -1132,23 +1197,23 @@ class Parser * @return FieldDefinitionNode * @throws SyntaxError */ - function parseFieldDefinition() + private function parseFieldDefinition() { - $start = $this->lexer->token; + $start = $this->lexer->token; $description = $this->parseDescription(); - $name = $this->parseName(); - $args = $this->parseArgumentDefs(); + $name = $this->parseName(); + $args = $this->parseArgumentDefs(); $this->expect(Token::COLON); - $type = $this->parseTypeReference(); + $type = $this->parseTypeReference(); $directives = $this->parseDirectives(true); return new FieldDefinitionNode([ - 'name' => $name, - 'arguments' => $args, - 'type' => $type, - 'directives' => $directives, - 'loc' => $this->loc($start), - 'description' => $description + 'name' => $name, + 'arguments' => $args, + 'type' => $type, + 'directives' => $directives, + 'loc' => $this->loc($start), + 'description' => $description, ]); } @@ -1156,37 +1221,45 @@ class Parser * @return InputValueDefinitionNode[]|NodeList * @throws SyntaxError */ - function parseArgumentDefs() + private function parseArgumentDefs() { - if (!$this->peek(Token::PAREN_L)) { + if (! $this->peek(Token::PAREN_L)) { return new NodeList([]); } - return $this->many(Token::PAREN_L, [$this, 'parseInputValueDef'], Token::PAREN_R); + + return $this->many( + Token::PAREN_L, + function () { + return $this->parseInputValueDef(); + }, + Token::PAREN_R + ); } /** * @return InputValueDefinitionNode * @throws SyntaxError */ - function parseInputValueDef() + private function parseInputValueDef() { - $start = $this->lexer->token; + $start = $this->lexer->token; $description = $this->parseDescription(); - $name = $this->parseName(); + $name = $this->parseName(); $this->expect(Token::COLON); - $type = $this->parseTypeReference(); + $type = $this->parseTypeReference(); $defaultValue = null; if ($this->skip(Token::EQUALS)) { $defaultValue = $this->parseConstValue(); } $directives = $this->parseDirectives(true); + return new InputValueDefinitionNode([ - 'name' => $name, - 'type' => $type, + 'name' => $name, + 'type' => $type, 'defaultValue' => $defaultValue, - 'directives' => $directives, - 'loc' => $this->loc($start), - 'description' => $description + 'directives' => $directives, + 'loc' => $this->loc($start), + 'description' => $description, ]); } @@ -1194,21 +1267,21 @@ class Parser * @return InterfaceTypeDefinitionNode * @throws SyntaxError */ - function parseInterfaceTypeDefinition() + private function parseInterfaceTypeDefinition() { - $start = $this->lexer->token; + $start = $this->lexer->token; $description = $this->parseDescription(); $this->expectKeyword('interface'); - $name = $this->parseName(); + $name = $this->parseName(); $directives = $this->parseDirectives(true); - $fields = $this->parseFieldsDefinition(); + $fields = $this->parseFieldsDefinition(); return new InterfaceTypeDefinitionNode([ - 'name' => $name, - 'directives' => $directives, - 'fields' => $fields, - 'loc' => $this->loc($start), - 'description' => $description + 'name' => $name, + 'directives' => $directives, + 'fields' => $fields, + 'loc' => $this->loc($start), + 'description' => $description, ]); } @@ -1219,21 +1292,21 @@ class Parser * @return UnionTypeDefinitionNode * @throws SyntaxError */ - function parseUnionTypeDefinition() + private function parseUnionTypeDefinition() { - $start = $this->lexer->token; + $start = $this->lexer->token; $description = $this->parseDescription(); $this->expectKeyword('union'); - $name = $this->parseName(); + $name = $this->parseName(); $directives = $this->parseDirectives(true); - $types = $this->parseUnionMemberTypes(); + $types = $this->parseUnionMemberTypes(); return new UnionTypeDefinitionNode([ - 'name' => $name, - 'directives' => $directives, - 'types' => $types, - 'loc' => $this->loc($start), - 'description' => $description + 'name' => $name, + 'directives' => $directives, + 'types' => $types, + 'loc' => $this->loc($start), + 'description' => $description, ]); } @@ -1244,7 +1317,7 @@ class Parser * * @return NamedTypeNode[] */ - function parseUnionMemberTypes() + private function parseUnionMemberTypes() { $types = []; if ($this->skip(Token::EQUALS)) { @@ -1254,6 +1327,7 @@ class Parser $types[] = $this->parseNamedType(); } while ($this->skip(Token::PIPE)); } + return $types; } @@ -1261,21 +1335,21 @@ class Parser * @return EnumTypeDefinitionNode * @throws SyntaxError */ - function parseEnumTypeDefinition() + private function parseEnumTypeDefinition() { - $start = $this->lexer->token; + $start = $this->lexer->token; $description = $this->parseDescription(); $this->expectKeyword('enum'); - $name = $this->parseName(); + $name = $this->parseName(); $directives = $this->parseDirectives(true); - $values = $this->parseEnumValuesDefinition(); + $values = $this->parseEnumValuesDefinition(); return new EnumTypeDefinitionNode([ - 'name' => $name, - 'directives' => $directives, - 'values' => $values, - 'loc' => $this->loc($start), - 'description' => $description + 'name' => $name, + 'directives' => $directives, + 'values' => $values, + 'loc' => $this->loc($start), + 'description' => $description, ]); } @@ -1283,14 +1357,16 @@ class Parser * @return EnumValueDefinitionNode[]|NodeList * @throws SyntaxError */ - function parseEnumValuesDefinition() + private function parseEnumValuesDefinition() { return $this->peek(Token::BRACE_L) ? $this->many( Token::BRACE_L, - [$this, 'parseEnumValueDefinition'], + function () { + return $this->parseEnumValueDefinition(); + }, Token::BRACE_R - ) + ) : new NodeList([]); } @@ -1298,18 +1374,18 @@ class Parser * @return EnumValueDefinitionNode * @throws SyntaxError */ - function parseEnumValueDefinition() + private function parseEnumValueDefinition() { - $start = $this->lexer->token; + $start = $this->lexer->token; $description = $this->parseDescription(); - $name = $this->parseName(); - $directives = $this->parseDirectives(true); + $name = $this->parseName(); + $directives = $this->parseDirectives(true); return new EnumValueDefinitionNode([ - 'name' => $name, - 'directives' => $directives, - 'loc' => $this->loc($start), - 'description' => $description + 'name' => $name, + 'directives' => $directives, + 'loc' => $this->loc($start), + 'description' => $description, ]); } @@ -1317,21 +1393,21 @@ class Parser * @return InputObjectTypeDefinitionNode * @throws SyntaxError */ - function parseInputObjectTypeDefinition() + private function parseInputObjectTypeDefinition() { - $start = $this->lexer->token; + $start = $this->lexer->token; $description = $this->parseDescription(); $this->expectKeyword('input'); - $name = $this->parseName(); + $name = $this->parseName(); $directives = $this->parseDirectives(true); - $fields = $this->parseInputFieldsDefinition(); + $fields = $this->parseInputFieldsDefinition(); return new InputObjectTypeDefinitionNode([ - 'name' => $name, - 'directives' => $directives, - 'fields' => $fields, - 'loc' => $this->loc($start), - 'description' => $description + 'name' => $name, + 'directives' => $directives, + 'fields' => $fields, + 'loc' => $this->loc($start), + 'description' => $description, ]); } @@ -1339,11 +1415,14 @@ class Parser * @return InputValueDefinitionNode[]|NodeList * @throws SyntaxError */ - function parseInputFieldsDefinition() { + private function parseInputFieldsDefinition() + { return $this->peek(Token::BRACE_L) ? $this->many( Token::BRACE_L, - [$this, 'parseInputValueDef'], + function () { + return $this->parseInputValueDef(); + }, Token::BRACE_R ) : new NodeList([]); @@ -1361,7 +1440,7 @@ class Parser * @return TypeExtensionNode * @throws SyntaxError */ - function parseTypeExtension() + private function parseTypeExtension() { $keywordToken = $this->lexer->lookahead(); @@ -1389,20 +1468,21 @@ class Parser * @return ScalarTypeExtensionNode * @throws SyntaxError */ - function parseScalarTypeExtension() { + private function parseScalarTypeExtension() + { $start = $this->lexer->token; $this->expectKeyword('extend'); $this->expectKeyword('scalar'); - $name = $this->parseName(); + $name = $this->parseName(); $directives = $this->parseDirectives(true); if (count($directives) === 0) { throw $this->unexpected(); } return new ScalarTypeExtensionNode([ - 'name' => $name, + 'name' => $name, 'directives' => $directives, - 'loc' => $this->loc($start) + 'loc' => $this->loc($start), ]); } @@ -1410,17 +1490,17 @@ class Parser * @return ObjectTypeExtensionNode * @throws SyntaxError */ - function parseObjectTypeExtension() { + private function parseObjectTypeExtension() + { $start = $this->lexer->token; $this->expectKeyword('extend'); $this->expectKeyword('type'); - $name = $this->parseName(); + $name = $this->parseName(); $interfaces = $this->parseImplementsInterfaces(); $directives = $this->parseDirectives(true); - $fields = $this->parseFieldsDefinition(); + $fields = $this->parseFieldsDefinition(); - if ( - !$interfaces && + if (count($interfaces) === 0 && count($directives) === 0 && count($fields) === 0 ) { @@ -1428,11 +1508,11 @@ class Parser } return new ObjectTypeExtensionNode([ - 'name' => $name, + 'name' => $name, 'interfaces' => $interfaces, 'directives' => $directives, - 'fields' => $fields, - 'loc' => $this->loc($start) + 'fields' => $fields, + 'loc' => $this->loc($start), ]); } @@ -1440,25 +1520,25 @@ class Parser * @return InterfaceTypeExtensionNode * @throws SyntaxError */ - function parseInterfaceTypeExtension() { + private function parseInterfaceTypeExtension() + { $start = $this->lexer->token; $this->expectKeyword('extend'); $this->expectKeyword('interface'); - $name = $this->parseName(); + $name = $this->parseName(); $directives = $this->parseDirectives(true); - $fields = $this->parseFieldsDefinition(); - if ( - count($directives) === 0 && + $fields = $this->parseFieldsDefinition(); + if (count($directives) === 0 && count($fields) === 0 ) { throw $this->unexpected(); } return new InterfaceTypeExtensionNode([ - 'name' => $name, + 'name' => $name, 'directives' => $directives, - 'fields' => $fields, - 'loc' => $this->loc($start) + 'fields' => $fields, + 'loc' => $this->loc($start), ]); } @@ -1470,25 +1550,25 @@ class Parser * @return UnionTypeExtensionNode * @throws SyntaxError */ - function parseUnionTypeExtension() { + private function parseUnionTypeExtension() + { $start = $this->lexer->token; $this->expectKeyword('extend'); $this->expectKeyword('union'); - $name = $this->parseName(); + $name = $this->parseName(); $directives = $this->parseDirectives(true); - $types = $this->parseUnionMemberTypes(); - if ( - count($directives) === 0 && - !$types + $types = $this->parseUnionMemberTypes(); + if (count($directives) === 0 && + ! $types ) { throw $this->unexpected(); } return new UnionTypeExtensionNode([ - 'name' => $name, + 'name' => $name, 'directives' => $directives, - 'types' => $types, - 'loc' => $this->loc($start) + 'types' => $types, + 'loc' => $this->loc($start), ]); } @@ -1496,25 +1576,25 @@ class Parser * @return EnumTypeExtensionNode * @throws SyntaxError */ - function parseEnumTypeExtension() { + private function parseEnumTypeExtension() + { $start = $this->lexer->token; $this->expectKeyword('extend'); $this->expectKeyword('enum'); - $name = $this->parseName(); + $name = $this->parseName(); $directives = $this->parseDirectives(true); - $values = $this->parseEnumValuesDefinition(); - if ( - count($directives) === 0 && + $values = $this->parseEnumValuesDefinition(); + if (count($directives) === 0 && count($values) === 0 ) { throw $this->unexpected(); } return new EnumTypeExtensionNode([ - 'name' => $name, + 'name' => $name, 'directives' => $directives, - 'values' => $values, - 'loc' => $this->loc($start) + 'values' => $values, + 'loc' => $this->loc($start), ]); } @@ -1522,25 +1602,25 @@ class Parser * @return InputObjectTypeExtensionNode * @throws SyntaxError */ - function parseInputObjectTypeExtension() { + private function parseInputObjectTypeExtension() + { $start = $this->lexer->token; $this->expectKeyword('extend'); $this->expectKeyword('input'); - $name = $this->parseName(); + $name = $this->parseName(); $directives = $this->parseDirectives(true); - $fields = $this->parseInputFieldsDefinition(); - if ( - count($directives) === 0 && + $fields = $this->parseInputFieldsDefinition(); + if (count($directives) === 0 && count($fields) === 0 ) { throw $this->unexpected(); } return new InputObjectTypeExtensionNode([ - 'name' => $name, + 'name' => $name, 'directives' => $directives, - 'fields' => $fields, - 'loc' => $this->loc($start) + 'fields' => $fields, + 'loc' => $this->loc($start), ]); } @@ -1551,9 +1631,9 @@ class Parser * @return DirectiveDefinitionNode * @throws SyntaxError */ - function parseDirectiveDefinition() + private function parseDirectiveDefinition() { - $start = $this->lexer->token; + $start = $this->lexer->token; $description = $this->parseDescription(); $this->expectKeyword('directive'); $this->expect(Token::AT); @@ -1563,11 +1643,11 @@ class Parser $locations = $this->parseDirectiveLocations(); return new DirectiveDefinitionNode([ - 'name' => $name, - 'arguments' => $args, - 'locations' => $locations, - 'loc' => $this->loc($start), - 'description' => $description + 'name' => $name, + 'arguments' => $args, + 'locations' => $locations, + 'loc' => $this->loc($start), + 'description' => $description, ]); } @@ -1575,7 +1655,7 @@ class Parser * @return NameNode[] * @throws SyntaxError */ - function parseDirectiveLocations() + private function parseDirectiveLocations() { // Optional leading pipe $this->skip(Token::PIPE); @@ -1583,6 +1663,7 @@ class Parser do { $locations[] = $this->parseDirectiveLocation(); } while ($this->skip(Token::PIPE)); + return $locations; } @@ -1590,10 +1671,10 @@ class Parser * @return NameNode * @throws SyntaxError */ - function parseDirectiveLocation() + private function parseDirectiveLocation() { $start = $this->lexer->token; - $name = $this->parseName(); + $name = $this->parseName(); if (DirectiveLocation::has($name->value)) { return $name; } diff --git a/src/Language/Printer.php b/src/Language/Printer.php index 49ea385..3c9873f 100644 --- a/src/Language/Printer.php +++ b/src/Language/Printer.php @@ -1,29 +1,32 @@ printAST($ast); } protected function __construct() - {} + { + } public function printAST($ast) { - return Visitor::visit($ast, [ - 'leave' => [ - NodeKind::NAME => function(Node $node) { - return '' . $node->value; - }, - NodeKind::VARIABLE => function($node) { - return '$' . $node->name; - }, - NodeKind::DOCUMENT => function(DocumentNode $node) { - return $this->join($node->definitions, "\n\n") . "\n"; - }, - NodeKind::OPERATION_DEFINITION => function(OperationDefinitionNode $node) { - $op = $node->operation; - $name = $node->name; - $varDefs = $this->wrap('(', $this->join($node->variableDefinitions, ', '), ')'); - $directives = $this->join($node->directives, ' '); - $selectionSet = $node->selectionSet; - // Anonymous queries with no directives or variable definitions can use - // the query short form. - return !$name && !$directives && !$varDefs && $op === 'query' - ? $selectionSet - : $this->join([$op, $this->join([$name, $varDefs]), $directives, $selectionSet], ' '); - }, - NodeKind::VARIABLE_DEFINITION => function(VariableDefinitionNode $node) { - return $node->variable . ': ' . $node->type . $this->wrap(' = ', $node->defaultValue); - }, - NodeKind::SELECTION_SET => function(SelectionSetNode $node) { - return $this->block($node->selections); - }, - NodeKind::FIELD => function(FieldNode $node) { - return $this->join([ - $this->wrap('', $node->alias, ': ') . $node->name . $this->wrap('(', $this->join($node->arguments, ', '), ')'), - $this->join($node->directives, ' '), - $node->selectionSet - ], ' '); - }, - NodeKind::ARGUMENT => function(ArgumentNode $node) { - return $node->name . ': ' . $node->value; - }, + return Visitor::visit( + $ast, + [ + 'leave' => [ + NodeKind::NAME => function (Node $node) { + return '' . $node->value; + }, - // Fragments - NodeKind::FRAGMENT_SPREAD => function(FragmentSpreadNode $node) { - return '...' . $node->name . $this->wrap(' ', $this->join($node->directives, ' ')); - }, - NodeKind::INLINE_FRAGMENT => function(InlineFragmentNode $node) { - return $this->join([ - "...", - $this->wrap('on ', $node->typeCondition), - $this->join($node->directives, ' '), - $node->selectionSet - ], ' '); - }, - NodeKind::FRAGMENT_DEFINITION => function(FragmentDefinitionNode $node) { - // 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, ' '), ' ') - . $node->selectionSet; - }, + NodeKind::VARIABLE => function ($node) { + return '$' . $node->name; + }, - // Value - NodeKind::INT => function(IntValueNode $node) { - return $node->value; - }, - NodeKind::FLOAT => function(FloatValueNode $node) { - return $node->value; - }, - NodeKind::STRING => function(StringValueNode $node, $key) { - if ($node->block) { - return $this->printBlockString($node->value, $key === 'description'); - } - return json_encode($node->value); - }, - NodeKind::BOOLEAN => function(BooleanValueNode $node) { - return $node->value ? 'true' : 'false'; - }, - NodeKind::NULL => function(NullValueNode $node) { - return 'null'; - }, - NodeKind::ENUM => function(EnumValueNode $node) { - return $node->value; - }, - NodeKind::LST => function(ListValueNode $node) { - return '[' . $this->join($node->values, ', ') . ']'; - }, - NodeKind::OBJECT => function(ObjectValueNode $node) { - return '{' . $this->join($node->fields, ', ') . '}'; - }, - NodeKind::OBJECT_FIELD => function(ObjectFieldNode $node) { - return $node->name . ': ' . $node->value; - }, + NodeKind::DOCUMENT => function (DocumentNode $node) { + return $this->join($node->definitions, "\n\n") . "\n"; + }, - // DirectiveNode - NodeKind::DIRECTIVE => function(DirectiveNode $node) { - return '@' . $node->name . $this->wrap('(', $this->join($node->arguments, ', '), ')'); - }, + NodeKind::OPERATION_DEFINITION => function (OperationDefinitionNode $node) { + $op = $node->operation; + $name = $node->name; + $varDefs = $this->wrap('(', $this->join($node->variableDefinitions, ', '), ')'); + $directives = $this->join($node->directives, ' '); + $selectionSet = $node->selectionSet; + // Anonymous queries with no directives or variable definitions can use + // the query short form. + return ! $name && ! $directives && ! $varDefs && $op === 'query' + ? $selectionSet + : $this->join([$op, $this->join([$name, $varDefs]), $directives, $selectionSet], ' '); + }, - // Type - NodeKind::NAMED_TYPE => function(NamedTypeNode $node) { - return $node->name; - }, - NodeKind::LIST_TYPE => function(ListTypeNode $node) { - return '[' . $node->type . ']'; - }, - NodeKind::NON_NULL_TYPE => function(NonNullTypeNode $node) { - return $node->type . '!'; - }, + NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $node) { + return $node->variable . ': ' . $node->type . $this->wrap(' = ', $node->defaultValue); + }, - // Type System Definitions - NodeKind::SCHEMA_DEFINITION => function(SchemaDefinitionNode $def) { - return $this->join([ - 'schema', - $this->join($def->directives, ' '), - $this->block($def->operationTypes) - ], ' '); - }, - NodeKind::OPERATION_TYPE_DEFINITION => function(OperationTypeDefinitionNode $def) { - return $def->operation . ': ' . $def->type; - }, + NodeKind::SELECTION_SET => function (SelectionSetNode $node) { + return $this->block($node->selections); + }, - NodeKind::SCALAR_TYPE_DEFINITION => $this->addDescription(function(ScalarTypeDefinitionNode $def) { - return $this->join(['scalar', $def->name, $this->join($def->directives, ' ')], ' '); - }), - NodeKind::OBJECT_TYPE_DEFINITION => $this->addDescription(function(ObjectTypeDefinitionNode $def) { - return $this->join([ - 'type', - $def->name, - $this->wrap('implements ', $this->join($def->interfaces, ' & ')), - $this->join($def->directives, ' '), - $this->block($def->fields) - ], ' '); - }), - NodeKind::FIELD_DEFINITION => $this->addDescription(function(FieldDefinitionNode $def) { - return $def->name - . $this->wrap('(', $this->join($def->arguments, ', '), ')') - . ': ' . $def->type - . $this->wrap(' ', $this->join($def->directives, ' ')); - }), - NodeKind::INPUT_VALUE_DEFINITION => $this->addDescription(function(InputValueDefinitionNode $def) { - return $this->join([ - $def->name . ': ' . $def->type, - $this->wrap('= ', $def->defaultValue), - $this->join($def->directives, ' ') - ], ' '); - }), - NodeKind::INTERFACE_TYPE_DEFINITION => $this->addDescription(function(InterfaceTypeDefinitionNode $def) { - return $this->join([ - 'interface', - $def->name, - $this->join($def->directives, ' '), - $this->block($def->fields) - ], ' '); - }), - NodeKind::UNION_TYPE_DEFINITION => $this->addDescription(function(UnionTypeDefinitionNode $def) { - return $this->join([ - 'union', - $def->name, - $this->join($def->directives, ' '), - $def->types - ? '= ' . $this->join($def->types, ' | ') - : '' - ], ' '); - }), - NodeKind::ENUM_TYPE_DEFINITION => $this->addDescription(function(EnumTypeDefinitionNode $def) { - return $this->join([ - 'enum', - $def->name, - $this->join($def->directives, ' '), - $this->block($def->values) - ], ' '); - }), - NodeKind::ENUM_VALUE_DEFINITION => $this->addDescription(function(EnumValueDefinitionNode $def) { - return $this->join([$def->name, $this->join($def->directives, ' ')], ' '); - }), - NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $this->addDescription(function(InputObjectTypeDefinitionNode $def) { - return $this->join([ - 'input', - $def->name, - $this->join($def->directives, ' '), - $this->block($def->fields) - ], ' '); - }), - NodeKind::SCALAR_TYPE_EXTENSION => function(ScalarTypeExtensionNode $def) { - return $this->join([ - 'extend scalar', - $def->name, - $this->join($def->directives, ' '), - ], ' '); - }, - NodeKind::OBJECT_TYPE_EXTENSION => function(ObjectTypeExtensionNode $def) { - return $this->join([ - 'extend type', - $def->name, - $this->wrap('implements ', $this->join($def->interfaces, ' & ')), - $this->join($def->directives, ' '), - $this->block($def->fields), - ], ' '); - }, - NodeKind::INTERFACE_TYPE_EXTENSION => function(InterfaceTypeExtensionNode $def) { - return $this->join([ - 'extend interface', - $def->name, - $this->join($def->directives, ' '), - $this->block($def->fields), - ], ' '); - }, - NodeKind::UNION_TYPE_EXTENSION => function(UnionTypeExtensionNode $def) { - return $this->join([ - 'extend union', - $def->name, - $this->join($def->directives, ' '), - $def->types - ? '= ' . $this->join($def->types, ' | ') - : '' - ], ' '); - }, - NodeKind::ENUM_TYPE_EXTENSION => function(EnumTypeExtensionNode $def) { - return $this->join([ - 'extend enum', - $def->name, - $this->join($def->directives, ' '), - $this->block($def->values), - ], ' '); - }, - NodeKind::INPUT_OBJECT_TYPE_EXTENSION => function(InputObjectTypeExtensionNode $def) { - return $this->join([ - 'extend input', - $def->name, - $this->join($def->directives, ' '), - $this->block($def->fields), - ], ' '); - }, - NodeKind::DIRECTIVE_DEFINITION => $this->addDescription(function(DirectiveDefinitionNode $def) { - return 'directive @' - . $def->name - . $this->wrap('(', $this->join($def->arguments, ', '), ')') - . ' on ' . $this->join($def->locations, ' | '); - }) + NodeKind::FIELD => function (FieldNode $node) { + return $this->join( + [ + $this->wrap('', $node->alias, ': ') . $node->name . $this->wrap( + '(', + $this->join($node->arguments, ', '), + ')' + ), + $this->join($node->directives, ' '), + $node->selectionSet, + ], + ' ' + ); + }, + + NodeKind::ARGUMENT => function (ArgumentNode $node) { + return $node->name . ': ' . $node->value; + }, + + NodeKind::FRAGMENT_SPREAD => function (FragmentSpreadNode $node) { + return '...' . $node->name . $this->wrap(' ', $this->join($node->directives, ' ')); + }, + + NodeKind::INLINE_FRAGMENT => function (InlineFragmentNode $node) { + return $this->join( + [ + '...', + $this->wrap('on ', $node->typeCondition), + $this->join($node->directives, ' '), + $node->selectionSet, + ], + ' ' + ); + }, + + NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) { + // Note: fragment variable definitions are experimental and may be changed or removed in the future. + return sprintf('fragment %s', $node->name) + . $this->wrap('(', $this->join($node->variableDefinitions, ', '), ')') + . sprintf(' on %s ', $node->typeCondition) + . $this->wrap('', $this->join($node->directives, ' '), ' ') + . $node->selectionSet; + }, + + NodeKind::INT => function (IntValueNode $node) { + return $node->value; + }, + + NodeKind::FLOAT => function (FloatValueNode $node) { + return $node->value; + }, + + NodeKind::STRING => function (StringValueNode $node, $key) { + if ($node->block) { + return $this->printBlockString($node->value, $key === 'description'); + } + + return json_encode($node->value); + }, + + NodeKind::BOOLEAN => function (BooleanValueNode $node) { + return $node->value ? 'true' : 'false'; + }, + + NodeKind::NULL => function (NullValueNode $node) { + return 'null'; + }, + + NodeKind::ENUM => function (EnumValueNode $node) { + return $node->value; + }, + + NodeKind::LST => function (ListValueNode $node) { + return '[' . $this->join($node->values, ', ') . ']'; + }, + + NodeKind::OBJECT => function (ObjectValueNode $node) { + return '{' . $this->join($node->fields, ', ') . '}'; + }, + + NodeKind::OBJECT_FIELD => function (ObjectFieldNode $node) { + return $node->name . ': ' . $node->value; + }, + + NodeKind::DIRECTIVE => function (DirectiveNode $node) { + return '@' . $node->name . $this->wrap('(', $this->join($node->arguments, ', '), ')'); + }, + + NodeKind::NAMED_TYPE => function (NamedTypeNode $node) { + return $node->name; + }, + + NodeKind::LIST_TYPE => function (ListTypeNode $node) { + return '[' . $node->type . ']'; + }, + + NodeKind::NON_NULL_TYPE => function (NonNullTypeNode $node) { + return $node->type . '!'; + }, + + NodeKind::SCHEMA_DEFINITION => function (SchemaDefinitionNode $def) { + return $this->join( + [ + 'schema', + $this->join($def->directives, ' '), + $this->block($def->operationTypes), + ], + ' ' + ); + }, + + NodeKind::OPERATION_TYPE_DEFINITION => function (OperationTypeDefinitionNode $def) { + return $def->operation . ': ' . $def->type; + }, + + NodeKind::SCALAR_TYPE_DEFINITION => $this->addDescription(function (ScalarTypeDefinitionNode $def) { + return $this->join(['scalar', $def->name, $this->join($def->directives, ' ')], ' '); + }), + + NodeKind::OBJECT_TYPE_DEFINITION => $this->addDescription(function (ObjectTypeDefinitionNode $def) { + return $this->join( + [ + 'type', + $def->name, + $this->wrap('implements ', $this->join($def->interfaces, ' & ')), + $this->join($def->directives, ' '), + $this->block($def->fields), + ], + ' ' + ); + }), + + NodeKind::FIELD_DEFINITION => $this->addDescription(function (FieldDefinitionNode $def) { + return $def->name + . $this->wrap('(', $this->join($def->arguments, ', '), ')') + . ': ' . $def->type + . $this->wrap(' ', $this->join($def->directives, ' ')); + }), + + NodeKind::INPUT_VALUE_DEFINITION => $this->addDescription(function (InputValueDefinitionNode $def) { + return $this->join( + [ + $def->name . ': ' . $def->type, + $this->wrap('= ', $def->defaultValue), + $this->join($def->directives, ' '), + ], + ' ' + ); + }), + + NodeKind::INTERFACE_TYPE_DEFINITION => $this->addDescription( + function (InterfaceTypeDefinitionNode $def) { + return $this->join( + [ + 'interface', + $def->name, + $this->join($def->directives, ' '), + $this->block($def->fields), + ], + ' ' + ); + } + ), + + NodeKind::UNION_TYPE_DEFINITION => $this->addDescription(function (UnionTypeDefinitionNode $def) { + return $this->join( + [ + 'union', + $def->name, + $this->join($def->directives, ' '), + $def->types + ? '= ' . $this->join($def->types, ' | ') + : '', + ], + ' ' + ); + }), + + NodeKind::ENUM_TYPE_DEFINITION => $this->addDescription(function (EnumTypeDefinitionNode $def) { + return $this->join( + [ + 'enum', + $def->name, + $this->join($def->directives, ' '), + $this->block($def->values), + ], + ' ' + ); + }), + + NodeKind::ENUM_VALUE_DEFINITION => $this->addDescription(function (EnumValueDefinitionNode $def) { + return $this->join([$def->name, $this->join($def->directives, ' ')], ' '); + }), + + NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $this->addDescription(function ( + InputObjectTypeDefinitionNode $def + ) { + return $this->join( + [ + 'input', + $def->name, + $this->join($def->directives, ' '), + $this->block($def->fields), + ], + ' ' + ); + }), + + NodeKind::SCALAR_TYPE_EXTENSION => function (ScalarTypeExtensionNode $def) { + return $this->join( + [ + 'extend scalar', + $def->name, + $this->join($def->directives, ' '), + ], + ' ' + ); + }, + + NodeKind::OBJECT_TYPE_EXTENSION => function (ObjectTypeExtensionNode $def) { + return $this->join( + [ + 'extend type', + $def->name, + $this->wrap('implements ', $this->join($def->interfaces, ' & ')), + $this->join($def->directives, ' '), + $this->block($def->fields), + ], + ' ' + ); + }, + + NodeKind::INTERFACE_TYPE_EXTENSION => function (InterfaceTypeExtensionNode $def) { + return $this->join( + [ + 'extend interface', + $def->name, + $this->join($def->directives, ' '), + $this->block($def->fields), + ], + ' ' + ); + }, + + NodeKind::UNION_TYPE_EXTENSION => function (UnionTypeExtensionNode $def) { + return $this->join( + [ + 'extend union', + $def->name, + $this->join($def->directives, ' '), + $def->types + ? '= ' . $this->join($def->types, ' | ') + : '', + ], + ' ' + ); + }, + + NodeKind::ENUM_TYPE_EXTENSION => function (EnumTypeExtensionNode $def) { + return $this->join( + [ + 'extend enum', + $def->name, + $this->join($def->directives, ' '), + $this->block($def->values), + ], + ' ' + ); + }, + + NodeKind::INPUT_OBJECT_TYPE_EXTENSION => function (InputObjectTypeExtensionNode $def) { + return $this->join( + [ + 'extend input', + $def->name, + $this->join($def->directives, ' '), + $this->block($def->fields), + ], + ' ' + ); + }, + + NodeKind::DIRECTIVE_DEFINITION => $this->addDescription(function (DirectiveDefinitionNode $def) { + return 'directive @' + . $def->name + . $this->wrap('(', $this->join($def->arguments, ', '), ')') + . ' on ' . $this->join($def->locations, ' | '); + }), + ], ] - ]); + ); } public function addDescription(\Closure $cb) @@ -371,7 +469,9 @@ class Printer $separator, Utils::filter( $maybeArray, - function($x) { return !!$x;} + function ($x) { + return ! ! $x; + } ) ) : ''; @@ -382,8 +482,10 @@ class Printer * 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. */ - private function printBlockString($value, $isDescription) { + private function printBlockString($value, $isDescription) + { $escaped = str_replace('"""', '\\"""', $value); + return (($value[0] === ' ' || $value[0] === "\t") && strpos($value, "\n") === false) ? ('"""' . preg_replace('/"$/', "\"\n", $escaped) . '"""') : ("\"\"\"\n" . ($isDescription ? $escaped : $this->indent($escaped)) . "\n\"\"\""); diff --git a/src/Language/Source.php b/src/Language/Source.php index 899d450..1beb8bc 100644 --- a/src/Language/Source.php +++ b/src/Language/Source.php @@ -1,36 +1,33 @@ body = $body; - $this->length = mb_strlen($body, 'UTF-8'); - $this->name = $name ?: 'GraphQL request'; + $this->body = $body; + $this->length = mb_strlen($body, 'UTF-8'); + $this->name = $name ?: 'GraphQL request'; $this->locationOffset = $location ?: new SourceLocation(1, 1); Utils::invariant( @@ -66,21 +62,22 @@ class Source } /** - * @param $position + * @param int $position * @return SourceLocation */ public function getLocation($position) { - $line = 1; + $line = 1; $column = $position + 1; - $utfChars = json_decode('"\u2028\u2029"'); - $lineRegexp = '/\r\n|[\n\r'.$utfChars.']/su'; - $matches = []; + $utfChars = json_decode('"\u2028\u2029"'); + $lineRegexp = '/\r\n|[\n\r' . $utfChars . ']/su'; + $matches = []; preg_match_all($lineRegexp, mb_substr($this->body, 0, $position, 'UTF-8'), $matches, PREG_OFFSET_CAPTURE); foreach ($matches[0] as $index => $match) { $line += 1; + $column = $position + 1 - ($match[1] + mb_strlen($match[0], 'UTF-8')); } diff --git a/src/Language/SourceLocation.php b/src/Language/SourceLocation.php index d09363b..2c406ca 100644 --- a/src/Language/SourceLocation.php +++ b/src/Language/SourceLocation.php @@ -1,30 +1,40 @@ line = $line; + $this->line = $line; $this->column = $col; } /** - * @return array + * @return int[] */ public function toArray() { return [ - 'line' => $this->line, - 'column' => $this->column + 'line' => $this->line, + 'column' => $this->column, ]; } /** - * @return array + * @return int[] */ public function toSerializableArray() { @@ -32,13 +42,9 @@ class SourceLocation implements \JsonSerializable } /** - * Specify data which should be serialized to JSON - * @link http://php.net/manual/en/jsonserializable.jsonserialize.php - * @return mixed data which can be serialized by json_encode, - * which is a value of any type other than a resource. - * @since 5.4.0 + * @return int[] */ - function jsonSerialize() + public function jsonSerialize() { return $this->toSerializableArray(); } diff --git a/src/Language/Token.php b/src/Language/Token.php index 880cb0d..a1c966c 100644 --- a/src/Language/Token.php +++ b/src/Language/Token.php @@ -1,4 +1,7 @@ '; - const EOF = ''; - const BANG = '!'; - const DOLLAR = '$'; - const AMP = '&'; - const PAREN_L = '('; - const PAREN_R = ')'; - const SPREAD = '...'; - const COLON = ':'; - const EQUALS = '='; - const AT = '@'; - const BRACKET_L = '['; - const BRACKET_R = ']'; - const BRACE_L = '{'; - const PIPE = '|'; - const BRACE_R = '}'; - const NAME = 'Name'; - const INT = 'Int'; - const FLOAT = 'Float'; - const STRING = 'String'; + const SOF = ''; + const EOF = ''; + const BANG = '!'; + const DOLLAR = '$'; + const AMP = '&'; + const PAREN_L = '('; + const PAREN_R = ')'; + const SPREAD = '...'; + const COLON = ':'; + const EQUALS = '='; + const AT = '@'; + const BRACKET_L = '['; + const BRACKET_R = ']'; + const BRACE_L = '{'; + const PIPE = '|'; + const BRACE_R = '}'; + const NAME = 'Name'; + const INT = 'Int'; + const FLOAT = 'Float'; + const STRING = 'String'; const BLOCK_STRING = 'BlockString'; - const COMMENT = 'Comment'; + const COMMENT = 'Comment'; /** * The kind of Token (see one of constants above). @@ -66,9 +69,7 @@ class Token */ public $column; - /** - * @var string|null - */ + /** @var string|null */ public $value; /** @@ -80,31 +81,28 @@ class Token */ public $prev; - /** - * @var Token - */ + /** @var Token */ public $next; /** - * Token constructor. - * @param $kind - * @param $start - * @param $end - * @param $line - * @param $column - * @param Token $previous - * @param null $value + * + * @param string $kind + * @param int $start + * @param int $end + * @param int $line + * @param int $column + * @param mixed|null $value */ - public function __construct($kind, $start, $end, $line, $column, Token $previous = null, $value = null) + public function __construct($kind, $start, $end, $line, $column, ?Token $previous = null, $value = null) { - $this->kind = $kind; - $this->start = (int) $start; - $this->end = (int) $end; - $this->line = (int) $line; - $this->column = (int) $column; - $this->prev = $previous; - $this->next = null; - $this->value = $value; + $this->kind = $kind; + $this->start = $start; + $this->end = $end; + $this->line = $line; + $this->column = $column; + $this->prev = $previous; + $this->next = null; + $this->value = $value; } /** @@ -112,19 +110,19 @@ class Token */ public function getDescription() { - return $this->kind . ($this->value ? ' "' . $this->value . '"' : ''); + return $this->kind . ($this->value ? ' "' . $this->value . '"' : ''); } /** - * @return array + * @return (string|int|null)[] */ public function toArray() { return [ - 'kind' => $this->kind, - 'value' => $this->value, - 'line' => $this->line, - 'column' => $this->column + 'kind' => $this->kind, + 'value' => $this->value, + 'line' => $this->line, + 'column' => $this->column, ]; } } diff --git a/src/Language/Visitor.php b/src/Language/Visitor.php index ab20d1e..e266f0f 100644 --- a/src/Language/Visitor.php +++ b/src/Language/Visitor.php @@ -1,19 +1,24 @@ [], - NodeKind::DOCUMENT => ['definitions'], + NodeKind::NAME => [], + NodeKind::DOCUMENT => ['definitions'], NodeKind::OPERATION_DEFINITION => ['name', 'variableDefinitions', 'directives', 'selectionSet'], - NodeKind::VARIABLE_DEFINITION => ['variable', 'type', 'defaultValue'], - NodeKind::VARIABLE => ['name'], - NodeKind::SELECTION_SET => ['selections'], - NodeKind::FIELD => ['alias', 'name', 'arguments', 'directives', 'selectionSet'], - NodeKind::ARGUMENT => ['name', 'value'], - NodeKind::FRAGMENT_SPREAD => ['name', 'directives'], - NodeKind::INLINE_FRAGMENT => ['typeCondition', 'directives', 'selectionSet'], - NodeKind::FRAGMENT_DEFINITION => [ + NodeKind::VARIABLE_DEFINITION => ['variable', 'type', 'defaultValue'], + NodeKind::VARIABLE => ['name'], + NodeKind::SELECTION_SET => ['selections'], + NodeKind::FIELD => ['alias', 'name', 'arguments', 'directives', 'selectionSet'], + NodeKind::ARGUMENT => ['name', 'value'], + NodeKind::FRAGMENT_SPREAD => ['name', 'directives'], + NodeKind::INLINE_FRAGMENT => ['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' + 'selectionSet', ], - NodeKind::INT => [], - NodeKind::FLOAT => [], - NodeKind::STRING => [], - NodeKind::BOOLEAN => [], - NodeKind::NULL => [], - NodeKind::ENUM => [], - NodeKind::LST => ['values'], - NodeKind::OBJECT => ['fields'], - NodeKind::OBJECT_FIELD => ['name', 'value'], - NodeKind::DIRECTIVE => ['name', 'arguments'], - NodeKind::NAMED_TYPE => ['name'], - NodeKind::LIST_TYPE => ['type'], + NodeKind::INT => [], + NodeKind::FLOAT => [], + NodeKind::STRING => [], + NodeKind::BOOLEAN => [], + NodeKind::NULL => [], + NodeKind::ENUM => [], + NodeKind::LST => ['values'], + NodeKind::OBJECT => ['fields'], + NodeKind::OBJECT_FIELD => ['name', 'value'], + NodeKind::DIRECTIVE => ['name', 'arguments'], + NodeKind::NAMED_TYPE => ['name'], + NodeKind::LIST_TYPE => ['type'], NodeKind::NON_NULL_TYPE => ['type'], - NodeKind::SCHEMA_DEFINITION => ['directives', 'operationTypes'], - NodeKind::OPERATION_TYPE_DEFINITION => ['type'], - NodeKind::SCALAR_TYPE_DEFINITION => ['description', 'name', 'directives'], - NodeKind::OBJECT_TYPE_DEFINITION => ['description', 'name', 'interfaces', 'directives', 'fields'], - NodeKind::FIELD_DEFINITION => ['description', 'name', 'arguments', 'type', 'directives'], - NodeKind::INPUT_VALUE_DEFINITION => ['description', 'name', 'type', 'defaultValue', 'directives'], - NodeKind::INTERFACE_TYPE_DEFINITION => ['description', 'name', 'directives', 'fields'], - NodeKind::UNION_TYPE_DEFINITION => ['description', 'name', 'directives', 'types'], - NodeKind::ENUM_TYPE_DEFINITION => ['description', 'name', 'directives', 'values'], - NodeKind::ENUM_VALUE_DEFINITION => ['description', 'name', 'directives'], + NodeKind::SCHEMA_DEFINITION => ['directives', 'operationTypes'], + NodeKind::OPERATION_TYPE_DEFINITION => ['type'], + NodeKind::SCALAR_TYPE_DEFINITION => ['description', 'name', 'directives'], + NodeKind::OBJECT_TYPE_DEFINITION => ['description', 'name', 'interfaces', 'directives', 'fields'], + NodeKind::FIELD_DEFINITION => ['description', 'name', 'arguments', 'type', 'directives'], + NodeKind::INPUT_VALUE_DEFINITION => ['description', 'name', 'type', 'defaultValue', 'directives'], + NodeKind::INTERFACE_TYPE_DEFINITION => ['description', 'name', 'directives', 'fields'], + NodeKind::UNION_TYPE_DEFINITION => ['description', 'name', 'directives', 'types'], + NodeKind::ENUM_TYPE_DEFINITION => ['description', 'name', 'directives', 'values'], + NodeKind::ENUM_VALUE_DEFINITION => ['description', 'name', 'directives'], NodeKind::INPUT_OBJECT_TYPE_DEFINITION => ['description', 'name', 'directives', 'fields'], - NodeKind::SCALAR_TYPE_EXTENSION => ['name', 'directives'], - NodeKind::OBJECT_TYPE_EXTENSION => ['name', 'interfaces', 'directives', 'fields'], - NodeKind::INTERFACE_TYPE_EXTENSION => ['name', 'directives', 'fields'], - NodeKind::UNION_TYPE_EXTENSION => ['name', 'directives', 'types'], - NodeKind::ENUM_TYPE_EXTENSION => ['name', 'directives', 'values'], + NodeKind::SCALAR_TYPE_EXTENSION => ['name', 'directives'], + NodeKind::OBJECT_TYPE_EXTENSION => ['name', 'interfaces', 'directives', 'fields'], + NodeKind::INTERFACE_TYPE_EXTENSION => ['name', 'directives', 'fields'], + NodeKind::UNION_TYPE_EXTENSION => ['name', 'directives', 'types'], + NodeKind::ENUM_TYPE_EXTENSION => ['name', 'directives', 'values'], NodeKind::INPUT_OBJECT_TYPE_EXTENSION => ['name', 'directives', 'fields'], - NodeKind::DIRECTIVE_DEFINITION => ['description', 'name', 'arguments', 'locations'] + NodeKind::DIRECTIVE_DEFINITION => ['description', 'name', 'arguments', 'locations'], ]; /** * Visit the AST (see class description for details) * * @api - * @param Node $root - * @param array $visitor - * @param array $keyMap + * @param Node|ArrayObject|stdClass $root + * @param callable[] $visitor + * @param mixed[]|null $keyMap * @return Node|mixed * @throws \Exception */ @@ -175,28 +181,28 @@ class Visitor { $visitorKeys = $keyMap ?: self::$visitorKeys; - $stack = null; - $inArray = $root instanceof NodeList || is_array($root); - $keys = [$root]; - $index = -1; - $edits = []; - $parent = null; - $path = []; + $stack = null; + $inArray = $root instanceof NodeList || is_array($root); + $keys = [$root]; + $index = -1; + $edits = []; + $parent = null; + $path = []; $ancestors = []; - $newRoot = $root; + $newRoot = $root; $UNDEFINED = null; do { $index++; $isLeaving = $index === count($keys); - $key = null; - $node = null; - $isEdited = $isLeaving && count($edits) !== 0; + $key = null; + $node = null; + $isEdited = $isLeaving && count($edits) !== 0; if ($isLeaving) { - $key = !$ancestors ? $UNDEFINED : $path[count($path) - 1]; - $node = $parent; + $key = ! $ancestors ? $UNDEFINED : $path[count($path) - 1]; + $node = $parent; $parent = array_pop($ancestors); if ($isEdited) { @@ -210,7 +216,7 @@ class Visitor } $editOffset = 0; for ($ii = 0; $ii < count($edits); $ii++) { - $editKey = $edits[$ii][0]; + $editKey = $edits[$ii][0]; $editValue = $edits[$ii][1]; if ($inArray) { @@ -232,13 +238,13 @@ class Visitor } } } - $index = $stack['index']; - $keys = $stack['keys']; - $edits = $stack['edits']; + $index = $stack['index']; + $keys = $stack['keys']; + $edits = $stack['edits']; $inArray = $stack['inArray']; - $stack = $stack['prev']; + $stack = $stack['prev']; } else { - $key = $parent ? ($inArray ? $index : $keys[$index]) : $UNDEFINED; + $key = $parent ? ($inArray ? $index : $keys[$index]) : $UNDEFINED; $node = $parent ? (($parent instanceof NodeList || is_array($parent)) ? $parent[$key] : $parent->{$key}) : $newRoot; if ($node === null || $node === $UNDEFINED) { continue; @@ -249,8 +255,8 @@ class Visitor } $result = null; - if (!$node instanceof NodeList && !is_array($node)) { - if (!($node instanceof Node)) { + if (! $node instanceof NodeList && ! is_array($node)) { + if (! ($node instanceof Node)) { throw new \Exception('Invalid AST Node: ' . json_encode($node)); } @@ -264,7 +270,7 @@ class Visitor if ($result->doBreak) { break; } - if (!$isLeaving && $result->doContinue) { + if (! $isLeaving && $result->doContinue) { array_pop($path); continue; } @@ -276,13 +282,13 @@ class Visitor } $edits[] = [$key, $editValue]; - if (!$isLeaving) { - if ($editValue instanceof Node) { - $node = $editValue; - } else { + if (! $isLeaving) { + if (! ($editValue instanceof Node)) { array_pop($path); continue; } + + $node = $editValue; } } } @@ -295,16 +301,16 @@ class Visitor if ($isLeaving) { array_pop($path); } else { - $stack = [ + $stack = [ 'inArray' => $inArray, - 'index' => $index, - 'keys' => $keys, - 'edits' => $edits, - 'prev' => $stack + 'index' => $index, + 'keys' => $keys, + 'edits' => $edits, + 'prev' => $stack, ]; $inArray = $node instanceof NodeList || is_array($node); - $keys = ($inArray ? $node : $visitorKeys[$node->kind]) ?: []; + $keys = ($inArray ? $node : $visitorKeys[$node->kind]) ?: []; $index = -1; $edits = []; if ($parent) { @@ -312,7 +318,6 @@ class Visitor } $parent = $node; } - } while ($stack); if (count($edits) !== 0) { @@ -330,8 +335,9 @@ class Visitor */ public static function stop() { - $r = new VisitorOperation(); + $r = new VisitorOperation(); $r->doBreak = true; + return $r; } @@ -343,8 +349,9 @@ class Visitor */ public static function skipNode() { - $r = new VisitorOperation(); + $r = new VisitorOperation(); $r->doContinue = true; + return $r; } @@ -356,66 +363,79 @@ class Visitor */ public static function removeNode() { - $r = new VisitorOperation(); + $r = new VisitorOperation(); $r->removeNode = true; + return $r; } /** - * @param $visitors - * @return array + * @param callable[][] $visitors + * @return callable[][] */ - static function visitInParallel($visitors) + public static function visitInParallel($visitors) { $visitorsCount = count($visitors); - $skipping = new \SplFixedArray($visitorsCount); + $skipping = new \SplFixedArray($visitorsCount); return [ - 'enter' => function ($node) use ($visitors, $skipping, $visitorsCount) { + 'enter' => function (Node $node) use ($visitors, $skipping, $visitorsCount) { for ($i = 0; $i < $visitorsCount; $i++) { - if (empty($skipping[$i])) { - $fn = self::getVisitFn($visitors[$i], $node->kind, /* isLeaving */ false); + if (! empty($skipping[$i])) { + continue; + } - if ($fn) { - $result = call_user_func_array($fn, func_get_args()); + $fn = self::getVisitFn( + $visitors[$i], + $node->kind, /* isLeaving */ + false + ); - if ($result instanceof VisitorOperation) { - if ($result->doContinue) { - $skipping[$i] = $node; - } else if ($result->doBreak) { - $skipping[$i] = $result; - } else if ($result->removeNode) { - return $result; - } - } else if ($result !== null) { - return $result; - } + if (! $fn) { + continue; + } + + $result = call_user_func_array($fn, func_get_args()); + + if ($result instanceof VisitorOperation) { + if ($result->doContinue) { + $skipping[$i] = $node; + } elseif ($result->doBreak) { + $skipping[$i] = $result; + } elseif ($result->removeNode) { + return $result; } + } elseif ($result !== null) { + return $result; } } }, - 'leave' => function ($node) use ($visitors, $skipping, $visitorsCount) { + 'leave' => function (Node $node) use ($visitors, $skipping, $visitorsCount) { for ($i = 0; $i < $visitorsCount; $i++) { if (empty($skipping[$i])) { - $fn = self::getVisitFn($visitors[$i], $node->kind, /* isLeaving */ true); + $fn = self::getVisitFn( + $visitors[$i], + $node->kind, /* isLeaving */ + true + ); if ($fn) { $result = call_user_func_array($fn, func_get_args()); if ($result instanceof VisitorOperation) { if ($result->doBreak) { $skipping[$i] = $result; - } else if ($result->removeNode) { + } elseif ($result->removeNode) { return $result; } - } else if ($result !== null) { + } elseif ($result !== null) { return $result; } } - } else if ($skipping[$i] === $node) { + } elseif ($skipping[$i] === $node) { $skipping[$i] = null; } } - } + }, ]; } @@ -423,10 +443,10 @@ class Visitor * Creates a new visitor instance which maintains a provided TypeInfo instance * along with visiting visitor. */ - static function visitWithTypeInfo(TypeInfo $typeInfo, $visitor) + public static function visitWithTypeInfo(TypeInfo $typeInfo, $visitor) { return [ - 'enter' => function ($node) use ($typeInfo, $visitor) { + 'enter' => function (Node $node) use ($typeInfo, $visitor) { $typeInfo->enter($node); $fn = self::getVisitFn($visitor, $node->kind, false); @@ -438,52 +458,58 @@ class Visitor $typeInfo->enter($result); } } + return $result; } + return null; }, - 'leave' => function ($node) use ($typeInfo, $visitor) { - $fn = self::getVisitFn($visitor, $node->kind, true); + 'leave' => function (Node $node) use ($typeInfo, $visitor) { + $fn = self::getVisitFn($visitor, $node->kind, true); $result = $fn ? call_user_func_array($fn, func_get_args()) : null; $typeInfo->leave($node); + return $result; - } + }, ]; } /** - * @param $visitor - * @param $kind - * @param $isLeaving - * @return null + * @param callable[]|null $visitor + * @param string $kind + * @param bool $isLeaving + * @return callable|null */ public static function getVisitFn($visitor, $kind, $isLeaving) { - if (!$visitor) { + if ($visitor === null) { return null; } - $kindVisitor = isset($visitor[$kind]) ? $visitor[$kind] : null; - if (!$isLeaving && is_callable($kindVisitor)) { + $kindVisitor = $visitor[$kind] ?? null; + + if (! $isLeaving && is_callable($kindVisitor)) { // { Kind() {} } return $kindVisitor; } if (is_array($kindVisitor)) { if ($isLeaving) { - $kindSpecificVisitor = isset($kindVisitor['leave']) ? $kindVisitor['leave'] : null; + $kindSpecificVisitor = $kindVisitor['leave'] ?? null; } else { - $kindSpecificVisitor = isset($kindVisitor['enter']) ? $kindVisitor['enter'] : null; + $kindSpecificVisitor = $kindVisitor['enter'] ?? null; } if ($kindSpecificVisitor && is_callable($kindSpecificVisitor)) { // { Kind: { enter() {}, leave() {} } } return $kindSpecificVisitor; } + return null; } $visitor += ['leave' => null, 'enter' => null]; + $specificVisitor = $isLeaving ? $visitor['leave'] : $visitor['enter']; if ($specificVisitor) { @@ -491,13 +517,14 @@ class Visitor // { enter() {}, leave() {} } return $specificVisitor; } - $specificKindVisitor = isset($specificVisitor[$kind]) ? $specificVisitor[$kind] : null; + $specificKindVisitor = $specificVisitor[$kind] ?? null; if (is_callable($specificKindVisitor)) { // { enter: { Kind() {} }, leave: { Kind() {} } } return $specificKindVisitor; } } + return null; } } diff --git a/src/Language/VisitorOperation.php b/src/Language/VisitorOperation.php new file mode 100644 index 0000000..2316261 --- /dev/null +++ b/src/Language/VisitorOperation.php @@ -0,0 +1,17 @@ +loc = Location::create($node['loc']['start'], $node['loc']['end']); } - foreach ($node as $key => $value) { - if ('loc' === $key || 'kind' === $key) { - continue ; + if ($key === 'loc' || $key === 'kind') { + continue; } if (is_array($value)) { if (isset($value[0]) || empty($value)) { @@ -94,6 +110,7 @@ class AST } $instance->{$key} = $value; } + return $instance; } @@ -101,8 +118,7 @@ class AST * Convert AST node to serializable array * * @api - * @param Node $node - * @return array + * @return mixed[] */ public static function toArray(Node $node) { @@ -128,17 +144,17 @@ class AST * | null | NullValue | * * @api - * @param $value - * @param InputType $type + * @param Type|mixed|null $value * @return ObjectValueNode|ListValueNode|BooleanValueNode|IntValueNode|FloatValueNode|EnumValueNode|StringValueNode|NullValueNode */ - static function astFromValue($value, InputType $type) + public static function astFromValue($value, InputType $type) { if ($type instanceof NonNull) { $astValue = self::astFromValue($value, $type->getWrappedType()); if ($astValue instanceof NullValueNode) { return null; } + return $astValue; } @@ -154,56 +170,65 @@ class AST $valuesNodes = []; foreach ($value as $item) { $itemNode = self::astFromValue($item, $itemType); - if ($itemNode) { - $valuesNodes[] = $itemNode; + if (! $itemNode) { + continue; } + + $valuesNodes[] = $itemNode; } + return new ListValueNode(['values' => $valuesNodes]); } + return self::astFromValue($value, $itemType); } // Populate the fields of the input object by creating ASTs from each value // in the PHP object according to the fields in the input type. if ($type instanceof InputObjectType) { - $isArray = is_array($value); + $isArray = is_array($value); $isArrayLike = $isArray || $value instanceof \ArrayAccess; - if ($value === null || (!$isArrayLike && !is_object($value))) { + if ($value === null || (! $isArrayLike && ! is_object($value))) { return null; } - $fields = $type->getFields(); + $fields = $type->getFields(); $fieldNodes = []; foreach ($fields as $fieldName => $field) { if ($isArrayLike) { - $fieldValue = isset($value[$fieldName]) ? $value[$fieldName] : null; + $fieldValue = $value[$fieldName] ?? null; } else { - $fieldValue = isset($value->{$fieldName}) ? $value->{$fieldName} : null; + $fieldValue = $value->{$fieldName} ?? null; } // Have to check additionally if key exists, since we differentiate between // "no key" and "value is null": - if (null !== $fieldValue) { + if ($fieldValue !== null) { $fieldExists = true; - } else if ($isArray) { + } elseif ($isArray) { $fieldExists = array_key_exists($fieldName, $value); - } else if ($isArrayLike) { + } elseif ($isArrayLike) { /** @var \ArrayAccess $value */ $fieldExists = $value->offsetExists($fieldName); } else { $fieldExists = property_exists($value, $fieldName); } - if ($fieldExists) { - $fieldNode = self::astFromValue($fieldValue, $field->getType()); - - if ($fieldNode) { - $fieldNodes[] = new ObjectFieldNode([ - 'name' => new NameNode(['value' => $fieldName]), - 'value' => $fieldNode - ]); - } + if (! $fieldExists) { + continue; } + + $fieldNode = self::astFromValue($fieldValue, $field->getType()); + + if (! $fieldNode) { + continue; + } + + $fieldNodes[] = new ObjectFieldNode([ + 'name' => new NameNode(['value' => $fieldName]), + 'value' => $fieldNode, + ]); } + return new ObjectValueNode(['fields' => $fieldNodes]); } @@ -232,9 +257,12 @@ class AST return new IntValueNode(['value' => $serialized]); } if (is_float($serialized)) { + // int cast with == used for performance reasons + // @codingStandardsIgnoreLine if ((int) $serialized == $serialized) { return new IntValueNode(['value' => $serialized]); } + return new FloatValueNode(['value' => $serialized]); } if (is_string($serialized)) { @@ -282,17 +310,16 @@ class AST * | Null Value | null | * * @api - * @param $valueNode - * @param InputType $type - * @param mixed[]|null $variables - * @return array|null|\stdClass + * @param ValueNode|null $valueNode + * @param mixed[]|null $variables + * @return mixed[]|null|\stdClass * @throws \Exception */ public static function valueFromAST($valueNode, InputType $type, $variables = null) { $undefined = Utils::undefined(); - if (!$valueNode) { + if ($valueNode === null) { // When there is no AST, then there is also no value. // Importantly, this is different from returning the GraphQL null value. return $undefined; @@ -303,6 +330,7 @@ class AST // Invalid: intentionally return no value. return $undefined; } + return self::valueFromAST($valueNode, $type->getWrappedType(), $variables); } @@ -314,7 +342,7 @@ class AST if ($valueNode instanceof VariableNode) { $variableName = $valueNode->name->value; - if (!$variables || !array_key_exists($variableName, $variables)) { + if (! $variables || ! array_key_exists($variableName, $variables)) { // No valid return value. return $undefined; } @@ -329,7 +357,7 @@ class AST if ($valueNode instanceof ListValueNode) { $coercedValues = []; - $itemNodes = $valueNode->values; + $itemNodes = $valueNode->values; foreach ($itemNodes as $itemNode) { if (self::isMissingVariable($itemNode, $variables)) { // If an array contains a missing variable, it is either coerced to @@ -348,6 +376,7 @@ class AST $coercedValues[] = $itemValue; } } + return $coercedValues; } $coercedValue = self::valueFromAST($valueNode, $itemType, $variables); @@ -355,31 +384,37 @@ class AST // Invalid: intentionally return no value. return $undefined; } + return [$coercedValue]; } if ($type instanceof InputObjectType) { - if (!$valueNode instanceof ObjectValueNode) { + if (! $valueNode instanceof ObjectValueNode) { // Invalid: intentionally return no value. return $undefined; } $coercedObj = []; - $fields = $type->getFields(); - $fieldNodes = Utils::keyMap($valueNode->fields, function($field) {return $field->name->value;}); + $fields = $type->getFields(); + $fieldNodes = Utils::keyMap( + $valueNode->fields, + function ($field) { + return $field->name->value; + } + ); foreach ($fields as $field) { /** @var ValueNode $fieldNode */ $fieldName = $field->name; - $fieldNode = isset($fieldNodes[$fieldName]) ? $fieldNodes[$fieldName] : null; + $fieldNode = $fieldNodes[$fieldName] ?? null; - if (!$fieldNode || self::isMissingVariable($fieldNode->value, $variables)) { + if (! $fieldNode || self::isMissingVariable($fieldNode->value, $variables)) { if ($field->defaultValueExists()) { $coercedObj[$fieldName] = $field->defaultValue; - } else if ($field->getType() instanceof NonNull) { + } elseif ($field->getType() instanceof NonNull) { // Invalid: intentionally return no value. return $undefined; } - continue ; + continue; } $fieldValue = self::valueFromAST($fieldNode ? $fieldNode->value : null, $field->getType(), $variables); @@ -390,15 +425,16 @@ class AST } $coercedObj[$fieldName] = $fieldValue; } + return $coercedObj; } if ($type instanceof EnumType) { - if (!$valueNode instanceof EnumValueNode) { + if (! $valueNode instanceof EnumValueNode) { return $undefined; } $enumValue = $type->getValue($valueNode->value); - if (!$enumValue) { + if (! $enumValue) { return $undefined; } @@ -438,12 +474,13 @@ class AST * | Null | null | * * @api - * @param Node $valueNode - * @param array|null $variables + * @param Node $valueNode + * @param mixed[]|null $variables * @return mixed * @throws \Exception */ - public static function valueFromASTUntyped($valueNode, array $variables = null) { + public static function valueFromASTUntyped($valueNode, ?array $variables = null) + { switch (true) { case $valueNode instanceof NullValueNode: return null; @@ -457,24 +494,29 @@ class AST return $valueNode->value; case $valueNode instanceof ListValueNode: return array_map( - function($node) use ($variables) { + function ($node) use ($variables) { return self::valueFromASTUntyped($node, $variables); }, iterator_to_array($valueNode->values) ); case $valueNode instanceof ObjectValueNode: - return array_combine( - array_map( - function($field) { return $field->name->value; }, - iterator_to_array($valueNode->fields) - ), - array_map( - function($field) use ($variables) { return self::valueFromASTUntyped($field->value, $variables); }, - iterator_to_array($valueNode->fields) - ) - ); + return array_combine( + array_map( + function ($field) { + return $field->name->value; + }, + iterator_to_array($valueNode->fields) + ), + array_map( + function ($field) use ($variables) { + return self::valueFromASTUntyped($field->value, $variables); + }, + iterator_to_array($valueNode->fields) + ) + ); case $valueNode instanceof VariableNode: $variableName = $valueNode->name->value; + return ($variables && isset($variables[$variableName])) ? $variables[$variableName] : null; @@ -487,19 +529,20 @@ class AST * Returns type definition for given AST Type node * * @api - * @param Schema $schema * @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode - * @return Type + * @return Type|null * @throws \Exception */ public static function typeFromAST(Schema $schema, $inputTypeNode) { if ($inputTypeNode instanceof ListTypeNode) { $innerType = self::typeFromAST($schema, $inputTypeNode->type); + return $innerType ? new ListOfType($innerType) : null; } if ($inputTypeNode instanceof NonNullTypeNode) { $innerType = self::typeFromAST($schema, $inputTypeNode->type); + return $innerType ? new NonNull($innerType) : null; } if ($inputTypeNode instanceof NamedTypeNode) { @@ -512,21 +555,20 @@ class AST /** * Returns true if the provided valueNode is a variable which is not defined * in the set of variables. - * @param $valueNode - * @param $variables + * @param ValueNode $valueNode + * @param mixed[] $variables * @return bool */ private static function isMissingVariable($valueNode, $variables) { return $valueNode instanceof VariableNode && - (!$variables || !array_key_exists($valueNode->name->value, $variables)); + (count($variables) === 0 || ! array_key_exists($valueNode->name->value, $variables)); } /** * Returns operation type ("query", "mutation" or "subscription") given a document and operation name * * @api - * @param DocumentNode $document * @param string $operationName * @return bool */ @@ -534,13 +576,16 @@ class AST { if ($document->definitions) { foreach ($document->definitions as $def) { - if ($def instanceof OperationDefinitionNode) { - if (!$operationName || (isset($def->name->value) && $def->name->value === $operationName)) { - return $def->operation; - } + if (! ($def instanceof OperationDefinitionNode)) { + continue; + } + + if (! $operationName || (isset($def->name->value) && $def->name->value === $operationName)) { + return $def->operation; } } } + return false; } } diff --git a/src/Utils/ASTDefinitionBuilder.php b/src/Utils/ASTDefinitionBuilder.php index 36c8ec8..8ca7d16 100644 --- a/src/Utils/ASTDefinitionBuilder.php +++ b/src/Utils/ASTDefinitionBuilder.php @@ -1,16 +1,21 @@ typeDefintionsMap = $typeDefintionsMap; + /** + * @param Node[] $typeDefintionsMap + * @param bool[] $options + */ + public function __construct( + array $typeDefintionsMap, + $options, + callable $resolveType, + ?callable $typeConfigDecorator = null + ) { + $this->typeDefintionsMap = $typeDefintionsMap; $this->typeConfigDecorator = $typeConfigDecorator; - $this->options = $options; - $this->resolveType = $resolveType; + $this->options = $options; + $this->resolveType = $resolveType; $this->cache = Type::getAllBuiltInTypes(); } /** - * @param Type $innerType * @param TypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode * @return Type */ private function buildWrappedType(Type $innerType, TypeNode $inputTypeNode) { - if ($inputTypeNode->kind == NodeKind::LIST_TYPE) { + if ($inputTypeNode->kind === NodeKind::LIST_TYPE) { return Type::listOf($this->buildWrappedType($innerType, $inputTypeNode->type)); } - if ($inputTypeNode->kind == NodeKind::NON_NULL_TYPE) { + if ($inputTypeNode->kind === NodeKind::NON_NULL_TYPE) { $wrappedType = $this->buildWrappedType($innerType, $inputTypeNode->type); + return Type::nonNull(NonNull::assertNullableType($wrappedType)); } + return $innerType; } @@ -95,37 +103,29 @@ class ASTDefinitionBuilder while ($namedType->kind === NodeKind::LIST_TYPE || $namedType->kind === NodeKind::NON_NULL_TYPE) { $namedType = $namedType->type; } + return $namedType; } /** - * @param string $typeName + * @param string $typeName * @param NamedTypeNode|null $typeNode * @return Type * @throws Error */ - private function internalBuildType($typeName, $typeNode = null) { - if (!isset($this->cache[$typeName])) { + private function internalBuildType($typeName, $typeNode = null) + { + if (! isset($this->cache[$typeName])) { if (isset($this->typeDefintionsMap[$typeName])) { $type = $this->makeSchemaDef($this->typeDefintionsMap[$typeName]); if ($this->typeConfigDecorator) { $fn = $this->typeConfigDecorator; try { $config = $fn($type->config, $this->typeDefintionsMap[$typeName], $this->typeDefintionsMap); - } catch (\Exception $e) { - throw new Error( - "Type config decorator passed to " . (static::class) . " threw an error " . - "when building $typeName type: {$e->getMessage()}", - null, - null, - null, - null, - $e - ); } catch (\Throwable $e) { throw new Error( - "Type config decorator passed to " . (static::class) . " threw an error " . - "when building $typeName type: {$e->getMessage()}", + sprintf('Type config decorator passed to %s threw an error ', static::class) . + sprintf('when building %s type: %s', $typeName, $e->getMessage()), null, null, null, @@ -133,17 +133,20 @@ class ASTDefinitionBuilder $e ); } - if (!is_array($config) || isset($config[0])) { + if (! is_array($config) || isset($config[0])) { throw new Error( - "Type config decorator passed to " . (static::class) . " is expected to return an array, but got " . - Utils::getVariableType($config) + sprintf( + 'Type config decorator passed to %s is expected to return an array, but got %s', + static::class, + Utils::getVariableType($config) + ) ); } $type = $this->makeSchemaDefFromConfig($this->typeDefintionsMap[$typeName], $config); } $this->cache[$typeName] = $type; } else { - $fn = $this->resolveType; + $fn = $this->resolveType; $this->cache[$typeName] = $fn($typeName, $typeNode); } } @@ -166,26 +169,29 @@ class ASTDefinitionBuilder } /** - * @param TypeNode $typeNode * @return Type|InputType * @throws Error */ private function internalBuildWrappedType(TypeNode $typeNode) { $typeDef = $this->buildType($this->getNamedTypeNode($typeNode)); + return $this->buildWrappedType($typeDef, $typeNode); } public function buildDirective(DirectiveDefinitionNode $directiveNode) { return new Directive([ - 'name' => $directiveNode->name->value, + 'name' => $directiveNode->name->value, 'description' => $this->getDescription($directiveNode), - 'locations' => Utils::map($directiveNode->locations, function ($node) { - return $node->value; - }), - 'args' => $directiveNode->arguments ? FieldArgument::createMap($this->makeInputValues($directiveNode->arguments)) : null, - 'astNode' => $directiveNode, + 'locations' => Utils::map( + $directiveNode->locations, + function ($node) { + return $node->value; + } + ), + 'args' => $directiveNode->arguments ? FieldArgument::createMap($this->makeInputValues($directiveNode->arguments)) : null, + 'astNode' => $directiveNode, ]); } @@ -195,17 +201,22 @@ class ASTDefinitionBuilder // Note: While this could make assertions to get the correctly typed // value, that would throw immediately while type system validation // with validateSchema() will produce more actionable results. - 'type' => $this->internalBuildWrappedType($field->type), - 'description' => $this->getDescription($field), - 'args' => $field->arguments ? $this->makeInputValues($field->arguments) : null, + 'type' => $this->internalBuildWrappedType($field->type), + 'description' => $this->getDescription($field), + 'args' => $field->arguments ? $this->makeInputValues($field->arguments) : null, 'deprecationReason' => $this->getDeprecationReason($field), - 'astNode' => $field, + 'astNode' => $field, ]; } + /** + * @param ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode|EnumTypeDefinitionNode|ScalarTypeDefinitionNode|InputObjectTypeDefinitionNode|UnionTypeDefinitionNode $def + * @return CustomScalarType|EnumType|InputObjectType|InterfaceType|ObjectType|UnionType + * @throws Error + */ private function makeSchemaDef($def) { - if (!$def) { + if (! $def) { throw new Error('def must be defined.'); } switch ($def->kind) { @@ -222,13 +233,19 @@ class ASTDefinitionBuilder case NodeKind::INPUT_OBJECT_TYPE_DEFINITION: return $this->makeInputObjectDef($def); default: - throw new Error("Type kind of {$def->kind} not supported."); + throw new Error(sprintf('Type kind of %s not supported.', $def->kind)); } } + /** + * @param ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode|EnumTypeExtensionNode|ScalarTypeDefinitionNode|InputObjectTypeDefinitionNode $def + * @param mixed[] $config + * @return CustomScalarType|EnumType|InputObjectType|InterfaceType|ObjectType|UnionType + * @throws Error + */ private function makeSchemaDefFromConfig($def, array $config) { - if (!$def) { + if (! $def) { throw new Error('def must be defined.'); } switch ($def->kind) { @@ -245,23 +262,24 @@ class ASTDefinitionBuilder case NodeKind::INPUT_OBJECT_TYPE_DEFINITION: return new InputObjectType($config); default: - throw new Error("Type kind of {$def->kind} not supported."); + throw new Error(sprintf('Type kind of %s not supported.', $def->kind)); } } private function makeTypeDef(ObjectTypeDefinitionNode $def) { $typeName = $def->name->value; + return new ObjectType([ - 'name' => $typeName, + 'name' => $typeName, 'description' => $this->getDescription($def), - 'fields' => function () use ($def) { + 'fields' => function () use ($def) { return $this->makeFieldDefMap($def); }, - 'interfaces' => function () use ($def) { + 'interfaces' => function () use ($def) { return $this->makeImplementedInterfaces($def); }, - 'astNode' => $def + 'astNode' => $def, ]); } @@ -286,10 +304,14 @@ class ASTDefinitionBuilder // Note: While this could make early assertions to get the correctly // typed values, that would throw immediately while type system // validation with validateSchema() will produce more actionable results. - return Utils::map($def->interfaces, function ($iface) { - return $this->buildType($iface); - }); + return Utils::map( + $def->interfaces, + function ($iface) { + return $this->buildType($iface); + } + ); } + return null; } @@ -304,16 +326,17 @@ class ASTDefinitionBuilder // Note: While this could make assertions to get the correctly typed // value, that would throw immediately while type system validation // with validateSchema() will produce more actionable results. - $type = $this->internalBuildWrappedType($value->type); + $type = $this->internalBuildWrappedType($value->type); $config = [ - 'name' => $value->name->value, - 'type' => $type, + 'name' => $value->name->value, + 'type' => $type, 'description' => $this->getDescription($value), - 'astNode' => $value + 'astNode' => $value, ]; if (isset($value->defaultValue)) { $config['defaultValue'] = AST::valueFromAST($value->defaultValue, $type); } + return $config; } ); @@ -322,22 +345,23 @@ class ASTDefinitionBuilder private function makeInterfaceDef(InterfaceTypeDefinitionNode $def) { $typeName = $def->name->value; + return new InterfaceType([ - 'name' => $typeName, + 'name' => $typeName, 'description' => $this->getDescription($def), - 'fields' => function () use ($def) { + 'fields' => function () use ($def) { return $this->makeFieldDefMap($def); }, - 'astNode' => $def + 'astNode' => $def, ]); } private function makeEnumDef(EnumTypeDefinitionNode $def) { return new EnumType([ - 'name' => $def->name->value, + 'name' => $def->name->value, 'description' => $this->getDescription($def), - 'values' => $def->values + 'values' => $def->values ? Utils::keyValMap( $def->values, function ($enumValue) { @@ -345,41 +369,44 @@ class ASTDefinitionBuilder }, function ($enumValue) { return [ - 'description' => $this->getDescription($enumValue), + 'description' => $this->getDescription($enumValue), 'deprecationReason' => $this->getDeprecationReason($enumValue), - 'astNode' => $enumValue + 'astNode' => $enumValue, ]; } ) : [], - 'astNode' => $def, + 'astNode' => $def, ]); } private function makeUnionDef(UnionTypeDefinitionNode $def) { return new UnionType([ - 'name' => $def->name->value, + 'name' => $def->name->value, 'description' => $this->getDescription($def), // Note: While this could make assertions to get the correctly typed // values below, that would throw immediately while type system // validation with validateSchema() will produce more actionable results. - 'types' => $def->types - ? Utils::map($def->types, function ($typeNode) { - return $this->buildType($typeNode); - }): + 'types' => $def->types + ? Utils::map( + $def->types, + function ($typeNode) { + return $this->buildType($typeNode); + } + ) : [], - 'astNode' => $def, + 'astNode' => $def, ]); } private function makeScalarDef(ScalarTypeDefinitionNode $def) { return new CustomScalarType([ - 'name' => $def->name->value, + 'name' => $def->name->value, 'description' => $this->getDescription($def), - 'astNode' => $def, - 'serialize' => function($value) { + 'astNode' => $def, + 'serialize' => function ($value) { return $value; }, ]); @@ -388,14 +415,14 @@ class ASTDefinitionBuilder private function makeInputObjectDef(InputObjectTypeDefinitionNode $def) { return new InputObjectType([ - 'name' => $def->name->value, + 'name' => $def->name->value, 'description' => $this->getDescription($def), - 'fields' => function () use ($def) { + 'fields' => function () use ($def) { return $def->fields ? $this->makeInputValues($def->fields) : []; }, - 'astNode' => $def, + 'astNode' => $def, ]); } @@ -409,7 +436,8 @@ class ASTDefinitionBuilder private function getDeprecationReason($node) { $deprecated = Values::getDirectiveValues(Directive::deprecatedDirective(), $node); - return isset($deprecated['reason']) ? $deprecated['reason'] : null; + + return $deprecated['reason'] ?? null; } /** @@ -433,21 +461,20 @@ class ASTDefinitionBuilder private function getLeadingCommentBlock($node) { $loc = $node->loc; - if (!$loc || !$loc->startToken) { + if (! $loc || ! $loc->startToken) { return null; } $comments = []; - $token = $loc->startToken->prev; - while ( - $token && + $token = $loc->startToken->prev; + while ($token && $token->kind === Token::COMMENT && $token->next && $token->prev && $token->line + 1 === $token->next->line && $token->line !== $token->prev->line ) { - $value = $token->value; + $value = $token->value; $comments[] = $value; - $token = $token->prev; + $token = $token->prev; } return implode("\n", array_reverse($comments)); diff --git a/src/Utils/BlockString.php b/src/Utils/BlockString.php index eac943d..5bc40e4 100644 --- a/src/Utils/BlockString.php +++ b/src/Utils/BlockString.php @@ -53,9 +53,9 @@ class BlockString { private static function leadingWhitespace($str) { $i = 0; while ($i < mb_strlen($str) && ($str[$i] === ' ' || $str[$i] === '\t')) { - $i++; + $i++; } return $i; } -} \ No newline at end of file +} diff --git a/src/Utils/TypeInfo.php b/src/Utils/TypeInfo.php index a1773a0..7c6b2de 100644 --- a/src/Utils/TypeInfo.php +++ b/src/Utils/TypeInfo.php @@ -58,7 +58,7 @@ class TypeInfo /** * @param Schema $schema * @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode - * @return Type + * @return Type|null * @throws InvariantViolation */ public static function typeFromAST(Schema $schema, $inputTypeNode) @@ -264,7 +264,7 @@ class TypeInfo } /** - * @return CompositeType + * @return Type */ function getParentType() { diff --git a/src/Validator/DocumentValidator.php b/src/Validator/DocumentValidator.php index 201a850..5e5540a 100644 --- a/src/Validator/DocumentValidator.php +++ b/src/Validator/DocumentValidator.php @@ -1,14 +1,15 @@ new ExecutableDefinitions(), - UniqueOperationNames::class => new UniqueOperationNames(), - LoneAnonymousOperation::class => new LoneAnonymousOperation(), - KnownTypeNames::class => new KnownTypeNames(), - FragmentsOnCompositeTypes::class => new FragmentsOnCompositeTypes(), - VariablesAreInputTypes::class => new VariablesAreInputTypes(), - ScalarLeafs::class => new ScalarLeafs(), - FieldsOnCorrectType::class => new FieldsOnCorrectType(), - UniqueFragmentNames::class => new UniqueFragmentNames(), - KnownFragmentNames::class => new KnownFragmentNames(), - NoUnusedFragments::class => new NoUnusedFragments(), - PossibleFragmentSpreads::class => new PossibleFragmentSpreads(), - NoFragmentCycles::class => new NoFragmentCycles(), - UniqueVariableNames::class => new UniqueVariableNames(), - NoUndefinedVariables::class => new NoUndefinedVariables(), - NoUnusedVariables::class => new NoUnusedVariables(), - KnownDirectives::class => new KnownDirectives(), - UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(), - KnownArgumentNames::class => new KnownArgumentNames(), - UniqueArgumentNames::class => new UniqueArgumentNames(), - ValuesOfCorrectType::class => new ValuesOfCorrectType(), - ProvidedNonNullArguments::class => new ProvidedNonNullArguments(), + ExecutableDefinitions::class => new ExecutableDefinitions(), + UniqueOperationNames::class => new UniqueOperationNames(), + LoneAnonymousOperation::class => new LoneAnonymousOperation(), + KnownTypeNames::class => new KnownTypeNames(), + FragmentsOnCompositeTypes::class => new FragmentsOnCompositeTypes(), + VariablesAreInputTypes::class => new VariablesAreInputTypes(), + ScalarLeafs::class => new ScalarLeafs(), + FieldsOnCorrectType::class => new FieldsOnCorrectType(), + UniqueFragmentNames::class => new UniqueFragmentNames(), + KnownFragmentNames::class => new KnownFragmentNames(), + NoUnusedFragments::class => new NoUnusedFragments(), + PossibleFragmentSpreads::class => new PossibleFragmentSpreads(), + NoFragmentCycles::class => new NoFragmentCycles(), + UniqueVariableNames::class => new UniqueVariableNames(), + NoUndefinedVariables::class => new NoUndefinedVariables(), + NoUnusedVariables::class => new NoUnusedVariables(), + KnownDirectives::class => new KnownDirectives(), + UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(), + KnownArgumentNames::class => new KnownArgumentNames(), + UniqueArgumentNames::class => new UniqueArgumentNames(), + ValuesOfCorrectType::class => new ValuesOfCorrectType(), + ProvidedNonNullArguments::class => new ProvidedNonNullArguments(), VariablesDefaultValueAllowed::class => new VariablesDefaultValueAllowed(), - VariablesInAllowedPosition::class => new VariablesInAllowedPosition(), + VariablesInAllowedPosition::class => new VariablesInAllowedPosition(), OverlappingFieldsCanBeMerged::class => new OverlappingFieldsCanBeMerged(), - UniqueInputFieldNames::class => new UniqueInputFieldNames(), + UniqueInputFieldNames::class => new UniqueInputFieldNames(), ]; } @@ -151,7 +159,7 @@ class DocumentValidator } /** - * @return array + * @return QuerySecurityRule[] */ public static function securityRules() { @@ -159,16 +167,36 @@ class DocumentValidator // When custom security rule is required - it should be just added via DocumentValidator::addRule(); // TODO: deprecate this - if (null === self::$securityRules) { + if (self::$securityRules === null) { self::$securityRules = [ DisableIntrospection::class => new DisableIntrospection(DisableIntrospection::DISABLED), // DEFAULT DISABLED - QueryDepth::class => new QueryDepth(QueryDepth::DISABLED), // default disabled - QueryComplexity::class => new QueryComplexity(QueryComplexity::DISABLED), // default disabled + QueryDepth::class => new QueryDepth(QueryDepth::DISABLED), // default disabled + QueryComplexity::class => new QueryComplexity(QueryComplexity::DISABLED), // default disabled ]; } + return self::$securityRules; } + /** + * This uses a specialized visitor which runs multiple visitors in parallel, + * while maintaining the visitor skip and break API. + * + * @param ValidationRule[] $rules + * @return Error[] + */ + public static function visitUsingRules(Schema $schema, TypeInfo $typeInfo, DocumentNode $documentNode, array $rules) + { + $context = new ValidationContext($schema, $documentNode, $typeInfo); + $visitors = []; + foreach ($rules as $rule) { + $visitors[] = $rule->getVisitor($context); + } + Visitor::visit($documentNode, Visitor::visitWithTypeInfo($typeInfo, Visitor::visitInParallel($visitors))); + + return $context->getErrors(); + } + /** * Returns global validation rule by name. Standard rules are named by class name, so * example usage for such rules: @@ -177,7 +205,7 @@ class DocumentValidator * * @api * @param string $name - * @return AbstractValidationRule + * @return ValidationRule */ public static function getRule($name) { @@ -187,17 +215,17 @@ class DocumentValidator return $rules[$name]; } - $name = "GraphQL\\Validator\\Rules\\$name"; - return isset($rules[$name]) ? $rules[$name] : null ; + $name = sprintf('GraphQL\\Validator\\Rules\\%s', $name); + + return $rules[$name] ?? null; } /** * Add rule to list of global validation rules * * @api - * @param AbstractValidationRule $rule */ - public static function addRule(AbstractValidationRule $rule) + public static function addRule(ValidationRule $rule) { self::$rules[$rule->getName()] = $rule; } @@ -205,7 +233,12 @@ class DocumentValidator public static function isError($value) { return is_array($value) - ? count(array_filter($value, function($item) { return $item instanceof \Exception || $item instanceof \Throwable;})) === count($value) + ? count(array_filter( + $value, + function ($item) { + return $item instanceof \Exception || $item instanceof \Throwable; + } + )) === count($value) : ($value instanceof \Exception || $value instanceof \Throwable); } @@ -216,6 +249,7 @@ class DocumentValidator } else { $arr[] = $items; } + return $arr; } @@ -230,33 +264,13 @@ class DocumentValidator public static function isValidLiteralValue(Type $type, $valueNode) { $emptySchema = new Schema([]); - $emptyDoc = new DocumentNode(['definitions' => []]); - $typeInfo = new TypeInfo($emptySchema, $type); - $context = new ValidationContext($emptySchema, $emptyDoc, $typeInfo); - $validator = new ValuesOfCorrectType(); - $visitor = $validator->getVisitor($context); + $emptyDoc = new DocumentNode(['definitions' => []]); + $typeInfo = new TypeInfo($emptySchema, $type); + $context = new ValidationContext($emptySchema, $emptyDoc, $typeInfo); + $validator = new ValuesOfCorrectType(); + $visitor = $validator->getVisitor($context); Visitor::visit($valueNode, Visitor::visitWithTypeInfo($typeInfo, $visitor)); - return $context->getErrors(); - } - /** - * This uses a specialized visitor which runs multiple visitors in parallel, - * while maintaining the visitor skip and break API. - * - * @param Schema $schema - * @param TypeInfo $typeInfo - * @param DocumentNode $documentNode - * @param AbstractValidationRule[] $rules - * @return array - */ - public static function visitUsingRules(Schema $schema, TypeInfo $typeInfo, DocumentNode $documentNode, array $rules) - { - $context = new ValidationContext($schema, $documentNode, $typeInfo); - $visitors = []; - foreach ($rules as $rule) { - $visitors[] = $rule->getVisitor($context); - } - Visitor::visit($documentNode, Visitor::visitWithTypeInfo($typeInfo, Visitor::visitInParallel($visitors))); return $context->getErrors(); } } diff --git a/src/Validator/Rules/CustomValidationRule.php b/src/Validator/Rules/CustomValidationRule.php index 5ccc606..83101a1 100644 --- a/src/Validator/Rules/CustomValidationRule.php +++ b/src/Validator/Rules/CustomValidationRule.php @@ -1,26 +1,30 @@ name = $name; + $this->name = $name; $this->visitorFn = $visitorFn; } /** - * @param ValidationContext $context * @return Error[] */ public function getVisitor(ValidationContext $context) { $fn = $this->visitorFn; + return $fn($context); } } diff --git a/src/Validator/Rules/DisableIntrospection.php b/src/Validator/Rules/DisableIntrospection.php index dec9f37..bdb7a7c 100644 --- a/src/Validator/Rules/DisableIntrospection.php +++ b/src/Validator/Rules/DisableIntrospection.php @@ -1,4 +1,7 @@ isEnabled = $enabled; } - static function introspectionDisabledMessage() + public function getVisitor(ValidationContext $context) + { + return $this->invokeIfNeeded( + $context, + [ + NodeKind::FIELD => function (FieldNode $node) use ($context) { + if ($node->name->value !== '__type' && $node->name->value !== '__schema') { + return; + } + + $context->reportError(new Error( + static::introspectionDisabledMessage(), + [$node] + )); + }, + ] + ); + } + + public static function introspectionDisabledMessage() { return 'GraphQL introspection is not allowed, but the query contained __schema or __type'; } @@ -30,21 +54,4 @@ class DisableIntrospection extends AbstractQuerySecurity { return $this->isEnabled !== static::DISABLED; } - - public function getVisitor(ValidationContext $context) - { - return $this->invokeIfNeeded( - $context, - [ - NodeKind::FIELD => function (FieldNode $node) use ($context) { - if ($node->name->value === '__type' || $node->name->value === '__schema') { - $context->reportError(new Error( - static::introspectionDisabledMessage(), - [$node] - )); - } - } - ] - ); - } } diff --git a/src/Validator/Rules/ExecutableDefinitions.php b/src/Validator/Rules/ExecutableDefinitions.php index f512d6d..325b320 100644 --- a/src/Validator/Rules/ExecutableDefinitions.php +++ b/src/Validator/Rules/ExecutableDefinitions.php @@ -1,4 +1,7 @@ function (DocumentNode $node) use ($context) { /** @var Node $definition */ foreach ($node->definitions as $definition) { - if ( - !$definition instanceof OperationDefinitionNode && - !$definition instanceof FragmentDefinitionNode + if ($definition instanceof OperationDefinitionNode || + $definition instanceof FragmentDefinitionNode ) { - $context->reportError(new Error( - self::nonExecutableDefinitionMessage($definition->name->value), - [$definition->name] - )); + continue; } + + $context->reportError(new Error( + self::nonExecutableDefinitionMessage($definition->name->value), + [$definition->name] + )); } return Visitor::skipNode(); - } + }, ]; } + + public static function nonExecutableDefinitionMessage($defName) + { + return sprintf('The "%s" definition is not executable.', $defName); + } } diff --git a/src/Validator/Rules/FieldsOnCorrectType.php b/src/Validator/Rules/FieldsOnCorrectType.php index 1244a72..104849c 100644 --- a/src/Validator/Rules/FieldsOnCorrectType.php +++ b/src/Validator/Rules/FieldsOnCorrectType.php @@ -1,4 +1,7 @@ function(FieldNode $node) use ($context) { + NodeKind::FIELD => function (FieldNode $node) use ($context) { $type = $context->getParentType(); - if ($type) { - $fieldDef = $context->getFieldDef(); - if (!$fieldDef) { - // This isn't valid. Let's find suggestions, if any. - $schema = $context->getSchema(); - $fieldName = $node->name->value; - // First determine if there are any suggested types to condition on. - $suggestedTypeNames = $this->getSuggestedTypeNames( - $schema, - $type, - $fieldName - ); - // If there are no suggested types, then perhaps this was a typo? - $suggestedFieldNames = $suggestedTypeNames - ? [] - : $this->getSuggestedFieldNames( - $schema, - $type, - $fieldName - ); - - // Report an error, including helpful suggestions. - $context->reportError(new Error( - static::undefinedFieldMessage( - $node->name->value, - $type->name, - $suggestedTypeNames, - $suggestedFieldNames - ), - [$node] - )); - } + if (! $type) { + return; } - } + + $fieldDef = $context->getFieldDef(); + if ($fieldDef) { + return; + } + + // This isn't valid. Let's find suggestions, if any. + $schema = $context->getSchema(); + $fieldName = $node->name->value; + // First determine if there are any suggested types to condition on. + $suggestedTypeNames = $this->getSuggestedTypeNames( + $schema, + $type, + $fieldName + ); + // If there are no suggested types, then perhaps this was a typo? + $suggestedFieldNames = $suggestedTypeNames + ? [] + : $this->getSuggestedFieldNames( + $schema, + $type, + $fieldName + ); + + // Report an error, including helpful suggestions. + $context->reportError(new Error( + static::undefinedFieldMessage( + $node->name->value, + $type->name, + $suggestedTypeNames, + $suggestedFieldNames + ), + [$node] + )); + }, ]; } @@ -75,32 +72,31 @@ class FieldsOnCorrectType extends AbstractValidationRule * suggest them, sorted by how often the type is referenced, starting * with Interfaces. * - * @param Schema $schema - * @param $type - * @param string $fieldName - * @return array + * @param ObjectType|InterfaceType $type + * @param string $fieldName + * @return string[] */ private function getSuggestedTypeNames(Schema $schema, $type, $fieldName) { if (Type::isAbstractType($type)) { $suggestedObjectTypes = []; - $interfaceUsageCount = []; + $interfaceUsageCount = []; - foreach($schema->getPossibleTypes($type) as $possibleType) { + foreach ($schema->getPossibleTypes($type) as $possibleType) { $fields = $possibleType->getFields(); - if (!isset($fields[$fieldName])) { + if (! isset($fields[$fieldName])) { continue; } // This object type defines this field. $suggestedObjectTypes[] = $possibleType->name; - foreach($possibleType->getInterfaces() as $possibleInterface) { + foreach ($possibleType->getInterfaces() as $possibleInterface) { $fields = $possibleInterface->getFields(); - if (!isset($fields[$fieldName])) { + if (! isset($fields[$fieldName])) { continue; } // This interface type defines this field. $interfaceUsageCount[$possibleInterface->name] = - !isset($interfaceUsageCount[$possibleInterface->name]) + ! isset($interfaceUsageCount[$possibleInterface->name]) ? 0 : $interfaceUsageCount[$possibleInterface->name] + 1; } @@ -122,18 +118,47 @@ class FieldsOnCorrectType extends AbstractValidationRule * For the field name provided, determine if there are any similar field names * that may be the result of a typo. * - * @param Schema $schema - * @param $type - * @param string $fieldName + * @param ObjectType|InterfaceType $type + * @param string $fieldName * @return array|string[] */ private function getSuggestedFieldNames(Schema $schema, $type, $fieldName) { if ($type instanceof ObjectType || $type instanceof InterfaceType) { $possibleFieldNames = array_keys($type->getFields()); + return Utils::suggestionList($fieldName, $possibleFieldNames); } + // Otherwise, must be a Union type, which does not define fields. return []; } + + /** + * @param string $fieldName + * @param string $type + * @param string[] $suggestedTypeNames + * @param string[] $suggestedFieldNames + * @return string + */ + public static function undefinedFieldMessage( + $fieldName, + $type, + array $suggestedTypeNames, + array $suggestedFieldNames + ) { + $message = sprintf('Cannot query field "%s" on type "%s".', $fieldName, $type); + + if ($suggestedTypeNames) { + $suggestions = Utils::quotedOrList($suggestedTypeNames); + + $message .= sprintf(' Did you mean to use an inline fragment on %s?', $suggestions); + } elseif (! empty($suggestedFieldNames)) { + $suggestions = Utils::quotedOrList($suggestedFieldNames); + + $message .= sprintf(' Did you mean %s?', $suggestions); + } + + return $message; + } } diff --git a/src/Validator/Rules/FragmentsOnCompositeTypes.php b/src/Validator/Rules/FragmentsOnCompositeTypes.php index f2731d0..e6f8f16 100644 --- a/src/Validator/Rules/FragmentsOnCompositeTypes.php +++ b/src/Validator/Rules/FragmentsOnCompositeTypes.php @@ -1,4 +1,7 @@ function(InlineFragmentNode $node) use ($context) { - if ($node->typeCondition) { - $type = TypeInfo::typeFromAST($context->getSchema(), $node->typeCondition); - if ($type && !Type::isCompositeType($type)) { - $context->reportError(new Error( - static::inlineFragmentOnNonCompositeErrorMessage($type), - [$node->typeCondition] - )); - } + NodeKind::INLINE_FRAGMENT => function (InlineFragmentNode $node) use ($context) { + if (! $node->typeCondition) { + return; } + + $type = TypeInfo::typeFromAST($context->getSchema(), $node->typeCondition); + if (! $type || Type::isCompositeType($type)) { + return; + } + + $context->reportError(new Error( + static::inlineFragmentOnNonCompositeErrorMessage($type), + [$node->typeCondition] + )); }, - NodeKind::FRAGMENT_DEFINITION => function(FragmentDefinitionNode $node) use ($context) { + NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) { $type = TypeInfo::typeFromAST($context->getSchema(), $node->typeCondition); - if ($type && !Type::isCompositeType($type)) { - $context->reportError(new Error( - static::fragmentOnNonCompositeErrorMessage($node->name->value, Printer::doPrint($node->typeCondition)), - [$node->typeCondition] - )); + if (! $type || Type::isCompositeType($type)) { + return; } - } + + $context->reportError(new Error( + static::fragmentOnNonCompositeErrorMessage( + $node->name->value, + Printer::doPrint($node->typeCondition) + ), + [$node->typeCondition] + )); + }, ]; } + + public static function inlineFragmentOnNonCompositeErrorMessage($type) + { + return sprintf('Fragment cannot condition on non composite type "%s".', $type); + } + + public static function fragmentOnNonCompositeErrorMessage($fragName, $type) + { + return sprintf('Fragment "%s" cannot condition on non composite type "%s".', $fragName, $type); + } } diff --git a/src/Validator/Rules/KnownArgumentNames.php b/src/Validator/Rules/KnownArgumentNames.php index 15a77ab..05a52ef 100644 --- a/src/Validator/Rules/KnownArgumentNames.php +++ b/src/Validator/Rules/KnownArgumentNames.php @@ -1,11 +1,19 @@ function(ArgumentNode $node, $key, $parent, $path, $ancestors) use ($context) { + NodeKind::ARGUMENT => function (ArgumentNode $node, $key, $parent, $path, $ancestors) use ($context) { + /** @var NodeList|Node[] $ancestors */ $argDef = $context->getArgument(); - if (!$argDef) { - $argumentOf = $ancestors[count($ancestors) - 1]; - if ($argumentOf->kind === NodeKind::FIELD) { - $fieldDef = $context->getFieldDef(); - $parentType = $context->getParentType(); - if ($fieldDef && $parentType) { - $context->reportError(new Error( - self::unknownArgMessage( + if ($argDef !== null) { + return; + } + + $argumentOf = $ancestors[count($ancestors) - 1]; + if ($argumentOf->kind === NodeKind::FIELD) { + $fieldDef = $context->getFieldDef(); + $parentType = $context->getParentType(); + if ($fieldDef && $parentType) { + $context->reportError(new Error( + self::unknownArgMessage( + $node->name->value, + $fieldDef->name, + $parentType->name, + Utils::suggestionList( $node->name->value, - $fieldDef->name, - $parentType->name, - Utils::suggestionList( - $node->name->value, - array_map(function ($arg) { return $arg->name; }, $fieldDef->args) + array_map( + function ($arg) { + return $arg->name; + }, + $fieldDef->args ) - ), - [$node] - )); - } - } else if ($argumentOf->kind === NodeKind::DIRECTIVE) { - $directive = $context->getDirective(); - if ($directive) { - $context->reportError(new Error( - self::unknownDirectiveArgMessage( + ) + ), + [$node] + )); + } + } elseif ($argumentOf->kind === NodeKind::DIRECTIVE) { + $directive = $context->getDirective(); + if ($directive) { + $context->reportError(new Error( + self::unknownDirectiveArgMessage( + $node->name->value, + $directive->name, + Utils::suggestionList( $node->name->value, - $directive->name, - Utils::suggestionList( - $node->name->value, - array_map(function ($arg) { return $arg->name; }, $directive->args) + array_map( + function ($arg) { + return $arg->name; + }, + $directive->args ) - ), - [$node] - )); - } + ) + ), + [$node] + )); } } - } + }, ]; } + + /** + * @param string[] $suggestedArgs + */ + public static function unknownArgMessage($argName, $fieldName, $typeName, array $suggestedArgs) + { + $message = sprintf('Unknown argument "%s" on field "%s" of type "%s".', $argName, $fieldName, $typeName); + if (! empty($suggestedArgs)) { + $message .= sprintf(' Did you mean %s?', Utils::quotedOrList($suggestedArgs)); + } + + return $message; + } + + /** + * @param string[] $suggestedArgs + */ + public static function unknownDirectiveArgMessage($argName, $directiveName, array $suggestedArgs) + { + $message = sprintf('Unknown argument "%s" on directive "@%s".', $argName, $directiveName); + if (! empty($suggestedArgs)) { + $message .= sprintf(' Did you mean %s?', Utils::quotedOrList($suggestedArgs)); + } + + return $message; + } } diff --git a/src/Validator/Rules/KnownDirectives.php b/src/Validator/Rules/KnownDirectives.php index 4ec3a01..ecccafd 100644 --- a/src/Validator/Rules/KnownDirectives.php +++ b/src/Validator/Rules/KnownDirectives.php @@ -1,4 +1,7 @@ reportError(new Error( self::unknownDirectiveMessage($node->name->value), [$node] )); + return; } $candidateLocation = $this->getDirectiveLocationForASTPath($ancestors); - if (!$candidateLocation) { + if (! $candidateLocation) { $context->reportError(new Error( self::misplacedDirectiveMessage($node->name->value, $node->type), [$node] )); - } else if (!in_array($candidateLocation, $directiveDef->locations)) { + } elseif (! in_array($candidateLocation, $directiveDef->locations)) { $context->reportError(new Error( self::misplacedDirectiveMessage($node->name->value, $candidateLocation), - [ $node ] + [$node] )); } - } + }, ]; } + public static function unknownDirectiveMessage($directiveName) + { + return sprintf('Unknown directive "%s".', $directiveName); + } + + /** + * @param (Node|NodeList)[] $ancestors + */ private function getDirectiveLocationForASTPath(array $ancestors) { $appliedTo = $ancestors[count($ancestors) - 1]; switch ($appliedTo->kind) { case NodeKind::OPERATION_DEFINITION: switch ($appliedTo->operation) { - case 'query': return DirectiveLocation::QUERY; - case 'mutation': return DirectiveLocation::MUTATION; - case 'subscription': return DirectiveLocation::SUBSCRIPTION; + case 'query': + return DirectiveLocation::QUERY; + case 'mutation': + return DirectiveLocation::MUTATION; + case 'subscription': + return DirectiveLocation::SUBSCRIPTION; } break; case NodeKind::FIELD: @@ -101,9 +109,15 @@ class KnownDirectives extends AbstractValidationRule return DirectiveLocation::INPUT_OBJECT; case NodeKind::INPUT_VALUE_DEFINITION: $parentNode = $ancestors[count($ancestors) - 3]; + return $parentNode instanceof InputObjectTypeDefinitionNode ? DirectiveLocation::INPUT_FIELD_DEFINITION : DirectiveLocation::ARGUMENT_DEFINITION; } } + + public static function misplacedDirectiveMessage($directiveName, $location) + { + return sprintf('Directive "%s" may not be used on "%s".', $directiveName, $location); + } } diff --git a/src/Validator/Rules/KnownFragmentNames.php b/src/Validator/Rules/KnownFragmentNames.php index ad5bc27..1b2b4e1 100644 --- a/src/Validator/Rules/KnownFragmentNames.php +++ b/src/Validator/Rules/KnownFragmentNames.php @@ -1,31 +1,40 @@ function(FragmentSpreadNode $node) use ($context) { + NodeKind::FRAGMENT_SPREAD => function (FragmentSpreadNode $node) use ($context) { $fragmentName = $node->name->value; - $fragment = $context->getFragment($fragmentName); - if (!$fragment) { - $context->reportError(new Error( - self::unknownFragmentMessage($fragmentName), - [$node->name] - )); + $fragment = $context->getFragment($fragmentName); + if ($fragment) { + return; } - } + + $context->reportError(new Error( + self::unknownFragmentMessage($fragmentName), + [$node->name] + )); + }, ]; } + + /** + * @param string $fragName + */ + public static function unknownFragmentMessage($fragName) + { + return sprintf('Unknown fragment "%s".', $fragName); + } } diff --git a/src/Validator/Rules/KnownTypeNames.php b/src/Validator/Rules/KnownTypeNames.php index 935b3ad..4700103 100644 --- a/src/Validator/Rules/KnownTypeNames.php +++ b/src/Validator/Rules/KnownTypeNames.php @@ -1,4 +1,7 @@ $skip, - NodeKind::INTERFACE_TYPE_DEFINITION => $skip, - NodeKind::UNION_TYPE_DEFINITION => $skip, + NodeKind::OBJECT_TYPE_DEFINITION => $skip, + NodeKind::INTERFACE_TYPE_DEFINITION => $skip, + NodeKind::UNION_TYPE_DEFINITION => $skip, NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $skip, - NodeKind::NAMED_TYPE => function(NamedTypeNode $node) use ($context) { - $schema = $context->getSchema(); + NodeKind::NAMED_TYPE => function (NamedTypeNode $node) use ($context) { + $schema = $context->getSchema(); $typeName = $node->name->value; - $type = $schema->getType($typeName); - if (!$type) { - $context->reportError(new Error( - self::unknownTypeMessage( - $typeName, - Utils::suggestionList($typeName, array_keys($schema->getTypeMap())) - ), [$node]) - ); + $type = $schema->getType($typeName); + if ($type !== null) { + return; } - } + + $context->reportError(new Error( + self::unknownTypeMessage( + $typeName, + Utils::suggestionList($typeName, array_keys($schema->getTypeMap())) + ), + [$node] + )); + }, ]; } + + /** + * @param string $type + * @param string[] $suggestedTypes + */ + public static function unknownTypeMessage($type, array $suggestedTypes) + { + $message = sprintf('Unknown type "%s".', $type); + if (! empty($suggestedTypes)) { + $suggestions = Utils::quotedOrList($suggestedTypes); + + $message .= sprintf(' Did you mean %s?', $suggestions); + } + + return $message; + } } diff --git a/src/Validator/Rules/LoneAnonymousOperation.php b/src/Validator/Rules/LoneAnonymousOperation.php index 7f848ef..06730f7 100644 --- a/src/Validator/Rules/LoneAnonymousOperation.php +++ b/src/Validator/Rules/LoneAnonymousOperation.php @@ -1,12 +1,17 @@ function(DocumentNode $node) use (&$operationCount) { + NodeKind::DOCUMENT => function (DocumentNode $node) use (&$operationCount) { $tmp = Utils::filter( $node->definitions, - function ($definition) { + function (Node $definition) { return $definition->kind === NodeKind::OPERATION_DEFINITION; } ); + $operationCount = count($tmp); }, - NodeKind::OPERATION_DEFINITION => function(OperationDefinitionNode $node) use (&$operationCount, $context) { - if (!$node->name && $operationCount > 1) { - $context->reportError( - new Error(self::anonOperationNotAloneMessage(), [$node]) - ); + NodeKind::OPERATION_DEFINITION => function (OperationDefinitionNode $node) use ( + &$operationCount, + $context + ) { + if ($node->name || $operationCount <= 1) { + return; } - } + + $context->reportError( + new Error(self::anonOperationNotAloneMessage(), [$node]) + ); + }, ]; } + + public static function anonOperationNotAloneMessage() + { + return 'This anonymous operation must be the only defined operation.'; + } } diff --git a/src/Validator/Rules/NoFragmentCycles.php b/src/Validator/Rules/NoFragmentCycles.php index df81b77..0b0a1e6 100644 --- a/src/Validator/Rules/NoFragmentCycles.php +++ b/src/Validator/Rules/NoFragmentCycles.php @@ -1,25 +1,33 @@ function () { return Visitor::skipNode(); }, - NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) { - if (!isset($this->visitedFrags[$node->name->value])) { + NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) { + if (! isset($this->visitedFrags[$node->name->value])) { $this->detectCycleRecursive($node, $context); } + return Visitor::skipNode(); - } + }, ]; } private function detectCycleRecursive(FragmentDefinitionNode $fragment, ValidationContext $context) { - $fragmentName = $fragment->name->value; + $fragmentName = $fragment->name->value; $this->visitedFrags[$fragmentName] = true; $spreadNodes = $context->getFragmentSpreads($fragment); @@ -63,7 +72,7 @@ class NoFragmentCycles extends AbstractValidationRule for ($i = 0; $i < count($spreadNodes); $i++) { $spreadNode = $spreadNodes[$i]; $spreadName = $spreadNode->name->value; - $cycleIndex = isset($this->spreadPathIndexByName[$spreadName]) ? $this->spreadPathIndexByName[$spreadName] : null; + $cycleIndex = $this->spreadPathIndexByName[$spreadName] ?? null; if ($cycleIndex === null) { $this->spreadPath[] = $spreadNode; @@ -76,7 +85,7 @@ class NoFragmentCycles extends AbstractValidationRule array_pop($this->spreadPath); } else { $cyclePath = array_slice($this->spreadPath, $cycleIndex); - $nodes = $cyclePath; + $nodes = $cyclePath; if (is_array($spreadNode)) { $nodes = array_merge($nodes, $spreadNode); @@ -87,9 +96,12 @@ class NoFragmentCycles extends AbstractValidationRule $context->reportError(new Error( self::cycleErrorMessage( $spreadName, - Utils::map($cyclePath, function ($s) { - return $s->name->value; - }) + Utils::map( + $cyclePath, + function ($s) { + return $s->name->value; + } + ) ), $nodes )); @@ -98,4 +110,16 @@ class NoFragmentCycles extends AbstractValidationRule $this->spreadPathIndexByName[$fragmentName] = null; } + + /** + * @param string[] $spreadNames + */ + public static function cycleErrorMessage($fragName, array $spreadNames = []) + { + return sprintf( + 'Cannot spread fragment "%s" within itself%s.', + $fragName, + ! empty($spreadNames) ? ' via ' . implode(', ', $spreadNames) : '' + ); + } } diff --git a/src/Validator/Rules/NoUndefinedVariables.php b/src/Validator/Rules/NoUndefinedVariables.php index 42aa3cc..af7bfd4 100644 --- a/src/Validator/Rules/NoUndefinedVariables.php +++ b/src/Validator/Rules/NoUndefinedVariables.php @@ -1,4 +1,7 @@ [ - 'enter' => function() use (&$variableNameDefined) { + 'enter' => function () use (&$variableNameDefined) { $variableNameDefined = []; }, - 'leave' => function(OperationDefinitionNode $operation) use (&$variableNameDefined, $context) { + 'leave' => function (OperationDefinitionNode $operation) use (&$variableNameDefined, $context) { $usages = $context->getRecursiveVariableUsages($operation); foreach ($usages as $usage) { - $node = $usage['node']; + $node = $usage['node']; $varName = $node->name->value; - if (empty($variableNameDefined[$varName])) { - $context->reportError(new Error( - self::undefinedVarMessage( - $varName, - $operation->name ? $operation->name->value : null - ), - [ $node, $operation ] - )); + if (! empty($variableNameDefined[$varName])) { + continue; } + + $context->reportError(new Error( + self::undefinedVarMessage( + $varName, + $operation->name ? $operation->name->value : null + ), + [$node, $operation] + )); } - } + }, ], - NodeKind::VARIABLE_DEFINITION => function(VariableDefinitionNode $def) use (&$variableNameDefined) { + NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $def) use (&$variableNameDefined) { $variableNameDefined[$def->variable->name->value] = true; - } + }, ]; } + + public static function undefinedVarMessage($varName, $opName = null) + { + return $opName + ? sprintf('Variable "$%s" is not defined by operation "%s".', $varName, $opName) + : sprintf('Variable "$%s" is not defined.', $varName); + } } diff --git a/src/Validator/Rules/NoUnusedFragments.php b/src/Validator/Rules/NoUnusedFragments.php index 2168586..d1cd366 100644 --- a/src/Validator/Rules/NoUnusedFragments.php +++ b/src/Validator/Rules/NoUnusedFragments.php @@ -1,39 +1,43 @@ operationDefs = []; - $this->fragmentDefs = []; + $this->fragmentDefs = []; return [ - NodeKind::OPERATION_DEFINITION => function($node) { + NodeKind::OPERATION_DEFINITION => function ($node) { $this->operationDefs[] = $node; + return Visitor::skipNode(); }, - NodeKind::FRAGMENT_DEFINITION => function(FragmentDefinitionNode $def) { + NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $def) { $this->fragmentDefs[] = $def; + return Visitor::skipNode(); }, - NodeKind::DOCUMENT => [ - 'leave' => function() use ($context) { + NodeKind::DOCUMENT => [ + 'leave' => function () use ($context) { $fragmentNameUsed = []; foreach ($this->operationDefs as $operation) { @@ -44,15 +48,22 @@ class NoUnusedFragments extends AbstractValidationRule foreach ($this->fragmentDefs as $fragmentDef) { $fragName = $fragmentDef->name->value; - if (empty($fragmentNameUsed[$fragName])) { - $context->reportError(new Error( - self::unusedFragMessage($fragName), - [ $fragmentDef ] - )); + if (! empty($fragmentNameUsed[$fragName])) { + continue; } + + $context->reportError(new Error( + self::unusedFragMessage($fragName), + [$fragmentDef] + )); } - } - ] + }, + ], ]; } + + public static function unusedFragMessage($fragName) + { + return sprintf('Fragment "%s" is never used.', $fragName); + } } diff --git a/src/Validator/Rules/NoUnusedVariables.php b/src/Validator/Rules/NoUnusedVariables.php index c004623..e8f7ff3 100644 --- a/src/Validator/Rules/NoUnusedVariables.php +++ b/src/Validator/Rules/NoUnusedVariables.php @@ -1,20 +1,19 @@ [ - 'enter' => function() { + 'enter' => function () { $this->variableDefs = []; }, - 'leave' => function(OperationDefinitionNode $operation) use ($context) { + 'leave' => function (OperationDefinitionNode $operation) use ($context) { $variableNameUsed = []; - $usages = $context->getRecursiveVariableUsages($operation); - $opName = $operation->name ? $operation->name->value : null; + $usages = $context->getRecursiveVariableUsages($operation); + $opName = $operation->name ? $operation->name->value : null; foreach ($usages as $usage) { - $node = $usage['node']; + $node = $usage['node']; $variableNameUsed[$node->name->value] = true; } foreach ($this->variableDefs as $variableDef) { $variableName = $variableDef->variable->name->value; - if (empty($variableNameUsed[$variableName])) { - $context->reportError(new Error( - self::unusedVariableMessage($variableName, $opName), - [$variableDef] - )); + if (! empty($variableNameUsed[$variableName])) { + continue; } + + $context->reportError(new Error( + self::unusedVariableMessage($variableName, $opName), + [$variableDef] + )); } - } + }, ], - NodeKind::VARIABLE_DEFINITION => function($def) { + NodeKind::VARIABLE_DEFINITION => function ($def) { $this->variableDefs[] = $def; - } + }, ]; } + + public static function unusedVariableMessage($varName, $opName = null) + { + return $opName + ? sprintf('Variable "$%s" is never used in operation "%s".', $varName, $opName) + : sprintf('Variable "$%s" is never used.', $varName); + } } diff --git a/src/Validator/Rules/OverlappingFieldsCanBeMerged.php b/src/Validator/Rules/OverlappingFieldsCanBeMerged.php index 7ab2838..9a4c56b 100644 --- a/src/Validator/Rules/OverlappingFieldsCanBeMerged.php +++ b/src/Validator/Rules/OverlappingFieldsCanBeMerged.php @@ -1,4 +1,7 @@ comparedFragmentPairs = new PairSet(); + $this->comparedFragmentPairs = new PairSet(); $this->cachedFieldsAndFragmentNames = new \SplObjectStorage(); return [ - NodeKind::SELECTION_SET => function(SelectionSetNode $selectionSet) use ($context) { + NodeKind::SELECTION_SET => function (SelectionSetNode $selectionSet) use ($context) { $conflicts = $this->findConflictsWithinSelectionSet( $context, $context->getParentType(), @@ -74,20 +66,112 @@ class OverlappingFieldsCanBeMerged extends AbstractValidationRule ); foreach ($conflicts as $conflict) { - $responseName = $conflict[0][0]; - $reason = $conflict[0][1]; - $fields1 = $conflict[1]; - $fields2 = $conflict[2]; + [[$responseName, $reason], $fields1, $fields2] = $conflict; $context->reportError(new Error( self::fieldsConflictMessage($responseName, $reason), array_merge($fields1, $fields2) )); } - } + }, ]; } + /** + * Find all conflicts found "within" a selection set, including those found + * via spreading in fragments. Called when visiting each SelectionSet in the + * GraphQL Document. + * + * @param CompositeType $parentType + * @return mixed[] + */ + private function findConflictsWithinSelectionSet( + ValidationContext $context, + $parentType, + SelectionSetNode $selectionSet + ) { + [$fieldMap, $fragmentNames] = $this->getFieldsAndFragmentNames( + $context, + $parentType, + $selectionSet + ); + + $conflicts = []; + + // (A) Find find all conflicts "within" the fields of this selection set. + // Note: this is the *only place* `collectConflictsWithin` is called. + $this->collectConflictsWithin( + $context, + $conflicts, + $fieldMap + ); + + $fragmentNamesLength = count($fragmentNames); + if ($fragmentNamesLength !== 0) { + // (B) Then collect conflicts between these fields and those represented by + // each spread fragment name found. + $comparedFragments = []; + for ($i = 0; $i < $fragmentNamesLength; $i++) { + $this->collectConflictsBetweenFieldsAndFragment( + $context, + $conflicts, + $comparedFragments, + false, + $fieldMap, + $fragmentNames[$i] + ); + // (C) Then compare this fragment with all other fragments found in this + // selection set to collect conflicts between fragments spread together. + // This compares each item in the list of fragment names to every other item + // in that same list (except for itself). + for ($j = $i + 1; $j < $fragmentNamesLength; $j++) { + $this->collectConflictsBetweenFragments( + $context, + $conflicts, + false, + $fragmentNames[$i], + $fragmentNames[$j] + ); + } + } + } + + return $conflicts; + } + + /** + * Given a selection set, return the collection of fields (a mapping of response + * name to field ASTs and definitions) as well as a list of fragment names + * referenced via fragment spreads. + * + * @param CompositeType $parentType + * @return mixed[]|\SplObjectStorage + */ + private function getFieldsAndFragmentNames( + ValidationContext $context, + $parentType, + SelectionSetNode $selectionSet + ) { + if (isset($this->cachedFieldsAndFragmentNames[$selectionSet])) { + $cached = $this->cachedFieldsAndFragmentNames[$selectionSet]; + } else { + $astAndDefs = []; + $fragmentNames = []; + + $this->internalCollectFieldsAndFragmentNames( + $context, + $parentType, + $selectionSet, + $astAndDefs, + $fragmentNames + ); + $cached = [$astAndDefs, array_keys($fragmentNames)]; + $this->cachedFieldsAndFragmentNames[$selectionSet] = $cached; + } + + return $cached; + } + /** * Algorithm: * @@ -144,221 +228,271 @@ class OverlappingFieldsCanBeMerged extends AbstractValidationRule */ /** - * Find all conflicts found "within" a selection set, including those found - * via spreading in fragments. Called when visiting each SelectionSet in the - * GraphQL Document. + * Given a reference to a fragment, return the represented collection of fields + * as well as a list of nested fragment names referenced via fragment spreads. * - * @param ValidationContext $context * @param CompositeType $parentType - * @param SelectionSetNode $selectionSet - * @return array + * @param mixed[][][] $astAndDefs + * @param bool[] $fragmentNames */ - private function findConflictsWithinSelectionSet( + private function internalCollectFieldsAndFragmentNames( ValidationContext $context, $parentType, - SelectionSetNode $selectionSet) - { - list($fieldMap, $fragmentNames) = $this->getFieldsAndFragmentNames( - $context, - $parentType, - $selectionSet - ); + SelectionSetNode $selectionSet, + array &$astAndDefs, + array &$fragmentNames + ) { + foreach ($selectionSet->selections as $selection) { + switch (true) { + case $selection instanceof FieldNode: + $fieldName = $selection->name->value; + $fieldDef = null; + if ($parentType instanceof ObjectType || + $parentType instanceof InterfaceType) { + $tmp = $parentType->getFields(); + if (isset($tmp[$fieldName])) { + $fieldDef = $tmp[$fieldName]; + } + } + $responseName = $selection->alias ? $selection->alias->value : $fieldName; - $conflicts = []; + if (! isset($astAndDefs[$responseName])) { + $astAndDefs[$responseName] = []; + } + $astAndDefs[$responseName][] = [$parentType, $selection, $fieldDef]; + break; + case $selection instanceof FragmentSpreadNode: + $fragmentNames[$selection->name->value] = true; + break; + case $selection instanceof InlineFragmentNode: + $typeCondition = $selection->typeCondition; + $inlineFragmentType = $typeCondition + ? TypeInfo::typeFromAST($context->getSchema(), $typeCondition) + : $parentType; - // (A) Find find all conflicts "within" the fields of this selection set. - // Note: this is the *only place* `collectConflictsWithin` is called. - $this->collectConflictsWithin( - $context, - $conflicts, - $fieldMap - ); - - - $fragmentNamesLength = count($fragmentNames); - if ($fragmentNamesLength !== 0) { - // (B) Then collect conflicts between these fields and those represented by - // each spread fragment name found. - $comparedFragments = []; - for ($i = 0; $i < $fragmentNamesLength; $i++) { - $this->collectConflictsBetweenFieldsAndFragment( - $context, - $conflicts, - $comparedFragments, - false, - $fieldMap, - $fragmentNames[$i] - ); - // (C) Then compare this fragment with all other fragments found in this - // selection set to collect conflicts between fragments spread together. - // This compares each item in the list of fragment names to every other item - // in that same list (except for itself). - for ($j = $i + 1; $j < $fragmentNamesLength; $j++) { - $this->collectConflictsBetweenFragments( + $this->internalCollectFieldsAndFragmentNames( $context, - $conflicts, - false, - $fragmentNames[$i], - $fragmentNames[$j] + $inlineFragmentType, + $selection->selectionSet, + $astAndDefs, + $fragmentNames ); + break; + } + } + } + + /** + * Collect all Conflicts "within" one collection of fields. + * + * @param mixed[][] $conflicts + * @param mixed[][] $fieldMap + */ + private function collectConflictsWithin( + ValidationContext $context, + array &$conflicts, + array $fieldMap + ) { + // A field map is a keyed collection, where each key represents a response + // name and the value at that key is a list of all fields which provide that + // response name. For every response name, if there are multiple fields, they + // must be compared to find a potential conflict. + foreach ($fieldMap as $responseName => $fields) { + // This compares every field in the list to every other field in this list + // (except to itself). If the list only has one item, nothing needs to + // be compared. + $fieldsLength = count($fields); + if ($fieldsLength <= 1) { + continue; + } + + for ($i = 0; $i < $fieldsLength; $i++) { + for ($j = $i + 1; $j < $fieldsLength; $j++) { + $conflict = $this->findConflict( + $context, + false, // within one collection is never mutually exclusive + $responseName, + $fields[$i], + $fields[$j] + ); + if (! $conflict) { + continue; + } + + $conflicts[] = $conflict; } } } - - return $conflicts; } /** - * Collect all conflicts found between a set of fields and a fragment reference - * including via spreading in any nested fragments. + * Determines if there is a conflict between two particular fields, including + * comparing their sub-fields. * - * @param ValidationContext $context - * @param array $conflicts - * @param array $comparedFragments - * @param bool $areMutuallyExclusive - * @param array $fieldMap - * @param string $fragmentName + * @param bool $parentFieldsAreMutuallyExclusive + * @param string $responseName + * @param mixed[] $field1 + * @param mixed[] $field2 + * @return mixed[]|null */ - private function collectConflictsBetweenFieldsAndFragment( + private function findConflict( ValidationContext $context, - array &$conflicts, - array &$comparedFragments, - $areMutuallyExclusive, - array $fieldMap, - $fragmentName + $parentFieldsAreMutuallyExclusive, + $responseName, + array $field1, + array $field2 ) { - if (isset($comparedFragments[$fragmentName])) { - return; - } - $comparedFragments[$fragmentName] = true; + [$parentType1, $ast1, $def1] = $field1; + [$parentType2, $ast2, $def2] = $field2; - $fragment = $context->getFragment($fragmentName); - if (!$fragment) { - return; + // If it is known that two fields could not possibly apply at the same + // time, due to the parent types, then it is safe to permit them to diverge + // in aliased field or arguments used as they will not present any ambiguity + // by differing. + // It is known that two parent types could never overlap if they are + // different Object types. Interface or Union types might overlap - if not + // in the current state of the schema, then perhaps in some future version, + // thus may not safely diverge. + $areMutuallyExclusive = + $parentFieldsAreMutuallyExclusive || + ( + $parentType1 !== $parentType2 && + $parentType1 instanceof ObjectType && + $parentType2 instanceof ObjectType + ); + + // The return type for each field. + $type1 = $def1 ? $def1->getType() : null; + $type2 = $def2 ? $def2->getType() : null; + + if (! $areMutuallyExclusive) { + // Two aliases must refer to the same field. + $name1 = $ast1->name->value; + $name2 = $ast2->name->value; + if ($name1 !== $name2) { + return [ + [$responseName, sprintf('%s and %s are different fields', $name1, $name2)], + [$ast1], + [$ast2], + ]; + } + + if (! $this->sameArguments($ast1->arguments ?: [], $ast2->arguments ?: [])) { + return [ + [$responseName, 'they have differing arguments'], + [$ast1], + [$ast2], + ]; + } } - list($fieldMap2, $fragmentNames2) = $this->getReferencedFieldsAndFragmentNames( - $context, - $fragment - ); - - if ($fieldMap === $fieldMap2) { - return; + if ($type1 && $type2 && $this->doTypesConflict($type1, $type2)) { + return [ + [$responseName, sprintf('they return conflicting types %s and %s', $type1, $type2)], + [$ast1], + [$ast2], + ]; } - // (D) First collect any conflicts between the provided collection of fields - // and the collection of fields represented by the given fragment. - $this->collectConflictsBetween( - $context, - $conflicts, - $areMutuallyExclusive, - $fieldMap, - $fieldMap2 - ); - - // (E) Then collect any conflicts between the provided collection of fields - // and any fragment names found in the given fragment. - $fragmentNames2Length = count($fragmentNames2); - for ($i = 0; $i < $fragmentNames2Length; $i++) { - $this->collectConflictsBetweenFieldsAndFragment( + // Collect and compare sub-fields. Use the same "visited fragment names" list + // for both collections so fields in a fragment reference are never + // compared to themselves. + $selectionSet1 = $ast1->selectionSet; + $selectionSet2 = $ast2->selectionSet; + if ($selectionSet1 && $selectionSet2) { + $conflicts = $this->findConflictsBetweenSubSelectionSets( $context, - $conflicts, - $comparedFragments, $areMutuallyExclusive, - $fieldMap, - $fragmentNames2[$i] + Type::getNamedType($type1), + $selectionSet1, + Type::getNamedType($type2), + $selectionSet2 + ); + + return $this->subfieldConflicts( + $conflicts, + $responseName, + $ast1, + $ast2 ); } + + return null; } /** - * Collect all conflicts found between two fragments, including via spreading in - * any nested fragments. + * @param ArgumentNode[] $arguments1 + * @param ArgumentNode[] $arguments2 * - * @param ValidationContext $context - * @param array $conflicts - * @param bool $areMutuallyExclusive - * @param string $fragmentName1 - * @param string $fragmentName2 + * @return bool */ - private function collectConflictsBetweenFragments( - ValidationContext $context, - array &$conflicts, - $areMutuallyExclusive, - $fragmentName1, - $fragmentName2 - ) { - // No need to compare a fragment to itself. - if ($fragmentName1 === $fragmentName2) { - return; + private function sameArguments($arguments1, $arguments2) + { + if (count($arguments1) !== count($arguments2)) { + return false; + } + foreach ($arguments1 as $argument1) { + $argument2 = null; + foreach ($arguments2 as $argument) { + if ($argument->name->value === $argument1->name->value) { + $argument2 = $argument; + break; + } + } + if (! $argument2) { + return false; + } + + if (! $this->sameValue($argument1->value, $argument2->value)) { + return false; + } } - // Memoize so two fragments are not compared for conflicts more than once. - if ( - $this->comparedFragmentPairs->has( - $fragmentName1, - $fragmentName2, - $areMutuallyExclusive - ) - ) { - return; - } - $this->comparedFragmentPairs->add( - $fragmentName1, - $fragmentName2, - $areMutuallyExclusive - ); + return true; + } - $fragment1 = $context->getFragment($fragmentName1); - $fragment2 = $context->getFragment($fragmentName2); - if (!$fragment1 || !$fragment2) { - return; + /** + * @return bool + */ + private function sameValue(Node $value1, Node $value2) + { + return (! $value1 && ! $value2) || (Printer::doPrint($value1) === Printer::doPrint($value2)); + } + + /** + * Two types conflict if both types could not apply to a value simultaneously. + * Composite types are ignored as their individual field types will be compared + * later recursively. However List and Non-Null types must match. + * + * @return bool + */ + private function doTypesConflict(OutputType $type1, OutputType $type2) + { + if ($type1 instanceof ListOfType) { + return $type2 instanceof ListOfType ? + $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) : + true; + } + if ($type2 instanceof ListOfType) { + return $type1 instanceof ListOfType ? + $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) : + true; + } + if ($type1 instanceof NonNull) { + return $type2 instanceof NonNull ? + $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) : + true; + } + if ($type2 instanceof NonNull) { + return $type1 instanceof NonNull ? + $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) : + true; + } + if (Type::isLeafType($type1) || Type::isLeafType($type2)) { + return $type1 !== $type2; } - list($fieldMap1, $fragmentNames1) = $this->getReferencedFieldsAndFragmentNames( - $context, - $fragment1 - ); - list($fieldMap2, $fragmentNames2) = $this->getReferencedFieldsAndFragmentNames( - $context, - $fragment2 - ); - - // (F) First, collect all conflicts between these two collections of fields - // (not including any nested fragments). - $this->collectConflictsBetween( - $context, - $conflicts, - $areMutuallyExclusive, - $fieldMap1, - $fieldMap2 - ); - - // (G) Then collect conflicts between the first fragment and any nested - // fragments spread in the second fragment. - $fragmentNames2Length = count($fragmentNames2); - for ($j = 0; $j < $fragmentNames2Length; $j++) { - $this->collectConflictsBetweenFragments( - $context, - $conflicts, - $areMutuallyExclusive, - $fragmentName1, - $fragmentNames2[$j] - ); - } - - // (G) Then collect conflicts between the second fragment and any nested - // fragments spread in the first fragment. - $fragmentNames1Length = count($fragmentNames1); - for ($i = 0; $i < $fragmentNames1Length; $i++) { - $this->collectConflictsBetweenFragments( - $context, - $conflicts, - $areMutuallyExclusive, - $fragmentNames1[$i], - $fragmentName2 - ); - } + return false; } /** @@ -366,13 +500,10 @@ class OverlappingFieldsCanBeMerged extends AbstractValidationRule * via spreading in fragments. Called when determining if conflicts exist * between the sub-fields of two overlapping fields. * - * @param ValidationContext $context - * @param bool $areMutuallyExclusive + * @param bool $areMutuallyExclusive * @param CompositeType $parentType1 - * @param $selectionSet1 * @param CompositeType $parentType2 - * @param $selectionSet2 - * @return array + * @return mixed[][] */ private function findConflictsBetweenSubSelectionSets( ValidationContext $context, @@ -384,12 +515,12 @@ class OverlappingFieldsCanBeMerged extends AbstractValidationRule ) { $conflicts = []; - list($fieldMap1, $fragmentNames1) = $this->getFieldsAndFragmentNames( + [$fieldMap1, $fragmentNames1] = $this->getFieldsAndFragmentNames( $context, $parentType1, $selectionSet1 ); - list($fieldMap2, $fragmentNames2) = $this->getFieldsAndFragmentNames( + [$fieldMap2, $fragmentNames2] = $this->getFieldsAndFragmentNames( $context, $parentType2, $selectionSet2 @@ -452,48 +583,8 @@ class OverlappingFieldsCanBeMerged extends AbstractValidationRule ); } } - return $conflicts; - } - /** - * Collect all Conflicts "within" one collection of fields. - * - * @param ValidationContext $context - * @param array $conflicts - * @param array $fieldMap - */ - private function collectConflictsWithin( - ValidationContext $context, - array &$conflicts, - array $fieldMap - ) - { - // A field map is a keyed collection, where each key represents a response - // name and the value at that key is a list of all fields which provide that - // response name. For every response name, if there are multiple fields, they - // must be compared to find a potential conflict. - foreach ($fieldMap as $responseName => $fields) { - // This compares every field in the list to every other field in this list - // (except to itself). If the list only has one item, nothing needs to - // be compared. - $fieldsLength = count($fields); - if ($fieldsLength > 1) { - for ($i = 0; $i < $fieldsLength; $i++) { - for ($j = $i + 1; $j < $fieldsLength; $j++) { - $conflict = $this->findConflict( - $context, - false, // within one collection is never mutually exclusive - $responseName, - $fields[$i], - $fields[$j] - ); - if ($conflict) { - $conflicts[] = $conflict; - } - } - } - } - } + return $conflicts; } /** @@ -503,11 +594,10 @@ class OverlappingFieldsCanBeMerged extends AbstractValidationRule * provided collection of fields. This is true because this validator traverses * each individual selection set. * - * @param ValidationContext $context - * @param array $conflicts - * @param bool $parentFieldsAreMutuallyExclusive - * @param array $fieldMap1 - * @param array $fieldMap2 + * @param mixed[][] $conflicts + * @param bool $parentFieldsAreMutuallyExclusive + * @param mixed[] $fieldMap1 + * @param mixed[] $fieldMap2 */ private function collectConflictsBetween( ValidationContext $context, @@ -522,253 +612,111 @@ class OverlappingFieldsCanBeMerged extends AbstractValidationRule // maps, each field from the first field map must be compared to every field // in the second field map to find potential conflicts. foreach ($fieldMap1 as $responseName => $fields1) { - if (isset($fieldMap2[$responseName])) { - $fields2 = $fieldMap2[$responseName]; - $fields1Length = count($fields1); - $fields2Length = count($fields2); - for ($i = 0; $i < $fields1Length; $i++) { - for ($j = 0; $j < $fields2Length; $j++) { - $conflict = $this->findConflict( - $context, - $parentFieldsAreMutuallyExclusive, - $responseName, - $fields1[$i], - $fields2[$j] - ); - if ($conflict) { - $conflicts[] = $conflict; - } + if (! isset($fieldMap2[$responseName])) { + continue; + } + + $fields2 = $fieldMap2[$responseName]; + $fields1Length = count($fields1); + $fields2Length = count($fields2); + for ($i = 0; $i < $fields1Length; $i++) { + for ($j = 0; $j < $fields2Length; $j++) { + $conflict = $this->findConflict( + $context, + $parentFieldsAreMutuallyExclusive, + $responseName, + $fields1[$i], + $fields2[$j] + ); + if (! $conflict) { + continue; } + + $conflicts[] = $conflict; } } } } /** - * Determines if there is a conflict between two particular fields, including - * comparing their sub-fields. + * Collect all conflicts found between a set of fields and a fragment reference + * including via spreading in any nested fragments. * - * @param ValidationContext $context - * @param bool $parentFieldsAreMutuallyExclusive - * @param string $responseName - * @param array $field1 - * @param array $field2 - * @return array|null + * @param mixed[][] $conflicts + * @param bool[] $comparedFragments + * @param bool $areMutuallyExclusive + * @param mixed[][] $fieldMap + * @param string $fragmentName */ - private function findConflict( + private function collectConflictsBetweenFieldsAndFragment( ValidationContext $context, - $parentFieldsAreMutuallyExclusive, - $responseName, - array $field1, - array $field2 - ) - { - list($parentType1, $ast1, $def1) = $field1; - list($parentType2, $ast2, $def2) = $field2; - - // If it is known that two fields could not possibly apply at the same - // time, due to the parent types, then it is safe to permit them to diverge - // in aliased field or arguments used as they will not present any ambiguity - // by differing. - // It is known that two parent types could never overlap if they are - // different Object types. Interface or Union types might overlap - if not - // in the current state of the schema, then perhaps in some future version, - // thus may not safely diverge. - $areMutuallyExclusive = - $parentFieldsAreMutuallyExclusive || - $parentType1 !== $parentType2 && - $parentType1 instanceof ObjectType && - $parentType2 instanceof ObjectType; - - // The return type for each field. - $type1 = $def1 ? $def1->getType() : null; - $type2 = $def2 ? $def2->getType() : null; - - if (!$areMutuallyExclusive) { - // Two aliases must refer to the same field. - $name1 = $ast1->name->value; - $name2 = $ast2->name->value; - if ($name1 !== $name2) { - return [ - [$responseName, "$name1 and $name2 are different fields"], - [$ast1], - [$ast2] - ]; - } - - if (!$this->sameArguments($ast1->arguments ?: [], $ast2->arguments ?: [])) { - return [ - [$responseName, 'they have differing arguments'], - [$ast1], - [$ast2] - ]; - } - } - - if ($type1 && $type2 && $this->doTypesConflict($type1, $type2)) { - return [ - [$responseName, "they return conflicting types $type1 and $type2"], - [$ast1], - [$ast2] - ]; - } - - // Collect and compare sub-fields. Use the same "visited fragment names" list - // for both collections so fields in a fragment reference are never - // compared to themselves. - $selectionSet1 = $ast1->selectionSet; - $selectionSet2 = $ast2->selectionSet; - if ($selectionSet1 && $selectionSet2) { - $conflicts = $this->findConflictsBetweenSubSelectionSets( - $context, - $areMutuallyExclusive, - Type::getNamedType($type1), - $selectionSet1, - Type::getNamedType($type2), - $selectionSet2 - ); - return $this->subfieldConflicts( - $conflicts, - $responseName, - $ast1, - $ast2 - ); - } - - return null; - } - - /** - * @param ArgumentNode[] $arguments1 - * @param ArgumentNode[] $arguments2 - * - * @return bool - */ - private function sameArguments($arguments1, $arguments2) - { - if (count($arguments1) !== count($arguments2)) { - return false; - } - foreach ($arguments1 as $argument1) { - $argument2 = null; - foreach ($arguments2 as $argument) { - if ($argument->name->value === $argument1->name->value) { - $argument2 = $argument; - break; - } - } - if (!$argument2) { - return false; - } - - if (!$this->sameValue($argument1->value, $argument2->value)) { - return false; - } - } - - return true; - } - - /** - * @param Node $value1 - * @param Node $value2 - * @return bool - */ - private function sameValue(Node $value1, Node $value2) - { - return (!$value1 && !$value2) || (Printer::doPrint($value1) === Printer::doPrint($value2)); - } - - /** - * Two types conflict if both types could not apply to a value simultaneously. - * Composite types are ignored as their individual field types will be compared - * later recursively. However List and Non-Null types must match. - * - * @param OutputType $type1 - * @param OutputType $type2 - * @return bool - */ - private function doTypesConflict(OutputType $type1, OutputType $type2) - { - if ($type1 instanceof ListOfType) { - return $type2 instanceof ListOfType ? - $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) : - true; - } - if ($type2 instanceof ListOfType) { - return $type1 instanceof ListOfType ? - $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) : - true; - } - if ($type1 instanceof NonNull) { - return $type2 instanceof NonNull ? - $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) : - true; - } - if ($type2 instanceof NonNull) { - return $type1 instanceof NonNull ? - $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) : - true; - } - if (Type::isLeafType($type1) || Type::isLeafType($type2)) { - return $type1 !== $type2; - } - return false; - } - - /** - * Given a selection set, return the collection of fields (a mapping of response - * name to field ASTs and definitions) as well as a list of fragment names - * referenced via fragment spreads. - * - * @param ValidationContext $context - * @param CompositeType $parentType - * @param SelectionSetNode $selectionSet - * @return array - */ - private function getFieldsAndFragmentNames( - ValidationContext $context, - $parentType, - SelectionSetNode $selectionSet + array &$conflicts, + array &$comparedFragments, + $areMutuallyExclusive, + array $fieldMap, + $fragmentName ) { - if (!isset($this->cachedFieldsAndFragmentNames[$selectionSet])) { - $astAndDefs = []; - $fragmentNames = []; - - $this->internalCollectFieldsAndFragmentNames( - $context, - $parentType, - $selectionSet, - $astAndDefs, - $fragmentNames - ); - $cached = [$astAndDefs, array_keys($fragmentNames)]; - $this->cachedFieldsAndFragmentNames[$selectionSet] = $cached; - } else { - $cached = $this->cachedFieldsAndFragmentNames[$selectionSet]; + if (isset($comparedFragments[$fragmentName])) { + return; + } + $comparedFragments[$fragmentName] = true; + + $fragment = $context->getFragment($fragmentName); + if (! $fragment) { + return; + } + + [$fieldMap2, $fragmentNames2] = $this->getReferencedFieldsAndFragmentNames( + $context, + $fragment + ); + + if ($fieldMap === $fieldMap2) { + return; + } + + // (D) First collect any conflicts between the provided collection of fields + // and the collection of fields represented by the given fragment. + $this->collectConflictsBetween( + $context, + $conflicts, + $areMutuallyExclusive, + $fieldMap, + $fieldMap2 + ); + + // (E) Then collect any conflicts between the provided collection of fields + // and any fragment names found in the given fragment. + $fragmentNames2Length = count($fragmentNames2); + for ($i = 0; $i < $fragmentNames2Length; $i++) { + $this->collectConflictsBetweenFieldsAndFragment( + $context, + $conflicts, + $comparedFragments, + $areMutuallyExclusive, + $fieldMap, + $fragmentNames2[$i] + ); } - return $cached; } /** * Given a reference to a fragment, return the represented collection of fields * as well as a list of nested fragment names referenced via fragment spreads. * - * @param ValidationContext $context - * @param FragmentDefinitionNode $fragment - * @return array|object + * @return mixed[]|\SplObjectStorage */ private function getReferencedFieldsAndFragmentNames( ValidationContext $context, FragmentDefinitionNode $fragment - ) - { + ) { // Short-circuit building a type from the AST if possible. if (isset($this->cachedFieldsAndFragmentNames[$fragment->selectionSet])) { return $this->cachedFieldsAndFragmentNames[$fragment->selectionSet]; } $fragmentType = TypeInfo::typeFromAST($context->getSchema(), $fragment->typeCondition); + return $this->getFieldsAndFragmentNames( $context, $fragmentType, @@ -777,63 +725,90 @@ class OverlappingFieldsCanBeMerged extends AbstractValidationRule } /** - * Given a reference to a fragment, return the represented collection of fields - * as well as a list of nested fragment names referenced via fragment spreads. + * Collect all conflicts found between two fragments, including via spreading in + * any nested fragments. * - * @param ValidationContext $context - * @param CompositeType $parentType - * @param SelectionSetNode $selectionSet - * @param array $astAndDefs - * @param array $fragmentNames + * @param mixed[][] $conflicts + * @param bool $areMutuallyExclusive + * @param string $fragmentName1 + * @param string $fragmentName2 */ - private function internalCollectFieldsAndFragmentNames( + private function collectConflictsBetweenFragments( ValidationContext $context, - $parentType, - SelectionSetNode $selectionSet, - array &$astAndDefs, - array &$fragmentNames - ) - { - $selectionSetLength = count($selectionSet->selections); - for ($i = 0; $i < $selectionSetLength; $i++) { - $selection = $selectionSet->selections[$i]; + array &$conflicts, + $areMutuallyExclusive, + $fragmentName1, + $fragmentName2 + ) { + // No need to compare a fragment to itself. + if ($fragmentName1 === $fragmentName2) { + return; + } - switch (true) { - case $selection instanceof FieldNode: - $fieldName = $selection->name->value; - $fieldDef = null; - if ($parentType instanceof ObjectType || - $parentType instanceof InterfaceType) { - $tmp = $parentType->getFields(); - if (isset($tmp[$fieldName])) { - $fieldDef = $tmp[$fieldName]; - } - } - $responseName = $selection->alias ? $selection->alias->value : $fieldName; + // Memoize so two fragments are not compared for conflicts more than once. + if ($this->comparedFragmentPairs->has( + $fragmentName1, + $fragmentName2, + $areMutuallyExclusive + ) + ) { + return; + } + $this->comparedFragmentPairs->add( + $fragmentName1, + $fragmentName2, + $areMutuallyExclusive + ); - if (!isset($astAndDefs[$responseName])) { - $astAndDefs[$responseName] = []; - } - $astAndDefs[$responseName][] = [$parentType, $selection, $fieldDef]; - break; - case $selection instanceof FragmentSpreadNode: - $fragmentNames[$selection->name->value] = true; - break; - case $selection instanceof InlineFragmentNode: - $typeCondition = $selection->typeCondition; - $inlineFragmentType = $typeCondition - ? TypeInfo::typeFromAST($context->getSchema(), $typeCondition) - : $parentType; + $fragment1 = $context->getFragment($fragmentName1); + $fragment2 = $context->getFragment($fragmentName2); + if (! $fragment1 || ! $fragment2) { + return; + } - $this->internalcollectFieldsAndFragmentNames( - $context, - $inlineFragmentType, - $selection->selectionSet, - $astAndDefs, - $fragmentNames - ); - break; - } + [$fieldMap1, $fragmentNames1] = $this->getReferencedFieldsAndFragmentNames( + $context, + $fragment1 + ); + [$fieldMap2, $fragmentNames2] = $this->getReferencedFieldsAndFragmentNames( + $context, + $fragment2 + ); + + // (F) First, collect all conflicts between these two collections of fields + // (not including any nested fragments). + $this->collectConflictsBetween( + $context, + $conflicts, + $areMutuallyExclusive, + $fieldMap1, + $fieldMap2 + ); + + // (G) Then collect conflicts between the first fragment and any nested + // fragments spread in the second fragment. + $fragmentNames2Length = count($fragmentNames2); + for ($j = 0; $j < $fragmentNames2Length; $j++) { + $this->collectConflictsBetweenFragments( + $context, + $conflicts, + $areMutuallyExclusive, + $fragmentName1, + $fragmentNames2[$j] + ); + } + + // (G) Then collect conflicts between the second fragment and any nested + // fragments spread in the first fragment. + $fragmentNames1Length = count($fragmentNames1); + for ($i = 0; $i < $fragmentNames1Length; $i++) { + $this->collectConflictsBetweenFragments( + $context, + $conflicts, + $areMutuallyExclusive, + $fragmentNames1[$i], + $fragmentName2 + ); } } @@ -841,42 +816,79 @@ class OverlappingFieldsCanBeMerged extends AbstractValidationRule * Given a series of Conflicts which occurred between two sub-fields, generate * a single Conflict. * - * @param array $conflicts - * @param string $responseName - * @param FieldNode $ast1 - * @param FieldNode $ast2 - * @return array|null + * @param mixed[][] $conflicts + * @param string $responseName + * @return mixed[]|null */ private function subfieldConflicts( array $conflicts, $responseName, FieldNode $ast1, FieldNode $ast2 - ) - { - if (count($conflicts) > 0) { - return [ - [ - $responseName, - array_map(function ($conflict) { - return $conflict[0]; - }, $conflicts), - ], - array_reduce( - $conflicts, - function ($allFields, $conflict) { - return array_merge($allFields, $conflict[1]); - }, - [$ast1] - ), - array_reduce( - $conflicts, - function ($allFields, $conflict) { - return array_merge($allFields, $conflict[2]); - }, - [$ast2] - ), - ]; + ) { + if (count($conflicts) === 0) { + return null; } + + return [ + [ + $responseName, + array_map( + function ($conflict) { + return $conflict[0]; + }, + $conflicts + ), + ], + array_reduce( + $conflicts, + function ($allFields, $conflict) { + return array_merge($allFields, $conflict[1]); + }, + [$ast1] + ), + array_reduce( + $conflicts, + function ($allFields, $conflict) { + return array_merge($allFields, $conflict[2]); + }, + [$ast2] + ), + ]; + } + + /** + * @param string $responseName + * @param string $reason + */ + public static function fieldsConflictMessage($responseName, $reason) + { + $reasonMessage = self::reasonMessage($reason); + + return sprintf( + 'Fields "%s" conflict because %s. Use different aliases on the fields to fetch both if this was intentional.', + $responseName, + $reasonMessage + ); + } + + public static function reasonMessage($reason) + { + if (is_array($reason)) { + $tmp = array_map( + function ($tmp) { + [$responseName, $subReason] = $tmp; + + $reasonMessage = self::reasonMessage($subReason); + + return sprintf('subfields "%s" conflict because %s', $responseName, $reasonMessage); + }, + $reason + ); + + return implode(' and ', $tmp); + } + + return $reason; } } diff --git a/src/Validator/Rules/PossibleFragmentSpreads.php b/src/Validator/Rules/PossibleFragmentSpreads.php index 0647e6f..26611e6 100644 --- a/src/Validator/Rules/PossibleFragmentSpreads.php +++ b/src/Validator/Rules/PossibleFragmentSpreads.php @@ -1,72 +1,61 @@ function(InlineFragmentNode $node) use ($context) { - $fragType = $context->getType(); + NodeKind::INLINE_FRAGMENT => function (InlineFragmentNode $node) use ($context) { + $fragType = $context->getType(); $parentType = $context->getParentType(); - if ($fragType instanceof CompositeType && - $parentType instanceof CompositeType && - !$this->doTypesOverlap($context->getSchema(), $fragType, $parentType)) { - $context->reportError(new Error( - self::typeIncompatibleAnonSpreadMessage($parentType, $fragType), - [$node] - )); + if (! ($fragType instanceof CompositeType) || + ! ($parentType instanceof CompositeType) || + $this->doTypesOverlap($context->getSchema(), $fragType, $parentType)) { + return; } + + $context->reportError(new Error( + self::typeIncompatibleAnonSpreadMessage($parentType, $fragType), + [$node] + )); }, - NodeKind::FRAGMENT_SPREAD => function(FragmentSpreadNode $node) use ($context) { - $fragName = $node->name->value; - $fragType = $this->getFragmentType($context, $fragName); + NodeKind::FRAGMENT_SPREAD => function (FragmentSpreadNode $node) use ($context) { + $fragName = $node->name->value; + $fragType = $this->getFragmentType($context, $fragName); $parentType = $context->getParentType(); - if ($fragType && $parentType && !$this->doTypesOverlap($context->getSchema(), $fragType, $parentType)) { - $context->reportError(new Error( - self::typeIncompatibleSpreadMessage($fragName, $parentType, $fragType), - [$node] - )); + if (! $fragType || + ! $parentType || + $this->doTypesOverlap($context->getSchema(), $fragType, $parentType) + ) { + return; } - } - ]; - } - private function getFragmentType(ValidationContext $context, $name) - { - $frag = $context->getFragment($name); - if ($frag) { - $type = TypeInfo::typeFromAST($context->getSchema(), $frag->typeCondition); - if ($type instanceof CompositeType) { - return $type; - } - } - return null; + $context->reportError(new Error( + self::typeIncompatibleSpreadMessage($fragName, $parentType, $fragType), + [$node] + )); + }, + ]; } private function doTypesOverlap(Schema $schema, CompositeType $fragType, CompositeType $parentType) @@ -136,4 +125,36 @@ class PossibleFragmentSpreads extends AbstractValidationRule return false; } + + public static function typeIncompatibleAnonSpreadMessage($parentType, $fragType) + { + return sprintf( + 'Fragment cannot be spread here as objects of type "%s" can never be of type "%s".', + $parentType, + $fragType + ); + } + + private function getFragmentType(ValidationContext $context, $name) + { + $frag = $context->getFragment($name); + if ($frag) { + $type = TypeInfo::typeFromAST($context->getSchema(), $frag->typeCondition); + if ($type instanceof CompositeType) { + return $type; + } + } + + return null; + } + + public static function typeIncompatibleSpreadMessage($fragName, $parentType, $fragType) + { + return sprintf( + 'Fragment "%s" cannot be spread here as objects of type "%s" can never be of type "%s".', + $fragName, + $parentType, + $fragType + ); + } } diff --git a/src/Validator/Rules/ProvidedNonNullArguments.php b/src/Validator/Rules/ProvidedNonNullArguments.php index 4d0c63c..56254af 100644 --- a/src/Validator/Rules/ProvidedNonNullArguments.php +++ b/src/Validator/Rules/ProvidedNonNullArguments.php @@ -1,4 +1,7 @@ [ - 'leave' => function(FieldNode $fieldNode) use ($context) { + NodeKind::FIELD => [ + 'leave' => function (FieldNode $fieldNode) use ($context) { $fieldDef = $context->getFieldDef(); - if (!$fieldDef) { + if (! $fieldDef) { return Visitor::skipNode(); } $argNodes = $fieldNode->arguments ?: []; @@ -38,39 +32,67 @@ class ProvidedNonNullArguments extends AbstractValidationRule $argNodeMap[$argNode->name->value] = $argNodes; } foreach ($fieldDef->args as $argDef) { - $argNode = isset($argNodeMap[$argDef->name]) ? $argNodeMap[$argDef->name] : null; - if (!$argNode && $argDef->getType() instanceof NonNull) { - $context->reportError(new Error( - self::missingFieldArgMessage($fieldNode->name->value, $argDef->name, $argDef->getType()), - [$fieldNode] - )); + $argNode = $argNodeMap[$argDef->name] ?? null; + if ($argNode || ! ($argDef->getType() instanceof NonNull)) { + continue; } + + $context->reportError(new Error( + self::missingFieldArgMessage($fieldNode->name->value, $argDef->name, $argDef->getType()), + [$fieldNode] + )); } - } + }, ], NodeKind::DIRECTIVE => [ - 'leave' => function(DirectiveNode $directiveNode) use ($context) { + 'leave' => function (DirectiveNode $directiveNode) use ($context) { $directiveDef = $context->getDirective(); - if (!$directiveDef) { + if (! $directiveDef) { return Visitor::skipNode(); } - $argNodes = $directiveNode->arguments ?: []; + $argNodes = $directiveNode->arguments ?: []; $argNodeMap = []; foreach ($argNodes as $argNode) { $argNodeMap[$argNode->name->value] = $argNodes; } foreach ($directiveDef->args as $argDef) { - $argNode = isset($argNodeMap[$argDef->name]) ? $argNodeMap[$argDef->name] : null; - if (!$argNode && $argDef->getType() instanceof NonNull) { - $context->reportError(new Error( - self::missingDirectiveArgMessage($directiveNode->name->value, $argDef->name, $argDef->getType()), - [$directiveNode] - )); + $argNode = $argNodeMap[$argDef->name] ?? null; + if ($argNode || ! ($argDef->getType() instanceof NonNull)) { + continue; } + + $context->reportError(new Error( + self::missingDirectiveArgMessage( + $directiveNode->name->value, + $argDef->name, + $argDef->getType() + ), + [$directiveNode] + )); } - } - ] + }, + ], ]; } + + public static function missingFieldArgMessage($fieldName, $argName, $type) + { + return sprintf( + 'Field "%s" argument "%s" of type "%s" is required but not provided.', + $fieldName, + $argName, + $type + ); + } + + public static function missingDirectiveArgMessage($directiveName, $argName, $type) + { + return sprintf( + 'Directive "@%s" argument "%s" of type "%s" is required but not provided.', + $directiveName, + $argName, + $type + ); + } } diff --git a/src/Validator/Rules/QueryComplexity.php b/src/Validator/Rules/QueryComplexity.php index 638010d..f126238 100644 --- a/src/Validator/Rules/QueryComplexity.php +++ b/src/Validator/Rules/QueryComplexity.php @@ -1,4 +1,7 @@ setMaxQueryComplexity($maxQueryComplexity); } - public static function maxQueryComplexityErrorMessage($max, $count) - { - return sprintf('Max query complexity should be %d but got %d.', $max, $count); - } - - /** - * Set max query complexity. If equal to 0 no check is done. Must be greater or equal to 0. - * - * @param $maxQueryComplexity - */ - public function setMaxQueryComplexity($maxQueryComplexity) - { - $this->checkIfGreaterOrEqualToZero('maxQueryComplexity', $maxQueryComplexity); - - $this->maxQueryComplexity = (int) $maxQueryComplexity; - } - - public function getMaxQueryComplexity() - { - return $this->maxQueryComplexity; - } - - public function setRawVariableValues(array $rawVariableValues = null) - { - $this->rawVariableValues = $rawVariableValues ?: []; - } - - public function getRawVariableValues() - { - return $this->rawVariableValues; - } - public function getVisitor(ValidationContext $context) { $this->context = $context; - $this->variableDefs = new \ArrayObject(); + $this->variableDefs = new \ArrayObject(); $this->fieldNodeAndDefs = new \ArrayObject(); - $complexity = 0; + $complexity = 0; return $this->invokeIfNeeded( $context, [ - NodeKind::SELECTION_SET => function (SelectionSetNode $selectionSet) use ($context) { + NodeKind::SELECTION_SET => function (SelectionSetNode $selectionSet) use ($context) { $this->fieldNodeAndDefs = $this->collectFieldASTsAndDefs( $context, $context->getParentType(), @@ -87,23 +65,31 @@ class QueryComplexity extends AbstractQuerySecurity $this->fieldNodeAndDefs ); }, - NodeKind::VARIABLE_DEFINITION => function ($def) { + NodeKind::VARIABLE_DEFINITION => function ($def) { $this->variableDefs[] = $def; + return Visitor::skipNode(); }, NodeKind::OPERATION_DEFINITION => [ 'leave' => function (OperationDefinitionNode $operationDefinition) use ($context, &$complexity) { $errors = $context->getErrors(); - if (empty($errors)) { - $complexity = $this->fieldComplexity($operationDefinition, $complexity); - - if ($complexity > $this->getMaxQueryComplexity()) { - $context->reportError( - new Error($this->maxQueryComplexityErrorMessage($this->getMaxQueryComplexity(), $complexity)) - ); - } + if (! empty($errors)) { + return; } + + $complexity = $this->fieldComplexity($operationDefinition, $complexity); + + if ($complexity <= $this->getMaxQueryComplexity()) { + return; + } + + $context->reportError( + new Error($this->maxQueryComplexityErrorMessage( + $this->getMaxQueryComplexity(), + $complexity + )) + ); }, ], ] @@ -125,9 +111,9 @@ class QueryComplexity extends AbstractQuerySecurity { switch ($node->kind) { case NodeKind::FIELD: - /* @var FieldNode $node */ + /** @var FieldNode $node */ // default values - $args = []; + $args = []; $complexityFn = FieldDefinition::DEFAULT_COMPLEXITY_FN; // calculate children complexity if needed @@ -139,7 +125,7 @@ class QueryComplexity extends AbstractQuerySecurity } $astFieldInfo = $this->astFieldInfo($node); - $fieldDef = $astFieldInfo[1]; + $fieldDef = $astFieldInfo[1]; if ($fieldDef instanceof FieldDefinition) { if ($this->directiveExcludesField($node)) { @@ -157,7 +143,7 @@ class QueryComplexity extends AbstractQuerySecurity break; case NodeKind::INLINE_FRAGMENT: - /* @var InlineFragmentNode $node */ + /** @var InlineFragmentNode $node */ // node has children? if (isset($node->selectionSet)) { $complexity = $this->fieldComplexity($node, $complexity); @@ -165,10 +151,10 @@ class QueryComplexity extends AbstractQuerySecurity break; case NodeKind::FRAGMENT_SPREAD: - /* @var FragmentSpreadNode $node */ + /** @var FragmentSpreadNode $node */ $fragment = $this->getFragment($node); - if (null !== $fragment) { + if ($fragment !== null) { $complexity = $this->fieldComplexity($fragment, $complexity); } break; @@ -179,11 +165,11 @@ class QueryComplexity extends AbstractQuerySecurity private function astFieldInfo(FieldNode $field) { - $fieldName = $this->getFieldName($field); + $fieldName = $this->getFieldName($field); $astFieldInfo = [null, null]; if (isset($this->fieldNodeAndDefs[$fieldName])) { foreach ($this->fieldNodeAndDefs[$fieldName] as $astAndDef) { - if ($astAndDef[0] == $field) { + if ($astAndDef[0] === $field) { $astFieldInfo = $astAndDef; break; } @@ -193,37 +179,8 @@ class QueryComplexity extends AbstractQuerySecurity return $astFieldInfo; } - private function buildFieldArguments(FieldNode $node) + private function directiveExcludesField(FieldNode $node) { - $rawVariableValues = $this->getRawVariableValues(); - $astFieldInfo = $this->astFieldInfo($node); - $fieldDef = $astFieldInfo[1]; - - $args = []; - - if ($fieldDef instanceof FieldDefinition) { - $variableValuesResult = Values::getVariableValues( - $this->context->getSchema(), - $this->variableDefs, - $rawVariableValues - ); - - if ($variableValuesResult['errors']) { - throw new Error(implode("\n\n", array_map( - function ($error) { - return $error->getMessage(); - } - , $variableValuesResult['errors']))); - } - $variableValues = $variableValuesResult['coerced']; - - $args = Values::getArgumentValues($fieldDef, $node, $variableValues); - } - - return $args; - } - - private function directiveExcludesField(FieldNode $node) { foreach ($node->directives as $directiveNode) { if ($directiveNode->name->value === 'deprecated') { return false; @@ -236,28 +193,99 @@ class QueryComplexity extends AbstractQuerySecurity ); if ($variableValuesResult['errors']) { - throw new Error(implode("\n\n", array_map( - function ($error) { - return $error->getMessage(); - } - , $variableValuesResult['errors']))); + throw new Error(implode( + "\n\n", + array_map( + function ($error) { + return $error->getMessage(); + }, + $variableValuesResult['errors'] + ) + )); } $variableValues = $variableValuesResult['coerced']; if ($directiveNode->name->value === 'include') { - $directive = Directive::includeDirective(); + $directive = Directive::includeDirective(); $directiveArgs = Values::getArgumentValues($directive, $directiveNode, $variableValues); - return !$directiveArgs['if']; - } else { - $directive = Directive::skipDirective(); - $directiveArgs = Values::getArgumentValues($directive, $directiveNode, $variableValues); - - return $directiveArgs['if']; + return ! $directiveArgs['if']; } + + $directive = Directive::skipDirective(); + $directiveArgs = Values::getArgumentValues($directive, $directiveNode, $variableValues); + + return $directiveArgs['if']; } } + public function getRawVariableValues() + { + return $this->rawVariableValues; + } + + /** + * @param mixed[]|null $rawVariableValues + */ + public function setRawVariableValues(?array $rawVariableValues = null) + { + $this->rawVariableValues = $rawVariableValues ?: []; + } + + private function buildFieldArguments(FieldNode $node) + { + $rawVariableValues = $this->getRawVariableValues(); + $astFieldInfo = $this->astFieldInfo($node); + $fieldDef = $astFieldInfo[1]; + + $args = []; + + if ($fieldDef instanceof FieldDefinition) { + $variableValuesResult = Values::getVariableValues( + $this->context->getSchema(), + $this->variableDefs, + $rawVariableValues + ); + + if ($variableValuesResult['errors']) { + throw new Error(implode( + "\n\n", + array_map( + function ($error) { + return $error->getMessage(); + }, + $variableValuesResult['errors'] + ) + )); + } + $variableValues = $variableValuesResult['coerced']; + + $args = Values::getArgumentValues($fieldDef, $node, $variableValues); + } + + return $args; + } + + public function getMaxQueryComplexity() + { + return $this->maxQueryComplexity; + } + + /** + * Set max query complexity. If equal to 0 no check is done. Must be greater or equal to 0. + */ + public function setMaxQueryComplexity($maxQueryComplexity) + { + $this->checkIfGreaterOrEqualToZero('maxQueryComplexity', $maxQueryComplexity); + + $this->maxQueryComplexity = (int) $maxQueryComplexity; + } + + public static function maxQueryComplexityErrorMessage($max, $count) + { + return sprintf('Max query complexity should be %d but got %d.', $max, $count); + } + protected function isEnabled() { return $this->getMaxQueryComplexity() !== static::DISABLED; diff --git a/src/Validator/Rules/QueryDepth.php b/src/Validator/Rules/QueryDepth.php index 5fb7065..1408eee 100644 --- a/src/Validator/Rules/QueryDepth.php +++ b/src/Validator/Rules/QueryDepth.php @@ -1,21 +1,20 @@ setMaxQueryDepth($maxQueryDepth); } - /** - * Set max query depth. If equal to 0 no check is done. Must be greater or equal to 0. - * - * @param $maxQueryDepth - */ - public function setMaxQueryDepth($maxQueryDepth) - { - $this->checkIfGreaterOrEqualToZero('maxQueryDepth', $maxQueryDepth); - - $this->maxQueryDepth = (int) $maxQueryDepth; - } - - public function getMaxQueryDepth() - { - return $this->maxQueryDepth; - } - - public static function maxQueryDepthErrorMessage($max, $count) - { - return sprintf('Max query depth should be %d but got %d.', $max, $count); - } - public function getVisitor(ValidationContext $context) { return $this->invokeIfNeeded( @@ -54,22 +31,19 @@ class QueryDepth extends AbstractQuerySecurity 'leave' => function (OperationDefinitionNode $operationDefinition) use ($context) { $maxDepth = $this->fieldDepth($operationDefinition); - if ($maxDepth > $this->getMaxQueryDepth()) { - $context->reportError( - new Error($this->maxQueryDepthErrorMessage($this->getMaxQueryDepth(), $maxDepth)) - ); + if ($maxDepth <= $this->getMaxQueryDepth()) { + return; } + + $context->reportError( + new Error($this->maxQueryDepthErrorMessage($this->getMaxQueryDepth(), $maxDepth)) + ); }, ], ] ); } - protected function isEnabled() - { - return $this->getMaxQueryDepth() !== static::DISABLED; - } - private function fieldDepth($node, $depth = 0, $maxDepth = 0) { if (isset($node->selectionSet) && $node->selectionSet instanceof SelectionSetNode) { @@ -85,9 +59,9 @@ class QueryDepth extends AbstractQuerySecurity { switch ($node->kind) { case NodeKind::FIELD: - /* @var FieldNode $node */ + /** @var FieldNode $node */ // node has children? - if (null !== $node->selectionSet) { + if ($node->selectionSet !== null) { // update maxDepth if needed if ($depth > $maxDepth) { $maxDepth = $depth; @@ -97,18 +71,18 @@ class QueryDepth extends AbstractQuerySecurity break; case NodeKind::INLINE_FRAGMENT: - /* @var InlineFragmentNode $node */ + /** @var InlineFragmentNode $node */ // node has children? - if (null !== $node->selectionSet) { + if ($node->selectionSet !== null) { $maxDepth = $this->fieldDepth($node, $depth, $maxDepth); } break; case NodeKind::FRAGMENT_SPREAD: - /* @var FragmentSpreadNode $node */ + /** @var FragmentSpreadNode $node */ $fragment = $this->getFragment($node); - if (null !== $fragment) { + if ($fragment !== null) { $maxDepth = $this->fieldDepth($fragment, $depth, $maxDepth); } break; @@ -116,4 +90,31 @@ class QueryDepth extends AbstractQuerySecurity return $maxDepth; } + + public function getMaxQueryDepth() + { + return $this->maxQueryDepth; + } + + /** + * Set max query depth. If equal to 0 no check is done. Must be greater or equal to 0. + * + * @param int $maxQueryDepth + */ + public function setMaxQueryDepth($maxQueryDepth) + { + $this->checkIfGreaterOrEqualToZero('maxQueryDepth', $maxQueryDepth); + + $this->maxQueryDepth = (int) $maxQueryDepth; + } + + public static function maxQueryDepthErrorMessage($max, $count) + { + return sprintf('Max query depth should be %d but got %d.', $max, $count); + } + + protected function isEnabled() + { + return $this->getMaxQueryDepth() !== static::DISABLED; + } } diff --git a/src/Validator/Rules/AbstractQuerySecurity.php b/src/Validator/Rules/QuerySecurityRule.php similarity index 73% rename from src/Validator/Rules/AbstractQuerySecurity.php rename to src/Validator/Rules/QuerySecurityRule.php index cfdc7bc..810a4d0 100644 --- a/src/Validator/Rules/AbstractQuerySecurity.php +++ b/src/Validator/Rules/QuerySecurityRule.php @@ -1,40 +1,35 @@ fragments; - } - /** * check if equal to 0 no check is done. Must be greater or equal to 0. * - * @param $value + * @param string $name + * @param int $value */ protected function checkIfGreaterOrEqualToZero($name, $value) { @@ -43,30 +38,30 @@ abstract class AbstractQuerySecurity extends AbstractValidationRule } } - protected function gatherFragmentDefinition(ValidationContext $context) - { - // Gather all the fragment definition. - // Importantly this does not include inline fragments. - $definitions = $context->getDocument()->definitions; - foreach ($definitions as $node) { - if ($node instanceof FragmentDefinitionNode) { - $this->fragments[$node->name->value] = $node; - } - } - } - protected function getFragment(FragmentSpreadNode $fragmentSpread) { $spreadName = $fragmentSpread->name->value; - $fragments = $this->getFragments(); + $fragments = $this->getFragments(); - return isset($fragments[$spreadName]) ? $fragments[$spreadName] : null; + return $fragments[$spreadName] ?? null; } + /** + * @return FragmentDefinitionNode[] + */ + protected function getFragments() + { + return $this->fragments; + } + + /** + * @param Closure[] $validators + * @return Closure[] + */ protected function invokeIfNeeded(ValidationContext $context, array $validators) { // is disabled? - if (!$this->isEnabled()) { + if (! $this->isEnabled()) { return []; } @@ -75,6 +70,22 @@ abstract class AbstractQuerySecurity extends AbstractValidationRule return $validators; } + abstract protected function isEnabled(); + + protected function gatherFragmentDefinition(ValidationContext $context) + { + // Gather all the fragment definition. + // Importantly this does not include inline fragments. + $definitions = $context->getDocument()->definitions; + foreach ($definitions as $node) { + if (! ($node instanceof FragmentDefinitionNode)) { + continue; + } + + $this->fragments[$node->name->value] = $node; + } + } + /** * Given a selectionSet, adds all of the fields in that selection to * the passed in map of fields, and returns it at the end. @@ -85,29 +96,30 @@ abstract class AbstractQuerySecurity extends AbstractValidationRule * * @see \GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged * - * @param ValidationContext $context - * @param Type|null $parentType - * @param SelectionSetNode $selectionSet - * @param \ArrayObject $visitedFragmentNames - * @param \ArrayObject $astAndDefs + * @param Type|null $parentType * * @return \ArrayObject */ - protected function collectFieldASTsAndDefs(ValidationContext $context, $parentType, SelectionSetNode $selectionSet, \ArrayObject $visitedFragmentNames = null, \ArrayObject $astAndDefs = null) - { + protected function collectFieldASTsAndDefs( + ValidationContext $context, + $parentType, + SelectionSetNode $selectionSet, + ?\ArrayObject $visitedFragmentNames = null, + ?\ArrayObject $astAndDefs = null + ) { $_visitedFragmentNames = $visitedFragmentNames ?: new \ArrayObject(); - $_astAndDefs = $astAndDefs ?: new \ArrayObject(); + $_astAndDefs = $astAndDefs ?: new \ArrayObject(); foreach ($selectionSet->selections as $selection) { switch ($selection->kind) { case NodeKind::FIELD: - /* @var FieldNode $selection */ + /** @var FieldNode $selection */ $fieldName = $selection->name->value; - $fieldDef = null; + $fieldDef = null; if ($parentType && method_exists($parentType, 'getFields')) { - $tmp = $parentType->getFields(); - $schemaMetaFieldDef = Introspection::schemaMetaFieldDef(); - $typeMetaFieldDef = Introspection::typeMetaFieldDef(); + $tmp = $parentType->getFields(); + $schemaMetaFieldDef = Introspection::schemaMetaFieldDef(); + $typeMetaFieldDef = Introspection::typeMetaFieldDef(); $typeNameMetaFieldDef = Introspection::typeNameMetaFieldDef(); if ($fieldName === $schemaMetaFieldDef->name && $context->getSchema()->getQueryType() === $parentType) { @@ -121,14 +133,14 @@ abstract class AbstractQuerySecurity extends AbstractValidationRule } } $responseName = $this->getFieldName($selection); - if (!isset($_astAndDefs[$responseName])) { + if (! isset($_astAndDefs[$responseName])) { $_astAndDefs[$responseName] = new \ArrayObject(); } // create field context $_astAndDefs[$responseName][] = [$selection, $fieldDef]; break; case NodeKind::INLINE_FRAGMENT: - /* @var InlineFragmentNode $selection */ + /** @var InlineFragmentNode $selection */ $_astAndDefs = $this->collectFieldASTsAndDefs( $context, TypeInfo::typeFromAST($context->getSchema(), $selection->typeCondition), @@ -138,12 +150,12 @@ abstract class AbstractQuerySecurity extends AbstractValidationRule ); break; case NodeKind::FRAGMENT_SPREAD: - /* @var FragmentSpreadNode $selection */ + /** @var FragmentSpreadNode $selection */ $fragName = $selection->name->value; if (empty($_visitedFragmentNames[$fragName])) { $_visitedFragmentNames[$fragName] = true; - $fragment = $context->getFragment($fragName); + $fragment = $context->getFragment($fragName); if ($fragment) { $_astAndDefs = $this->collectFieldASTsAndDefs( @@ -165,10 +177,9 @@ abstract class AbstractQuerySecurity extends AbstractValidationRule protected function getFieldName(FieldNode $node) { $fieldName = $node->name->value; - $responseName = $node->alias ? $node->alias->value : $fieldName; - return $responseName; + return $node->alias ? $node->alias->value : $fieldName; } - - abstract protected function isEnabled(); } + +class_alias(QuerySecurityRule::class, 'GraphQL\Validator\Rules\AbstractQuerySecurity'); diff --git a/src/Validator/Rules/ScalarLeafs.php b/src/Validator/Rules/ScalarLeafs.php index 670403d..a96b5d8 100644 --- a/src/Validator/Rules/ScalarLeafs.php +++ b/src/Validator/Rules/ScalarLeafs.php @@ -1,4 +1,7 @@ function(FieldNode $node) use ($context) { + NodeKind::FIELD => function (FieldNode $node) use ($context) { $type = $context->getType(); - if ($type) { - if (Type::isLeafType(Type::getNamedType($type))) { - if ($node->selectionSet) { - $context->reportError(new Error( - self::noSubselectionAllowedMessage($node->name->value, $type), - [$node->selectionSet] - )); - } - } else if (!$node->selectionSet) { + if (! $type) { + return; + } + + if (Type::isLeafType(Type::getNamedType($type))) { + if ($node->selectionSet) { $context->reportError(new Error( - self::requiredSubselectionMessage($node->name->value, $type), - [$node] + self::noSubselectionAllowedMessage($node->name->value, $type), + [$node->selectionSet] )); } + } elseif (! $node->selectionSet) { + $context->reportError(new Error( + self::requiredSubselectionMessage($node->name->value, $type), + [$node] + )); } - } + }, ]; } + + public static function noSubselectionAllowedMessage($field, $type) + { + return sprintf('Field "%s" of type "%s" must not have a sub selection.', $field, $type); + } + + public static function requiredSubselectionMessage($field, $type) + { + return sprintf('Field "%s" of type "%s" must have a sub selection.', $field, $type); + } } diff --git a/src/Validator/Rules/UniqueArgumentNames.php b/src/Validator/Rules/UniqueArgumentNames.php index 7c6eef1..2e83d43 100644 --- a/src/Validator/Rules/UniqueArgumentNames.php +++ b/src/Validator/Rules/UniqueArgumentNames.php @@ -1,20 +1,20 @@ knownArgNames = []; return [ - NodeKind::FIELD => function () { - $this->knownArgNames = [];; + NodeKind::FIELD => function () { + $this->knownArgNames = []; }, NodeKind::DIRECTIVE => function () { $this->knownArgNames = []; }, - NodeKind::ARGUMENT => function (ArgumentNode $node) use ($context) { + NodeKind::ARGUMENT => function (ArgumentNode $node) use ($context) { $argName = $node->name->value; - if (!empty($this->knownArgNames[$argName])) { + if (! empty($this->knownArgNames[$argName])) { $context->reportError(new Error( self::duplicateArgMessage($argName), [$this->knownArgNames[$argName], $node->name] @@ -38,8 +38,14 @@ class UniqueArgumentNames extends AbstractValidationRule } else { $this->knownArgNames[$argName] = $node->name; } + return Visitor::skipNode(); - } + }, ]; } + + public static function duplicateArgMessage($argName) + { + return sprintf('There can be only one argument named "%s".', $argName); + } } diff --git a/src/Validator/Rules/UniqueDirectivesPerLocation.php b/src/Validator/Rules/UniqueDirectivesPerLocation.php index 08d778a..c049c82 100644 --- a/src/Validator/Rules/UniqueDirectivesPerLocation.php +++ b/src/Validator/Rules/UniqueDirectivesPerLocation.php @@ -1,38 +1,44 @@ function(Node $node) use ($context) { - if (isset($node->directives)) { - $knownDirectives = []; - foreach ($node->directives as $directive) { - /** @var DirectiveNode $directive */ - $directiveName = $directive->name->value; - if (isset($knownDirectives[$directiveName])) { - $context->reportError(new Error( - self::duplicateDirectiveMessage($directiveName), - [$knownDirectives[$directiveName], $directive] - )); - } else { - $knownDirectives[$directiveName] = $directive; - } + 'enter' => function (Node $node) use ($context) { + if (! isset($node->directives)) { + return; + } + + $knownDirectives = []; + foreach ($node->directives as $directive) { + /** @var DirectiveNode $directive */ + $directiveName = $directive->name->value; + if (isset($knownDirectives[$directiveName])) { + $context->reportError(new Error( + self::duplicateDirectiveMessage($directiveName), + [$knownDirectives[$directiveName], $directive] + )); + } else { + $knownDirectives[$directiveName] = $directive; } } - } + }, ]; } + + public static function duplicateDirectiveMessage($directiveName) + { + return sprintf('The directive "%s" can only be used once at this location.', $directiveName); + } } diff --git a/src/Validator/Rules/UniqueFragmentNames.php b/src/Validator/Rules/UniqueFragmentNames.php index ebeb0b2..e29de7b 100644 --- a/src/Validator/Rules/UniqueFragmentNames.php +++ b/src/Validator/Rules/UniqueFragmentNames.php @@ -1,19 +1,20 @@ function () { return Visitor::skipNode(); }, - NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) { + NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) { $fragmentName = $node->name->value; - if (!empty($this->knownFragmentNames[$fragmentName])) { + if (empty($this->knownFragmentNames[$fragmentName])) { + $this->knownFragmentNames[$fragmentName] = $node->name; + } else { $context->reportError(new Error( self::duplicateFragmentNameMessage($fragmentName), - [ $this->knownFragmentNames[$fragmentName], $node->name ] + [$this->knownFragmentNames[$fragmentName], $node->name] )); - } else { - $this->knownFragmentNames[$fragmentName] = $node->name; } + return Visitor::skipNode(); - } + }, ]; } + + public static function duplicateFragmentNameMessage($fragName) + { + return sprintf('There can be only one fragment named "%s".', $fragName); + } } diff --git a/src/Validator/Rules/UniqueInputFieldNames.php b/src/Validator/Rules/UniqueInputFieldNames.php index 1e48d48..6426b43 100644 --- a/src/Validator/Rules/UniqueInputFieldNames.php +++ b/src/Validator/Rules/UniqueInputFieldNames.php @@ -1,4 +1,7 @@ knownNames = []; + $this->knownNames = []; $this->knownNameStack = []; return [ - NodeKind::OBJECT => [ - 'enter' => function() { + NodeKind::OBJECT => [ + 'enter' => function () { $this->knownNameStack[] = $this->knownNames; - $this->knownNames = []; + $this->knownNames = []; }, - 'leave' => function() { + 'leave' => function () { $this->knownNames = array_pop($this->knownNameStack); - } + }, ], - NodeKind::OBJECT_FIELD => function(ObjectFieldNode $node) use ($context) { + NodeKind::OBJECT_FIELD => function (ObjectFieldNode $node) use ($context) { $fieldName = $node->name->value; - if (!empty($this->knownNames[$fieldName])) { + if (! empty($this->knownNames[$fieldName])) { $context->reportError(new Error( self::duplicateInputFieldMessage($fieldName), - [ $this->knownNames[$fieldName], $node->name ] + [$this->knownNames[$fieldName], $node->name] )); } else { $this->knownNames[$fieldName] = $node->name; } + return Visitor::skipNode(); - } + }, ]; } + + public static function duplicateInputFieldMessage($fieldName) + { + return sprintf('There can be only one input field named "%s".', $fieldName); + } } diff --git a/src/Validator/Rules/UniqueOperationNames.php b/src/Validator/Rules/UniqueOperationNames.php index 80a352c..232380d 100644 --- a/src/Validator/Rules/UniqueOperationNames.php +++ b/src/Validator/Rules/UniqueOperationNames.php @@ -1,19 +1,20 @@ knownOperationNames = []; return [ - NodeKind::OPERATION_DEFINITION => function(OperationDefinitionNode $node) use ($context) { + NodeKind::OPERATION_DEFINITION => function (OperationDefinitionNode $node) use ($context) { $operationName = $node->name; if ($operationName) { - if (!empty($this->knownOperationNames[$operationName->value])) { + if (empty($this->knownOperationNames[$operationName->value])) { + $this->knownOperationNames[$operationName->value] = $operationName; + } else { $context->reportError(new Error( self::duplicateOperationNameMessage($operationName->value), - [ $this->knownOperationNames[$operationName->value], $operationName ] + [$this->knownOperationNames[$operationName->value], $operationName] )); - } else { - $this->knownOperationNames[$operationName->value] = $operationName; } } + return Visitor::skipNode(); }, - NodeKind::FRAGMENT_DEFINITION => function() { + NodeKind::FRAGMENT_DEFINITION => function () { return Visitor::skipNode(); - } + }, ]; } + + public static function duplicateOperationNameMessage($operationName) + { + return sprintf('There can be only one operation named "%s".', $operationName); + } } diff --git a/src/Validator/Rules/UniqueVariableNames.php b/src/Validator/Rules/UniqueVariableNames.php index 4329721..f050a73 100644 --- a/src/Validator/Rules/UniqueVariableNames.php +++ b/src/Validator/Rules/UniqueVariableNames.php @@ -1,18 +1,19 @@ knownVariableNames = []; return [ - NodeKind::OPERATION_DEFINITION => function() { + NodeKind::OPERATION_DEFINITION => function () { $this->knownVariableNames = []; }, - NodeKind::VARIABLE_DEFINITION => function(VariableDefinitionNode $node) use ($context) { + NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $node) use ($context) { $variableName = $node->variable->name->value; - if (!empty($this->knownVariableNames[$variableName])) { + if (empty($this->knownVariableNames[$variableName])) { + $this->knownVariableNames[$variableName] = $node->variable->name; + } else { $context->reportError(new Error( self::duplicateVariableMessage($variableName), - [ $this->knownVariableNames[$variableName], $node->variable->name ] + [$this->knownVariableNames[$variableName], $node->variable->name] )); - } else { - $this->knownVariableNames[$variableName] = $node->variable->name; } - } + }, ]; } + + public static function duplicateVariableMessage($variableName) + { + return sprintf('There can be only one variable named "%s".', $variableName); + } } diff --git a/src/Validator/Rules/AbstractValidationRule.php b/src/Validator/Rules/ValidationRule.php similarity index 67% rename from src/Validator/Rules/AbstractValidationRule.php rename to src/Validator/Rules/ValidationRule.php index 0926c29..c35388f 100644 --- a/src/Validator/Rules/AbstractValidationRule.php +++ b/src/Validator/Rules/ValidationRule.php @@ -1,10 +1,16 @@ function(NullValueNode $node) use ($context) { + NodeKind::NULL => function (NullValueNode $node) use ($context) { $type = $context->getInputType(); - if ($type instanceof NonNull) { - $context->reportError( - new Error( - self::badValueMessage((string) $type, Printer::doPrint($node)), - $node - ) - ); + if (! ($type instanceof NonNull)) { + return; } + + $context->reportError( + new Error( + self::badValueMessage((string) $type, Printer::doPrint($node)), + $node + ) + ); }, - NodeKind::LST => function(ListValueNode $node) use ($context) { + NodeKind::LST => function (ListValueNode $node) use ($context) { // Note: TypeInfo will traverse into a list's item type, so look to the // parent input type to check if it is a list. $type = Type::getNullableType($context->getParentInputType()); - if (!$type instanceof ListOfType) { + if (! $type instanceof ListOfType) { $this->isValidScalar($context, $node); + return Visitor::skipNode(); } }, - NodeKind::OBJECT => function(ObjectValueNode $node) use ($context) { + NodeKind::OBJECT => function (ObjectValueNode $node) use ($context) { // Note: TypeInfo will traverse into a list's item type, so look to the // parent input type to check if it is a list. $type = Type::getNamedType($context->getInputType()); - if (!$type instanceof InputObjectType) { + if (! $type instanceof InputObjectType) { $this->isValidScalar($context, $node); + return Visitor::skipNode(); } // Ensure every required field exists. - $inputFields = $type->getFields(); - $nodeFields = iterator_to_array($node->fields); + $inputFields = $type->getFields(); + $nodeFields = iterator_to_array($node->fields); $fieldNodeMap = array_combine( - array_map(function ($field) { return $field->name->value; }, $nodeFields), + array_map( + function ($field) { + return $field->name->value; + }, + $nodeFields + ), array_values($nodeFields) ); foreach ($inputFields as $fieldName => $fieldDef) { $fieldType = $fieldDef->getType(); - if (!isset($fieldNodeMap[$fieldName]) && $fieldType instanceof NonNull) { - $context->reportError( - new Error( - self::requiredFieldMessage($type->name, $fieldName, (string) $fieldType), - $node - ) - ); + if (isset($fieldNodeMap[$fieldName]) || ! ($fieldType instanceof NonNull)) { + continue; } - } - }, - NodeKind::OBJECT_FIELD => function(ObjectFieldNode $node) use ($context) { - $parentType = Type::getNamedType($context->getParentInputType()); - $fieldType = $context->getInputType(); - if (!$fieldType && $parentType instanceof InputObjectType) { - $suggestions = Utils::suggestionList( - $node->name->value, - array_keys($parentType->getFields()) - ); - $didYouMean = $suggestions - ? "Did you mean " . Utils::orList($suggestions) . "?" - : null; $context->reportError( new Error( - self::unknownFieldMessage($parentType->name, $node->name->value, $didYouMean), + self::requiredFieldMessage($type->name, $fieldName, (string) $fieldType), $node ) ); } }, - NodeKind::ENUM => function(EnumValueNode $node) use ($context) { + NodeKind::OBJECT_FIELD => function (ObjectFieldNode $node) use ($context) { + $parentType = Type::getNamedType($context->getParentInputType()); + $fieldType = $context->getInputType(); + if ($fieldType || ! ($parentType instanceof InputObjectType)) { + return; + } + + $suggestions = Utils::suggestionList( + $node->name->value, + array_keys($parentType->getFields()) + ); + $didYouMean = $suggestions + ? 'Did you mean ' . Utils::orList($suggestions) . '?' + : null; + + $context->reportError( + new Error( + self::unknownFieldMessage($parentType->name, $node->name->value, $didYouMean), + $node + ) + ); + }, + NodeKind::ENUM => function (EnumValueNode $node) use ($context) { $type = Type::getNamedType($context->getInputType()); - if (!$type instanceof EnumType) { + if (! $type instanceof EnumType) { $this->isValidScalar($context, $node); - } else if (!$type->getValue($node->value)) { + } elseif (! $type->getValue($node->value)) { $context->reportError( new Error( self::badValueMessage( @@ -140,25 +142,39 @@ class ValuesOfCorrectType extends AbstractValidationRule ); } }, - NodeKind::INT => function (IntValueNode $node) use ($context) { $this->isValidScalar($context, $node); }, - NodeKind::FLOAT => function (FloatValueNode $node) use ($context) { $this->isValidScalar($context, $node); }, - NodeKind::STRING => function (StringValueNode $node) use ($context) { $this->isValidScalar($context, $node); }, - NodeKind::BOOLEAN => function (BooleanValueNode $node) use ($context) { $this->isValidScalar($context, $node); }, + NodeKind::INT => function (IntValueNode $node) use ($context) { + $this->isValidScalar($context, $node); + }, + NodeKind::FLOAT => function (FloatValueNode $node) use ($context) { + $this->isValidScalar($context, $node); + }, + NodeKind::STRING => function (StringValueNode $node) use ($context) { + $this->isValidScalar($context, $node); + }, + NodeKind::BOOLEAN => function (BooleanValueNode $node) use ($context) { + $this->isValidScalar($context, $node); + }, ]; } + public static function badValueMessage($typeName, $valueName, $message = null) + { + return sprintf('Expected type %s, found %s', $typeName, $valueName) . + ($message ? "; ${message}" : '.'); + } + private function isValidScalar(ValidationContext $context, ValueNode $node) { // Report any error at the full type expected by the location. $locationType = $context->getInputType(); - if (!$locationType) { + if (! $locationType) { return; } $type = Type::getNamedType($locationType); - if (!$type instanceof ScalarType) { + if (! $type instanceof ScalarType) { $context->reportError( new Error( self::badValueMessage( @@ -169,6 +185,7 @@ class ValuesOfCorrectType extends AbstractValidationRule $node ) ); + return; } @@ -216,12 +233,26 @@ class ValuesOfCorrectType extends AbstractValidationRule if ($type instanceof EnumType) { $suggestions = Utils::suggestionList( Printer::doPrint($node), - array_map(function (EnumValueDefinition $value) { - return $value->name; - }, $type->getValues()) + array_map( + function (EnumValueDefinition $value) { + return $value->name; + }, + $type->getValues() + ) ); return $suggestions ? 'Did you mean the enum value ' . Utils::orList($suggestions) . '?' : null; } } + + public static function requiredFieldMessage($typeName, $fieldName, $fieldTypeName) + { + return sprintf('Field %s.%s of required type %s was not provided.', $typeName, $fieldName, $fieldTypeName); + } + + public static function unknownFieldMessage($typeName, $fieldName, $message = null) + { + return sprintf('Field "%s" is not defined by type %s', $fieldName, $typeName) . + ($message ? sprintf('; %s', $message) : '.'); + } } diff --git a/src/Validator/Rules/VariablesAreInputTypes.php b/src/Validator/Rules/VariablesAreInputTypes.php index a8f1bbf..4abc2e5 100644 --- a/src/Validator/Rules/VariablesAreInputTypes.php +++ b/src/Validator/Rules/VariablesAreInputTypes.php @@ -1,39 +1,42 @@ function(VariableDefinitionNode $node) use ($context) { + NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $node) use ($context) { $type = TypeInfo::typeFromAST($context->getSchema(), $node->type); // If the variable type is not an input type, return an error. - if ($type && !Type::isInputType($type)) { - $variableName = $node->variable->name->value; - $context->reportError(new Error( - self::nonInputTypeOnVarMessage($variableName, Printer::doPrint($node->type)), - [ $node->type ] - )); + if (! $type || Type::isInputType($type)) { + return; } - } + + $variableName = $node->variable->name->value; + $context->reportError(new Error( + self::nonInputTypeOnVarMessage($variableName, Printer::doPrint($node->type)), + [$node->type] + )); + }, ]; } + + public static function nonInputTypeOnVarMessage($variableName, $typeName) + { + return sprintf('Variable "$%s" cannot be non-input type "%s".', $variableName, $typeName); + } } diff --git a/src/Validator/Rules/VariablesDefaultValueAllowed.php b/src/Validator/Rules/VariablesDefaultValueAllowed.php index fcbbef4..64808fc 100644 --- a/src/Validator/Rules/VariablesDefaultValueAllowed.php +++ b/src/Validator/Rules/VariablesDefaultValueAllowed.php @@ -1,4 +1,7 @@ function(VariableDefinitionNode $node) use ($context) { - $name = $node->variable->name->value; + NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $node) use ($context) { + $name = $node->variable->name->value; $defaultValue = $node->defaultValue; - $type = $context->getInputType(); + $type = $context->getInputType(); if ($type instanceof NonNull && $defaultValue) { $context->reportError( - new Error( - self::defaultForRequiredVarMessage( - $name, - $type, - $type->getWrappedType() - ), - [$defaultValue] - ) + new Error( + self::defaultForRequiredVarMessage( + $name, + $type, + $type->getWrappedType() + ), + [$defaultValue] + ) ); } return Visitor::skipNode(); }, - NodeKind::SELECTION_SET => function(SelectionSetNode $node) use ($context) { + NodeKind::SELECTION_SET => function (SelectionSetNode $node) use ($context) { return Visitor::skipNode(); }, - NodeKind::FRAGMENT_DEFINITION => function(FragmentDefinitionNode $node) use ($context) { + NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) { return Visitor::skipNode(); }, ]; } + + public static function defaultForRequiredVarMessage($varName, $type, $guessType) + { + return sprintf( + 'Variable "$%s" of type "%s" is required and will not use the default value. Perhaps you meant to use type "%s".', + $varName, + $type, + $guessType + ); + } } diff --git a/src/Validator/Rules/VariablesInAllowedPosition.php b/src/Validator/Rules/VariablesInAllowedPosition.php index c0608ff..a593610 100644 --- a/src/Validator/Rules/VariablesInAllowedPosition.php +++ b/src/Validator/Rules/VariablesInAllowedPosition.php @@ -1,4 +1,7 @@ function () { $this->varDefMap = []; }, - 'leave' => function(OperationDefinitionNode $operation) use ($context) { + 'leave' => function (OperationDefinitionNode $operation) use ($context) { $usages = $context->getRecursiveVariableUsages($operation); foreach ($usages as $usage) { - $node = $usage['node']; - $type = $usage['type']; + $node = $usage['node']; + $type = $usage['type']; $varName = $node->name->value; - $varDef = isset($this->varDefMap[$varName]) ? $this->varDefMap[$varName] : null; + $varDef = $this->varDefMap[$varName] ?? null; - if ($varDef && $type) { - // A var type is allowed if it is the same or more strict (e.g. is - // a subtype of) than the expected type. It can be more strict if - // the variable type is non-null when the expected type is nullable. - // If both are list types, the variable item type can be more strict - // than the expected item type (contravariant). - $schema = $context->getSchema(); - $varType = TypeInfo::typeFromAST($schema, $varDef->type); - - if ($varType && !TypeComparators::isTypeSubTypeOf($schema, $this->effectiveType($varType, $varDef), $type)) { - $context->reportError(new Error( - self::badVarPosMessage($varName, $varType, $type), - [$varDef, $node] - )); - } + if (! $varDef || ! $type) { + continue; } + + // A var type is allowed if it is the same or more strict (e.g. is + // a subtype of) than the expected type. It can be more strict if + // the variable type is non-null when the expected type is nullable. + // If both are list types, the variable item type can be more strict + // than the expected item type (contravariant). + $schema = $context->getSchema(); + $varType = TypeInfo::typeFromAST($schema, $varDef->type); + + if (! $varType || TypeComparators::isTypeSubTypeOf( + $schema, + $this->effectiveType($varType, $varDef), + $type + )) { + continue; + } + + $context->reportError(new Error( + self::badVarPosMessage($varName, $varType, $type), + [$varDef, $node] + )); } - } + }, ], - NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $varDefNode) { + NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $varDefNode) { $this->varDefMap[$varDefNode->variable->name->value] = $varDefNode; - } + }, ]; } - // A var type is allowed if it is the same or more strict than the expected - // type. It can be more strict if the variable type is non-null when the - // expected type is nullable. If both are list types, the variable item type can - // be more strict than the expected item type. + private function effectiveType($varType, $varDef) + { + return (! $varDef->defaultValue || $varType instanceof NonNull) ? $varType : new NonNull($varType); + } + + /** + * A var type is allowed if it is the same or more strict than the expected + * type. It can be more strict if the variable type is non-null when the + * expected type is nullable. If both are list types, the variable item type can + * be more strict than the expected item type. + */ + public static function badVarPosMessage($varName, $varType, $expectedType) + { + return sprintf( + 'Variable "$%s" of type "%s" used in position expecting type "%s".', + $varName, + $varType, + $expectedType + ); + } + + /** If a variable definition has a default value, it's effectively non-null. */ private function varTypeAllowedForType($varType, $expectedType) { if ($expectedType instanceof NonNull) { if ($varType instanceof NonNull) { return $this->varTypeAllowedForType($varType->getWrappedType(), $expectedType->getWrappedType()); } + return false; } if ($varType instanceof NonNull) { @@ -80,13 +106,7 @@ class VariablesInAllowedPosition extends AbstractValidationRule if ($varType instanceof ListOfType && $expectedType instanceof ListOfType) { return $this->varTypeAllowedForType($varType->getWrappedType(), $expectedType->getWrappedType()); } + return $varType === $expectedType; } - - // If a variable definition has a default value, it's effectively non-null. - private function effectiveType($varType, $varDef) - { - return (!$varDef->defaultValue || $varType instanceof NonNull) ? $varType : new NonNull($varType); - } - } diff --git a/src/Validator/ValidationContext.php b/src/Validator/ValidationContext.php index 4d82ce4..d8a834e 100644 --- a/src/Validator/ValidationContext.php +++ b/src/Validator/ValidationContext.php @@ -1,22 +1,28 @@ schema = $schema; - $this->ast = $ast; - $this->typeInfo = $typeInfo; - $this->errors = []; - $this->fragmentSpreads = new SplObjectStorage(); + $this->schema = $schema; + $this->ast = $ast; + $this->typeInfo = $typeInfo; + $this->errors = []; + $this->fragmentSpreads = new SplObjectStorage(); $this->recursivelyReferencedFragments = new SplObjectStorage(); - $this->variableUsages = new SplObjectStorage(); - $this->recursiveVariableUsages = new SplObjectStorage(); + $this->variableUsages = new SplObjectStorage(); + $this->recursiveVariableUsages = new SplObjectStorage(); } - /** - * @param Error $error - */ - function reportError(Error $error) + public function reportError(Error $error) { $this->errors[] = $error; } @@ -100,7 +78,7 @@ class ValidationContext /** * @return Error[] */ - function getErrors() + public function getErrors() { return $this->errors; } @@ -108,159 +86,176 @@ class ValidationContext /** * @return Schema */ - function getSchema() + public function getSchema() { return $this->schema; } /** - * @return DocumentNode + * @return mixed[][] List of ['node' => VariableNode, 'type' => ?InputObjectType] */ - function getDocument() + public function getRecursiveVariableUsages(OperationDefinitionNode $operation) { - return $this->ast; - } + $usages = $this->recursiveVariableUsages[$operation] ?? null; - /** - * @param string $name - * @return FragmentDefinitionNode|null - */ - function getFragment($name) - { - $fragments = $this->fragments; - if (!$fragments) { - $fragments = []; - foreach ($this->getDocument()->definitions as $statement) { - if ($statement->kind === NodeKind::FRAGMENT_DEFINITION) { - $fragments[$statement->name->value] = $statement; - } - } - $this->fragments = $fragments; - } - return isset($fragments[$name]) ? $fragments[$name] : null; - } - - /** - * @param HasSelectionSet $node - * @return FragmentSpreadNode[] - */ - function getFragmentSpreads(HasSelectionSet $node) - { - $spreads = isset($this->fragmentSpreads[$node]) ? $this->fragmentSpreads[$node] : null; - if (!$spreads) { - $spreads = []; - $setsToVisit = [$node->selectionSet]; - while (!empty($setsToVisit)) { - $set = array_pop($setsToVisit); - - for ($i = 0; $i < count($set->selections); $i++) { - $selection = $set->selections[$i]; - if ($selection->kind === NodeKind::FRAGMENT_SPREAD) { - $spreads[] = $selection; - } else if ($selection->selectionSet) { - $setsToVisit[] = $selection->selectionSet; - } - } - } - $this->fragmentSpreads[$node] = $spreads; - } - return $spreads; - } - - /** - * @param OperationDefinitionNode $operation - * @return FragmentDefinitionNode[] - */ - function getRecursivelyReferencedFragments(OperationDefinitionNode $operation) - { - $fragments = isset($this->recursivelyReferencedFragments[$operation]) ? $this->recursivelyReferencedFragments[$operation] : null; - - if (!$fragments) { - $fragments = []; - $collectedNames = []; - $nodesToVisit = [$operation]; - while (!empty($nodesToVisit)) { - $node = array_pop($nodesToVisit); - $spreads = $this->getFragmentSpreads($node); - for ($i = 0; $i < count($spreads); $i++) { - $fragName = $spreads[$i]->name->value; - - if (empty($collectedNames[$fragName])) { - $collectedNames[$fragName] = true; - $fragment = $this->getFragment($fragName); - if ($fragment) { - $fragments[] = $fragment; - $nodesToVisit[] = $fragment; - } - } - } - } - $this->recursivelyReferencedFragments[$operation] = $fragments; - } - return $fragments; - } - - /** - * @param HasSelectionSet $node - * @return array List of ['node' => VariableNode, 'type' => ?InputObjectType] - */ - function getVariableUsages(HasSelectionSet $node) - { - $usages = isset($this->variableUsages[$node]) ? $this->variableUsages[$node] : null; - - if (!$usages) { - $newUsages = []; - $typeInfo = new TypeInfo($this->schema); - Visitor::visit($node, Visitor::visitWithTypeInfo($typeInfo, [ - NodeKind::VARIABLE_DEFINITION => function () { - return false; - }, - NodeKind::VARIABLE => function (VariableNode $variable) use (&$newUsages, $typeInfo) { - $newUsages[] = ['node' => $variable, 'type' => $typeInfo->getInputType()]; - } - ])); - $usages = $newUsages; - $this->variableUsages[$node] = $usages; - } - return $usages; - } - - /** - * @param OperationDefinitionNode $operation - * @return array List of ['node' => VariableNode, 'type' => ?InputObjectType] - */ - function getRecursiveVariableUsages(OperationDefinitionNode $operation) - { - $usages = isset($this->recursiveVariableUsages[$operation]) ? $this->recursiveVariableUsages[$operation] : null; - - if (!$usages) { - $usages = $this->getVariableUsages($operation); + if (! $usages) { + $usages = $this->getVariableUsages($operation); $fragments = $this->getRecursivelyReferencedFragments($operation); $tmp = [$usages]; for ($i = 0; $i < count($fragments); $i++) { $tmp[] = $this->getVariableUsages($fragments[$i]); } - $usages = call_user_func_array('array_merge', $tmp); + $usages = call_user_func_array('array_merge', $tmp); $this->recursiveVariableUsages[$operation] = $usages; } + return $usages; } + /** + * @return mixed[][] List of ['node' => VariableNode, 'type' => ?InputObjectType] + */ + private function getVariableUsages(HasSelectionSet $node) + { + $usages = $this->variableUsages[$node] ?? null; + + if (! $usages) { + $newUsages = []; + $typeInfo = new TypeInfo($this->schema); + Visitor::visit( + $node, + Visitor::visitWithTypeInfo( + $typeInfo, + [ + NodeKind::VARIABLE_DEFINITION => function () { + return false; + }, + NodeKind::VARIABLE => function (VariableNode $variable) use ( + &$newUsages, + $typeInfo + ) { + $newUsages[] = ['node' => $variable, 'type' => $typeInfo->getInputType()]; + }, + ] + ) + ); + $usages = $newUsages; + $this->variableUsages[$node] = $usages; + } + + return $usages; + } + + /** + * @return FragmentDefinitionNode[] + */ + public function getRecursivelyReferencedFragments(OperationDefinitionNode $operation) + { + $fragments = $this->recursivelyReferencedFragments[$operation] ?? null; + + if (! $fragments) { + $fragments = []; + $collectedNames = []; + $nodesToVisit = [$operation]; + while (! empty($nodesToVisit)) { + $node = array_pop($nodesToVisit); + $spreads = $this->getFragmentSpreads($node); + for ($i = 0; $i < count($spreads); $i++) { + $fragName = $spreads[$i]->name->value; + + if (! empty($collectedNames[$fragName])) { + continue; + } + + $collectedNames[$fragName] = true; + $fragment = $this->getFragment($fragName); + if (! $fragment) { + continue; + } + + $fragments[] = $fragment; + $nodesToVisit[] = $fragment; + } + } + $this->recursivelyReferencedFragments[$operation] = $fragments; + } + + return $fragments; + } + + /** + * @return FragmentSpreadNode[] + */ + public function getFragmentSpreads(HasSelectionSet $node) + { + $spreads = $this->fragmentSpreads[$node] ?? null; + if (! $spreads) { + $spreads = []; + /** @var SelectionSetNode[] $setsToVisit */ + $setsToVisit = [$node->selectionSet]; + while (! empty($setsToVisit)) { + $set = array_pop($setsToVisit); + + for ($i = 0; $i < count($set->selections); $i++) { + $selection = $set->selections[$i]; + if ($selection->kind === NodeKind::FRAGMENT_SPREAD) { + $spreads[] = $selection; + } elseif ($selection->selectionSet) { + $setsToVisit[] = $selection->selectionSet; + } + } + } + $this->fragmentSpreads[$node] = $spreads; + } + + return $spreads; + } + + /** + * @param string $name + * @return FragmentDefinitionNode|null + */ + public function getFragment($name) + { + $fragments = $this->fragments; + if (! $fragments) { + $fragments = []; + foreach ($this->getDocument()->definitions as $statement) { + if ($statement->kind !== NodeKind::FRAGMENT_DEFINITION) { + continue; + } + + $fragments[$statement->name->value] = $statement; + } + $this->fragments = $fragments; + } + + return $fragments[$name] ?? null; + } + + /** + * @return DocumentNode + */ + public function getDocument() + { + return $this->ast; + } + /** * Returns OutputType * * @return Type */ - function getType() + public function getType() { return $this->typeInfo->getType(); } /** - * @return CompositeType + * @return Type */ - function getParentType() + public function getParentType() { return $this->typeInfo->getParentType(); } @@ -268,7 +263,7 @@ class ValidationContext /** * @return InputType */ - function getInputType() + public function getInputType() { return $this->typeInfo->getInputType(); } @@ -276,7 +271,7 @@ class ValidationContext /** * @return InputType */ - function getParentInputType() + public function getParentInputType() { return $this->typeInfo->getParentInputType(); } @@ -284,17 +279,17 @@ class ValidationContext /** * @return FieldDefinition */ - function getFieldDef() + public function getFieldDef() { return $this->typeInfo->getFieldDef(); } - function getDirective() + public function getDirective() { return $this->typeInfo->getDirective(); } - function getArgument() + public function getArgument() { return $this->typeInfo->getArgument(); } diff --git a/tests/Language/VisitorTest.php b/tests/Language/VisitorTest.php index 4ed2805..fd7bb20 100644 --- a/tests/Language/VisitorTest.php +++ b/tests/Language/VisitorTest.php @@ -1,4 +1,7 @@ toArray()) : $parent; + $parentArray = $parent && ! is_array($parent) ? ($parent instanceof NodeList ? iterator_to_array($parent) : $parent->toArray()) : $parent; $this->assertInstanceOf(Node::class, $node); $this->assertContains($node->kind, array_keys(NodeKind::$classMap)); $isRoot = $key === null; if ($isRoot) { - if (!$isEdited) { + if (! $isEdited) { $this->assertEquals($ast, $node); } $this->assertEquals(null, $parent); @@ -60,14 +71,16 @@ class VisitorTest extends ValidatorTestCase $this->assertInternalType('array', $ancestors); $this->assertCount(count($path) - 1, $ancestors); - if (!$isEdited) { - $this->assertEquals($node, $parentArray[$key]); - $this->assertEquals($node, $this->getNodeByPath($ast, $path)); - $ancestorsLength = count($ancestors); - for ($i = 0; $i < $ancestorsLength; ++$i) { - $ancestorPath = array_slice($path, 0, $i); - $this->assertEquals($ancestors[$i], $this->getNodeByPath($ast, $ancestorPath)); - } + if ($isEdited) { + return; + } + + $this->assertEquals($node, $parentArray[$key]); + $this->assertEquals($node, $this->getNodeByPath($ast, $path)); + $ancestorsLength = count($ancestors); + for ($i = 0; $i < $ancestorsLength; ++$i) { + $ancestorPath = array_slice($path, 0, $i); + $this->assertEquals($ancestors[$i], $this->getNodeByPath($ast, $ancestorPath)); } } @@ -104,94 +117,84 @@ class VisitorTest extends ValidatorTestCase $this->assertEquals($expected, $visited); } - /** - * @it allows editing a node both on enter and on leave - */ public function testAllowsEditingNodeOnEnterAndOnLeave() { $ast = Parser::parse('{ a, b, c { a, b, c } }', [ 'noLocation' => true ]); $selectionSet = null; - $editedAst = Visitor::visit($ast, [ + $editedAst = Visitor::visit($ast, [ NodeKind::OPERATION_DEFINITION => [ - 'enter' => function(OperationDefinitionNode $node) use (&$selectionSet, $ast) { + 'enter' => function (OperationDefinitionNode $node) use (&$selectionSet, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); $selectionSet = $node->selectionSet; - $newNode = clone $node; + $newNode = clone $node; $newNode->selectionSet = new SelectionSetNode([ - 'selections' => [] + 'selections' => [], ]); - $newNode->didEnter = true; + $newNode->didEnter = true; return $newNode; }, - 'leave' => function(OperationDefinitionNode $node) use (&$selectionSet, $ast) { + 'leave' => function (OperationDefinitionNode $node) use (&$selectionSet, $ast) { $this->checkVisitorFnArgs($ast, func_get_args(), true); - $newNode = clone $node; + $newNode = clone $node; $newNode->selectionSet = $selectionSet; - $newNode->didLeave = true; + $newNode->didLeave = true; return $newNode; - } - ] + }, + ], ]); $this->assertNotEquals($ast, $editedAst); - $expected = $ast->cloneDeep(); + $expected = $ast->cloneDeep(); $expected->definitions[0]->didEnter = true; $expected->definitions[0]->didLeave = true; $this->assertEquals($expected, $editedAst); } - /** - * @it allows editing the root node on enter and on leave - */ public function testAllowsEditingRootNodeOnEnterAndLeave() { - $ast = Parser::parse('{ a, b, c { a, b, c } }', [ 'noLocation' => true ]); + $ast = Parser::parse('{ a, b, c { a, b, c } }', [ 'noLocation' => true ]); $definitions = $ast->definitions; $editedAst = Visitor::visit($ast, [ NodeKind::DOCUMENT => [ 'enter' => function (DocumentNode $node) use ($ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $tmp = clone $node; + $tmp = clone $node; $tmp->definitions = []; - $tmp->didEnter = true; + $tmp->didEnter = true; return $tmp; }, - 'leave' => function(DocumentNode $node) use ($definitions, $ast) { + 'leave' => function (DocumentNode $node) use ($definitions, $ast) { $this->checkVisitorFnArgs($ast, func_get_args(), true); - $tmp = clone $node; $node->definitions = $definitions; - $node->didLeave = true; - } - ] + $node->didLeave = true; + }, + ], ]); $this->assertNotEquals($ast, $editedAst); - $tmp = $ast->cloneDeep(); + $tmp = $ast->cloneDeep(); $tmp->didEnter = true; $tmp->didLeave = true; $this->assertEquals($tmp, $editedAst); } - /** - * @it allows for editing on enter - */ public function testAllowsForEditingOnEnter() { - $ast = Parser::parse('{ a, b, c { a, b, c } }', ['noLocation' => true]); + $ast = Parser::parse('{ a, b, c { a, b, c } }', ['noLocation' => true]); $editedAst = Visitor::visit($ast, [ - 'enter' => function($node) use ($ast) { + 'enter' => function ($node) use ($ast) { $this->checkVisitorFnArgs($ast, func_get_args()); if ($node instanceof FieldNode && $node->name->value === 'b') { return Visitor::removeNode(); } - } + }, ]); $this->assertEquals( @@ -204,19 +207,16 @@ class VisitorTest extends ValidatorTestCase ); } - /** - * @it allows for editing on leave - */ public function testAllowsForEditingOnLeave() { - $ast = Parser::parse('{ a, b, c { a, b, c } }', ['noLocation' => true]); + $ast = Parser::parse('{ a, b, c { a, b, c } }', ['noLocation' => true]); $editedAst = Visitor::visit($ast, [ - 'leave' => function($node) use ($ast) { + 'leave' => function ($node) use ($ast) { $this->checkVisitorFnArgs($ast, func_get_args(), true); if ($node instanceof FieldNode && $node->name->value === 'b') { return Visitor::removeNode(); } - } + }, ]); $this->assertEquals( @@ -230,60 +230,54 @@ class VisitorTest extends ValidatorTestCase ); } - /** - * @it visits edited node - */ public function testVisitsEditedNode() { - $addedField = new FieldNode(array( - 'name' => new NameNode(array( - 'value' => '__typename' - )) - )); + $addedField = new FieldNode([ + 'name' => new NameNode(['value' => '__typename']), + ]); $didVisitAddedField = false; $ast = Parser::parse('{ a { x } }', ['noLocation' => true]); Visitor::visit($ast, [ - 'enter' => function($node) use ($addedField, &$didVisitAddedField, $ast) { + 'enter' => function ($node) use ($addedField, &$didVisitAddedField, $ast) { $this->checkVisitorFnArgs($ast, func_get_args(), true); if ($node instanceof FieldNode && $node->name->value === 'a') { return new FieldNode([ - 'selectionSet' => new SelectionSetNode(array( - 'selections' => NodeList::create([$addedField])->merge($node->selectionSet->selections) - )) + 'selectionSet' => new SelectionSetNode([ + 'selections' => NodeList::create([$addedField])->merge($node->selectionSet->selections), + ]), ]); } - if ($node === $addedField) { - $didVisitAddedField = true; + if ($node !== $addedField) { + return; } - } + + $didVisitAddedField = true; + }, ]); $this->assertTrue($didVisitAddedField); } - /** - * @it allows skipping a sub-tree - */ public function testAllowsSkippingASubTree() { $visited = []; - $ast = Parser::parse('{ a, b { x }, c }', ['noLocation' => true]); + $ast = Parser::parse('{ a, b { x }, c }', ['noLocation' => true]); Visitor::visit($ast, [ - 'enter' => function(Node $node) use (&$visited, $ast) { + 'enter' => function (Node $node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $visited[] = ['enter', $node->kind, isset($node->value) ? $node->value : null]; + $visited[] = ['enter', $node->kind, $node->value ?? null]; if ($node instanceof FieldNode && $node->name->value === 'b') { return Visitor::skipNode(); } }, 'leave' => function (Node $node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $visited[] = ['leave', $node->kind, isset($node->value) ? $node->value : null]; - } + $visited[] = ['leave', $node->kind, $node->value ?? null]; + }, ]); $expected = [ @@ -301,32 +295,29 @@ class VisitorTest extends ValidatorTestCase [ 'leave', 'Field', null ], [ 'leave', 'SelectionSet', null ], [ 'leave', 'OperationDefinition', null ], - [ 'leave', 'Document', null ] + [ 'leave', 'Document', null ], ]; $this->assertEquals($expected, $visited); } - /** - * @it allows early exit while visiting - */ public function testAllowsEarlyExitWhileVisiting() { $visited = []; - $ast = Parser::parse('{ a, b { x }, c }', ['noLocation' => true]); + $ast = Parser::parse('{ a, b { x }, c }', ['noLocation' => true]); Visitor::visit($ast, [ - 'enter' => function(Node $node) use (&$visited, $ast) { + 'enter' => function (Node $node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $visited[] = ['enter', $node->kind, isset($node->value) ? $node->value : null]; + $visited[] = ['enter', $node->kind, $node->value ?? null]; if ($node instanceof NameNode && $node->value === 'x') { return Visitor::stop(); } }, - 'leave' => function(Node $node) use (&$visited, $ast) { + 'leave' => function (Node $node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $visited[] = ['leave', $node->kind, isset($node->value) ? $node->value : null]; - } + $visited[] = ['leave', $node->kind, $node->value ?? null]; + }, ]); $expected = [ @@ -342,33 +333,30 @@ class VisitorTest extends ValidatorTestCase [ 'leave', 'Name', 'b' ], [ 'enter', 'SelectionSet', null ], [ 'enter', 'Field', null ], - [ 'enter', 'Name', 'x' ] + [ 'enter', 'Name', 'x' ], ]; $this->assertEquals($expected, $visited); } - /** - * @it allows early exit while leaving - */ public function testAllowsEarlyExitWhileLeaving() { $visited = []; $ast = Parser::parse('{ a, b { x }, c }', ['noLocation' => true]); Visitor::visit($ast, [ - 'enter' => function($node) use (&$visited, $ast) { + 'enter' => function ($node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $visited[] = ['enter', $node->kind, isset($node->value) ? $node->value : null]; + $visited[] = ['enter', $node->kind, $node->value ?? null]; }, - 'leave' => function($node) use (&$visited, $ast) { + 'leave' => function ($node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $visited[] = ['leave', $node->kind, isset($node->value) ? $node->value : null]; + $visited[] = ['leave', $node->kind, $node->value ?? null]; if ($node->kind === NodeKind::NAME && $node->value === 'x') { return Visitor::stop(); } - } + }, ]); $this->assertEquals($visited, [ @@ -385,33 +373,30 @@ class VisitorTest extends ValidatorTestCase [ 'enter', 'SelectionSet', null ], [ 'enter', 'Field', null ], [ 'enter', 'Name', 'x' ], - [ 'leave', 'Name', 'x' ] + [ 'leave', 'Name', 'x' ], ]); } - /** - * @it allows a named functions visitor API - */ public function testAllowsANamedFunctionsVisitorAPI() { $visited = []; - $ast = Parser::parse('{ a, b { x }, c }', ['noLocation' => true]); + $ast = Parser::parse('{ a, b { x }, c }', ['noLocation' => true]); Visitor::visit($ast, [ - NodeKind::NAME => function(NameNode $node) use (&$visited, $ast) { + NodeKind::NAME => function (NameNode $node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['enter', $node->kind, $node->value]; }, NodeKind::SELECTION_SET => [ - 'enter' => function(SelectionSetNode $node) use (&$visited, $ast) { + 'enter' => function (SelectionSetNode $node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['enter', $node->kind, null]; }, - 'leave' => function(SelectionSetNode $node) use (&$visited, $ast) { + 'leave' => function (SelectionSetNode $node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['leave', $node->kind, null]; - } - ] + }, + ], ]); $expected = [ @@ -428,12 +413,9 @@ class VisitorTest extends ValidatorTestCase $this->assertEquals($expected, $visited); } - /** - * @it Experimental: visits variables defined in fragments - */ public function testExperimentalVisitsVariablesDefinedInFragments() { - $ast = Parser::parse( + $ast = Parser::parse( 'fragment a($v: Boolean = false) on t { f }', [ 'noLocation' => true, @@ -443,13 +425,13 @@ class VisitorTest extends ValidatorTestCase $visited = []; Visitor::visit($ast, [ - 'enter' => function($node) use (&$visited, $ast) { + 'enter' => function ($node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $visited[] = ['enter', $node->kind, isset($node->value) ? $node->value : null]; + $visited[] = ['enter', $node->kind, $node->value ?? null]; }, - 'leave' => function($node) use (&$visited, $ast) { + 'leave' => function ($node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $visited[] = ['leave', $node->kind, isset($node->value) ? $node->value : null]; + $visited[] = ['leave', $node->kind, $node->value ?? null]; }, ]); @@ -487,26 +469,23 @@ class VisitorTest extends ValidatorTestCase $this->assertEquals($expected, $visited); } - /** - * @it visits kitchen sink - */ public function testVisitsKitchenSink() { $kitchenSink = file_get_contents(__DIR__ . '/kitchen-sink.graphql'); - $ast = Parser::parse($kitchenSink); + $ast = Parser::parse($kitchenSink); $visited = []; Visitor::visit($ast, [ - 'enter' => function(Node $node, $key, $parent) use (&$visited, $ast) { + 'enter' => function (Node $node, $key, $parent) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $r = ['enter', $node->kind, $key, $parent instanceof Node ? $parent->kind : null]; + $r = ['enter', $node->kind, $key, $parent instanceof Node ? $parent->kind : null]; $visited[] = $r; }, - 'leave' => function(Node $node, $key, $parent) use (&$visited, $ast) { + 'leave' => function (Node $node, $key, $parent) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $r = ['leave', $node->kind, $key, $parent instanceof Node ? $parent->kind : null]; + $r = ['leave', $node->kind, $key, $parent instanceof Node ? $parent->kind : null]; $visited[] = $r; - } + }, ]); $expected = [ @@ -819,17 +798,15 @@ class VisitorTest extends ValidatorTestCase [ 'leave', 'Field', 1, null ], [ 'leave', 'SelectionSet', 'selectionSet', 'OperationDefinition' ], [ 'leave', 'OperationDefinition', 4, null ], - [ 'leave', 'Document', null, null ] + [ 'leave', 'Document', null, null ], ]; $this->assertEquals($expected, $visited); } - // Describe: visitInParallel - // Note: nearly identical to the above test of the same test but using visitInParallel. - /** - * @it allows skipping a sub-tree + * Describe: visitInParallel + * Note: nearly identical to the above test of the same test but using visitInParallel. */ public function testAllowsSkippingSubTree() { @@ -838,20 +815,20 @@ class VisitorTest extends ValidatorTestCase $ast = Parser::parse('{ a, b { x }, c }'); Visitor::visit($ast, Visitor::visitInParallel([ [ - 'enter' => function($node) use (&$visited, $ast) { + 'enter' => function ($node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $visited[] = [ 'enter', $node->kind, isset($node->value) ? $node->value : null]; + $visited[] = [ 'enter', $node->kind, $node->value ?? null]; if ($node->kind === 'Field' && isset($node->name->value) && $node->name->value === 'b') { return Visitor::skipNode(); } }, - 'leave' => function($node) use (&$visited, $ast) { + 'leave' => function ($node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $visited[] = ['leave', $node->kind, isset($node->value) ? $node->value : null]; - } - ] + $visited[] = ['leave', $node->kind, $node->value ?? null]; + }, + ], ])); $this->assertEquals([ @@ -873,9 +850,6 @@ class VisitorTest extends ValidatorTestCase ], $visited); } - /** - * @it allows skipping different sub-trees - */ public function testAllowsSkippingDifferentSubTrees() { $visited = []; @@ -883,31 +857,31 @@ class VisitorTest extends ValidatorTestCase $ast = Parser::parse('{ a { x }, b { y} }'); Visitor::visit($ast, Visitor::visitInParallel([ [ - 'enter' => function($node) use (&$visited, $ast) { + 'enter' => function ($node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $visited[] = ['no-a', 'enter', $node->kind, isset($node->value) ? $node->value : null]; + $visited[] = ['no-a', 'enter', $node->kind, $node->value ?? null]; if ($node->kind === 'Field' && isset($node->name->value) && $node->name->value === 'a') { return Visitor::skipNode(); } }, - 'leave' => function($node) use (&$visited, $ast) { + 'leave' => function ($node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $visited[] = [ 'no-a', 'leave', $node->kind, isset($node->value) ? $node->value : null ]; - } + $visited[] = [ 'no-a', 'leave', $node->kind, $node->value ?? null ]; + }, ], [ - 'enter' => function($node) use (&$visited, $ast) { + 'enter' => function ($node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $visited[] = ['no-b', 'enter', $node->kind, isset($node->value) ? $node->value : null]; + $visited[] = ['no-b', 'enter', $node->kind, $node->value ?? null]; if ($node->kind === 'Field' && isset($node->name->value) && $node->name->value === 'b') { return Visitor::skipNode(); } }, - 'leave' => function($node) use (&$visited, $ast) { + 'leave' => function ($node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $visited[] = ['no-b', 'leave', $node->kind, isset($node->value) ? $node->value : null]; - } - ] + $visited[] = ['no-b', 'leave', $node->kind, $node->value ?? null]; + }, + ], ])); $this->assertEquals([ @@ -948,28 +922,26 @@ class VisitorTest extends ValidatorTestCase ], $visited); } - /** - * @it allows early exit while visiting - */ public function testAllowsEarlyExitWhileVisiting2() { $visited = []; $ast = Parser::parse('{ a, b { x }, c }'); Visitor::visit($ast, Visitor::visitInParallel([ [ - 'enter' => function($node) use (&$visited, $ast) { + 'enter' => function ($node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $value = isset($node->value) ? $node->value : null; + $value = $node->value ?? null; $visited[] = ['enter', $node->kind, $value]; if ($node->kind === 'Name' && $value === 'x') { return Visitor::stop(); } }, - 'leave' => function($node) use (&$visited, $ast) { + 'leave' => function ($node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $visited[] = ['leave', $node->kind, isset($node->value) ? $node->value : null]; - } - ] ])); + $visited[] = ['leave', $node->kind, $node->value ?? null]; + }, + ], + ])); $this->assertEquals([ [ 'enter', 'Document', null ], @@ -984,13 +956,10 @@ class VisitorTest extends ValidatorTestCase [ 'leave', 'Name', 'b' ], [ 'enter', 'SelectionSet', null ], [ 'enter', 'Field', null ], - [ 'enter', 'Name', 'x' ] + [ 'enter', 'Name', 'x' ], ], $visited); } - /** - * @it allows early exit from different points - */ public function testAllowsEarlyExitFromDifferentPoints() { $visited = []; @@ -998,32 +967,32 @@ class VisitorTest extends ValidatorTestCase $ast = Parser::parse('{ a { y }, b { x } }'); Visitor::visit($ast, Visitor::visitInParallel([ [ - 'enter' => function($node) use (&$visited, $ast) { + 'enter' => function ($node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $value = isset($node->value) ? $node->value : null; + $value = $node->value ?? null; $visited[] = ['break-a', 'enter', $node->kind, $value]; if ($node->kind === 'Name' && $value === 'a') { return Visitor::stop(); } }, - 'leave' => function($node) use (&$visited, $ast) { + 'leave' => function ($node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $visited[] = [ 'break-a', 'leave', $node->kind, isset($node->value) ? $node->value : null ]; - } + $visited[] = [ 'break-a', 'leave', $node->kind, $node->value ?? null ]; + }, ], [ - 'enter' => function($node) use (&$visited, $ast) { + 'enter' => function ($node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $value = isset($node->value) ? $node->value : null; + $value = $node->value ?? null; $visited[] = ['break-b', 'enter', $node->kind, $value]; if ($node->kind === 'Name' && $value === 'b') { return Visitor::stop(); } }, - 'leave' => function($node) use (&$visited, $ast) { + 'leave' => function ($node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $visited[] = ['break-b', 'leave', $node->kind, isset($node->value) ? $node->value : null]; - } + $visited[] = ['break-b', 'leave', $node->kind, $node->value ?? null]; + }, ], ])); @@ -1047,32 +1016,30 @@ class VisitorTest extends ValidatorTestCase [ 'break-b', 'leave', 'SelectionSet', null ], [ 'break-b', 'leave', 'Field', null ], [ 'break-b', 'enter', 'Field', null ], - [ 'break-b', 'enter', 'Name', 'b' ] + [ 'break-b', 'enter', 'Name', 'b' ], ], $visited); } - /** - * @it allows early exit while leaving - */ public function testAllowsEarlyExitWhileLeaving2() { $visited = []; $ast = Parser::parse('{ a, b { x }, c }'); Visitor::visit($ast, Visitor::visitInParallel([ [ - 'enter' => function($node) use (&$visited, $ast) { + 'enter' => function ($node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $visited[] = ['enter', $node->kind, isset($node->value) ? $node->value : null]; + $visited[] = ['enter', $node->kind, $node->value ?? null]; }, - 'leave' => function($node) use (&$visited, $ast) { + 'leave' => function ($node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $value = isset($node->value) ? $node->value : null; + $value = $node->value ?? null; $visited[] = ['leave', $node->kind, $value]; if ($node->kind === 'Name' && $value === 'x') { return Visitor::stop(); } - } - ] ])); + }, + ], + ])); $this->assertEquals([ [ 'enter', 'Document', null ], @@ -1088,13 +1055,10 @@ class VisitorTest extends ValidatorTestCase [ 'enter', 'SelectionSet', null ], [ 'enter', 'Field', null ], [ 'enter', 'Name', 'x' ], - [ 'leave', 'Name', 'x' ] + [ 'leave', 'Name', 'x' ], ], $visited); } - /** - * @it allows early exit from leaving different points - */ public function testAllowsEarlyExitFromLeavingDifferentPoints() { $visited = []; @@ -1102,30 +1066,30 @@ class VisitorTest extends ValidatorTestCase $ast = Parser::parse('{ a { y }, b { x } }'); Visitor::visit($ast, Visitor::visitInParallel([ [ - 'enter' => function($node) use (&$visited, $ast) { + 'enter' => function ($node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $visited[] = ['break-a', 'enter', $node->kind, isset($node->value) ? $node->value : null]; + $visited[] = ['break-a', 'enter', $node->kind, $node->value ?? null]; }, - 'leave' => function($node) use (&$visited, $ast) { + 'leave' => function ($node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $visited[] = ['break-a', 'leave', $node->kind, isset($node->value) ? $node->value : null]; + $visited[] = ['break-a', 'leave', $node->kind, $node->value ?? null]; if ($node->kind === 'Field' && isset($node->name->value) && $node->name->value === 'a') { return Visitor::stop(); } - } + }, ], [ - 'enter' => function($node) use (&$visited, $ast) { + 'enter' => function ($node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $visited[] = ['break-b', 'enter', $node->kind, isset($node->value) ? $node->value : null]; + $visited[] = ['break-b', 'enter', $node->kind, $node->value ?? null]; }, - 'leave' => function($node) use (&$visited, $ast) { + 'leave' => function ($node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $visited[] = ['break-b', 'leave', $node->kind, isset($node->value) ? $node->value : null]; + $visited[] = ['break-b', 'leave', $node->kind, $node->value ?? null]; if ($node->kind === 'Field' && isset($node->name->value) && $node->name->value === 'b') { return Visitor::stop(); } - } + }, ], ])); @@ -1165,18 +1129,15 @@ class VisitorTest extends ValidatorTestCase [ 'break-b', 'leave', 'Name', 'x' ], [ 'break-b', 'leave', 'Field', null ], [ 'break-b', 'leave', 'SelectionSet', null ], - [ 'break-b', 'leave', 'Field', null ] + [ 'break-b', 'leave', 'Field', null ], ], $visited); } - /** - * @it allows for editing on enter - */ public function testAllowsForEditingOnEnter2() { $visited = []; - $ast = Parser::parse('{ a, b, c { a, b, c } }', ['noLocation' => true]); + $ast = Parser::parse('{ a, b, c { a, b, c } }', ['noLocation' => true]); $editedAst = Visitor::visit($ast, Visitor::visitInParallel([ [ 'enter' => function ($node) use (&$visited, $ast) { @@ -1184,17 +1145,17 @@ class VisitorTest extends ValidatorTestCase if ($node->kind === 'Field' && isset($node->name->value) && $node->name->value === 'b') { return Visitor::removeNode(); } - } + }, ], [ 'enter' => function ($node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $visited[] = ['enter', $node->kind, isset($node->value) ? $node->value : null]; + $visited[] = ['enter', $node->kind, $node->value ?? null]; }, 'leave' => function ($node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args(), true); - $visited[] = ['leave', $node->kind, isset($node->value) ? $node->value : null]; - } + $visited[] = ['leave', $node->kind, $node->value ?? null]; + }, ], ])); @@ -1232,18 +1193,15 @@ class VisitorTest extends ValidatorTestCase ['leave', 'Field', null], ['leave', 'SelectionSet', null], ['leave', 'OperationDefinition', null], - ['leave', 'Document', null] + ['leave', 'Document', null], ], $visited); } - /** - * @it allows for editing on leave - */ public function testAllowsForEditingOnLeave2() { $visited = []; - $ast = Parser::parse('{ a, b, c { a, b, c } }', ['noLocation' => true]); + $ast = Parser::parse('{ a, b, c { a, b, c } }', ['noLocation' => true]); $editedAst = Visitor::visit($ast, Visitor::visitInParallel([ [ 'leave' => function ($node) use (&$visited, $ast) { @@ -1251,17 +1209,17 @@ class VisitorTest extends ValidatorTestCase if ($node->kind === 'Field' && isset($node->name->value) && $node->name->value === 'b') { return Visitor::removeNode(); } - } + }, ], [ 'enter' => function ($node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); - $visited[] = ['enter', $node->kind, isset($node->value) ? $node->value : null]; + $visited[] = ['enter', $node->kind, $node->value ?? null]; }, 'leave' => function ($node) use (&$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args(), true); - $visited[] = ['leave', $node->kind, isset($node->value) ? $node->value : null]; - } + $visited[] = ['leave', $node->kind, $node->value ?? null]; + }, ], ])); @@ -1305,14 +1263,13 @@ class VisitorTest extends ValidatorTestCase ['leave', 'Field', null], ['leave', 'SelectionSet', null], ['leave', 'OperationDefinition', null], - ['leave', 'Document', null] + ['leave', 'Document', null], ], $visited); } - // Describe: visitWithTypeInfo /** - * @it maintains type info during visit + * Describe: visitWithTypeInfo */ public function testMaintainsTypeInfoDuringVisit() { @@ -1325,31 +1282,31 @@ class VisitorTest extends ValidatorTestCase 'enter' => function ($node) use ($typeInfo, &$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); $parentType = $typeInfo->getParentType(); - $type = $typeInfo->getType(); - $inputType = $typeInfo->getInputType(); - $visited[] = [ + $type = $typeInfo->getType(); + $inputType = $typeInfo->getInputType(); + $visited[] = [ 'enter', $node->kind, $node->kind === 'Name' ? $node->value : null, - $parentType ? (string)$parentType : null, - $type ? (string)$type : null, - $inputType ? (string)$inputType : null + $parentType ? (string) $parentType : null, + $type ? (string) $type : null, + $inputType ? (string) $inputType : null, ]; }, 'leave' => function ($node) use ($typeInfo, &$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args()); $parentType = $typeInfo->getParentType(); - $type = $typeInfo->getType(); - $inputType = $typeInfo->getInputType(); - $visited[] = [ + $type = $typeInfo->getType(); + $inputType = $typeInfo->getInputType(); + $visited[] = [ 'leave', $node->kind, $node->kind === 'Name' ? $node->value : null, - $parentType ? (string)$parentType : null, - $type ? (string)$type : null, - $inputType ? (string)$inputType : null + $parentType ? (string) $parentType : null, + $type ? (string) $type : null, + $inputType ? (string) $inputType : null, ]; - } + }, ])); $this->assertEquals([ @@ -1392,40 +1349,36 @@ class VisitorTest extends ValidatorTestCase ['leave', 'Field', null, 'QueryRoot', 'Human', null], ['leave', 'SelectionSet', null, 'QueryRoot', 'QueryRoot', null], ['leave', 'OperationDefinition', null, null, 'QueryRoot', null], - ['leave', 'Document', null, null, null, null] + ['leave', 'Document', null, null, null, null], ], $visited); } - /** - * @it maintains type info during edit - */ public function testMaintainsTypeInfoDuringEdit() { - $visited = []; + $visited = []; $typeInfo = new TypeInfo(ValidatorTestCase::getTestSchema()); - $ast = Parser::parse( + $ast = Parser::parse( '{ human(id: 4) { name, pets }, alien }' ); $editedAst = Visitor::visit($ast, Visitor::visitWithTypeInfo($typeInfo, [ 'enter' => function ($node) use ($typeInfo, &$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args(), true); $parentType = $typeInfo->getParentType(); - $type = $typeInfo->getType(); - $inputType = $typeInfo->getInputType(); - $visited[] = [ + $type = $typeInfo->getType(); + $inputType = $typeInfo->getInputType(); + $visited[] = [ 'enter', $node->kind, $node->kind === 'Name' ? $node->value : null, - $parentType ? (string)$parentType : null, - $type ? (string)$type : null, - $inputType ? (string)$inputType : null + $parentType ? (string) $parentType : null, + $type ? (string) $type : null, + $inputType ? (string) $inputType : null, ]; // Make a query valid by adding missing selection sets. - if ( - $node->kind === 'Field' && - !$node->selectionSet && + if ($node->kind === 'Field' && + ! $node->selectionSet && Type::isCompositeType(Type::getNamedType($type)) ) { return new FieldNode([ @@ -1435,29 +1388,28 @@ class VisitorTest extends ValidatorTestCase 'directives' => $node->directives, 'selectionSet' => new SelectionSetNode([ 'kind' => 'SelectionSet', - 'selections' => [ - new FieldNode([ - 'name' => new NameNode(['value' => '__typename']) - ]) - ] - ]) + 'selections' => [new FieldNode([ + 'name' => new NameNode(['value' => '__typename']), + ]), + ], + ]), ]); } }, 'leave' => function ($node) use ($typeInfo, &$visited, $ast) { $this->checkVisitorFnArgs($ast, func_get_args(), true); $parentType = $typeInfo->getParentType(); - $type = $typeInfo->getType(); - $inputType = $typeInfo->getInputType(); - $visited[] = [ + $type = $typeInfo->getType(); + $inputType = $typeInfo->getInputType(); + $visited[] = [ 'leave', $node->kind, $node->kind === 'Name' ? $node->value : null, - $parentType ? (string)$parentType : null, - $type ? (string)$type : null, - $inputType ? (string)$inputType : null + $parentType ? (string) $parentType : null, + $type ? (string) $type : null, + $inputType ? (string) $inputType : null, ]; - } + }, ])); $this->assertEquals(Printer::doPrint(Parser::parse( @@ -1510,7 +1462,7 @@ class VisitorTest extends ValidatorTestCase ['leave', 'Field', null, 'QueryRoot', 'Alien', null], ['leave', 'SelectionSet', null, 'QueryRoot', 'QueryRoot', null], ['leave', 'OperationDefinition', null, null, 'QueryRoot', null], - ['leave', 'Document', null, null, null, null] + ['leave', 'Document', null, null, null, null], ], $visited); } } diff --git a/tests/Validator/QuerySecurityTestCase.php b/tests/Validator/QuerySecurityTestCase.php index cfdb29a..9bb07aa 100644 --- a/tests/Validator/QuerySecurityTestCase.php +++ b/tests/Validator/QuerySecurityTestCase.php @@ -5,7 +5,7 @@ use GraphQL\Error\FormattedError; use GraphQL\Language\Parser; use GraphQL\Type\Introspection; use GraphQL\Validator\DocumentValidator; -use GraphQL\Validator\Rules\AbstractQuerySecurity; +use GraphQL\Validator\Rules\QuerySecurityRule; use PHPUnit\Framework\TestCase; abstract class QuerySecurityTestCase extends TestCase @@ -13,7 +13,7 @@ abstract class QuerySecurityTestCase extends TestCase /** * @param $max * - * @return AbstractQuerySecurity + * @return QuerySecurityRule */ abstract protected function getRule($max); @@ -89,8 +89,9 @@ abstract class QuerySecurityTestCase extends TestCase { $this->assertDocumentValidator($query, $maxExpected); $newMax = $maxExpected - 1; - if ($newMax !== AbstractQuerySecurity::DISABLED) { - $this->assertDocumentValidator($query, $newMax, [$this->createFormattedError($newMax, $maxExpected)]); + if ($newMax === QuerySecurityRule::DISABLED) { + return; } + $this->assertDocumentValidator($query, $newMax, [$this->createFormattedError($newMax, $maxExpected)]); } }