Fix CS in Validator folder

This commit is contained in:
Simon Podlipsky 2018-08-07 00:35:37 +02:00
parent 49ec89b28f
commit 4c327a6c16
No known key found for this signature in database
GPG Key ID: 725C2BD962B42663
47 changed files with 2129 additions and 1710 deletions

View File

@ -1,6 +1,8 @@
# Changelog # Changelog
## dev-master ## dev-master
- Spec compliance: error extensions are displayed under `extensions` key - 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 #### v0.12.5
- Execution performance optimization for lists - Execution performance optimization for lists

View File

@ -119,7 +119,7 @@ static function getStandardTypes()
* Returns standard validation rules implementing GraphQL spec * Returns standard validation rules implementing GraphQL spec
* *
* @api * @api
* @return AbstractValidationRule[] * @return ValidationRule[]
*/ */
static function getStandardValidationRules() static function getStandardValidationRules()
``` ```
@ -1241,7 +1241,7 @@ an empty array if no errors were encountered and the document is valid.
A list of specific validation rules may be provided. If not provided, the A list of specific validation rules may be provided. If not provided, the
default list of rules defined by the GraphQL specification will be used. default list of rules defined by the GraphQL specification will be used.
Each validation rule is an instance of GraphQL\Validator\Rules\AbstractValidationRule Each validation rule is an instance of GraphQL\Validator\Rules\ValidationRule
which returns a visitor (see the [GraphQL\Language\Visitor API](reference.md#graphqllanguagevisitor)). which returns a visitor (see the [GraphQL\Language\Visitor API](reference.md#graphqllanguagevisitor)).
Visitor methods are expected to return an instance of [GraphQL\Error\Error](reference.md#graphqlerrorerror), Visitor methods are expected to return an instance of [GraphQL\Error\Error](reference.md#graphqlerrorerror),
@ -1258,7 +1258,7 @@ will be created from the provided schema.
* @api * @api
* @param Schema $schema * @param Schema $schema
* @param DocumentNode $ast * @param DocumentNode $ast
* @param AbstractValidationRule[]|null $rules * @param ValidationRule[]|null $rules
* @param TypeInfo|null $typeInfo * @param TypeInfo|null $typeInfo
* @return Error[] * @return Error[]
*/ */
@ -1275,7 +1275,7 @@ static function validate(
* Returns all global validation rules. * Returns all global validation rules.
* *
* @api * @api
* @return AbstractValidationRule[] * @return ValidationRule[]
*/ */
static function allRules() static function allRules()
``` ```
@ -1289,7 +1289,7 @@ static function allRules()
* *
* @api * @api
* @param string $name * @param string $name
* @return AbstractValidationRule * @return ValidationRule
*/ */
static function getRule($name) static function getRule($name)
``` ```
@ -1299,9 +1299,9 @@ static function getRule($name)
* Add rule to list of global validation rules * Add rule to list of global validation rules
* *
* @api * @api
* @param AbstractValidationRule $rule * @param ValidationRule $rule
*/ */
static function addRule(GraphQL\Validator\Rules\AbstractValidationRule $rule) static function addRule(GraphQL\Validator\Rules\ValidationRule $rule)
``` ```
# GraphQL\Error\Error # GraphQL\Error\Error
Describes an Error found during the parse, validate, or Describes an Error found during the parse, validate, or

View File

@ -8,9 +8,9 @@ use GraphQL\Language\AST\Node;
use GraphQL\Language\Source; use GraphQL\Language\Source;
use GraphQL\Language\SourceLocation; use GraphQL\Language\SourceLocation;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
use Traversable;
use function array_filter; use function array_filter;
use function array_map; use function array_map;
use function array_merge;
use function is_array; use function is_array;
use function iterator_to_array; use function iterator_to_array;
@ -81,12 +81,12 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
protected $extensions; protected $extensions;
/** /**
* @param string $message * @param string $message
* @param Node[]|null $nodes * @param Node|Node[]|Traversable|null $nodes
* @param mixed[]|null $positions * @param mixed[]|null $positions
* @param mixed[]|null $path * @param mixed[]|null $path
* @param \Throwable $previous * @param \Throwable $previous
* @param mixed[] $extensions * @param mixed[] $extensions
*/ */
public function __construct( public function __construct(
$message, $message,

View File

@ -13,7 +13,7 @@ use GraphQL\Executor\Promise\PromiseAdapter;
use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Validator\DocumentValidator; use GraphQL\Validator\DocumentValidator;
use GraphQL\Validator\Rules\AbstractValidationRule; use GraphQL\Validator\Rules\ValidationRule;
use GraphQL\Validator\Rules\QueryComplexity; use GraphQL\Validator\Rules\QueryComplexity;
/** /**
@ -272,7 +272,7 @@ class GraphQL
* Returns standard validation rules implementing GraphQL spec * Returns standard validation rules implementing GraphQL spec
* *
* @api * @api
* @return AbstractValidationRule[] * @return ValidationRule[]
*/ */
public static function getStandardValidationRules() public static function getStandardValidationRules()
{ {

View File

@ -1,4 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Language\AST; namespace GraphQL\Language\AST;
class FieldNode extends Node implements SelectionNode class FieldNode extends Node implements SelectionNode
@ -29,4 +32,9 @@ class FieldNode extends Node implements SelectionNode
* @var SelectionSetNode|null * @var SelectionSetNode|null
*/ */
public $selectionSet; public $selectionSet;
public function getKind() : string
{
return NodeKind::FIELD;
}
} }

View File

@ -1,4 +1,5 @@
<?php <?php
namespace GraphQL\Language\AST; namespace GraphQL\Language\AST;
class FragmentSpreadNode extends Node implements SelectionNode class FragmentSpreadNode extends Node implements SelectionNode
@ -14,4 +15,9 @@ class FragmentSpreadNode extends Node implements SelectionNode
* @var DirectiveNode[] * @var DirectiveNode[]
*/ */
public $directives; public $directives;
public function getKind() : string
{
return NodeKind::FRAGMENT_SPREAD;
}
} }

View File

@ -1,4 +1,5 @@
<?php <?php
namespace GraphQL\Language\AST; namespace GraphQL\Language\AST;
class InlineFragmentNode extends Node implements SelectionNode class InlineFragmentNode extends Node implements SelectionNode
@ -19,4 +20,9 @@ class InlineFragmentNode extends Node implements SelectionNode
* @var SelectionSetNode * @var SelectionSetNode
*/ */
public $selectionSet; public $selectionSet;
public function getKind() : string
{
return NodeKind::INLINE_FRAGMENT;
}
} }

View File

@ -1,9 +1,13 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Language\AST; namespace GraphQL\Language\AST;
interface SelectionNode interface SelectionNode
{ {
/** /**
* export type SelectionNode = FieldNode | FragmentSpreadNode | InlineFragmentNode * export type SelectionNode = FieldNode | FragmentSpreadNode | InlineFragmentNode
*/ */
public function getKind() : string;
} }

View File

@ -8,7 +8,7 @@ use GraphQL\Error\InvariantViolation;
use GraphQL\Executor\Promise\PromiseAdapter; use GraphQL\Executor\Promise\PromiseAdapter;
use GraphQL\Type\Schema; use GraphQL\Type\Schema;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
use GraphQL\Validator\Rules\AbstractValidationRule; use GraphQL\Validator\Rules\ValidationRule;
use function is_array; use function is_array;
use function is_callable; use function is_callable;
use function method_exists; use function method_exists;
@ -73,7 +73,7 @@ class ServerConfig
/** @var bool */ /** @var bool */
private $queryBatching = false; private $queryBatching = false;
/** @var AbstractValidationRule[]|callable */ /** @var ValidationRule[]|callable */
private $validationRules; private $validationRules;
/** @var callable */ /** @var callable */
@ -150,7 +150,7 @@ class ServerConfig
* Set validation rules for this server. * Set validation rules for this server.
* *
* @api * @api
* @param AbstractValidationRule[]|callable $validationRules * @param ValidationRule[]|callable $validationRules
* @return self * @return self
*/ */
public function setValidationRules($validationRules) public function setValidationRules($validationRules)
@ -281,7 +281,7 @@ class ServerConfig
} }
/** /**
* @return AbstractValidationRule[]|callable * @return ValidationRule[]|callable
*/ */
public function getValidationRules() public function getValidationRules()
{ {

View File

@ -204,7 +204,7 @@ class Schema
* *
* @api * @api
* @param string $name * @param string $name
* @return Type * @return Type|null
*/ */
public function getType($name) public function getType($name)
{ {

View File

@ -487,7 +487,7 @@ class AST
* @api * @api
* @param Schema $schema * @param Schema $schema
* @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode * @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode
* @return Type * @return Type|null
* @throws \Exception * @throws \Exception
*/ */
public static function typeFromAST(Schema $schema, $inputTypeNode) public static function typeFromAST(Schema $schema, $inputTypeNode)

View File

@ -58,7 +58,7 @@ class TypeInfo
/** /**
* @param Schema $schema * @param Schema $schema
* @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode * @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode
* @return Type * @return Type|null
* @throws InvariantViolation * @throws InvariantViolation
*/ */
public static function typeFromAST(Schema $schema, $inputTypeNode) public static function typeFromAST(Schema $schema, $inputTypeNode)
@ -249,7 +249,7 @@ class TypeInfo
} }
/** /**
* @return CompositeType * @return Type
*/ */
function getParentType() function getParentType()
{ {

View File

@ -1,14 +1,15 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator; namespace GraphQL\Validator;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Language\AST\DocumentNode; use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\Visitor; use GraphQL\Language\Visitor;
use GraphQL\Type\Schema;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;
use GraphQL\Utils\TypeInfo; use GraphQL\Utils\TypeInfo;
use GraphQL\Validator\Rules\AbstractValidationRule;
use GraphQL\Validator\Rules\ValuesOfCorrectType;
use GraphQL\Validator\Rules\DisableIntrospection; use GraphQL\Validator\Rules\DisableIntrospection;
use GraphQL\Validator\Rules\ExecutableDefinitions; use GraphQL\Validator\Rules\ExecutableDefinitions;
use GraphQL\Validator\Rules\FieldsOnCorrectType; use GraphQL\Validator\Rules\FieldsOnCorrectType;
@ -27,6 +28,7 @@ use GraphQL\Validator\Rules\PossibleFragmentSpreads;
use GraphQL\Validator\Rules\ProvidedNonNullArguments; use GraphQL\Validator\Rules\ProvidedNonNullArguments;
use GraphQL\Validator\Rules\QueryComplexity; use GraphQL\Validator\Rules\QueryComplexity;
use GraphQL\Validator\Rules\QueryDepth; use GraphQL\Validator\Rules\QueryDepth;
use GraphQL\Validator\Rules\QuerySecurityRule;
use GraphQL\Validator\Rules\ScalarLeafs; use GraphQL\Validator\Rules\ScalarLeafs;
use GraphQL\Validator\Rules\UniqueArgumentNames; use GraphQL\Validator\Rules\UniqueArgumentNames;
use GraphQL\Validator\Rules\UniqueDirectivesPerLocation; use GraphQL\Validator\Rules\UniqueDirectivesPerLocation;
@ -34,9 +36,16 @@ use GraphQL\Validator\Rules\UniqueFragmentNames;
use GraphQL\Validator\Rules\UniqueInputFieldNames; use GraphQL\Validator\Rules\UniqueInputFieldNames;
use GraphQL\Validator\Rules\UniqueOperationNames; use GraphQL\Validator\Rules\UniqueOperationNames;
use GraphQL\Validator\Rules\UniqueVariableNames; use GraphQL\Validator\Rules\UniqueVariableNames;
use GraphQL\Validator\Rules\ValidationRule;
use GraphQL\Validator\Rules\ValuesOfCorrectType;
use GraphQL\Validator\Rules\VariablesAreInputTypes; use GraphQL\Validator\Rules\VariablesAreInputTypes;
use GraphQL\Validator\Rules\VariablesDefaultValueAllowed; use GraphQL\Validator\Rules\VariablesDefaultValueAllowed;
use GraphQL\Validator\Rules\VariablesInAllowedPosition; use GraphQL\Validator\Rules\VariablesInAllowedPosition;
use function array_filter;
use function array_merge;
use function count;
use function is_array;
use function sprintf;
/** /**
* Implements the "Validation" section of the spec. * Implements the "Validation" section of the spec.
@ -47,7 +56,7 @@ use GraphQL\Validator\Rules\VariablesInAllowedPosition;
* A list of specific validation rules may be provided. If not provided, the * A list of specific validation rules may be provided. If not provided, the
* default list of rules defined by the GraphQL specification will be used. * default list of rules defined by the GraphQL specification will be used.
* *
* Each validation rule is an instance of GraphQL\Validator\Rules\AbstractValidationRule * Each validation rule is an instance of GraphQL\Validator\Rules\ValidationRule
* which returns a visitor (see the [GraphQL\Language\Visitor API](reference.md#graphqllanguagevisitor)). * which returns a visitor (see the [GraphQL\Language\Visitor API](reference.md#graphqllanguagevisitor)).
* *
* Visitor methods are expected to return an instance of [GraphQL\Error\Error](reference.md#graphqlerrorerror), * Visitor methods are expected to return an instance of [GraphQL\Error\Error](reference.md#graphqlerrorerror),
@ -58,56 +67,55 @@ use GraphQL\Validator\Rules\VariablesInAllowedPosition;
*/ */
class DocumentValidator class DocumentValidator
{ {
/** @var ValidationRule[] */
private static $rules = []; private static $rules = [];
/** @var ValidationRule[]|null */
private static $defaultRules; private static $defaultRules;
/** @var QuerySecurityRule[]|null */
private static $securityRules; private static $securityRules;
/** @var bool */
private static $initRules = false; private static $initRules = false;
/** /**
* Primary method for query validation. See class description for details. * Primary method for query validation. See class description for details.
* *
* @api * @api
* @param Schema $schema * @param ValidationRule[]|null $rules
* @param DocumentNode $ast
* @param AbstractValidationRule[]|null $rules
* @param TypeInfo|null $typeInfo
* @return Error[] * @return Error[]
*/ */
public static function validate( public static function validate(
Schema $schema, Schema $schema,
DocumentNode $ast, DocumentNode $ast,
array $rules = null, ?array $rules = null,
TypeInfo $typeInfo = null ?TypeInfo $typeInfo = null
) ) {
{ if ($rules === null) {
if (null === $rules) {
$rules = static::allRules(); $rules = static::allRules();
} }
if (true === is_array($rules) && 0 === count($rules)) { if (is_array($rules) === true && count($rules) === 0) {
// Skip validation if there are no rules // Skip validation if there are no rules
return []; return [];
} }
$typeInfo = $typeInfo ?: new TypeInfo($schema); $typeInfo = $typeInfo ?: new TypeInfo($schema);
$errors = static::visitUsingRules($schema, $typeInfo, $ast, $rules);
return $errors;
}
return static::visitUsingRules($schema, $typeInfo, $ast, $rules);
}
/** /**
* Returns all global validation rules. * Returns all global validation rules.
* *
* @api * @api
* @return AbstractValidationRule[] * @return ValidationRule[]
*/ */
public static function allRules() public static function allRules()
{ {
if (!self::$initRules) { if (! self::$initRules) {
static::$rules = array_merge(static::defaultRules(), self::securityRules(), self::$rules); static::$rules = array_merge(static::defaultRules(), self::securityRules(), self::$rules);
static::$initRules = true; static::$initRules = true;
} }
@ -116,34 +124,34 @@ class DocumentValidator
public static function defaultRules() public static function defaultRules()
{ {
if (null === self::$defaultRules) { if (self::$defaultRules === null) {
self::$defaultRules = [ self::$defaultRules = [
ExecutableDefinitions::class => new ExecutableDefinitions(), ExecutableDefinitions::class => new ExecutableDefinitions(),
UniqueOperationNames::class => new UniqueOperationNames(), UniqueOperationNames::class => new UniqueOperationNames(),
LoneAnonymousOperation::class => new LoneAnonymousOperation(), LoneAnonymousOperation::class => new LoneAnonymousOperation(),
KnownTypeNames::class => new KnownTypeNames(), KnownTypeNames::class => new KnownTypeNames(),
FragmentsOnCompositeTypes::class => new FragmentsOnCompositeTypes(), FragmentsOnCompositeTypes::class => new FragmentsOnCompositeTypes(),
VariablesAreInputTypes::class => new VariablesAreInputTypes(), VariablesAreInputTypes::class => new VariablesAreInputTypes(),
ScalarLeafs::class => new ScalarLeafs(), ScalarLeafs::class => new ScalarLeafs(),
FieldsOnCorrectType::class => new FieldsOnCorrectType(), FieldsOnCorrectType::class => new FieldsOnCorrectType(),
UniqueFragmentNames::class => new UniqueFragmentNames(), UniqueFragmentNames::class => new UniqueFragmentNames(),
KnownFragmentNames::class => new KnownFragmentNames(), KnownFragmentNames::class => new KnownFragmentNames(),
NoUnusedFragments::class => new NoUnusedFragments(), NoUnusedFragments::class => new NoUnusedFragments(),
PossibleFragmentSpreads::class => new PossibleFragmentSpreads(), PossibleFragmentSpreads::class => new PossibleFragmentSpreads(),
NoFragmentCycles::class => new NoFragmentCycles(), NoFragmentCycles::class => new NoFragmentCycles(),
UniqueVariableNames::class => new UniqueVariableNames(), UniqueVariableNames::class => new UniqueVariableNames(),
NoUndefinedVariables::class => new NoUndefinedVariables(), NoUndefinedVariables::class => new NoUndefinedVariables(),
NoUnusedVariables::class => new NoUnusedVariables(), NoUnusedVariables::class => new NoUnusedVariables(),
KnownDirectives::class => new KnownDirectives(), KnownDirectives::class => new KnownDirectives(),
UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(), UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(),
KnownArgumentNames::class => new KnownArgumentNames(), KnownArgumentNames::class => new KnownArgumentNames(),
UniqueArgumentNames::class => new UniqueArgumentNames(), UniqueArgumentNames::class => new UniqueArgumentNames(),
ValuesOfCorrectType::class => new ValuesOfCorrectType(), ValuesOfCorrectType::class => new ValuesOfCorrectType(),
ProvidedNonNullArguments::class => new ProvidedNonNullArguments(), ProvidedNonNullArguments::class => new ProvidedNonNullArguments(),
VariablesDefaultValueAllowed::class => new VariablesDefaultValueAllowed(), VariablesDefaultValueAllowed::class => new VariablesDefaultValueAllowed(),
VariablesInAllowedPosition::class => new VariablesInAllowedPosition(), VariablesInAllowedPosition::class => new VariablesInAllowedPosition(),
OverlappingFieldsCanBeMerged::class => new OverlappingFieldsCanBeMerged(), 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() public static function securityRules()
{ {
@ -159,16 +167,36 @@ class DocumentValidator
// When custom security rule is required - it should be just added via DocumentValidator::addRule(); // When custom security rule is required - it should be just added via DocumentValidator::addRule();
// TODO: deprecate this // TODO: deprecate this
if (null === self::$securityRules) { if (self::$securityRules === null) {
self::$securityRules = [ self::$securityRules = [
DisableIntrospection::class => new DisableIntrospection(DisableIntrospection::DISABLED), // DEFAULT DISABLED DisableIntrospection::class => new DisableIntrospection(DisableIntrospection::DISABLED), // DEFAULT DISABLED
QueryDepth::class => new QueryDepth(QueryDepth::DISABLED), // default disabled QueryDepth::class => new QueryDepth(QueryDepth::DISABLED), // default disabled
QueryComplexity::class => new QueryComplexity(QueryComplexity::DISABLED), // default disabled QueryComplexity::class => new QueryComplexity(QueryComplexity::DISABLED), // default disabled
]; ];
} }
return self::$securityRules; 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 * Returns global validation rule by name. Standard rules are named by class name, so
* example usage for such rules: * example usage for such rules:
@ -177,7 +205,7 @@ class DocumentValidator
* *
* @api * @api
* @param string $name * @param string $name
* @return AbstractValidationRule * @return ValidationRule
*/ */
public static function getRule($name) public static function getRule($name)
{ {
@ -187,17 +215,17 @@ class DocumentValidator
return $rules[$name]; return $rules[$name];
} }
$name = "GraphQL\\Validator\\Rules\\$name"; $name = sprintf('GraphQL\\Validator\\Rules\\%s', $name);
return isset($rules[$name]) ? $rules[$name] : null ;
return $rules[$name] ?? null;
} }
/** /**
* Add rule to list of global validation rules * Add rule to list of global validation rules
* *
* @api * @api
* @param AbstractValidationRule $rule
*/ */
public static function addRule(AbstractValidationRule $rule) public static function addRule(ValidationRule $rule)
{ {
self::$rules[$rule->getName()] = $rule; self::$rules[$rule->getName()] = $rule;
} }
@ -205,7 +233,12 @@ class DocumentValidator
public static function isError($value) public static function isError($value)
{ {
return is_array($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); : ($value instanceof \Exception || $value instanceof \Throwable);
} }
@ -216,6 +249,7 @@ class DocumentValidator
} else { } else {
$arr[] = $items; $arr[] = $items;
} }
return $arr; return $arr;
} }
@ -230,33 +264,13 @@ class DocumentValidator
public static function isValidLiteralValue(Type $type, $valueNode) public static function isValidLiteralValue(Type $type, $valueNode)
{ {
$emptySchema = new Schema([]); $emptySchema = new Schema([]);
$emptyDoc = new DocumentNode(['definitions' => []]); $emptyDoc = new DocumentNode(['definitions' => []]);
$typeInfo = new TypeInfo($emptySchema, $type); $typeInfo = new TypeInfo($emptySchema, $type);
$context = new ValidationContext($emptySchema, $emptyDoc, $typeInfo); $context = new ValidationContext($emptySchema, $emptyDoc, $typeInfo);
$validator = new ValuesOfCorrectType(); $validator = new ValuesOfCorrectType();
$visitor = $validator->getVisitor($context); $visitor = $validator->getVisitor($context);
Visitor::visit($valueNode, Visitor::visitWithTypeInfo($typeInfo, $visitor)); 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(); return $context->getErrors();
} }
} }

View File

@ -1,26 +1,30 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
class CustomValidationRule extends AbstractValidationRule class CustomValidationRule extends ValidationRule
{ {
/** @var callable */
private $visitorFn; private $visitorFn;
public function __construct($name, callable $visitorFn) public function __construct($name, callable $visitorFn)
{ {
$this->name = $name; $this->name = $name;
$this->visitorFn = $visitorFn; $this->visitorFn = $visitorFn;
} }
/** /**
* @param ValidationContext $context
* @return Error[] * @return Error[]
*/ */
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
{ {
$fn = $this->visitorFn; $fn = $this->visitorFn;
return $fn($context); return $fn($context);
} }
} }

View File

@ -1,4 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
@ -6,9 +9,11 @@ use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NodeKind;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
class DisableIntrospection extends AbstractQuerySecurity class DisableIntrospection extends QuerySecurityRule
{ {
const ENABLED = 1; public const ENABLED = 1;
/** @var bool */
private $isEnabled; private $isEnabled;
public function __construct($enabled = self::ENABLED) public function __construct($enabled = self::ENABLED)
@ -21,7 +26,26 @@ class DisableIntrospection extends AbstractQuerySecurity
$this->isEnabled = $enabled; $this->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'; 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; 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]
));
}
}
]
);
}
} }

View File

@ -1,4 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
@ -9,6 +12,7 @@ use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\OperationDefinitionNode; use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\Visitor; use GraphQL\Language\Visitor;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function sprintf;
/** /**
* Executable definitions * Executable definitions
@ -16,32 +20,33 @@ use GraphQL\Validator\ValidationContext;
* A GraphQL document is only valid for execution if all definitions are either * A GraphQL document is only valid for execution if all definitions are either
* operation or fragment definitions. * operation or fragment definitions.
*/ */
class ExecutableDefinitions extends AbstractValidationRule class ExecutableDefinitions extends ValidationRule
{ {
static function nonExecutableDefinitionMessage($defName)
{
return "The \"$defName\" definition is not executable.";
}
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
{ {
return [ return [
NodeKind::DOCUMENT => function (DocumentNode $node) use ($context) { NodeKind::DOCUMENT => function (DocumentNode $node) use ($context) {
/** @var Node $definition */ /** @var Node $definition */
foreach ($node->definitions as $definition) { foreach ($node->definitions as $definition) {
if ( if ($definition instanceof OperationDefinitionNode ||
!$definition instanceof OperationDefinitionNode && $definition instanceof FragmentDefinitionNode
!$definition instanceof FragmentDefinitionNode
) { ) {
$context->reportError(new Error( continue;
self::nonExecutableDefinitionMessage($definition->name->value),
[$definition->name]
));
} }
$context->reportError(new Error(
self::nonExecutableDefinitionMessage($definition->name->value),
[$definition->name]
));
} }
return Visitor::skipNode(); return Visitor::skipNode();
} },
]; ];
} }
public static function nonExecutableDefinitionMessage($defName)
{
return sprintf('The "%s" definition is not executable.', $defName);
}
} }

View File

@ -1,4 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
@ -10,62 +13,56 @@ use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema; use GraphQL\Type\Schema;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function array_keys;
use function array_merge;
use function arsort;
use function sprintf;
class FieldsOnCorrectType extends AbstractValidationRule class FieldsOnCorrectType extends ValidationRule
{ {
static function undefinedFieldMessage($fieldName, $type, array $suggestedTypeNames, array $suggestedFieldNames)
{
$message = 'Cannot query field "' . $fieldName . '" on type "' . $type.'".';
if ($suggestedTypeNames) {
$suggestions = Utils::quotedOrList($suggestedTypeNames);
$message .= " Did you mean to use an inline fragment on $suggestions?";
} else if ($suggestedFieldNames) {
$suggestions = Utils::quotedOrList($suggestedFieldNames);
$message .= " Did you mean {$suggestions}?";
}
return $message;
}
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
{ {
return [ return [
NodeKind::FIELD => function(FieldNode $node) use ($context) { NodeKind::FIELD => function (FieldNode $node) use ($context) {
$type = $context->getParentType(); $type = $context->getParentType();
if ($type) { if (! $type) {
$fieldDef = $context->getFieldDef(); return;
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]
));
}
} }
}
$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 * suggest them, sorted by how often the type is referenced, starting
* with Interfaces. * with Interfaces.
* *
* @param Schema $schema * @param ObjectType|InterfaceType $type
* @param $type * @param string $fieldName
* @param string $fieldName * @return string[]
* @return array
*/ */
private function getSuggestedTypeNames(Schema $schema, $type, $fieldName) private function getSuggestedTypeNames(Schema $schema, $type, $fieldName)
{ {
if (Type::isAbstractType($type)) { if (Type::isAbstractType($type)) {
$suggestedObjectTypes = []; $suggestedObjectTypes = [];
$interfaceUsageCount = []; $interfaceUsageCount = [];
foreach($schema->getPossibleTypes($type) as $possibleType) { foreach ($schema->getPossibleTypes($type) as $possibleType) {
$fields = $possibleType->getFields(); $fields = $possibleType->getFields();
if (!isset($fields[$fieldName])) { if (! isset($fields[$fieldName])) {
continue; continue;
} }
// This object type defines this field. // This object type defines this field.
$suggestedObjectTypes[] = $possibleType->name; $suggestedObjectTypes[] = $possibleType->name;
foreach($possibleType->getInterfaces() as $possibleInterface) { foreach ($possibleType->getInterfaces() as $possibleInterface) {
$fields = $possibleInterface->getFields(); $fields = $possibleInterface->getFields();
if (!isset($fields[$fieldName])) { if (! isset($fields[$fieldName])) {
continue; continue;
} }
// This interface type defines this field. // This interface type defines this field.
$interfaceUsageCount[$possibleInterface->name] = $interfaceUsageCount[$possibleInterface->name] =
!isset($interfaceUsageCount[$possibleInterface->name]) ! isset($interfaceUsageCount[$possibleInterface->name])
? 0 ? 0
: $interfaceUsageCount[$possibleInterface->name] + 1; : $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 * For the field name provided, determine if there are any similar field names
* that may be the result of a typo. * that may be the result of a typo.
* *
* @param Schema $schema * @param ObjectType|InterfaceType $type
* @param $type * @param string $fieldName
* @param string $fieldName
* @return array|string[] * @return array|string[]
*/ */
private function getSuggestedFieldNames(Schema $schema, $type, $fieldName) private function getSuggestedFieldNames(Schema $schema, $type, $fieldName)
{ {
if ($type instanceof ObjectType || $type instanceof InterfaceType) { if ($type instanceof ObjectType || $type instanceof InterfaceType) {
$possibleFieldNames = array_keys($type->getFields()); $possibleFieldNames = array_keys($type->getFields());
return Utils::suggestionList($fieldName, $possibleFieldNames); return Utils::suggestionList($fieldName, $possibleFieldNames);
} }
// Otherwise, must be a Union type, which does not define fields. // Otherwise, must be a Union type, which does not define fields.
return []; 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;
}
} }

View File

@ -1,4 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
@ -9,43 +12,53 @@ use GraphQL\Language\Printer;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Utils\TypeInfo; use GraphQL\Utils\TypeInfo;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function sprintf;
class FragmentsOnCompositeTypes extends AbstractValidationRule class FragmentsOnCompositeTypes extends ValidationRule
{ {
static function inlineFragmentOnNonCompositeErrorMessage($type)
{
return "Fragment cannot condition on non composite type \"$type\".";
}
static function fragmentOnNonCompositeErrorMessage($fragName, $type)
{
return "Fragment \"$fragName\" cannot condition on non composite type \"$type\".";
}
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
{ {
return [ return [
NodeKind::INLINE_FRAGMENT => function(InlineFragmentNode $node) use ($context) { NodeKind::INLINE_FRAGMENT => function (InlineFragmentNode $node) use ($context) {
if ($node->typeCondition) { if (! $node->typeCondition) {
$type = TypeInfo::typeFromAST($context->getSchema(), $node->typeCondition); return;
if ($type && !Type::isCompositeType($type)) {
$context->reportError(new Error(
static::inlineFragmentOnNonCompositeErrorMessage($type),
[$node->typeCondition]
));
}
} }
$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); $type = TypeInfo::typeFromAST($context->getSchema(), $node->typeCondition);
if ($type && !Type::isCompositeType($type)) { if (! $type || Type::isCompositeType($type)) {
$context->reportError(new Error( return;
static::fragmentOnNonCompositeErrorMessage($node->name->value, Printer::doPrint($node->typeCondition)),
[$node->typeCondition]
));
} }
}
$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);
}
} }

View File

@ -1,4 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
@ -6,6 +9,9 @@ use GraphQL\Language\AST\ArgumentNode;
use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NodeKind;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function array_map;
use function count;
use function sprintf;
/** /**
* Known argument names * Known argument names
@ -13,68 +19,88 @@ use GraphQL\Validator\ValidationContext;
* A GraphQL field is only valid if all supplied arguments are defined by * A GraphQL field is only valid if all supplied arguments are defined by
* that field. * that field.
*/ */
class KnownArgumentNames extends AbstractValidationRule class KnownArgumentNames extends ValidationRule
{ {
public static function unknownArgMessage($argName, $fieldName, $typeName, array $suggestedArgs)
{
$message = "Unknown argument \"$argName\" on field \"$fieldName\" of type \"$typeName\".";
if ($suggestedArgs) {
$message .= ' Did you mean ' . Utils::quotedOrList($suggestedArgs) . '?';
}
return $message;
}
public static function unknownDirectiveArgMessage($argName, $directiveName, array $suggestedArgs)
{
$message = "Unknown argument \"$argName\" on directive \"@$directiveName\".";
if ($suggestedArgs) {
$message .= ' Did you mean ' . Utils::quotedOrList($suggestedArgs) . '?';
}
return $message;
}
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
{ {
return [ return [
NodeKind::ARGUMENT => function(ArgumentNode $node, $key, $parent, $path, $ancestors) use ($context) { NodeKind::ARGUMENT => function (ArgumentNode $node, $key, $parent, $path, $ancestors) use ($context) {
$argDef = $context->getArgument(); $argDef = $context->getArgument();
if (!$argDef) { if ($argDef !== null) {
$argumentOf = $ancestors[count($ancestors) - 1]; return;
if ($argumentOf->kind === NodeKind::FIELD) { }
$fieldDef = $context->getFieldDef();
$parentType = $context->getParentType(); $argumentOf = $ancestors[count($ancestors) - 1];
if ($fieldDef && $parentType) { if ($argumentOf->kind === NodeKind::FIELD) {
$context->reportError(new Error( $fieldDef = $context->getFieldDef();
self::unknownArgMessage( $parentType = $context->getParentType();
if ($fieldDef && $parentType) {
$context->reportError(new Error(
self::unknownArgMessage(
$node->name->value,
$fieldDef->name,
$parentType->name,
Utils::suggestionList(
$node->name->value, $node->name->value,
$fieldDef->name, array_map(
$parentType->name, function ($arg) {
Utils::suggestionList( return $arg->name;
$node->name->value, },
array_map(function ($arg) { return $arg->name; }, $fieldDef->args) $fieldDef->args
) )
), )
[$node] ),
)); [$node]
} ));
} else if ($argumentOf->kind === NodeKind::DIRECTIVE) { }
$directive = $context->getDirective(); } elseif ($argumentOf->kind === NodeKind::DIRECTIVE) {
if ($directive) { $directive = $context->getDirective();
$context->reportError(new Error( if ($directive) {
self::unknownDirectiveArgMessage( $context->reportError(new Error(
self::unknownDirectiveArgMessage(
$node->name->value,
$directive->name,
Utils::suggestionList(
$node->name->value, $node->name->value,
$directive->name, array_map(
Utils::suggestionList( function ($arg) {
$node->name->value, return $arg->name;
array_map(function ($arg) { return $arg->name; }, $directive->args) },
$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;
}
} }

View File

@ -1,4 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
@ -7,19 +10,12 @@ use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\DirectiveLocation; use GraphQL\Language\DirectiveLocation;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function count;
use function in_array;
use function sprintf;
class KnownDirectives extends AbstractValidationRule class KnownDirectives extends ValidationRule
{ {
static function unknownDirectiveMessage($directiveName)
{
return "Unknown directive \"$directiveName\".";
}
static function misplacedDirectiveMessage($directiveName, $location)
{
return "Directive \"$directiveName\" may not be used on \"$location\".";
}
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
{ {
return [ return [
@ -32,39 +28,51 @@ class KnownDirectives extends AbstractValidationRule
} }
} }
if (!$directiveDef) { if (! $directiveDef) {
$context->reportError(new Error( $context->reportError(new Error(
self::unknownDirectiveMessage($node->name->value), self::unknownDirectiveMessage($node->name->value),
[$node] [$node]
)); ));
return; return;
} }
$candidateLocation = $this->getDirectiveLocationForASTPath($ancestors); $candidateLocation = $this->getDirectiveLocationForASTPath($ancestors);
if (!$candidateLocation) { if (! $candidateLocation) {
$context->reportError(new Error( $context->reportError(new Error(
self::misplacedDirectiveMessage($node->name->value, $node->type), self::misplacedDirectiveMessage($node->name->value, $node->type),
[$node] [$node]
)); ));
} else if (!in_array($candidateLocation, $directiveDef->locations)) { } elseif (! in_array($candidateLocation, $directiveDef->locations)) {
$context->reportError(new Error( $context->reportError(new Error(
self::misplacedDirectiveMessage($node->name->value, $candidateLocation), 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) private function getDirectiveLocationForASTPath(array $ancestors)
{ {
$appliedTo = $ancestors[count($ancestors) - 1]; $appliedTo = $ancestors[count($ancestors) - 1];
switch ($appliedTo->kind) { switch ($appliedTo->kind) {
case NodeKind::OPERATION_DEFINITION: case NodeKind::OPERATION_DEFINITION:
switch ($appliedTo->operation) { switch ($appliedTo->operation) {
case 'query': return DirectiveLocation::QUERY; case 'query':
case 'mutation': return DirectiveLocation::MUTATION; return DirectiveLocation::QUERY;
case 'subscription': return DirectiveLocation::SUBSCRIPTION; case 'mutation':
return DirectiveLocation::MUTATION;
case 'subscription':
return DirectiveLocation::SUBSCRIPTION;
} }
break; break;
case NodeKind::FIELD: case NodeKind::FIELD:
@ -101,9 +109,15 @@ class KnownDirectives extends AbstractValidationRule
return DirectiveLocation::INPUT_OBJECT; return DirectiveLocation::INPUT_OBJECT;
case NodeKind::INPUT_VALUE_DEFINITION: case NodeKind::INPUT_VALUE_DEFINITION:
$parentNode = $ancestors[count($ancestors) - 3]; $parentNode = $ancestors[count($ancestors) - 3];
return $parentNode instanceof InputObjectTypeDefinitionNode return $parentNode instanceof InputObjectTypeDefinitionNode
? DirectiveLocation::INPUT_FIELD_DEFINITION ? DirectiveLocation::INPUT_FIELD_DEFINITION
: DirectiveLocation::ARGUMENT_DEFINITION; : DirectiveLocation::ARGUMENT_DEFINITION;
} }
} }
public static function misplacedDirectiveMessage($directiveName, $location)
{
return sprintf('Directive "%s" may not be used on "%s".', $directiveName, $location);
}
} }

View File

@ -1,31 +1,40 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Language\AST\FragmentSpreadNode; use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NodeKind;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function sprintf;
class KnownFragmentNames extends AbstractValidationRule class KnownFragmentNames extends ValidationRule
{ {
static function unknownFragmentMessage($fragName)
{
return "Unknown fragment \"$fragName\".";
}
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
{ {
return [ return [
NodeKind::FRAGMENT_SPREAD => function(FragmentSpreadNode $node) use ($context) { NodeKind::FRAGMENT_SPREAD => function (FragmentSpreadNode $node) use ($context) {
$fragmentName = $node->name->value; $fragmentName = $node->name->value;
$fragment = $context->getFragment($fragmentName); $fragment = $context->getFragment($fragmentName);
if (!$fragment) { if ($fragment) {
$context->reportError(new Error( return;
self::unknownFragmentMessage($fragmentName),
[$node->name]
));
} }
}
$context->reportError(new Error(
self::unknownFragmentMessage($fragmentName),
[$node->name]
));
},
]; ];
} }
/**
* @param string $fragName
*/
public static function unknownFragmentMessage($fragName)
{
return sprintf('Unknown fragment "%s".', $fragName);
}
} }

View File

@ -1,4 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
@ -7,6 +10,8 @@ use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Visitor; use GraphQL\Language\Visitor;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function array_keys;
use function sprintf;
/** /**
* Known type names * Known type names
@ -14,43 +19,54 @@ use GraphQL\Validator\ValidationContext;
* A GraphQL document is only valid if referenced types (specifically * A GraphQL document is only valid if referenced types (specifically
* variable definitions and fragment conditions) are defined by the type schema. * variable definitions and fragment conditions) are defined by the type schema.
*/ */
class KnownTypeNames extends AbstractValidationRule class KnownTypeNames extends ValidationRule
{ {
static function unknownTypeMessage($type, array $suggestedTypes)
{
$message = "Unknown type \"$type\".";
if ($suggestedTypes) {
$suggestions = Utils::quotedOrList($suggestedTypes);
$message .= " Did you mean $suggestions?";
}
return $message;
}
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
{ {
$skip = function() { return Visitor::skipNode(); }; $skip = function () {
return Visitor::skipNode();
};
return [ return [
// TODO: when validating IDL, re-enable these. Experimental version does not // TODO: when validating IDL, re-enable these. Experimental version does not
// add unreferenced types, resulting in false-positive errors. Squelched // add unreferenced types, resulting in false-positive errors. Squelched
// errors for now. // errors for now.
NodeKind::OBJECT_TYPE_DEFINITION => $skip, NodeKind::OBJECT_TYPE_DEFINITION => $skip,
NodeKind::INTERFACE_TYPE_DEFINITION => $skip, NodeKind::INTERFACE_TYPE_DEFINITION => $skip,
NodeKind::UNION_TYPE_DEFINITION => $skip, NodeKind::UNION_TYPE_DEFINITION => $skip,
NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $skip, NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $skip,
NodeKind::NAMED_TYPE => function(NamedTypeNode $node) use ($context) { NodeKind::NAMED_TYPE => function (NamedTypeNode $node) use ($context) {
$schema = $context->getSchema(); $schema = $context->getSchema();
$typeName = $node->name->value; $typeName = $node->name->value;
$type = $schema->getType($typeName); $type = $schema->getType($typeName);
if (!$type) { if ($type !== null) {
$context->reportError(new Error( return;
self::unknownTypeMessage(
$typeName,
Utils::suggestionList($typeName, array_keys($schema->getTypeMap()))
), [$node])
);
} }
}
$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;
}
} }

View File

@ -1,4 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
@ -7,6 +10,7 @@ use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\OperationDefinitionNode; use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function count;
/** /**
* Lone anonymous operation * Lone anonymous operation
@ -14,33 +18,40 @@ use GraphQL\Validator\ValidationContext;
* A GraphQL document is only valid if when it contains an anonymous operation * A GraphQL document is only valid if when it contains an anonymous operation
* (the query short-hand) that it contains only that one operation definition. * (the query short-hand) that it contains only that one operation definition.
*/ */
class LoneAnonymousOperation extends AbstractValidationRule class LoneAnonymousOperation extends ValidationRule
{ {
static function anonOperationNotAloneMessage()
{
return 'This anonymous operation must be the only defined operation.';
}
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
{ {
$operationCount = 0; $operationCount = 0;
return [ return [
NodeKind::DOCUMENT => function(DocumentNode $node) use (&$operationCount) { NodeKind::DOCUMENT => function (DocumentNode $node) use (&$operationCount) {
$tmp = Utils::filter( $tmp = Utils::filter(
$node->definitions, $node->definitions,
function ($definition) { function ($definition) {
return $definition->kind === NodeKind::OPERATION_DEFINITION; return $definition->kind === NodeKind::OPERATION_DEFINITION;
} }
); );
$operationCount = count($tmp); $operationCount = count($tmp);
}, },
NodeKind::OPERATION_DEFINITION => function(OperationDefinitionNode $node) use (&$operationCount, $context) { NodeKind::OPERATION_DEFINITION => function (OperationDefinitionNode $node) use (
if (!$node->name && $operationCount > 1) { &$operationCount,
$context->reportError( $context
new Error(self::anonOperationNotAloneMessage(), [$node]) ) {
); 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.';
}
} }

View File

@ -1,25 +1,33 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Language\AST\FragmentDefinitionNode; use GraphQL\Language\AST\FragmentDefinitionNode;
use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Visitor; use GraphQL\Language\Visitor;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function array_merge;
use function array_pop;
use function array_slice;
use function count;
use function implode;
use function is_array;
use function sprintf;
class NoFragmentCycles extends AbstractValidationRule class NoFragmentCycles extends ValidationRule
{ {
static function cycleErrorMessage($fragName, array $spreadNames = []) /** @var bool[] */
{
$via = !empty($spreadNames) ? ' via ' . implode(', ', $spreadNames) : '';
return "Cannot spread fragment \"$fragName\" within itself$via.";
}
public $visitedFrags; public $visitedFrags;
/** @var FragmentSpreadNode[] */
public $spreadPath; public $spreadPath;
/** @var (int|null)[] */
public $spreadPathIndexByName; public $spreadPathIndexByName;
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
@ -38,18 +46,19 @@ class NoFragmentCycles extends AbstractValidationRule
NodeKind::OPERATION_DEFINITION => function () { NodeKind::OPERATION_DEFINITION => function () {
return Visitor::skipNode(); return Visitor::skipNode();
}, },
NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) { NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) {
if (!isset($this->visitedFrags[$node->name->value])) { if (! isset($this->visitedFrags[$node->name->value])) {
$this->detectCycleRecursive($node, $context); $this->detectCycleRecursive($node, $context);
} }
return Visitor::skipNode(); return Visitor::skipNode();
} },
]; ];
} }
private function detectCycleRecursive(FragmentDefinitionNode $fragment, ValidationContext $context) private function detectCycleRecursive(FragmentDefinitionNode $fragment, ValidationContext $context)
{ {
$fragmentName = $fragment->name->value; $fragmentName = $fragment->name->value;
$this->visitedFrags[$fragmentName] = true; $this->visitedFrags[$fragmentName] = true;
$spreadNodes = $context->getFragmentSpreads($fragment); $spreadNodes = $context->getFragmentSpreads($fragment);
@ -63,7 +72,7 @@ class NoFragmentCycles extends AbstractValidationRule
for ($i = 0; $i < count($spreadNodes); $i++) { for ($i = 0; $i < count($spreadNodes); $i++) {
$spreadNode = $spreadNodes[$i]; $spreadNode = $spreadNodes[$i];
$spreadName = $spreadNode->name->value; $spreadName = $spreadNode->name->value;
$cycleIndex = isset($this->spreadPathIndexByName[$spreadName]) ? $this->spreadPathIndexByName[$spreadName] : null; $cycleIndex = $this->spreadPathIndexByName[$spreadName] ?? null;
if ($cycleIndex === null) { if ($cycleIndex === null) {
$this->spreadPath[] = $spreadNode; $this->spreadPath[] = $spreadNode;
@ -76,7 +85,7 @@ class NoFragmentCycles extends AbstractValidationRule
array_pop($this->spreadPath); array_pop($this->spreadPath);
} else { } else {
$cyclePath = array_slice($this->spreadPath, $cycleIndex); $cyclePath = array_slice($this->spreadPath, $cycleIndex);
$nodes = $cyclePath; $nodes = $cyclePath;
if (is_array($spreadNode)) { if (is_array($spreadNode)) {
$nodes = array_merge($nodes, $spreadNode); $nodes = array_merge($nodes, $spreadNode);
@ -87,9 +96,12 @@ class NoFragmentCycles extends AbstractValidationRule
$context->reportError(new Error( $context->reportError(new Error(
self::cycleErrorMessage( self::cycleErrorMessage(
$spreadName, $spreadName,
Utils::map($cyclePath, function ($s) { Utils::map(
return $s->name->value; $cyclePath,
}) function ($s) {
return $s->name->value;
}
)
), ),
$nodes $nodes
)); ));
@ -98,4 +110,16 @@ class NoFragmentCycles extends AbstractValidationRule
$this->spreadPathIndexByName[$fragmentName] = null; $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) : ''
);
}
} }

View File

@ -1,4 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
@ -6,55 +9,56 @@ use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\OperationDefinitionNode; use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\VariableDefinitionNode; use GraphQL\Language\AST\VariableDefinitionNode;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function sprintf;
/** /**
* Class NoUndefinedVariables * Class NoUndefinedVariables
* *
* A GraphQL operation is only valid if all variables encountered, both directly * A GraphQL operation is only valid if all variables encountered, both directly
* and via fragment spreads, are defined by that operation. * and via fragment spreads, are defined by that operation.
*
* @package GraphQL\Validator\Rules
*/ */
class NoUndefinedVariables extends AbstractValidationRule class NoUndefinedVariables extends ValidationRule
{ {
static function undefinedVarMessage($varName, $opName = null)
{
return $opName
? "Variable \"$$varName\" is not defined by operation \"$opName\"."
: "Variable \"$$varName\" is not defined.";
}
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
{ {
$variableNameDefined = []; $variableNameDefined = [];
return [ return [
NodeKind::OPERATION_DEFINITION => [ NodeKind::OPERATION_DEFINITION => [
'enter' => function() use (&$variableNameDefined) { 'enter' => function () use (&$variableNameDefined) {
$variableNameDefined = []; $variableNameDefined = [];
}, },
'leave' => function(OperationDefinitionNode $operation) use (&$variableNameDefined, $context) { 'leave' => function (OperationDefinitionNode $operation) use (&$variableNameDefined, $context) {
$usages = $context->getRecursiveVariableUsages($operation); $usages = $context->getRecursiveVariableUsages($operation);
foreach ($usages as $usage) { foreach ($usages as $usage) {
$node = $usage['node']; $node = $usage['node'];
$varName = $node->name->value; $varName = $node->name->value;
if (empty($variableNameDefined[$varName])) { if (! empty($variableNameDefined[$varName])) {
$context->reportError(new Error( continue;
self::undefinedVarMessage(
$varName,
$operation->name ? $operation->name->value : null
),
[ $node, $operation ]
));
} }
$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; $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);
}
} }

View File

@ -1,39 +1,43 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Language\AST\FragmentDefinitionNode; use GraphQL\Language\AST\FragmentDefinitionNode;
use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\Visitor; use GraphQL\Language\Visitor;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function sprintf;
class NoUnusedFragments extends AbstractValidationRule class NoUnusedFragments extends ValidationRule
{ {
static function unusedFragMessage($fragName) /** @var OperationDefinitionNode[] */
{
return "Fragment \"$fragName\" is never used.";
}
public $operationDefs; public $operationDefs;
/** @var FragmentDefinitionNode[] */
public $fragmentDefs; public $fragmentDefs;
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
{ {
$this->operationDefs = []; $this->operationDefs = [];
$this->fragmentDefs = []; $this->fragmentDefs = [];
return [ return [
NodeKind::OPERATION_DEFINITION => function($node) { NodeKind::OPERATION_DEFINITION => function ($node) {
$this->operationDefs[] = $node; $this->operationDefs[] = $node;
return Visitor::skipNode(); return Visitor::skipNode();
}, },
NodeKind::FRAGMENT_DEFINITION => function(FragmentDefinitionNode $def) { NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $def) {
$this->fragmentDefs[] = $def; $this->fragmentDefs[] = $def;
return Visitor::skipNode(); return Visitor::skipNode();
}, },
NodeKind::DOCUMENT => [ NodeKind::DOCUMENT => [
'leave' => function() use ($context) { 'leave' => function () use ($context) {
$fragmentNameUsed = []; $fragmentNameUsed = [];
foreach ($this->operationDefs as $operation) { foreach ($this->operationDefs as $operation) {
@ -44,15 +48,22 @@ class NoUnusedFragments extends AbstractValidationRule
foreach ($this->fragmentDefs as $fragmentDef) { foreach ($this->fragmentDefs as $fragmentDef) {
$fragName = $fragmentDef->name->value; $fragName = $fragmentDef->name->value;
if (empty($fragmentNameUsed[$fragName])) { if (! empty($fragmentNameUsed[$fragName])) {
$context->reportError(new Error( continue;
self::unusedFragMessage($fragName),
[ $fragmentDef ]
));
} }
$context->reportError(new Error(
self::unusedFragMessage($fragName),
[$fragmentDef]
));
} }
} },
] ],
]; ];
} }
public static function unusedFragMessage($fragName)
{
return sprintf('Fragment "%s" is never used.', $fragName);
}
} }

View File

@ -1,20 +1,19 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\OperationDefinitionNode; use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\VariableDefinitionNode;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function sprintf;
class NoUnusedVariables extends AbstractValidationRule class NoUnusedVariables extends ValidationRule
{ {
static function unusedVariableMessage($varName, $opName = null) /** @var VariableDefinitionNode[] */
{
return $opName
? "Variable \"$$varName\" is never used in operation \"$opName\"."
: "Variable \"$$varName\" is never used.";
}
public $variableDefs; public $variableDefs;
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
@ -23,34 +22,43 @@ class NoUnusedVariables extends AbstractValidationRule
return [ return [
NodeKind::OPERATION_DEFINITION => [ NodeKind::OPERATION_DEFINITION => [
'enter' => function() { 'enter' => function () {
$this->variableDefs = []; $this->variableDefs = [];
}, },
'leave' => function(OperationDefinitionNode $operation) use ($context) { 'leave' => function (OperationDefinitionNode $operation) use ($context) {
$variableNameUsed = []; $variableNameUsed = [];
$usages = $context->getRecursiveVariableUsages($operation); $usages = $context->getRecursiveVariableUsages($operation);
$opName = $operation->name ? $operation->name->value : null; $opName = $operation->name ? $operation->name->value : null;
foreach ($usages as $usage) { foreach ($usages as $usage) {
$node = $usage['node']; $node = $usage['node'];
$variableNameUsed[$node->name->value] = true; $variableNameUsed[$node->name->value] = true;
} }
foreach ($this->variableDefs as $variableDef) { foreach ($this->variableDefs as $variableDef) {
$variableName = $variableDef->variable->name->value; $variableName = $variableDef->variable->name->value;
if (empty($variableNameUsed[$variableName])) { if (! empty($variableNameUsed[$variableName])) {
$context->reportError(new Error( continue;
self::unusedVariableMessage($variableName, $opName),
[$variableDef]
));
} }
$context->reportError(new Error(
self::unusedVariableMessage($variableName, $opName),
[$variableDef]
));
} }
} },
], ],
NodeKind::VARIABLE_DEFINITION => function($def) { NodeKind::VARIABLE_DEFINITION => function ($def) {
$this->variableDefs[] = $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);
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,72 +1,61 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Language\AST\FragmentSpreadNode; use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\InlineFragmentNode; use GraphQL\Language\AST\InlineFragmentNode;
use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NodeKind;
use GraphQL\Type\Schema;
use GraphQL\Type\Definition\AbstractType; use GraphQL\Type\Definition\AbstractType;
use GraphQL\Type\Definition\CompositeType; use GraphQL\Type\Definition\CompositeType;
use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Definition\UnionType;
use GraphQL\Validator\ValidationContext; use GraphQL\Type\Schema;
use GraphQL\Utils\TypeInfo; use GraphQL\Utils\TypeInfo;
use GraphQL\Validator\ValidationContext;
use function sprintf;
class PossibleFragmentSpreads extends AbstractValidationRule class PossibleFragmentSpreads extends ValidationRule
{ {
static function typeIncompatibleSpreadMessage($fragName, $parentType, $fragType)
{
return "Fragment \"$fragName\" cannot be spread here as objects of type \"$parentType\" can never be of type \"$fragType\".";
}
static function typeIncompatibleAnonSpreadMessage($parentType, $fragType)
{
return "Fragment cannot be spread here as objects of type \"$parentType\" can never be of type \"$fragType\".";
}
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
{ {
return [ return [
NodeKind::INLINE_FRAGMENT => function(InlineFragmentNode $node) use ($context) { NodeKind::INLINE_FRAGMENT => function (InlineFragmentNode $node) use ($context) {
$fragType = $context->getType(); $fragType = $context->getType();
$parentType = $context->getParentType(); $parentType = $context->getParentType();
if ($fragType instanceof CompositeType && if (! ($fragType instanceof CompositeType) ||
$parentType instanceof CompositeType && ! ($parentType instanceof CompositeType) ||
!$this->doTypesOverlap($context->getSchema(), $fragType, $parentType)) { $this->doTypesOverlap($context->getSchema(), $fragType, $parentType)) {
$context->reportError(new Error( return;
self::typeIncompatibleAnonSpreadMessage($parentType, $fragType),
[$node]
));
} }
$context->reportError(new Error(
self::typeIncompatibleAnonSpreadMessage($parentType, $fragType),
[$node]
));
}, },
NodeKind::FRAGMENT_SPREAD => function(FragmentSpreadNode $node) use ($context) { NodeKind::FRAGMENT_SPREAD => function (FragmentSpreadNode $node) use ($context) {
$fragName = $node->name->value; $fragName = $node->name->value;
$fragType = $this->getFragmentType($context, $fragName); $fragType = $this->getFragmentType($context, $fragName);
$parentType = $context->getParentType(); $parentType = $context->getParentType();
if ($fragType && $parentType && !$this->doTypesOverlap($context->getSchema(), $fragType, $parentType)) { if (! $fragType ||
$context->reportError(new Error( ! $parentType ||
self::typeIncompatibleSpreadMessage($fragName, $parentType, $fragType), $this->doTypesOverlap($context->getSchema(), $fragType, $parentType)
[$node] ) {
)); return;
} }
}
];
}
private function getFragmentType(ValidationContext $context, $name) $context->reportError(new Error(
{ self::typeIncompatibleSpreadMessage($fragName, $parentType, $fragType),
$frag = $context->getFragment($name); [$node]
if ($frag) { ));
$type = TypeInfo::typeFromAST($context->getSchema(), $frag->typeCondition); },
if ($type instanceof CompositeType) { ];
return $type;
}
}
return null;
} }
private function doTypesOverlap(Schema $schema, CompositeType $fragType, CompositeType $parentType) private function doTypesOverlap(Schema $schema, CompositeType $fragType, CompositeType $parentType)
@ -136,4 +125,36 @@ class PossibleFragmentSpreads extends AbstractValidationRule
return false; 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
);
}
} }

View File

@ -1,4 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
@ -8,27 +11,18 @@ use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Visitor; use GraphQL\Language\Visitor;
use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\NonNull;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function sprintf;
class ProvidedNonNullArguments extends AbstractValidationRule class ProvidedNonNullArguments extends ValidationRule
{ {
static function missingFieldArgMessage($fieldName, $argName, $type)
{
return "Field \"$fieldName\" argument \"$argName\" of type \"$type\" is required but not provided.";
}
static function missingDirectiveArgMessage($directiveName, $argName, $type)
{
return "Directive \"@$directiveName\" argument \"$argName\" of type \"$type\" is required but not provided.";
}
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
{ {
return [ return [
NodeKind::FIELD => [ NodeKind::FIELD => [
'leave' => function(FieldNode $fieldNode) use ($context) { 'leave' => function (FieldNode $fieldNode) use ($context) {
$fieldDef = $context->getFieldDef(); $fieldDef = $context->getFieldDef();
if (!$fieldDef) { if (! $fieldDef) {
return Visitor::skipNode(); return Visitor::skipNode();
} }
$argNodes = $fieldNode->arguments ?: []; $argNodes = $fieldNode->arguments ?: [];
@ -38,39 +32,67 @@ class ProvidedNonNullArguments extends AbstractValidationRule
$argNodeMap[$argNode->name->value] = $argNodes; $argNodeMap[$argNode->name->value] = $argNodes;
} }
foreach ($fieldDef->args as $argDef) { foreach ($fieldDef->args as $argDef) {
$argNode = isset($argNodeMap[$argDef->name]) ? $argNodeMap[$argDef->name] : null; $argNode = $argNodeMap[$argDef->name] ?? null;
if (!$argNode && $argDef->getType() instanceof NonNull) { if ($argNode || ! ($argDef->getType() instanceof NonNull)) {
$context->reportError(new Error( continue;
self::missingFieldArgMessage($fieldNode->name->value, $argDef->name, $argDef->getType()),
[$fieldNode]
));
} }
$context->reportError(new Error(
self::missingFieldArgMessage($fieldNode->name->value, $argDef->name, $argDef->getType()),
[$fieldNode]
));
} }
} },
], ],
NodeKind::DIRECTIVE => [ NodeKind::DIRECTIVE => [
'leave' => function(DirectiveNode $directiveNode) use ($context) { 'leave' => function (DirectiveNode $directiveNode) use ($context) {
$directiveDef = $context->getDirective(); $directiveDef = $context->getDirective();
if (!$directiveDef) { if (! $directiveDef) {
return Visitor::skipNode(); return Visitor::skipNode();
} }
$argNodes = $directiveNode->arguments ?: []; $argNodes = $directiveNode->arguments ?: [];
$argNodeMap = []; $argNodeMap = [];
foreach ($argNodes as $argNode) { foreach ($argNodes as $argNode) {
$argNodeMap[$argNode->name->value] = $argNodes; $argNodeMap[$argNode->name->value] = $argNodes;
} }
foreach ($directiveDef->args as $argDef) { foreach ($directiveDef->args as $argDef) {
$argNode = isset($argNodeMap[$argDef->name]) ? $argNodeMap[$argDef->name] : null; $argNode = $argNodeMap[$argDef->name] ?? null;
if (!$argNode && $argDef->getType() instanceof NonNull) { if ($argNode || ! ($argDef->getType() instanceof NonNull)) {
$context->reportError(new Error( continue;
self::missingDirectiveArgMessage($directiveNode->name->value, $argDef->name, $argDef->getType()),
[$directiveNode]
));
} }
$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
);
}
} }

View File

@ -1,4 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
@ -14,20 +17,27 @@ use GraphQL\Language\Visitor;
use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function array_map;
use function call_user_func_array;
use function implode;
use function method_exists;
use function sprintf;
class QueryComplexity extends AbstractQuerySecurity class QueryComplexity extends QuerySecurityRule
{ {
/** @var int */
private $maxQueryComplexity; private $maxQueryComplexity;
/** @var mixed[]|null */
private $rawVariableValues = []; private $rawVariableValues = [];
/** @var \ArrayObject */
private $variableDefs; private $variableDefs;
/** @var \ArrayObject */
private $fieldNodeAndDefs; private $fieldNodeAndDefs;
/** /** @var ValidationContext */
* @var ValidationContext
*/
private $context; private $context;
public function __construct($maxQueryComplexity) public function __construct($maxQueryComplexity)
@ -35,50 +45,18 @@ class QueryComplexity extends AbstractQuerySecurity
$this->setMaxQueryComplexity($maxQueryComplexity); $this->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) public function getVisitor(ValidationContext $context)
{ {
$this->context = $context; $this->context = $context;
$this->variableDefs = new \ArrayObject(); $this->variableDefs = new \ArrayObject();
$this->fieldNodeAndDefs = new \ArrayObject(); $this->fieldNodeAndDefs = new \ArrayObject();
$complexity = 0; $complexity = 0;
return $this->invokeIfNeeded( return $this->invokeIfNeeded(
$context, $context,
[ [
NodeKind::SELECTION_SET => function (SelectionSetNode $selectionSet) use ($context) { NodeKind::SELECTION_SET => function (SelectionSetNode $selectionSet) use ($context) {
$this->fieldNodeAndDefs = $this->collectFieldASTsAndDefs( $this->fieldNodeAndDefs = $this->collectFieldASTsAndDefs(
$context, $context,
$context->getParentType(), $context->getParentType(),
@ -87,23 +65,31 @@ class QueryComplexity extends AbstractQuerySecurity
$this->fieldNodeAndDefs $this->fieldNodeAndDefs
); );
}, },
NodeKind::VARIABLE_DEFINITION => function ($def) { NodeKind::VARIABLE_DEFINITION => function ($def) {
$this->variableDefs[] = $def; $this->variableDefs[] = $def;
return Visitor::skipNode(); return Visitor::skipNode();
}, },
NodeKind::OPERATION_DEFINITION => [ NodeKind::OPERATION_DEFINITION => [
'leave' => function (OperationDefinitionNode $operationDefinition) use ($context, &$complexity) { 'leave' => function (OperationDefinitionNode $operationDefinition) use ($context, &$complexity) {
$errors = $context->getErrors(); $errors = $context->getErrors();
if (empty($errors)) { if (! empty($errors)) {
$complexity = $this->fieldComplexity($operationDefinition, $complexity); return;
if ($complexity > $this->getMaxQueryComplexity()) {
$context->reportError(
new Error($this->maxQueryComplexityErrorMessage($this->getMaxQueryComplexity(), $complexity))
);
}
} }
$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) { switch ($node->kind) {
case NodeKind::FIELD: case NodeKind::FIELD:
/* @var FieldNode $node */ /** @var FieldNode $node */
// default values // default values
$args = []; $args = [];
$complexityFn = FieldDefinition::DEFAULT_COMPLEXITY_FN; $complexityFn = FieldDefinition::DEFAULT_COMPLEXITY_FN;
// calculate children complexity if needed // calculate children complexity if needed
@ -139,7 +125,7 @@ class QueryComplexity extends AbstractQuerySecurity
} }
$astFieldInfo = $this->astFieldInfo($node); $astFieldInfo = $this->astFieldInfo($node);
$fieldDef = $astFieldInfo[1]; $fieldDef = $astFieldInfo[1];
if ($fieldDef instanceof FieldDefinition) { if ($fieldDef instanceof FieldDefinition) {
if ($this->directiveExcludesField($node)) { if ($this->directiveExcludesField($node)) {
@ -157,7 +143,7 @@ class QueryComplexity extends AbstractQuerySecurity
break; break;
case NodeKind::INLINE_FRAGMENT: case NodeKind::INLINE_FRAGMENT:
/* @var InlineFragmentNode $node */ /** @var InlineFragmentNode $node */
// node has children? // node has children?
if (isset($node->selectionSet)) { if (isset($node->selectionSet)) {
$complexity = $this->fieldComplexity($node, $complexity); $complexity = $this->fieldComplexity($node, $complexity);
@ -165,10 +151,10 @@ class QueryComplexity extends AbstractQuerySecurity
break; break;
case NodeKind::FRAGMENT_SPREAD: case NodeKind::FRAGMENT_SPREAD:
/* @var FragmentSpreadNode $node */ /** @var FragmentSpreadNode $node */
$fragment = $this->getFragment($node); $fragment = $this->getFragment($node);
if (null !== $fragment) { if ($fragment !== null) {
$complexity = $this->fieldComplexity($fragment, $complexity); $complexity = $this->fieldComplexity($fragment, $complexity);
} }
break; break;
@ -179,11 +165,11 @@ class QueryComplexity extends AbstractQuerySecurity
private function astFieldInfo(FieldNode $field) private function astFieldInfo(FieldNode $field)
{ {
$fieldName = $this->getFieldName($field); $fieldName = $this->getFieldName($field);
$astFieldInfo = [null, null]; $astFieldInfo = [null, null];
if (isset($this->fieldNodeAndDefs[$fieldName])) { if (isset($this->fieldNodeAndDefs[$fieldName])) {
foreach ($this->fieldNodeAndDefs[$fieldName] as $astAndDef) { foreach ($this->fieldNodeAndDefs[$fieldName] as $astAndDef) {
if ($astAndDef[0] == $field) { if ($astAndDef[0] === $field) {
$astFieldInfo = $astAndDef; $astFieldInfo = $astAndDef;
break; break;
} }
@ -193,37 +179,8 @@ class QueryComplexity extends AbstractQuerySecurity
return $astFieldInfo; 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) { foreach ($node->directives as $directiveNode) {
if ($directiveNode->name->value === 'deprecated') { if ($directiveNode->name->value === 'deprecated') {
return false; return false;
@ -236,28 +193,99 @@ class QueryComplexity extends AbstractQuerySecurity
); );
if ($variableValuesResult['errors']) { if ($variableValuesResult['errors']) {
throw new Error(implode("\n\n", array_map( throw new Error(implode(
function ($error) { "\n\n",
return $error->getMessage(); array_map(
} function ($error) {
, $variableValuesResult['errors']))); return $error->getMessage();
},
$variableValuesResult['errors']
)
));
} }
$variableValues = $variableValuesResult['coerced']; $variableValues = $variableValuesResult['coerced'];
if ($directiveNode->name->value === 'include') { if ($directiveNode->name->value === 'include') {
$directive = Directive::includeDirective(); $directive = Directive::includeDirective();
$directiveArgs = Values::getArgumentValues($directive, $directiveNode, $variableValues); $directiveArgs = Values::getArgumentValues($directive, $directiveNode, $variableValues);
return !$directiveArgs['if']; return ! $directiveArgs['if'];
} else {
$directive = Directive::skipDirective();
$directiveArgs = Values::getArgumentValues($directive, $directiveNode, $variableValues);
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() protected function isEnabled()
{ {
return $this->getMaxQueryComplexity() !== static::DISABLED; return $this->getMaxQueryComplexity() !== static::DISABLED;

View File

@ -1,21 +1,20 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\InlineFragmentNode;
use GraphQL\Language\AST\Node; use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\OperationDefinitionNode; use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\SelectionSetNode; use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function sprintf;
class QueryDepth extends AbstractQuerySecurity class QueryDepth extends QuerySecurityRule
{ {
/** /** @var int */
* @var int
*/
private $maxQueryDepth; private $maxQueryDepth;
public function __construct($maxQueryDepth) public function __construct($maxQueryDepth)
@ -23,28 +22,6 @@ class QueryDepth extends AbstractQuerySecurity
$this->setMaxQueryDepth($maxQueryDepth); $this->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) public function getVisitor(ValidationContext $context)
{ {
return $this->invokeIfNeeded( return $this->invokeIfNeeded(
@ -54,22 +31,19 @@ class QueryDepth extends AbstractQuerySecurity
'leave' => function (OperationDefinitionNode $operationDefinition) use ($context) { 'leave' => function (OperationDefinitionNode $operationDefinition) use ($context) {
$maxDepth = $this->fieldDepth($operationDefinition); $maxDepth = $this->fieldDepth($operationDefinition);
if ($maxDepth > $this->getMaxQueryDepth()) { if ($maxDepth <= $this->getMaxQueryDepth()) {
$context->reportError( return;
new Error($this->maxQueryDepthErrorMessage($this->getMaxQueryDepth(), $maxDepth))
);
} }
$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) private function fieldDepth($node, $depth = 0, $maxDepth = 0)
{ {
if (isset($node->selectionSet) && $node->selectionSet instanceof SelectionSetNode) { if (isset($node->selectionSet) && $node->selectionSet instanceof SelectionSetNode) {
@ -85,9 +59,9 @@ class QueryDepth extends AbstractQuerySecurity
{ {
switch ($node->kind) { switch ($node->kind) {
case NodeKind::FIELD: case NodeKind::FIELD:
/* @var FieldNode $node */ /** @var FieldNode $node */
// node has children? // node has children?
if (null !== $node->selectionSet) { if ($node->selectionSet !== null) {
// update maxDepth if needed // update maxDepth if needed
if ($depth > $maxDepth) { if ($depth > $maxDepth) {
$maxDepth = $depth; $maxDepth = $depth;
@ -97,18 +71,18 @@ class QueryDepth extends AbstractQuerySecurity
break; break;
case NodeKind::INLINE_FRAGMENT: case NodeKind::INLINE_FRAGMENT:
/* @var InlineFragmentNode $node */ /** @var InlineFragmentNode $node */
// node has children? // node has children?
if (null !== $node->selectionSet) { if ($node->selectionSet !== null) {
$maxDepth = $this->fieldDepth($node, $depth, $maxDepth); $maxDepth = $this->fieldDepth($node, $depth, $maxDepth);
} }
break; break;
case NodeKind::FRAGMENT_SPREAD: case NodeKind::FRAGMENT_SPREAD:
/* @var FragmentSpreadNode $node */ /** @var FragmentSpreadNode $node */
$fragment = $this->getFragment($node); $fragment = $this->getFragment($node);
if (null !== $fragment) { if ($fragment !== null) {
$maxDepth = $this->fieldDepth($fragment, $depth, $maxDepth); $maxDepth = $this->fieldDepth($fragment, $depth, $maxDepth);
} }
break; break;
@ -116,4 +90,31 @@ class QueryDepth extends AbstractQuerySecurity
return $maxDepth; 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;
}
} }

View File

@ -1,40 +1,35 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use Closure;
use GraphQL\Language\AST\FieldNode; use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\FragmentDefinitionNode; use GraphQL\Language\AST\FragmentDefinitionNode;
use GraphQL\Language\AST\FragmentSpreadNode; use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\InlineFragmentNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\SelectionSetNode; use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Type\Introspection; use GraphQL\Type\Introspection;
use GraphQL\Utils\TypeInfo; use GraphQL\Utils\TypeInfo;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function class_alias;
use function method_exists;
use function sprintf;
abstract class AbstractQuerySecurity extends AbstractValidationRule abstract class QuerySecurityRule extends ValidationRule
{ {
const DISABLED = 0; public const DISABLED = 0;
/** /** @var FragmentDefinitionNode[] */
* @var FragmentDefinitionNode[]
*/
private $fragments = []; private $fragments = [];
/**
* @return \GraphQL\Language\AST\FragmentDefinitionNode[]
*/
protected function getFragments()
{
return $this->fragments;
}
/** /**
* check if equal to 0 no check is done. Must be greater or equal to 0. * 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) 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) protected function getFragment(FragmentSpreadNode $fragmentSpread)
{ {
$spreadName = $fragmentSpread->name->value; $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) protected function invokeIfNeeded(ValidationContext $context, array $validators)
{ {
// is disabled? // is disabled?
if (!$this->isEnabled()) { if (! $this->isEnabled()) {
return []; return [];
} }
@ -75,6 +70,22 @@ abstract class AbstractQuerySecurity extends AbstractValidationRule
return $validators; 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 * Given a selectionSet, adds all of the fields in that selection to
* the passed in map of fields, and returns it at the end. * 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 * @see \GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged
* *
* @param ValidationContext $context * @param Type|null $parentType
* @param Type|null $parentType
* @param SelectionSetNode $selectionSet
* @param \ArrayObject $visitedFragmentNames
* @param \ArrayObject $astAndDefs
* *
* @return \ArrayObject * @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(); $_visitedFragmentNames = $visitedFragmentNames ?: new \ArrayObject();
$_astAndDefs = $astAndDefs ?: new \ArrayObject(); $_astAndDefs = $astAndDefs ?: new \ArrayObject();
foreach ($selectionSet->selections as $selection) { foreach ($selectionSet->selections as $selection) {
switch ($selection->kind) { switch ($selection->getKind()) {
case NodeKind::FIELD: case NodeKind::FIELD:
/* @var FieldNode $selection */ /** @var FieldNode $selection */
$fieldName = $selection->name->value; $fieldName = $selection->name->value;
$fieldDef = null; $fieldDef = null;
if ($parentType && method_exists($parentType, 'getFields')) { if ($parentType && method_exists($parentType, 'getFields')) {
$tmp = $parentType->getFields(); $tmp = $parentType->getFields();
$schemaMetaFieldDef = Introspection::schemaMetaFieldDef(); $schemaMetaFieldDef = Introspection::schemaMetaFieldDef();
$typeMetaFieldDef = Introspection::typeMetaFieldDef(); $typeMetaFieldDef = Introspection::typeMetaFieldDef();
$typeNameMetaFieldDef = Introspection::typeNameMetaFieldDef(); $typeNameMetaFieldDef = Introspection::typeNameMetaFieldDef();
if ($fieldName === $schemaMetaFieldDef->name && $context->getSchema()->getQueryType() === $parentType) { if ($fieldName === $schemaMetaFieldDef->name && $context->getSchema()->getQueryType() === $parentType) {
@ -121,14 +133,14 @@ abstract class AbstractQuerySecurity extends AbstractValidationRule
} }
} }
$responseName = $this->getFieldName($selection); $responseName = $this->getFieldName($selection);
if (!isset($_astAndDefs[$responseName])) { if (! isset($_astAndDefs[$responseName])) {
$_astAndDefs[$responseName] = new \ArrayObject(); $_astAndDefs[$responseName] = new \ArrayObject();
} }
// create field context // create field context
$_astAndDefs[$responseName][] = [$selection, $fieldDef]; $_astAndDefs[$responseName][] = [$selection, $fieldDef];
break; break;
case NodeKind::INLINE_FRAGMENT: case NodeKind::INLINE_FRAGMENT:
/* @var InlineFragmentNode $selection */ /** @var InlineFragmentNode $selection */
$_astAndDefs = $this->collectFieldASTsAndDefs( $_astAndDefs = $this->collectFieldASTsAndDefs(
$context, $context,
TypeInfo::typeFromAST($context->getSchema(), $selection->typeCondition), TypeInfo::typeFromAST($context->getSchema(), $selection->typeCondition),
@ -138,12 +150,12 @@ abstract class AbstractQuerySecurity extends AbstractValidationRule
); );
break; break;
case NodeKind::FRAGMENT_SPREAD: case NodeKind::FRAGMENT_SPREAD:
/* @var FragmentSpreadNode $selection */ /** @var FragmentSpreadNode $selection */
$fragName = $selection->name->value; $fragName = $selection->name->value;
if (empty($_visitedFragmentNames[$fragName])) { if (empty($_visitedFragmentNames[$fragName])) {
$_visitedFragmentNames[$fragName] = true; $_visitedFragmentNames[$fragName] = true;
$fragment = $context->getFragment($fragName); $fragment = $context->getFragment($fragName);
if ($fragment) { if ($fragment) {
$_astAndDefs = $this->collectFieldASTsAndDefs( $_astAndDefs = $this->collectFieldASTsAndDefs(
@ -165,10 +177,9 @@ abstract class AbstractQuerySecurity extends AbstractValidationRule
protected function getFieldName(FieldNode $node) protected function getFieldName(FieldNode $node)
{ {
$fieldName = $node->name->value; $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');

View File

@ -1,4 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
@ -6,40 +9,43 @@ use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NodeKind;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function sprintf;
class ScalarLeafs extends AbstractValidationRule class ScalarLeafs extends ValidationRule
{ {
static function noSubselectionAllowedMessage($field, $type)
{
return "Field \"$field\" of type \"$type\" must not have a sub selection.";
}
static function requiredSubselectionMessage($field, $type)
{
return "Field \"$field\" of type \"$type\" must have a sub selection.";
}
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
{ {
return [ return [
NodeKind::FIELD => function(FieldNode $node) use ($context) { NodeKind::FIELD => function (FieldNode $node) use ($context) {
$type = $context->getType(); $type = $context->getType();
if ($type) { if (! $type) {
if (Type::isLeafType(Type::getNamedType($type))) { return;
if ($node->selectionSet) { }
$context->reportError(new Error(
self::noSubselectionAllowedMessage($node->name->value, $type), if (Type::isLeafType(Type::getNamedType($type))) {
[$node->selectionSet] if ($node->selectionSet) {
));
}
} else if (!$node->selectionSet) {
$context->reportError(new Error( $context->reportError(new Error(
self::requiredSubselectionMessage($node->name->value, $type), self::noSubselectionAllowedMessage($node->name->value, $type),
[$node] [$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);
}
} }

View File

@ -1,20 +1,20 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Language\AST\ArgumentNode; use GraphQL\Language\AST\ArgumentNode;
use GraphQL\Language\AST\Node; use GraphQL\Language\AST\NameNode;
use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Visitor; use GraphQL\Language\Visitor;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function sprintf;
class UniqueArgumentNames extends AbstractValidationRule class UniqueArgumentNames extends ValidationRule
{ {
static function duplicateArgMessage($argName) /** @var NameNode[] */
{
return "There can be only one argument named \"$argName\".";
}
public $knownArgNames; public $knownArgNames;
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
@ -22,15 +22,15 @@ class UniqueArgumentNames extends AbstractValidationRule
$this->knownArgNames = []; $this->knownArgNames = [];
return [ return [
NodeKind::FIELD => function () { NodeKind::FIELD => function () {
$this->knownArgNames = [];; $this->knownArgNames = [];
}, },
NodeKind::DIRECTIVE => function () { NodeKind::DIRECTIVE => function () {
$this->knownArgNames = []; $this->knownArgNames = [];
}, },
NodeKind::ARGUMENT => function (ArgumentNode $node) use ($context) { NodeKind::ARGUMENT => function (ArgumentNode $node) use ($context) {
$argName = $node->name->value; $argName = $node->name->value;
if (!empty($this->knownArgNames[$argName])) { if (! empty($this->knownArgNames[$argName])) {
$context->reportError(new Error( $context->reportError(new Error(
self::duplicateArgMessage($argName), self::duplicateArgMessage($argName),
[$this->knownArgNames[$argName], $node->name] [$this->knownArgNames[$argName], $node->name]
@ -38,8 +38,14 @@ class UniqueArgumentNames extends AbstractValidationRule
} else { } else {
$this->knownArgNames[$argName] = $node->name; $this->knownArgNames[$argName] = $node->name;
} }
return Visitor::skipNode(); return Visitor::skipNode();
} },
]; ];
} }
public static function duplicateArgMessage($argName)
{
return sprintf('There can be only one argument named "%s".', $argName);
}
} }

View File

@ -1,38 +1,44 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Language\AST\DirectiveNode; use GraphQL\Language\AST\DirectiveNode;
use GraphQL\Language\AST\Node; use GraphQL\Language\AST\Node;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function sprintf;
class UniqueDirectivesPerLocation extends AbstractValidationRule class UniqueDirectivesPerLocation extends ValidationRule
{ {
static function duplicateDirectiveMessage($directiveName)
{
return 'The directive "'.$directiveName.'" can only be used once at this location.';
}
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
{ {
return [ return [
'enter' => function(Node $node) use ($context) { 'enter' => function (Node $node) use ($context) {
if (isset($node->directives)) { if (! isset($node->directives)) {
$knownDirectives = []; return;
foreach ($node->directives as $directive) { }
/** @var DirectiveNode $directive */
$directiveName = $directive->name->value; $knownDirectives = [];
if (isset($knownDirectives[$directiveName])) { foreach ($node->directives as $directive) {
$context->reportError(new Error( /** @var DirectiveNode $directive */
self::duplicateDirectiveMessage($directiveName), $directiveName = $directive->name->value;
[$knownDirectives[$directiveName], $directive] if (isset($knownDirectives[$directiveName])) {
)); $context->reportError(new Error(
} else { self::duplicateDirectiveMessage($directiveName),
$knownDirectives[$directiveName] = $directive; [$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);
}
} }

View File

@ -1,19 +1,20 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Language\AST\FragmentDefinitionNode; use GraphQL\Language\AST\FragmentDefinitionNode;
use GraphQL\Language\AST\NameNode;
use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Visitor; use GraphQL\Language\Visitor;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function sprintf;
class UniqueFragmentNames extends AbstractValidationRule class UniqueFragmentNames extends ValidationRule
{ {
static function duplicateFragmentNameMessage($fragName) /** @var NameNode[] */
{
return "There can be only one fragment named \"$fragName\".";
}
public $knownFragmentNames; public $knownFragmentNames;
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
@ -24,18 +25,24 @@ class UniqueFragmentNames extends AbstractValidationRule
NodeKind::OPERATION_DEFINITION => function () { NodeKind::OPERATION_DEFINITION => function () {
return Visitor::skipNode(); return Visitor::skipNode();
}, },
NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) { NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) {
$fragmentName = $node->name->value; $fragmentName = $node->name->value;
if (!empty($this->knownFragmentNames[$fragmentName])) { if (empty($this->knownFragmentNames[$fragmentName])) {
$this->knownFragmentNames[$fragmentName] = $node->name;
} else {
$context->reportError(new Error( $context->reportError(new Error(
self::duplicateFragmentNameMessage($fragmentName), self::duplicateFragmentNameMessage($fragmentName),
[ $this->knownFragmentNames[$fragmentName], $node->name ] [$this->knownFragmentNames[$fragmentName], $node->name]
)); ));
} else {
$this->knownFragmentNames[$fragmentName] = $node->name;
} }
return Visitor::skipNode(); return Visitor::skipNode();
} },
]; ];
} }
public static function duplicateFragmentNameMessage($fragName)
{
return sprintf('There can be only one fragment named "%s".', $fragName);
}
} }

View File

@ -1,4 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
@ -6,45 +9,51 @@ use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\ObjectFieldNode; use GraphQL\Language\AST\ObjectFieldNode;
use GraphQL\Language\Visitor; use GraphQL\Language\Visitor;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function array_pop;
use function sprintf;
class UniqueInputFieldNames extends AbstractValidationRule class UniqueInputFieldNames extends ValidationRule
{ {
static function duplicateInputFieldMessage($fieldName) /** @var string[] */
{
return "There can be only one input field named \"$fieldName\".";
}
public $knownNames; public $knownNames;
/** @var string[][] */
public $knownNameStack; public $knownNameStack;
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
{ {
$this->knownNames = []; $this->knownNames = [];
$this->knownNameStack = []; $this->knownNameStack = [];
return [ return [
NodeKind::OBJECT => [ NodeKind::OBJECT => [
'enter' => function() { 'enter' => function () {
$this->knownNameStack[] = $this->knownNames; $this->knownNameStack[] = $this->knownNames;
$this->knownNames = []; $this->knownNames = [];
}, },
'leave' => function() { 'leave' => function () {
$this->knownNames = array_pop($this->knownNameStack); $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; $fieldName = $node->name->value;
if (!empty($this->knownNames[$fieldName])) { if (! empty($this->knownNames[$fieldName])) {
$context->reportError(new Error( $context->reportError(new Error(
self::duplicateInputFieldMessage($fieldName), self::duplicateInputFieldMessage($fieldName),
[ $this->knownNames[$fieldName], $node->name ] [$this->knownNames[$fieldName], $node->name]
)); ));
} else { } else {
$this->knownNames[$fieldName] = $node->name; $this->knownNames[$fieldName] = $node->name;
} }
return Visitor::skipNode(); return Visitor::skipNode();
} },
]; ];
} }
public static function duplicateInputFieldMessage($fieldName)
{
return sprintf('There can be only one input field named "%s".', $fieldName);
}
} }

View File

@ -1,19 +1,20 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Language\AST\NameNode;
use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\OperationDefinitionNode; use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\Visitor; use GraphQL\Language\Visitor;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function sprintf;
class UniqueOperationNames extends AbstractValidationRule class UniqueOperationNames extends ValidationRule
{ {
static function duplicateOperationNameMessage($operationName) /** @var NameNode[] */
{
return "There can be only one operation named \"$operationName\".";
}
public $knownOperationNames; public $knownOperationNames;
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
@ -21,24 +22,30 @@ class UniqueOperationNames extends AbstractValidationRule
$this->knownOperationNames = []; $this->knownOperationNames = [];
return [ return [
NodeKind::OPERATION_DEFINITION => function(OperationDefinitionNode $node) use ($context) { NodeKind::OPERATION_DEFINITION => function (OperationDefinitionNode $node) use ($context) {
$operationName = $node->name; $operationName = $node->name;
if ($operationName) { if ($operationName) {
if (!empty($this->knownOperationNames[$operationName->value])) { if (empty($this->knownOperationNames[$operationName->value])) {
$this->knownOperationNames[$operationName->value] = $operationName;
} else {
$context->reportError(new Error( $context->reportError(new Error(
self::duplicateOperationNameMessage($operationName->value), self::duplicateOperationNameMessage($operationName->value),
[ $this->knownOperationNames[$operationName->value], $operationName ] [$this->knownOperationNames[$operationName->value], $operationName]
)); ));
} else {
$this->knownOperationNames[$operationName->value] = $operationName;
} }
} }
return Visitor::skipNode(); return Visitor::skipNode();
}, },
NodeKind::FRAGMENT_DEFINITION => function() { NodeKind::FRAGMENT_DEFINITION => function () {
return Visitor::skipNode(); return Visitor::skipNode();
} },
]; ];
} }
public static function duplicateOperationNameMessage($operationName)
{
return sprintf('There can be only one operation named "%s".', $operationName);
}
} }

View File

@ -1,18 +1,19 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Language\AST\NameNode;
use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\VariableDefinitionNode; use GraphQL\Language\AST\VariableDefinitionNode;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function sprintf;
class UniqueVariableNames extends AbstractValidationRule class UniqueVariableNames extends ValidationRule
{ {
static function duplicateVariableMessage($variableName) /** @var NameNode[] */
{
return "There can be only one variable named \"$variableName\".";
}
public $knownVariableNames; public $knownVariableNames;
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
@ -20,20 +21,25 @@ class UniqueVariableNames extends AbstractValidationRule
$this->knownVariableNames = []; $this->knownVariableNames = [];
return [ return [
NodeKind::OPERATION_DEFINITION => function() { NodeKind::OPERATION_DEFINITION => function () {
$this->knownVariableNames = []; $this->knownVariableNames = [];
}, },
NodeKind::VARIABLE_DEFINITION => function(VariableDefinitionNode $node) use ($context) { NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $node) use ($context) {
$variableName = $node->variable->name->value; $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( $context->reportError(new Error(
self::duplicateVariableMessage($variableName), 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);
}
} }

View File

@ -1,10 +1,16 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function class_alias;
use function get_class;
abstract class AbstractValidationRule abstract class ValidationRule
{ {
/** @var string */
protected $name; protected $name;
public function getName() public function getName()
@ -21,8 +27,9 @@ abstract class AbstractValidationRule
* Returns structure suitable for GraphQL\Language\Visitor * Returns structure suitable for GraphQL\Language\Visitor
* *
* @see \GraphQL\Language\Visitor * @see \GraphQL\Language\Visitor
* @param ValidationContext $context * @return mixed[]
* @return array
*/ */
abstract public function getVisitor(ValidationContext $context); abstract public function getVisitor(ValidationContext $context);
} }
class_alias(ValidationRule::class, 'GraphQL\Validator\Rules\AbstractValidationRule');

View File

@ -1,4 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
@ -24,6 +27,12 @@ use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function array_combine;
use function array_keys;
use function array_map;
use function array_values;
use function iterator_to_array;
use function sprintf;
/** /**
* Value literals of correct type * Value literals of correct type
@ -31,103 +40,96 @@ use GraphQL\Validator\ValidationContext;
* A GraphQL document is only valid if all value literals are of the type * A GraphQL document is only valid if all value literals are of the type
* expected at their position. * expected at their position.
*/ */
class ValuesOfCorrectType extends AbstractValidationRule class ValuesOfCorrectType extends ValidationRule
{ {
static function badValueMessage($typeName, $valueName, $message = null)
{
return "Expected type {$typeName}, found {$valueName}" .
($message ? "; ${message}" : '.');
}
static function requiredFieldMessage($typeName, $fieldName, $fieldTypeName)
{
return "Field {$typeName}.{$fieldName} of required type " .
"{$fieldTypeName} was not provided.";
}
static function unknownFieldMessage($typeName, $fieldName, $message = null)
{
return (
"Field \"{$fieldName}\" is not defined by type {$typeName}" .
($message ? "; {$message}" : '.')
);
}
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
{ {
return [ return [
NodeKind::NULL => function(NullValueNode $node) use ($context) { NodeKind::NULL => function (NullValueNode $node) use ($context) {
$type = $context->getInputType(); $type = $context->getInputType();
if ($type instanceof NonNull) { if (! ($type instanceof NonNull)) {
$context->reportError( return;
new Error(
self::badValueMessage((string) $type, Printer::doPrint($node)),
$node
)
);
} }
$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 // Note: TypeInfo will traverse into a list's item type, so look to the
// parent input type to check if it is a list. // parent input type to check if it is a list.
$type = Type::getNullableType($context->getParentInputType()); $type = Type::getNullableType($context->getParentInputType());
if (!$type instanceof ListOfType) { if (! $type instanceof ListOfType) {
$this->isValidScalar($context, $node); $this->isValidScalar($context, $node);
return Visitor::skipNode(); 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 // Note: TypeInfo will traverse into a list's item type, so look to the
// parent input type to check if it is a list. // parent input type to check if it is a list.
$type = Type::getNamedType($context->getInputType()); $type = Type::getNamedType($context->getInputType());
if (!$type instanceof InputObjectType) { if (! $type instanceof InputObjectType) {
$this->isValidScalar($context, $node); $this->isValidScalar($context, $node);
return Visitor::skipNode(); return Visitor::skipNode();
} }
// Ensure every required field exists. // Ensure every required field exists.
$inputFields = $type->getFields(); $inputFields = $type->getFields();
$nodeFields = iterator_to_array($node->fields); $nodeFields = iterator_to_array($node->fields);
$fieldNodeMap = array_combine( $fieldNodeMap = array_combine(
array_map(function ($field) { return $field->name->value; }, $nodeFields), array_map(
function ($field) {
return $field->name->value;
},
$nodeFields
),
array_values($nodeFields) array_values($nodeFields)
); );
foreach ($inputFields as $fieldName => $fieldDef) { foreach ($inputFields as $fieldName => $fieldDef) {
$fieldType = $fieldDef->getType(); $fieldType = $fieldDef->getType();
if (!isset($fieldNodeMap[$fieldName]) && $fieldType instanceof NonNull) { if (isset($fieldNodeMap[$fieldName]) || ! ($fieldType instanceof NonNull)) {
$context->reportError( continue;
new Error(
self::requiredFieldMessage($type->name, $fieldName, (string) $fieldType),
$node
)
);
} }
}
},
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( $context->reportError(
new Error( new Error(
self::unknownFieldMessage($parentType->name, $node->name->value, $didYouMean), self::requiredFieldMessage($type->name, $fieldName, (string) $fieldType),
$node $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()); $type = Type::getNamedType($context->getInputType());
if (!$type instanceof EnumType) { if (! $type instanceof EnumType) {
$this->isValidScalar($context, $node); $this->isValidScalar($context, $node);
} else if (!$type->getValue($node->value)) { } elseif (! $type->getValue($node->value)) {
$context->reportError( $context->reportError(
new Error( new Error(
self::badValueMessage( self::badValueMessage(
@ -140,25 +142,39 @@ class ValuesOfCorrectType extends AbstractValidationRule
); );
} }
}, },
NodeKind::INT => function (IntValueNode $node) use ($context) { $this->isValidScalar($context, $node); }, NodeKind::INT => function (IntValueNode $node) use ($context) {
NodeKind::FLOAT => function (FloatValueNode $node) use ($context) { $this->isValidScalar($context, $node); }, $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::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) private function isValidScalar(ValidationContext $context, ValueNode $node)
{ {
// Report any error at the full type expected by the location. // Report any error at the full type expected by the location.
$locationType = $context->getInputType(); $locationType = $context->getInputType();
if (!$locationType) { if (! $locationType) {
return; return;
} }
$type = Type::getNamedType($locationType); $type = Type::getNamedType($locationType);
if (!$type instanceof ScalarType) { if (! $type instanceof ScalarType) {
$context->reportError( $context->reportError(
new Error( new Error(
self::badValueMessage( self::badValueMessage(
@ -169,6 +185,7 @@ class ValuesOfCorrectType extends AbstractValidationRule
$node $node
) )
); );
return; return;
} }
@ -216,12 +233,26 @@ class ValuesOfCorrectType extends AbstractValidationRule
if ($type instanceof EnumType) { if ($type instanceof EnumType) {
$suggestions = Utils::suggestionList( $suggestions = Utils::suggestionList(
Printer::doPrint($node), Printer::doPrint($node),
array_map(function (EnumValueDefinition $value) { array_map(
return $value->name; function (EnumValueDefinition $value) {
}, $type->getValues()) return $value->name;
},
$type->getValues()
)
); );
return $suggestions ? 'Did you mean the enum value ' . Utils::orList($suggestions) . '?' : null; 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) : '.');
}
} }

View File

@ -1,39 +1,42 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\VariableDefinitionNode; use GraphQL\Language\AST\VariableDefinitionNode;
use GraphQL\Language\Printer; use GraphQL\Language\Printer;
use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Utils\TypeInfo; use GraphQL\Utils\TypeInfo;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function sprintf;
class VariablesAreInputTypes extends AbstractValidationRule class VariablesAreInputTypes extends ValidationRule
{ {
static function nonInputTypeOnVarMessage($variableName, $typeName)
{
return "Variable \"\$$variableName\" cannot be non-input type \"$typeName\".";
}
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
{ {
return [ return [
NodeKind::VARIABLE_DEFINITION => function(VariableDefinitionNode $node) use ($context) { NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $node) use ($context) {
$type = TypeInfo::typeFromAST($context->getSchema(), $node->type); $type = TypeInfo::typeFromAST($context->getSchema(), $node->type);
// If the variable type is not an input type, return an error. // If the variable type is not an input type, return an error.
if ($type && !Type::isInputType($type)) { if (! $type || Type::isInputType($type)) {
$variableName = $node->variable->name->value; return;
$context->reportError(new Error(
self::nonInputTypeOnVarMessage($variableName, Printer::doPrint($node->type)),
[ $node->type ]
));
} }
}
$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);
}
} }

View File

@ -1,4 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
@ -9,6 +12,7 @@ use GraphQL\Language\AST\VariableDefinitionNode;
use GraphQL\Language\Visitor; use GraphQL\Language\Visitor;
use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\NonNull;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function sprintf;
/** /**
* Variable's default value is allowed * Variable's default value is allowed
@ -16,45 +20,46 @@ use GraphQL\Validator\ValidationContext;
* A GraphQL document is only valid if all variable default values are allowed * A GraphQL document is only valid if all variable default values are allowed
* due to a variable not being required. * due to a variable not being required.
*/ */
class VariablesDefaultValueAllowed extends AbstractValidationRule class VariablesDefaultValueAllowed extends ValidationRule
{ {
static function defaultForRequiredVarMessage($varName, $type, $guessType)
{
return (
"Variable \"\${$varName}\" of type \"{$type}\" is required and " .
'will not use the default value. ' .
"Perhaps you meant to use type \"{$guessType}\"."
);
}
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
{ {
return [ return [
NodeKind::VARIABLE_DEFINITION => function(VariableDefinitionNode $node) use ($context) { NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $node) use ($context) {
$name = $node->variable->name->value; $name = $node->variable->name->value;
$defaultValue = $node->defaultValue; $defaultValue = $node->defaultValue;
$type = $context->getInputType(); $type = $context->getInputType();
if ($type instanceof NonNull && $defaultValue) { if ($type instanceof NonNull && $defaultValue) {
$context->reportError( $context->reportError(
new Error( new Error(
self::defaultForRequiredVarMessage( self::defaultForRequiredVarMessage(
$name, $name,
$type, $type,
$type->getWrappedType() $type->getWrappedType()
), ),
[$defaultValue] [$defaultValue]
) )
); );
} }
return Visitor::skipNode(); return Visitor::skipNode();
}, },
NodeKind::SELECTION_SET => function(SelectionSetNode $node) use ($context) { NodeKind::SELECTION_SET => function (SelectionSetNode $node) use ($context) {
return Visitor::skipNode(); return Visitor::skipNode();
}, },
NodeKind::FRAGMENT_DEFINITION => function(FragmentDefinitionNode $node) use ($context) { NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) {
return Visitor::skipNode(); 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
);
}
} }

View File

@ -1,4 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
@ -10,15 +13,11 @@ use GraphQL\Type\Definition\NonNull;
use GraphQL\Utils\TypeComparators; use GraphQL\Utils\TypeComparators;
use GraphQL\Utils\TypeInfo; use GraphQL\Utils\TypeInfo;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use function sprintf;
class VariablesInAllowedPosition extends AbstractValidationRule class VariablesInAllowedPosition extends ValidationRule
{ {
static function badVarPosMessage($varName, $varType, $expectedType) /** @var */
{
return "Variable \"\$$varName\" of type \"$varType\" used in position expecting ".
"type \"$expectedType\".";
}
public $varDefMap; public $varDefMap;
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
@ -28,50 +27,77 @@ class VariablesInAllowedPosition extends AbstractValidationRule
'enter' => function () { 'enter' => function () {
$this->varDefMap = []; $this->varDefMap = [];
}, },
'leave' => function(OperationDefinitionNode $operation) use ($context) { 'leave' => function (OperationDefinitionNode $operation) use ($context) {
$usages = $context->getRecursiveVariableUsages($operation); $usages = $context->getRecursiveVariableUsages($operation);
foreach ($usages as $usage) { foreach ($usages as $usage) {
$node = $usage['node']; $node = $usage['node'];
$type = $usage['type']; $type = $usage['type'];
$varName = $node->name->value; $varName = $node->name->value;
$varDef = isset($this->varDefMap[$varName]) ? $this->varDefMap[$varName] : null; $varDef = $this->varDefMap[$varName] ?? null;
if ($varDef && $type) { if (! $varDef || ! $type) {
// A var type is allowed if it is the same or more strict (e.g. is continue;
// 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]
));
}
} }
// 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; $this->varDefMap[$varDefNode->variable->name->value] = $varDefNode;
} },
]; ];
} }
// A var type is allowed if it is the same or more strict than the expected private function effectiveType($varType, $varDef)
// 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 return (! $varDef->defaultValue || $varType instanceof NonNull) ? $varType : new NonNull($varType);
// be more strict than the expected item type. }
/**
* 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) private function varTypeAllowedForType($varType, $expectedType)
{ {
if ($expectedType instanceof NonNull) { if ($expectedType instanceof NonNull) {
if ($varType instanceof NonNull) { if ($varType instanceof NonNull) {
return $this->varTypeAllowedForType($varType->getWrappedType(), $expectedType->getWrappedType()); return $this->varTypeAllowedForType($varType->getWrappedType(), $expectedType->getWrappedType());
} }
return false; return false;
} }
if ($varType instanceof NonNull) { if ($varType instanceof NonNull) {
@ -80,13 +106,7 @@ class VariablesInAllowedPosition extends AbstractValidationRule
if ($varType instanceof ListOfType && $expectedType instanceof ListOfType) { if ($varType instanceof ListOfType && $expectedType instanceof ListOfType) {
return $this->varTypeAllowedForType($varType->getWrappedType(), $expectedType->getWrappedType()); return $this->varTypeAllowedForType($varType->getWrappedType(), $expectedType->getWrappedType());
} }
return $varType === $expectedType; 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);
}
} }

View File

@ -1,22 +1,27 @@
<?php <?php
declare(strict_types=1);
namespace GraphQL\Validator; namespace GraphQL\Validator;
use GraphQL\Error\Error;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\FragmentDefinitionNode;
use GraphQL\Language\AST\FragmentSpreadNode; use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\HasSelectionSet; use GraphQL\Language\AST\HasSelectionSet;
use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\OperationDefinitionNode; use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\VariableNode; use GraphQL\Language\AST\VariableNode;
use GraphQL\Language\Visitor; use GraphQL\Language\Visitor;
use \SplObjectStorage;
use GraphQL\Error\Error;
use GraphQL\Type\Schema;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\FragmentDefinitionNode;
use GraphQL\Type\Definition\CompositeType;
use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\InputType; use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;
use GraphQL\Utils\TypeInfo; use GraphQL\Utils\TypeInfo;
use SplObjectStorage;
use function array_pop;
use function call_user_func_array;
use function count;
/** /**
* An instance of this class is passed as the "this" context to all validators, * An instance of this class is passed as the "this" context to all validators,
@ -25,74 +30,46 @@ use GraphQL\Utils\TypeInfo;
*/ */
class ValidationContext class ValidationContext
{ {
/** /** @var Schema */
* @var Schema
*/
private $schema; private $schema;
/** /** @var DocumentNode */
* @var DocumentNode
*/
private $ast; private $ast;
/** /** @var TypeInfo */
* @var TypeInfo
*/
private $typeInfo; private $typeInfo;
/** /** @var Error[] */
* @var Error[]
*/
private $errors; private $errors;
/** /** @var FragmentDefinitionNode[] */
* @var FragmentDefinitionNode[]
*/
private $fragments; private $fragments;
/** /** @var SplObjectStorage */
* @var SplObjectStorage
*/
private $fragmentSpreads; private $fragmentSpreads;
/** /** @var SplObjectStorage */
* @var SplObjectStorage
*/
private $recursivelyReferencedFragments; private $recursivelyReferencedFragments;
/** /** @var SplObjectStorage */
* @var SplObjectStorage
*/
private $variableUsages; private $variableUsages;
/** /** @var SplObjectStorage */
* @var SplObjectStorage
*/
private $recursiveVariableUsages; private $recursiveVariableUsages;
/** public function __construct(Schema $schema, DocumentNode $ast, TypeInfo $typeInfo)
* ValidationContext constructor.
*
* @param Schema $schema
* @param DocumentNode $ast
* @param TypeInfo $typeInfo
*/
function __construct(Schema $schema, DocumentNode $ast, TypeInfo $typeInfo)
{ {
$this->schema = $schema; $this->schema = $schema;
$this->ast = $ast; $this->ast = $ast;
$this->typeInfo = $typeInfo; $this->typeInfo = $typeInfo;
$this->errors = []; $this->errors = [];
$this->fragmentSpreads = new SplObjectStorage(); $this->fragmentSpreads = new SplObjectStorage();
$this->recursivelyReferencedFragments = new SplObjectStorage(); $this->recursivelyReferencedFragments = new SplObjectStorage();
$this->variableUsages = new SplObjectStorage(); $this->variableUsages = new SplObjectStorage();
$this->recursiveVariableUsages = new SplObjectStorage(); $this->recursiveVariableUsages = new SplObjectStorage();
} }
/** public function reportError(Error $error)
* @param Error $error
*/
function reportError(Error $error)
{ {
$this->errors[] = $error; $this->errors[] = $error;
} }
@ -100,7 +77,7 @@ class ValidationContext
/** /**
* @return Error[] * @return Error[]
*/ */
function getErrors() public function getErrors()
{ {
return $this->errors; return $this->errors;
} }
@ -108,159 +85,175 @@ class ValidationContext
/** /**
* @return Schema * @return Schema
*/ */
function getSchema() public function getSchema()
{ {
return $this->schema; 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;
}
/** if (! $usages) {
* @param string $name $usages = $this->getVariableUsages($operation);
* @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);
$fragments = $this->getRecursivelyReferencedFragments($operation); $fragments = $this->getRecursivelyReferencedFragments($operation);
$tmp = [$usages]; $tmp = [$usages];
for ($i = 0; $i < count($fragments); $i++) { for ($i = 0; $i < count($fragments); $i++) {
$tmp[] = $this->getVariableUsages($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; $this->recursiveVariableUsages[$operation] = $usages;
} }
return $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 = [];
$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 * Returns OutputType
* *
* @return Type * @return Type
*/ */
function getType() public function getType()
{ {
return $this->typeInfo->getType(); return $this->typeInfo->getType();
} }
/** /**
* @return CompositeType * @return Type
*/ */
function getParentType() public function getParentType()
{ {
return $this->typeInfo->getParentType(); return $this->typeInfo->getParentType();
} }
@ -268,7 +261,7 @@ class ValidationContext
/** /**
* @return InputType * @return InputType
*/ */
function getInputType() public function getInputType()
{ {
return $this->typeInfo->getInputType(); return $this->typeInfo->getInputType();
} }
@ -276,7 +269,7 @@ class ValidationContext
/** /**
* @return InputType * @return InputType
*/ */
function getParentInputType() public function getParentInputType()
{ {
return $this->typeInfo->getParentInputType(); return $this->typeInfo->getParentInputType();
} }
@ -284,17 +277,17 @@ class ValidationContext
/** /**
* @return FieldDefinition * @return FieldDefinition
*/ */
function getFieldDef() public function getFieldDef()
{ {
return $this->typeInfo->getFieldDef(); return $this->typeInfo->getFieldDef();
} }
function getDirective() public function getDirective()
{ {
return $this->typeInfo->getDirective(); return $this->typeInfo->getDirective();
} }
function getArgument() public function getArgument()
{ {
return $this->typeInfo->getArgument(); return $this->typeInfo->getArgument();
} }

View File

@ -5,7 +5,7 @@ use GraphQL\Error\FormattedError;
use GraphQL\Language\Parser; use GraphQL\Language\Parser;
use GraphQL\Type\Introspection; use GraphQL\Type\Introspection;
use GraphQL\Validator\DocumentValidator; use GraphQL\Validator\DocumentValidator;
use GraphQL\Validator\Rules\AbstractQuerySecurity; use GraphQL\Validator\Rules\QuerySecurityRule;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
abstract class QuerySecurityTestCase extends TestCase abstract class QuerySecurityTestCase extends TestCase
@ -13,7 +13,7 @@ abstract class QuerySecurityTestCase extends TestCase
/** /**
* @param $max * @param $max
* *
* @return AbstractQuerySecurity * @return QuerySecurityRule
*/ */
abstract protected function getRule($max); abstract protected function getRule($max);
@ -89,8 +89,9 @@ abstract class QuerySecurityTestCase extends TestCase
{ {
$this->assertDocumentValidator($query, $maxExpected); $this->assertDocumentValidator($query, $maxExpected);
$newMax = $maxExpected - 1; $newMax = $maxExpected - 1;
if ($newMax !== AbstractQuerySecurity::DISABLED) { if ($newMax === QuerySecurityRule::DISABLED) {
$this->assertDocumentValidator($query, $newMax, [$this->createFormattedError($newMax, $maxExpected)]); return;
} }
$this->assertDocumentValidator($query, $newMax, [$this->createFormattedError($newMax, $maxExpected)]);
} }
} }